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.
@@ -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 # Yfin.get_yf_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 || DateTime.now
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
- Rails.logger.error("#{@ticker}: #{err_msg}")
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 = (DateTime.now - (99.years)).to_i
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 = DateTime.now.to_i
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
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} url = #{url}" }
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 = DateTime.now #.new_offset(0)
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
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} url = #{url}, params = #{params.inspect}" }
634
+ logger.info { "#{__FILE__}:#{__LINE__} url = #{url}, params = #{params.inspect}" }
634
635
  data = get(url, nil, params).parsed_response
635
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} data = #{data.inspect}" }
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.text.include?("Will be right back") || data.nil?
646
+ ) if (data.is_a?(String) && data.include?("Will be right back")) || data.nil?
641
647
 
642
- data = HashWithIndifferentAccess.new(data)
643
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} data = #{data.inspect}" }
644
- rescue Exception
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.present? ? fin.to_date : Time.at(DateTime.now.tomorrow).to_i
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 = DateTime.now
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
@@ -3,7 +3,6 @@ require 'tulirb'
3
3
  class YfAsDataframe
4
4
  module PriceTechnical
5
5
  extend ActiveSupport::Concern
6
- include ActionView::Helpers::NumberHelper
7
6
 
8
7
 
9
8
  def ad(df)
@@ -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
- Rails.logger.error("ERROR: #{e.message}")
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 = (DateTime.now.utc.midnight - 6.months).to_i #datetime.timedelta(days=365 // 2)
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 = DateTime.now.utc.tomorrow.midnight.to_i
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 = Rails.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 = DateTime.now.in_time_zone(tz)
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: DateTime.now.utc.to_date-548.days, fin: DateTime.now.utc.to_date)
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 = Rails.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.month
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.months
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.months
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.months
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.months
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.year
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.year
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.year
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.year
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.year
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.year
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
- 1.week
329
+ 7.days
287
330
  when '2wk'
288
- 2.week
331
+ 14.days
289
332
  when '3wk'
290
- 3.week
333
+ 21.days
291
334
  when '4wk'
292
- 4.week
335
+ 28.days
293
336
  else
294
- Rails.logger.warn { "#{__FILE__}:#{__LINE__} #{interval} not a recognized interval" }
337
+ Logger.new(STDOUT).warn { "#{__FILE__}:#{__LINE__} #{interval} not a recognized interval" }
295
338
  interval
296
339
  end
297
340
  end
@@ -1,3 +1,3 @@
1
1
  class YfAsDataframe
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end