Class: YfAsDataframe::Ticker

Inherits:
Object
  • Object
show all
Includes:
Analysis, Financials, Fundamentals, Holders, PriceHistory, Quote, YfConnection
Defined in:
lib/yf_as_dataframe/ticker.rb

Defined Under Namespace

Classes: SymbolNotFoundException, YahooFinanceException

Constant Summary collapse

ROOT_URL =
'https://finance.yahoo.com'.freeze
BASE_URL =
'https://query2.finance.yahoo.com'.freeze

Constants included from Holders

Holders::QUOTE_SUMMARY_URL

Constants included from PriceHistory

PriceHistory::PRICE_COLNAMES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Financials

#balance_sheet, #cash_flow, included, #income_stmt, #initialize_financials, #quarterly_balance_sheet, #quarterly_cash_flow, #quarterly_income_stmt

Methods included from Quote

#calendar, included, #info, #initialize_quote, #recommendations, #sustainability, #upgrades_downgrades, #valid_modules

Methods included from Holders

included, #initialize_holders, #insider_purchases, #insider_roster, #insider_transactions, #institutional, #major, #mutualfund

Methods included from Fundamentals

#earnings, included, #initialize_fundamentals

Methods included from Analysis

#analyst_price_target, #analyst_trend_details, #earnings_trend, #eps_est, included, #initialize_analysis, #rev_est

Methods included from PriceHistory

#actions, #capital_gains, #currency, #day_high, #day_low, #dividends, #exchange, #fifty_day_average, #history, #history_metadata, included, #initialize_price_history, #last_price, #last_volume, #market_cap, #open, #previous_close, #quote_type, #regular_market_previous_close, #splits, #ten_day_average_volume, #three_month_average_volume, #timezone, #two_hundred_day_average, #year_change, #year_high, #year_low

Methods included from YfConnection

#cache_get, enable_curl_impersonate, enable_curl_impersonate_fallback, #get, get_available_curl_impersonate_executables, get_curl_impersonate_config, #get_original, #get_raw_json, set_curl_impersonate_connect_timeout, set_curl_impersonate_process_timeout, set_curl_impersonate_retries, set_curl_impersonate_timeout, #yfconn_initialize

Constructor Details

#initialize(ticker) ⇒ Ticker

Returns a new instance of Ticker.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/yf_as_dataframe/ticker.rb', line 20

def initialize(ticker)
  @proxy = nil 
  @timeout = 30
  @tz = TZInfo::Timezone.get('America/New_York')

  @isin = nil
  @news = []
  @shares = nil

  @earnings_dates = {}
  @expirations = {}
  @underlying = {}

  @ticker = (YfAsDataframe::Utils.is_isin(ticker.upcase) ? YfAsDataframe::Utils.get_ticker_by_isin(ticker.upcase, nil, @session) : ticker).upcase

  yfconn_initialize
end

Instance Attribute Details

#error_messageObject (readonly)

Returns the value of attribute error_message.



12
13
14
# File 'lib/yf_as_dataframe/ticker.rb', line 12

def error_message
  @error_message
end

#isinObject

Returns the value of attribute isin.



11
12
13
# File 'lib/yf_as_dataframe/ticker.rb', line 11

def isin
  @isin
end

#proxyObject

Returns the value of attribute proxy.



11
12
13
# File 'lib/yf_as_dataframe/ticker.rb', line 11

def proxy
  @proxy
end

#tickerObject (readonly) Also known as: symbol

Returns the value of attribute ticker.



12
13
14
# File 'lib/yf_as_dataframe/ticker.rb', line 12

def ticker
  @ticker
end

#timeoutObject

Returns the value of attribute timeout.



11
12
13
# File 'lib/yf_as_dataframe/ticker.rb', line 11

def timeout
  @timeout
end

#tzObject Also known as: _get_ticker_tz

Returns the value of attribute tz.



11
12
13
# File 'lib/yf_as_dataframe/ticker.rb', line 11

def tz
  @tz
end

Instance Method Details

#download_options(date = nil) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/yf_as_dataframe/ticker.rb', line 243

def download_options(date = nil)
  url = date.nil? ? "#{BASE_URL}/v7/finance/options/#{@ticker}" : "#{BASE_URL}/v7/finance/options/#{@ticker}?date=#{date}"

  response = get(url).parsed_response  #Net::HTTP.get(uri)

  if response['optionChain'].key?('result') #r.dig('optionChain', 'result')&.any?
    response['optionChain']['result'][0]['expirationDates'].each do |exp|
      @expirations[Time.at(exp).utc.strftime('%Y-%m-%d')] = exp
    end

    @underlying = response['optionChain']['result'][0]['quote'] || {}

    opt = response['optionChain']['result'][0]['options'] || []

    return opt.empty? ? {} : opt[0].merge('underlying' => @underlying) 
  end
  {}
end

#earnings_dates(limit = 12) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/yf_as_dataframe/ticker.rb', line 145

def earnings_dates(limit = 12)
  #   """
  # Get earning dates (future and historic)
  # :param limit: max amount of upcoming and recent earnings dates to return.
  #               Default value 12 should return next 4 quarters and last 8 quarters.
  #               Increase if more history is needed.

  # :return: Polars dataframe
  # """
  return @earnings_dates[limit] if @earnings_dates && @earnings_dates[limit]

  # logger = Logger.new(STDOUT)

  page_size = [limit, 100].min  # YF caps at 100, don't go higher
  page_offset = 0
  dates = nil
  # while true
    url = "#{ROOT_URL}/calendar/earnings?symbol=#{@ticker}&offset=#{page_offset}&size=#{page_size}"
    data = get(url).parsed_response # @data.cache_get(url: url).text

    if data.include?("Will be right back")
      raise RuntimeError, "*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\nOur engineers are working quickly to resolve the issue. Thank you for your patience."
    end

    csv = ''
    doc = Nokogiri::HTML(data)
    tbl = doc.xpath("//table").first
    tbl.search('tr').each do |tr|
      cells = tr.search('th, td')
      csv += CSV.generate_line(cells)
    end
    csv = CSV.parse(csv)

    df = {}
    (0..csv[0].length-1).each{|i| df[csv[0][i]] = csv[1..-1].transpose[i] }
    dates = Polars::DataFrame.new(df)
  # end

  # Drop redundant columns
  dates = dates.drop(["Symbol", "Company"]) #, axis: 1)

  # Convert types
  ["EPS Estimate", "Reported EPS", "Surprise(%)"].each do |cn|
    s = Polars::Series.new([Float::NAN] * (dates.shape.first))
    (0..(dates.shape.first-1)).to_a.each {|i| s[i] = dates[cn][i].to_f unless dates[cn][i] == '-' }
    dates.replace(cn, s)
  end

  # Convert % to range 0->1:
  dates["Surprise(%)"] *= 0.01

  # Parse earnings date string
  s = Polars::Series.new(dates['Earnings Date'].map{|t| Time.at(t.to_datetime.to_i).to_datetime }, dtype: :i64)
  dates.replace('Earnings Date', s)


  @earnings_dates[limit] = dates

  dates
end

#is_valid_timezone(tz) ⇒ Object



230
231
232
233
234
235
236
237
# File 'lib/yf_as_dataframe/ticker.rb', line 230

def is_valid_timezone(tz)
  begin
    _tz.timezone(tz)
  rescue UnknownTimeZoneError
    return false
  end
  return true
end

#newsObject



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/yf_as_dataframe/ticker.rb', line 128

def news()
  return @news unless @news.empty?

  url = "#{BASE_URL}/v1/finance/search?q=#{@ticker}"
  data = get(url).parsed_response
  if data.include?("Will be right back")
    raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\nOur engineers are working quickly to resolve the issue. Thank you for your patience.")
  end

  @news = {}
  data['news'].each do |item|
    @news[item['title']] = item['link']
  end

  return @news
end

#option_chain(date = nil, tz = nil) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/yf_as_dataframe/ticker.rb', line 206

def option_chain(date = nil, tz = nil)
  options = if date.nil?
    download_options
  else
    download_options if @expirations.empty? || date.nil?
    raise "Expiration `#{date}` cannot be found. Available expirations are: [#{@expirations.keys.join(', ')}]" unless @expirations.key?(date)

    download_options(@expirations[date])
  end

  df = OpenStruct.new(
    calls: _options_to_df(options['calls'], tz),
    puts: _options_to_df(options['puts'], tz),
    underlying: options['underlying']
  )
end

#optionsObject Also known as: option_expiration_dates



223
224
225
226
# File 'lib/yf_as_dataframe/ticker.rb', line 223

def options
  download_options if @expirations.empty?
  @expirations.keys
end

#sharesObject



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/yf_as_dataframe/ticker.rb', line 110

def shares
  return @shares unless @shares.nil?

  full_shares = shares_full(start: Time.now.utc.to_date-548.days, fin: Time.now.utc.to_date)
  # Rails.logger.info { "#{__FILE__}:#{__LINE__} full_shares = #{full_shares.inspect}" }

  # if shares.nil?
  #     # Requesting 18 months failed, so fallback to shares which should include last year
  #     shares = @ticker.get_shares()

  # if shares.nil?
  full_shares = full_shares['Shares'] if full_shares.is_a?(Polars::DataFrame)
  # Rails.logger.info { "#{__FILE__}:#{__LINE__} full_shares = #{full_shares.inspect}" }
  @shares = full_shares[-1].to_i
  # end
  # return @shares
end

#shares_full(start: nil, fin: nil) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/yf_as_dataframe/ticker.rb', line 49

def shares_full(start: nil, fin: nil)
  # logger = Logger.new(STDOUT)

  # Rails.logger.info { "#{__FILE__}:#{__LINE__} start = #{start.inspect}, fin = #{fin.inspect}" } 

  if start
    start_ts = YfAsDataframe::Utils.parse_user_dt(start, tz)
    # Rails.logger.info { "#{__FILE__}:#{__LINE__} start_ts = #{start_ts}" }
    start = Time.at(start_ts)
    # Rails.logger.info { "#{__FILE__}:#{__LINE__} start = #{start.inspect}, fin = #{fin.inspect}" } 
  end
  if fin
    end_ts = YfAsDataframe::Utils.parse_user_dt(fin, tz)
    # Rails.logger.info { "#{__FILE__}:#{__LINE__} end_ts = #{end_ts}" }
    fin = Time.at(end_ts)
    # Rails.logger.info { "#{__FILE__}:#{__LINE__} start = #{start.inspect}, fin = #{fin.inspect}" } 
  end

  # Rails.logger.info { "#{__FILE__}:#{__LINE__} start = #{start.inspect}, fin = #{fin.inspect}" } 

  dt_now = Time.now
  fin ||= dt_now
  start ||= Time.new(fin.year, fin.month, fin.day) - 548*24*60*60

  if start >= fin
    # logger.error("Start date (#{start}) must be before end (#{fin})")
    return nil
  end

  ts_url_base = "https://query2.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/#{@ticker}?symbol=#{@ticker}"
  shares_url = "#{ts_url_base}&period1=#{Time.new(start.year, start.month, start.day).to_i}&period2=#{Time.new((fin + 86400).year, (fin + 86400).month, (fin + 86400).day).to_i}"

  begin
    json_data = get(shares_url).parsed_response
  rescue #_json.JSONDecodeError, requests.exceptions.RequestException
    # logger.error("#{@ticker}: Yahoo web request for share count failed")
    return nil
  end

  fail = json_data["finance"]["error"]["code"] == "Bad Request" rescue false
  if fail
    # logger.error("#{@ticker}: Yahoo web request for share count failed")
    return nil
  end

  shares_data = json_data["timeseries"]["result"]

  return nil if !shares_data[0].key?("shares_out")

  timestamps = shares_data[0]["timestamp"].map{|t| Time.at(t) }

  df = Polars::DataFrame.new(
    {
      'Timestamps': timestamps,
      "Shares": shares_data[0]["shares_out"]
    }
  )

  return df
end

#to_sObject



239
240
241
# File 'lib/yf_as_dataframe/ticker.rb', line 239

def to_s
  "yfinance.Ticker object <#{ticker}>"
end