Class: Yfinrb::Ticker

Inherits:
Object
  • Object
show all
Includes:
Analysis, Financials, Fundamentals, Holders, PriceHistory, Quote, YfConnection
Defined in:
lib/yfinrb/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

#get, #get_raw_json, #yfconn_initialize

Constructor Details

#initialize(ticker) ⇒ Ticker

Returns a new instance of Ticker.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/yfinrb/ticker.rb', line 17

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

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

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

  @ticker = (Yfinrb::Utils.is_isin(ticker.upcase) ? Yfinrb::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.



9
10
11
# File 'lib/yfinrb/ticker.rb', line 9

def error_message
  @error_message
end

#isinObject

Returns the value of attribute isin.



8
9
10
# File 'lib/yfinrb/ticker.rb', line 8

def isin
  @isin
end

#proxyObject

Returns the value of attribute proxy.



8
9
10
# File 'lib/yfinrb/ticker.rb', line 8

def proxy
  @proxy
end

#tickerObject (readonly) Also known as: symbol

Returns the value of attribute ticker.



9
10
11
# File 'lib/yfinrb/ticker.rb', line 9

def ticker
  @ticker
end

#timeoutObject

Returns the value of attribute timeout.



8
9
10
# File 'lib/yfinrb/ticker.rb', line 8

def timeout
  @timeout
end

#tzObject Also known as: _get_ticker_tz

Returns the value of attribute tz.



8
9
10
# File 'lib/yfinrb/ticker.rb', line 8

def tz
  @tz
end

Instance Method Details

#download_options(date = nil) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/yfinrb/ticker.rb', line 240

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



142
143
144
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
# File 'lib/yfinrb/ticker.rb', line 142

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 = Rails.logger

  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



227
228
229
230
231
232
233
234
# File 'lib/yfinrb/ticker.rb', line 227

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

#newsObject



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/yfinrb/ticker.rb', line 125

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



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/yfinrb/ticker.rb', line 203

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



220
221
222
223
# File 'lib/yfinrb/ticker.rb', line 220

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

#sharesObject



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/yfinrb/ticker.rb', line 107

def shares
  return @shares unless @shares.nil?

  full_shares = shares_full(start: DateTime.now.utc.to_date-548.days, fin: DateTime.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



46
47
48
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
# File 'lib/yfinrb/ticker.rb', line 46

def shares_full(start: nil, fin: nil)
  logger = Rails.logger

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

  if start
    start_ts = Yfinrb::Utils.parse_user_dt(start, tz)
    # Rails.logger.info { "#{__FILE__}:#{__LINE__} start_ts = #{start_ts}" }
    start = Time.at(start_ts).in_time_zone(tz)
    # Rails.logger.info { "#{__FILE__}:#{__LINE__} start = #{start.inspect}, fin = #{fin.inspect}" } 
  end
  if fin
    end_ts = Yfinrb::Utils.parse_user_dt(fin, tz)
    # Rails.logger.info { "#{__FILE__}:#{__LINE__} end_ts = #{end_ts}" }
    fin = Time.at(end_ts).in_time_zone(tz)
    # 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 = DateTime.now.in_time_zone(tz)
  fin ||= dt_now
  start ||= (fin - 548.days).midnight

  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=#{start.to_i}&period2=#{fin.tomorrow.midnight.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).to_datetime }

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

  return df
end

#to_sObject



236
237
238
# File 'lib/yfinrb/ticker.rb', line 236

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