yfinrb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,342 @@
1
+
2
+ class Yfin
3
+ module Quote
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.included(base) # built-in Ruby hook for modules
7
+ base.class_eval do
8
+ original_method = instance_method(:initialize)
9
+ define_method(:initialize) do |*args, &block|
10
+ original_method.bind(self).call(*args, &block)
11
+ initialize_quote # (your module code here)
12
+ end
13
+ end
14
+ end
15
+
16
+ def initialize_quote
17
+ @info = nil
18
+ @retired_info = nil
19
+ @sustainability = nil
20
+ @recommendations = nil
21
+ @upgrades_downgrades = nil
22
+ @calendar = nil
23
+
24
+ @already_scraped = false
25
+ @already_fetched = false
26
+ @already_fetched_complementary = false
27
+ end
28
+
29
+ def info #(self)
30
+ if @info.nil?
31
+ _fetch_info() #(@proxy)
32
+ _fetch_complementary() #(@proxy)
33
+ end
34
+ return @info
35
+ end
36
+
37
+ def sustainability
38
+ raise YFNotImplementedError.new('sustainability') if @sustainability.nil?
39
+ return @sustainability
40
+ end
41
+
42
+ def recommendations
43
+ Polars::Config.set_tbl_rows(-1)
44
+ if @recommendations.nil?
45
+
46
+ result = _fetch(['recommendationTrend']).parsed_response
47
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} result = #{result.inspect}" }
48
+ # if result.nil?
49
+ # @recommendations = Utils.empty_df() #Polars::DataFrame()
50
+ # else
51
+ begin
52
+ data = result["quoteSummary"]["result"][0]["recommendationTrend"]["trend"]
53
+ rescue KeyError, IndexError => e
54
+ raise YfinDataException(f"Failed to parse json response from Yahoo Finance: #{e.result}")
55
+ end
56
+ @recommendations = Polars::DataFrame.new(data)
57
+ # end
58
+ end
59
+ return @recommendations
60
+ end
61
+
62
+ alias_method :recommendation_summary, :recommendations
63
+ alias_method :recommendations_summary, :recommendations
64
+
65
+ def upgrades_downgrades
66
+ Polars::Config.set_tbl_rows(-1)
67
+ if @upgrades_downgrades.nil?
68
+ result = _fetch(['upgradeDowngradeHistory']).parsed_response
69
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} result = #{result.inspect}" }
70
+
71
+ # if result.nil?
72
+ # @upgrades_downgrades = Utils.empty_df() #Polars::DataFrame()
73
+ # else
74
+ begin
75
+ data = result["quoteSummary"]["result"][0]["upgradeDowngradeHistory"]["history"]
76
+
77
+ raise YfinDataException("No upgrade/downgrade history found for #{ticker.symbol}") if (data).length.zero?
78
+
79
+ df = Polars::DataFrame.new(data)
80
+ df.rename({"epochGradeDate" => "GradeDate", 'firm' => 'Firm', 'toGrade' => 'ToGrade', 'fromGrade' => 'FromGrade', 'action' => 'Action'})
81
+ # df.set_index('GradeDate', inplace=true)
82
+ # df.index = pd.to_datetime(df.index, unit='s')
83
+ @upgrades_downgrades = df
84
+ rescue KeyError, IndexError => e
85
+ raise YfinDataException("Failed to parse json response from Yahoo Finance: #{e.result}")
86
+ end
87
+ # end
88
+ end
89
+ return @upgrades_downgrades
90
+ end
91
+
92
+ def calendar
93
+ self._fetch_calendar() if @calendar.nil?
94
+ return @calendar
95
+ end
96
+
97
+ def valid_modules()
98
+ return QUOTE_SUMMARY_VALID_MODULES
99
+ end
100
+
101
+ # quote_methods = [:info, :sustainability, :recommendations, :recommendations_summary, :recommendation_summary, \
102
+ # :upgrades_downgrades, :calendar]
103
+ # quote_methods.each do |meth|
104
+ # # define_method "get_#{meth}".to_sym do
105
+ # # data = @quote.send(meth.to_sym)
106
+ # # return data
107
+ # # end
108
+ # alias_method "get_#{meth}".to_sym, meth
109
+ # end
110
+
111
+
112
+
113
+
114
+
115
+
116
+ private
117
+
118
+ def _fetch(modules) #(self, proxy, modules: list)
119
+ # raise YahooFinanceException("Should provide a list of modules, see available modules using `valid_modules`") if !modules.is_a?(Array)
120
+
121
+ modules = modules.intersection(QUOTE_SUMMARY_VALID_MODULES) #[m for m in modules if m in quote_summary_valid_modules])
122
+
123
+ raise YahooFinanceException("No valid modules provided.") if modules.empty?
124
+
125
+ params_dict = {"modules": modules.join(','), "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": symbol}
126
+
127
+ begin
128
+ result = get_raw_json(QUOTE_SUMMARY_URL + "/#{symbol}", user_agent_headers=user_agent_headers, params=params_dict)
129
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} result = #{result.inspect}" }
130
+ rescue Exception => e
131
+ Rails.logger.error("ERROR: #{e.message}")
132
+ return nil
133
+ end
134
+ return result
135
+ end
136
+
137
+ def _format(k, v)
138
+ v2 = nil
139
+ if isinstance(v, dict) && "raw".in?(v) && "fmt".in?(v)
140
+ v2 = k.in?(["regularMarketTime", "postMarketTime"]) ? v["fmt"] : v["raw"]
141
+ elsif isinstance(v, list)
142
+ v2 = v.map{|vv| _format(nil, vv)}
143
+ elsif isinstance(v, dict)
144
+ v2 = v.items().map{|k,x| _format(k,x) } #{k: _format(k, x) for k, x in v.items()}
145
+ elsif isinstance(v, str)
146
+ v2 = v.replace("\xa0", " ")
147
+ else
148
+ v2 = v
149
+ end
150
+ return v2
151
+
152
+ query1_info.items().each do |k,v|
153
+ query1_info[k] = _format(k, v)
154
+ @info = query1_info
155
+ end
156
+ end
157
+
158
+ def _fetch_complementary() #(proxy) # (self, proxy)
159
+ return if @already_fetched_complementary
160
+
161
+ # self._scrape(proxy) # decrypt broken
162
+ self._fetch_info() #(proxy)
163
+ return if @info.nil?
164
+
165
+ # Complementary key-statistics. For now just want 'trailing PEG ratio'
166
+ keys = ["trailingPegRatio"] #{"trailingPegRatio"}
167
+ if keys
168
+ # Simplified the original scrape code for key-statistics. Very expensive for fetching
169
+ # just one value, best if scraping most/all:
170
+ #
171
+ # p = _re.compile(r'root\.App\.main = (.*);')
172
+ # url = 'https://finance.yahoo.com/quote/{}/key-statistics?p={}'.format(self._ticker.ticker, self._ticker.ticker)
173
+ # try:
174
+ # r = session.get(url, headers=utils.user_agent_headers)
175
+ # data = _json.loads(p.findall(r.text)[0])
176
+ # key_stats = data['context']['dispatcher']['stores']['QuoteTimeSeriesStore']["timeSeries"]
177
+ # for k in keys:
178
+ # if k not in key_stats or len(key_stats[k])==0:
179
+ # # Yahoo website prints N/A, indicates Yahoo lacks necessary data to calculate
180
+ # v = nil
181
+ # else:
182
+ # # Select most recent (last) raw value in list:
183
+ # v = key_stats[k][-1]["reportedValue"]["raw"]
184
+ # self._info[k] = v
185
+ # except Exception:
186
+ # raise
187
+ # pass
188
+ #
189
+ # For just one/few variable is faster to query directly:
190
+ url = "https://query1.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/#{symbol}?symbol=#{symbol}"
191
+ keys.each { |k| url += "&type=" + k }
192
+
193
+ # Request 6 months of data
194
+ start = (DateTime.now.utc.midnight - 6.months).to_i #datetime.timedelta(days=365 // 2)
195
+ # start = int(start.timestamp())
196
+
197
+ ending = DateTime.now.utc.tomorrow.midnight.to_i
198
+ # ending = int(ending.timestamp())
199
+ url += "&period1=#{start}&period2=#{ending}"
200
+
201
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} url = #{url}" }
202
+ json_str = get(url).parsed_response # , proxy=proxy).parsed_response #@data.cache_get(url=url, proxy=proxy).text
203
+ json_data = json_str #json.loads(json_str)
204
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} json_data = #{json_data.inspect}" }
205
+ json_result = json_data.try(:[],"timeseries") or json_data.try(:[], "finance")
206
+ unless json_result["error"].nil?
207
+ raise YfinException("Failed to parse json response from Yahoo Finance: #{json_result["error"]}")
208
+
209
+ keys.each do |k|
210
+ keydict = json_result["result"][0]
211
+
212
+ @info[k] = k.in?(keydict) ? keydict[k][-1]["reportedValue"]["raw"] : nil
213
+ end
214
+ end
215
+ end
216
+
217
+ @already_fetched_complementary = true
218
+ end
219
+
220
+ def _fetch_calendar
221
+ begin
222
+ # secFilings return too old data, so not requesting it for now
223
+ result = self._fetch(['calendarEvents']) #(@proxy, modules=['calendarEvents'])
224
+ if result.nil?
225
+ @calendar = {}
226
+ return
227
+ end
228
+
229
+ rescue KeyError, IndexError => e
230
+ raise YfinDataException("Failed to parse json response from Yahoo Finance: #{e.result}")
231
+ rescue => e
232
+ @calendar = {} #dict()
233
+ _events = result["quoteSummary"]["result"][0]["calendarEvents"]
234
+ if 'dividendDate'.in?(_events)
235
+ @calendar['Dividend Date'] = datetime.datetime.fromtimestamp(_events['dividendDate']).date()
236
+ if 'exDividendDate'.in?(_events)
237
+ @calendar['Ex-Dividend Date'] = datetime.datetime.fromtimestamp(_events['exDividendDate']).date()
238
+ # splits = _events.get('splitDate') # need to check later, i will add code for this if found data
239
+ earnings = _events.get('earnings')
240
+ if !earnings.nil?
241
+ @calendar['Earnings Date'] = earnings.get('earningsDate', []).map{|d| d.to_date } # [datetime.datetime.fromtimestamp(d).date() for d in earnings.get('earningsDate', [])]
242
+ @calendar['Earnings High'] = earnings.get('earningsHigh', nil)
243
+ @calendar['Earnings Low'] = earnings.get('earningsLow', nil)
244
+ @calendar['Earnings Average'] = earnings.get('earningsAverage', nil)
245
+ @calendar['Revenue High'] = earnings.get('revenueHigh', nil)
246
+ @calendar['Revenue Low'] = earnings.get('revenueLow', nil)
247
+ @calendar['Revenue Average'] = earnings.get('revenueAverage', nil)
248
+ # Likely need to decipher and restore the following
249
+ # except
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ def _fetch_info()
257
+ return if @already_fetched
258
+
259
+ modules = ['financialData', 'quoteType', 'defaultKeyStatistics', 'assetProfile', 'summaryDetail']
260
+
261
+ result = _fetch(modules)
262
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} result = #{result.inspect}" }
263
+ if result.parsed_response.nil?
264
+ @info = {}
265
+ return
266
+ end
267
+
268
+ result["quoteSummary"]["result"][0]["symbol"] = symbol
269
+
270
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} result[quoteSummary][result] = #{result["quoteSummary"]["result"].inspect}" }
271
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} result[quoteSummary][result].first.keys = #{result["quoteSummary"]["result"].first.keys.inspect}" }
272
+ query1_info = result["quoteSummary"]["result"].first
273
+
274
+ # Likely need to decipher and restore the following
275
+ # query1_info = next(
276
+ # (info for info in result.get("quoteSummary", {}).get("result", []) if info["symbol"] == @symbol),
277
+ # nil,
278
+ # )
279
+
280
+ # Most keys that appear in multiple dicts have same value. Except 'maxAge' because
281
+ # Yahoo not consistent with days vs seconds. Fix it here:
282
+
283
+ query1_info.keys.each {|k|
284
+ query1_info[k]["maxAge"] = 86400 if "maxAge".in?(query1_info[k]) && query1_info[k]["maxAge"] == 1
285
+ }
286
+
287
+ # Likely need to decipher and restore the following
288
+ # query1_info = {
289
+ # k1: v1
290
+ # for k, v in query1_info.items()
291
+ # if isinstance(v, dict)
292
+ # for k1, v1 in v.items()
293
+ # if v1
294
+ # }
295
+ # # recursively format but only because of 'companyOfficers'
296
+
297
+ @info = query1_info
298
+ @already_fetched = true
299
+ end
300
+
301
+
302
+ BASE_URL = 'https://query2.finance.yahoo.com'
303
+ QUOTE_SUMMARY_URL = "#{BASE_URL}/v10/finance/quoteSummary"
304
+
305
+ QUOTE_SUMMARY_VALID_MODULES = [
306
+ "summaryProfile", # contains general information about the company
307
+ "summaryDetail", # prices + volume + market cap + etc
308
+ "assetProfile", # summaryProfile + company officers
309
+ "fundProfile",
310
+ "price", # current prices
311
+ "quoteType", # quoteType
312
+ "esgScores", # Environmental, social, and governance (ESG) scores, sustainability and ethical performance of companies
313
+ "incomeStatementHistory",
314
+ "incomeStatementHistoryQuarterly",
315
+ "balanceSheetHistory",
316
+ "balanceSheetHistoryQuarterly",
317
+ "cashFlowStatementHistory",
318
+ "cashFlowStatementHistoryQuarterly",
319
+ "defaultKeyStatistics", # KPIs (PE, enterprise value, EPS, EBITA, and more)
320
+ "financialData", # Financial KPIs (revenue, gross margins, operating cash flow, free cash flow, and more)
321
+ "calendarEvents", # future earnings date
322
+ "secFilings", # SEC filings, such as 10K and 10Q reports
323
+ "upgradeDowngradeHistory", # upgrades and downgrades that analysts have given a company's stock
324
+ "institutionOwnership", # institutional ownership, holders and shares outstanding
325
+ "fundOwnership", # mutual fund ownership, holders and shares outstanding
326
+ "majorDirectHolders",
327
+ "majorHoldersBreakdown",
328
+ "insiderTransactions", # insider transactions, such as the number of shares bought and sold by company executives
329
+ "insiderHolders", # insider holders, such as the number of shares held by company executives
330
+ "netSharePurchaseActivity", # net share purchase activity, such as the number of shares bought and sold by company executives
331
+ "earnings", # earnings history
332
+ "earningsHistory",
333
+ "earningsTrend", # earnings trend
334
+ "industryTrend",
335
+ "indexTrend",
336
+ "sectorTrend",
337
+ "recommendationTrend",
338
+ "futuresChain",
339
+ ].freeze
340
+
341
+ end
342
+ end