yf_as_dataframe 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +99 -0
- data/MINIMAL_INTEGRATION.md +227 -0
- data/README.md +65 -0
- data/lib/yf_as_dataframe/curl_impersonate_integration.rb +110 -0
- data/lib/yf_as_dataframe/financials.rb +3 -2
- data/lib/yf_as_dataframe/holders.rb +4 -2
- data/lib/yf_as_dataframe/multi.rb +2 -1
- data/lib/yf_as_dataframe/price_history.rb +46 -16
- data/lib/yf_as_dataframe/price_technical.rb +0 -1
- data/lib/yf_as_dataframe/quote.rb +4 -3
- data/lib/yf_as_dataframe/ticker.rb +7 -4
- data/lib/yf_as_dataframe/utils.rb +59 -16
- data/lib/yf_as_dataframe/version.rb +1 -1
- data/lib/yf_as_dataframe/yf_connection.rb +295 -49
- data/lib/yf_as_dataframe/yf_connection_minimal_patch.rb +97 -0
- data/lib/yf_as_dataframe/yfinance_exception.rb +3 -1
- data/lib/yf_as_dataframe.rb +2 -0
- data/quick_test.rb +143 -0
- data/test_minimal_integration.rb +121 -0
- metadata +53 -5
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'polars'
|
2
2
|
require 'polars-df'
|
3
|
+
require 'logger'
|
3
4
|
|
4
5
|
class YfAsDataframe
|
5
6
|
module PriceHistory
|
6
7
|
extend ActiveSupport::Concern
|
7
|
-
include ActionView::Helpers::NumberHelper
|
8
8
|
|
9
9
|
PRICE_COLNAMES = ['Open', 'High', 'Low', 'Close', 'Adj Close']
|
10
10
|
BASE_URL = 'https://query2.finance.yahoo.com'
|
@@ -35,10 +35,10 @@ class YfAsDataframe
|
|
35
35
|
def history(period: "1mo", interval: "1d", start: nil, fin: nil, prepost: false,
|
36
36
|
actions: true, auto_adjust: true, back_adjust: false, repair: false, keepna: false,
|
37
37
|
rounding: false, raise_errors: false, returns: false)
|
38
|
-
logger = Rails.logger
|
38
|
+
logger = Logger.new(STDOUT) # Replace Rails.logger with standard Ruby logger
|
39
39
|
start_user = start
|
40
40
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} here" }
|
41
|
-
end_user = fin ||
|
41
|
+
end_user = fin || Time.now
|
42
42
|
|
43
43
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} here" }
|
44
44
|
params = _preprocess_params(start, fin, interval, period, prepost, raise_errors)
|
@@ -572,7 +572,7 @@ class YfAsDataframe
|
|
572
572
|
if raise_errors
|
573
573
|
raise Exception.new("#{@ticker}: #{err_msg}")
|
574
574
|
else
|
575
|
-
|
575
|
+
Logger.new(STDOUT).error("#{@ticker}: #{err_msg}")
|
576
576
|
end
|
577
577
|
return YfAsDataframe::Utils.empty_df
|
578
578
|
end
|
@@ -585,7 +585,7 @@ class YfAsDataframe
|
|
585
585
|
if interval == "1m"
|
586
586
|
start = (fin - 1.week).to_i
|
587
587
|
else
|
588
|
-
max_start_datetime = (
|
588
|
+
max_start_datetime = (Time.now - (99.years)).to_i
|
589
589
|
start = max_start_datetime.to_i
|
590
590
|
end
|
591
591
|
else
|
@@ -598,7 +598,7 @@ class YfAsDataframe
|
|
598
598
|
else
|
599
599
|
period = period.downcase
|
600
600
|
# params = { "range" => period }
|
601
|
-
fin =
|
601
|
+
fin = Time.now.to_i
|
602
602
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} here fin= #{fin}, period = #{period}" }
|
603
603
|
start = (fin - YfAsDataframe::Utils.interval_to_timedelta(period)).to_i
|
604
604
|
params = { "period1" => start, "period2" => fin }
|
@@ -617,31 +617,40 @@ class YfAsDataframe
|
|
617
617
|
def _get_data(ticker, params, fin, raise_errors)
|
618
618
|
url = "https://query2.finance.yahoo.com/v8/finance/chart/#{CGI.escape ticker}"
|
619
619
|
# url = "https://query1.finance.yahoo.com/v7/finance/download/#{ticker}" ... Deprecated
|
620
|
-
|
620
|
+
logger = Logger.new(STDOUT)
|
621
|
+
logger.info { "#{__FILE__}:#{__LINE__} url = #{url}" }
|
621
622
|
data = nil
|
622
623
|
# get_fn = @data.method(:get)
|
623
624
|
|
624
625
|
if fin
|
625
626
|
end_dt = DateTime.strptime(fin.to_s, '%s') #.new_offset(0)
|
626
|
-
dt_now =
|
627
|
+
dt_now = Time.now #.new_offset(0)
|
627
628
|
data_delay = Rational(30, 24 * 60)
|
628
629
|
|
629
630
|
# get_fn = @data.method(:cache_get) if end_dt + data_delay <= dt_now
|
630
631
|
end
|
631
632
|
|
632
633
|
begin
|
633
|
-
|
634
|
+
logger.info { "#{__FILE__}:#{__LINE__} url = #{url}, params = #{params.inspect}" }
|
634
635
|
data = get(url, nil, params).parsed_response
|
635
|
-
|
636
|
+
logger.info { "#{__FILE__}:#{__LINE__} data = #{data.inspect}" }
|
637
|
+
|
638
|
+
# Validate response before processing
|
639
|
+
unless validate_yahoo_response(data)
|
640
|
+
raise RuntimeError.new("Invalid response from Yahoo Finance: #{data.inspect}")
|
641
|
+
end
|
636
642
|
|
637
643
|
raise RuntimeError.new(
|
638
644
|
"*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n" +
|
639
645
|
"Our engineers are working quickly to resolve the issue. Thank you for your patience."
|
640
|
-
) if data.
|
646
|
+
) if (data.is_a?(String) && data.include?("Will be right back")) || data.nil?
|
641
647
|
|
642
|
-
|
643
|
-
|
644
|
-
|
648
|
+
# Use standard Ruby Hash
|
649
|
+
data = data.is_a?(Hash) ? data : JSON.parse(data.to_s) rescue data
|
650
|
+
logger.info { "#{__FILE__}:#{__LINE__} data = #{data.inspect}" }
|
651
|
+
rescue Exception => e
|
652
|
+
logger.error { "#{__FILE__}:#{__LINE__} Exception caught: #{e.message}" }
|
653
|
+
logger.error { "#{__FILE__}:#{__LINE__} Exception backtrace: #{e.backtrace.first(5).join("\n")}" }
|
645
654
|
raise if raise_errors
|
646
655
|
end
|
647
656
|
|
@@ -710,7 +719,7 @@ class YfAsDataframe
|
|
710
719
|
# startDt = quotes.index[0].floor('D')
|
711
720
|
startDt = quotes['Timestamps'].to_a.map(&:to_date).min
|
712
721
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} startDt = #{startDt.inspect}" }
|
713
|
-
endDt = fin.
|
722
|
+
endDt = !fin.nil? && !(fin.respond_to?(:empty?) && fin.empty?) ? fin.to_date : Time.at((Time.now + 1.day).to_i).to_i
|
714
723
|
|
715
724
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} @history[events][dividends] = #{@history['events']["dividends"].inspect}" }
|
716
725
|
# divi = {}
|
@@ -2019,7 +2028,7 @@ class YfAsDataframe
|
|
2019
2028
|
end
|
2020
2029
|
|
2021
2030
|
def _exchange_open_now
|
2022
|
-
t =
|
2031
|
+
t = Time.now
|
2023
2032
|
_get_exchange_metadata
|
2024
2033
|
|
2025
2034
|
# if self._today_open is nil and self._today_close.nil?
|
@@ -2041,5 +2050,26 @@ class YfAsDataframe
|
|
2041
2050
|
# print("_exchange_open_now returning", r)
|
2042
2051
|
# return r
|
2043
2052
|
end
|
2053
|
+
|
2054
|
+
private
|
2055
|
+
|
2056
|
+
def validate_yahoo_response(response)
|
2057
|
+
return false if response.nil?
|
2058
|
+
return false if response.is_a?(String) && response.include?("Too Many Requests")
|
2059
|
+
return false if response.is_a?(String) && response.include?("Will be right back")
|
2060
|
+
return false if response.is_a?(String) && response.include?("<html>")
|
2061
|
+
return false if response.is_a?(String) && response.strip.empty?
|
2062
|
+
|
2063
|
+
# If it's a Hash, validate it has the expected structure
|
2064
|
+
if response.is_a?(Hash)
|
2065
|
+
return false unless response.key?("chart")
|
2066
|
+
return false unless response["chart"].is_a?(Hash)
|
2067
|
+
return false unless response["chart"].key?("result")
|
2068
|
+
return false unless response["chart"]["result"].is_a?(Array)
|
2069
|
+
return false if response["chart"]["result"].empty?
|
2070
|
+
end
|
2071
|
+
|
2072
|
+
true
|
2073
|
+
end
|
2044
2074
|
end
|
2045
2075
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'httparty'
|
2
|
+
require 'logger'
|
2
3
|
|
3
4
|
class YfAsDataframe
|
4
5
|
module Quote
|
@@ -129,7 +130,7 @@ class YfAsDataframe
|
|
129
130
|
result = get_raw_json(QUOTE_SUMMARY_URL + "/#{symbol}", user_agent_headers=user_agent_headers, params=params_dict)
|
130
131
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} result = #{result.inspect}" }
|
131
132
|
rescue Exception => e
|
132
|
-
|
133
|
+
Logger.new(STDOUT).error("ERROR: #{e.message}")
|
133
134
|
return nil
|
134
135
|
end
|
135
136
|
return result
|
@@ -192,10 +193,10 @@ class YfAsDataframe
|
|
192
193
|
keys.each { |k| url += "&type=" + k }
|
193
194
|
|
194
195
|
# Request 6 months of data
|
195
|
-
start = (
|
196
|
+
start = (Time.now.utc.midnight - 6.months).to_i #datetime.timedelta(days=365 // 2)
|
196
197
|
# start = int(start.timestamp())
|
197
198
|
|
198
|
-
ending =
|
199
|
+
ending = Time.now.utc.tomorrow.midnight.to_i
|
199
200
|
# ending = int(ending.timestamp())
|
200
201
|
url += "&period1=#{start}&period2=#{ending}"
|
201
202
|
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'tzinfo'
|
2
|
+
require 'logger'
|
3
|
+
|
1
4
|
class YfAsDataframe
|
2
5
|
class Ticker
|
3
6
|
ROOT_URL = 'https://finance.yahoo.com'.freeze
|
@@ -44,7 +47,7 @@ class YfAsDataframe
|
|
44
47
|
def symbol; @ticker; end
|
45
48
|
|
46
49
|
def shares_full(start: nil, fin: nil)
|
47
|
-
logger =
|
50
|
+
logger = Logger.new(STDOUT)
|
48
51
|
|
49
52
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} start = #{start.inspect}, fin = #{fin.inspect}" }
|
50
53
|
|
@@ -63,7 +66,7 @@ class YfAsDataframe
|
|
63
66
|
|
64
67
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} start = #{start.inspect}, fin = #{fin.inspect}" }
|
65
68
|
|
66
|
-
dt_now =
|
69
|
+
dt_now = Time.now.in_time_zone(tz)
|
67
70
|
fin ||= dt_now
|
68
71
|
start ||= (fin - 548.days).midnight
|
69
72
|
|
@@ -107,7 +110,7 @@ class YfAsDataframe
|
|
107
110
|
def shares
|
108
111
|
return @shares unless @shares.nil?
|
109
112
|
|
110
|
-
full_shares = shares_full(start:
|
113
|
+
full_shares = shares_full(start: Time.now.utc.to_date-548.days, fin: Time.now.utc.to_date)
|
111
114
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} full_shares = #{full_shares.inspect}" }
|
112
115
|
|
113
116
|
# if shares.nil?
|
@@ -150,7 +153,7 @@ class YfAsDataframe
|
|
150
153
|
# """
|
151
154
|
return @earnings_dates[limit] if @earnings_dates && @earnings_dates[limit]
|
152
155
|
|
153
|
-
logger =
|
156
|
+
logger = Logger.new(STDOUT)
|
154
157
|
|
155
158
|
page_size = [limit, 100].min # YF caps at 100, don't go higher
|
156
159
|
page_offset = 0
|
@@ -261,37 +261,80 @@ class YfAsDataframe
|
|
261
261
|
def self.interval_to_timedelta(interval)
|
262
262
|
case interval
|
263
263
|
when '1mo'
|
264
|
-
1
|
264
|
+
# Calculate 1 month from now to get accurate days
|
265
|
+
now = Time.now
|
266
|
+
next_month = Time.new(now.year, now.month + 1, now.day)
|
267
|
+
# Handle year rollover
|
268
|
+
next_month = Time.new(now.year + 1, 1, now.day) if next_month.month == 1
|
269
|
+
(next_month - now).to_i
|
265
270
|
when '2mo'
|
266
|
-
2
|
271
|
+
# Calculate 2 months from now
|
272
|
+
now = Time.now
|
273
|
+
next_month = Time.new(now.year, now.month + 2, now.day)
|
274
|
+
# Handle year rollover
|
275
|
+
next_month = Time.new(now.year + 1, next_month.month, now.day) if next_month.month <= 2
|
276
|
+
(next_month - now).to_i
|
267
277
|
when '3mo'
|
268
|
-
3
|
278
|
+
# Calculate 3 months from now
|
279
|
+
now = Time.now
|
280
|
+
next_month = Time.new(now.year, now.month + 3, now.day)
|
281
|
+
# Handle year rollover
|
282
|
+
next_month = Time.new(now.year + 1, next_month.month, now.day) if next_month.month <= 3
|
283
|
+
(next_month - now).to_i
|
269
284
|
when '6mo'
|
270
|
-
6
|
285
|
+
# Calculate 6 months from now
|
286
|
+
now = Time.now
|
287
|
+
next_month = Time.new(now.year, now.month + 6, now.day)
|
288
|
+
# Handle year rollover
|
289
|
+
next_month = Time.new(now.year + 1, next_month.month, now.day) if next_month.month <= 6
|
290
|
+
(next_month - now).to_i
|
271
291
|
when '9mo'
|
272
|
-
9
|
292
|
+
# Calculate 9 months from now
|
293
|
+
now = Time.now
|
294
|
+
next_month = Time.new(now.year, now.month + 9, now.day)
|
295
|
+
# Handle year rollover
|
296
|
+
next_month = Time.new(now.year + 1, next_month.month, now.day) if next_month.month <= 9
|
297
|
+
(next_month - now).to_i
|
273
298
|
when '12mo'
|
274
|
-
1
|
299
|
+
# Calculate 12 months (1 year) from now
|
300
|
+
now = Time.now
|
301
|
+
next_year = Time.new(now.year + 1, now.month, now.day)
|
302
|
+
(next_year - now).to_i
|
275
303
|
when '1y'
|
276
|
-
1
|
304
|
+
# Calculate 1 year from now
|
305
|
+
now = Time.now
|
306
|
+
next_year = Time.new(now.year + 1, now.month, now.day)
|
307
|
+
(next_year - now).to_i
|
277
308
|
when '2y'
|
278
|
-
2
|
309
|
+
# Calculate 2 years from now
|
310
|
+
now = Time.now
|
311
|
+
next_year = Time.new(now.year + 2, now.month, now.day)
|
312
|
+
(next_year - now).to_i
|
279
313
|
when '3y'
|
280
|
-
3
|
314
|
+
# Calculate 3 years from now
|
315
|
+
now = Time.now
|
316
|
+
next_year = Time.new(now.year + 3, now.month, now.day)
|
317
|
+
(next_year - now).to_i
|
281
318
|
when '4y'
|
282
|
-
4
|
319
|
+
# Calculate 4 years from now
|
320
|
+
now = Time.now
|
321
|
+
next_year = Time.new(now.year + 4, now.month, now.day)
|
322
|
+
(next_year - now).to_i
|
283
323
|
when '5y'
|
284
|
-
5
|
324
|
+
# Calculate 5 years from now
|
325
|
+
now = Time.now
|
326
|
+
next_year = Time.new(now.year + 5, now.month, now.day)
|
327
|
+
(next_year - now).to_i
|
285
328
|
when '1wk'
|
286
|
-
|
329
|
+
7.days
|
287
330
|
when '2wk'
|
288
|
-
|
331
|
+
14.days
|
289
332
|
when '3wk'
|
290
|
-
|
333
|
+
21.days
|
291
334
|
when '4wk'
|
292
|
-
|
335
|
+
28.days
|
293
336
|
else
|
294
|
-
|
337
|
+
Logger.new(STDOUT).warn { "#{__FILE__}:#{__LINE__} #{interval} not a recognized interval" }
|
295
338
|
interval
|
296
339
|
end
|
297
340
|
end
|