yf_as_dataframe 0.3.1 → 0.4.1

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,3 +1,5 @@
1
+ require 'active_support'
2
+ require 'active_support/concern'
1
3
  # require 'requests'
2
4
  # require 'requests_cache'
3
5
  require 'thread'
@@ -5,10 +7,12 @@ require 'date'
5
7
  require 'nokogiri'
6
8
  require 'zache'
7
9
  require 'httparty'
10
+ require 'uri'
11
+ require 'json'
8
12
 
9
13
  class YfAsDataframe
10
14
  module YfConnection
11
- extend ActiveSupport::Concern
15
+ extend ::ActiveSupport::Concern
12
16
  # extend HTTParty
13
17
 
14
18
  # """
@@ -94,18 +98,50 @@ class YfAsDataframe
94
98
  @@cookie = nil
95
99
  @@cookie_strategy = 'basic'
96
100
  @@cookie_lock = ::Mutex.new()
101
+
102
+ # Add session tracking
103
+ @@session_created_at = Time.now
104
+ @@session_refresh_interval = 3600 # 1 hour
105
+ @@request_count = 0
106
+ @@last_request_time = nil
107
+
108
+ # Circuit breaker state
109
+ @@circuit_breaker_state = :closed # :closed, :open, :half_open
110
+ @@failure_count = 0
111
+ @@last_failure_time = nil
112
+ @@circuit_breaker_threshold = 3
113
+ @@circuit_breaker_timeout = 60 # seconds
114
+ @@circuit_breaker_base_timeout = 60 # seconds
97
115
  end
98
116
 
99
117
 
100
118
  def get(url, headers=nil, params=nil)
101
- # Important: treat input arguments as immutable.
102
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} url = #{url}, headers = #{headers}, params=#{params.inspect}" }
119
+ # Check circuit breaker first
120
+ unless circuit_breaker_allow_request?
121
+ raise RuntimeError.new("Circuit breaker is open - too many recent failures. Please try again later.")
122
+ end
123
+
124
+ # Add request throttling to be respectful of rate limits
125
+ throttle_request
126
+
127
+ # Track session usage
128
+ track_session_usage
129
+
130
+ # Refresh session if needed
131
+ refresh_session_if_needed
132
+
133
+ # Only fetch crumb for /v7/finance/download endpoint
134
+ crumb_needed = url.include?('/v7/finance/download')
103
135
 
104
136
  headers ||= {}
105
137
  params ||= {}
106
- params.merge!(crumb: @@crumb) unless @@crumb.nil?
107
- cookie, crumb, strategy = _get_cookie_and_crumb()
108
- crumbs = !crumb.nil? ? {'crumb' => crumb} : {}
138
+ # params.merge!(crumb: @@crumb) unless @@crumb.nil? # Commented out: crumb not needed for most endpoints
139
+ if crumb_needed
140
+ crumb = get_crumb_scrape_quote_page(params[:symbol] || params['symbol'])
141
+ params.merge!(crumb: crumb) unless crumb.nil?
142
+ end
143
+ cookie, _, strategy = _get_cookie_and_crumb(crumb_needed)
144
+ crumbs = {} # crumb logic handled above if needed
109
145
 
110
146
  request_args = {
111
147
  url: url,
@@ -118,17 +154,24 @@ class YfAsDataframe
118
154
 
119
155
  cookie_hash = ::HTTParty::CookieHash.new
120
156
  cookie_hash.add_cookies(@@cookie)
121
- options = { headers: headers.dup.merge(@@user_agent_headers).merge({ 'cookie' => cookie_hash.to_cookie_string, 'crumb' => crumb })} #, debug_output: STDOUT }
157
+ options = { headers: headers.dup.merge(@@user_agent_headers).merge({ 'cookie' => cookie_hash.to_cookie_string })} #, debug_output: STDOUT }
122
158
 
123
159
  u = (request_args[:url]).dup.to_s
124
- joiner = ('?'.in?(request_args[:url]) ? '&' : '?')
125
- u += (joiner + CGI.unescape(request_args[:params].to_query)) unless request_args[:params].empty?
126
-
127
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} u=#{u}, options = #{options.inspect}" }
128
- response = ::HTTParty.get(u, options)
129
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} response=#{response.inspect}" }
160
+ joiner = (request_args[:url].include?('?') ? '&' : '?')
161
+ u += (joiner + URI.encode_www_form(request_args[:params])) unless request_args[:params].empty?
130
162
 
131
- return response
163
+ begin
164
+ response = ::HTTParty.get(u, options)
165
+ if response_failure?(response)
166
+ circuit_breaker_record_failure
167
+ raise RuntimeError.new("Yahoo Finance request failed: #{response.code} - #{response.body}")
168
+ end
169
+ circuit_breaker_record_success
170
+ return response
171
+ rescue => e
172
+ circuit_breaker_record_failure
173
+ raise e
174
+ end
132
175
  end
133
176
 
134
177
  alias_method :cache_get, :get
@@ -173,33 +216,17 @@ class YfAsDataframe
173
216
  end
174
217
  end
175
218
 
176
- def _get_cookie_and_crumb()
219
+ def _get_cookie_and_crumb(crumb_needed=false)
177
220
  cookie, crumb, strategy = nil, nil, nil
178
- # puts "cookie_mode = '#{@@cookie_strategy}'"
179
-
180
221
  @@cookie_lock.synchronize do
181
- if @@cookie_strategy == 'csrf'
182
- crumb = _get_crumb_csrf()
183
- if crumb.nil?
184
- # Fail
185
- _set_cookie_strategy('basic', have_lock=true)
186
- cookie, crumb = __get_cookie_and_crumb_basic()
187
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} cookie = #{cookie}, crumb = #{crumb}" }
188
- end
189
- else
190
- # Fallback strategy
222
+ if crumb_needed
191
223
  cookie, crumb = __get_cookie_and_crumb_basic()
192
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} cookie = #{cookie}, crumb = #{crumb}" }
193
- if cookie.nil? || crumb.nil?
194
- # Fail
195
- _set_cookie_strategy('csrf', have_lock=true)
196
- crumb = _get_crumb_csrf()
197
- end
224
+ else
225
+ cookie = _get_cookie_basic()
226
+ crumb = nil
198
227
  end
199
228
  strategy = @@cookie_strategy
200
229
  end
201
-
202
- # Rails.logger.info { "#{__FILE__}:#{__LINE__} cookie = #{cookie}, crumb = #{crumb}, strategy=#{strategy}" }
203
230
  return cookie, crumb, strategy
204
231
  end
205
232
 
@@ -229,18 +256,58 @@ class YfAsDataframe
229
256
 
230
257
  def _get_crumb_basic()
231
258
  return @@crumb unless @@crumb.nil?
232
- return nil if (cookie = _get_cookie_basic()).nil?
233
-
234
- cookie_hash = ::HTTParty::CookieHash.new
235
- cookie_hash.add_cookies(cookie)
236
- options = {headers: @@user_agent_headers.dup.merge(
237
- { 'cookie' => cookie_hash.to_cookie_string }
238
- )} #, debug_output: STDOUT }
239
-
240
- crumb_response = ::HTTParty.get('https://query1.finance.yahoo.com/v1/test/getcrumb', options)
241
- @@crumb = crumb_response.parsed_response
259
+
260
+ # Retry logic similar to yfinance: try up to 3 times
261
+ 3.times do |attempt|
262
+ begin
263
+ # Clear cookie on retry (except first attempt) to get fresh session
264
+ if attempt > 0
265
+ @@cookie = nil
266
+ # Clear curl-impersonate executables cache to force re-selection
267
+ CurlImpersonateIntegration.instance_variable_set(:@available_executables, nil)
268
+ # warn "[yf_as_dataframe] Retrying crumb fetch (attempt #{attempt + 1}/3)"
269
+ # Add delay between retries to be respectful of rate limits
270
+ sleep(2 ** attempt) # Exponential backoff: 2s, 4s, 8s
271
+ end
272
+
273
+ return nil if (cookie = _get_cookie_basic()).nil?
274
+
275
+ cookie_hash = ::HTTParty::CookieHash.new
276
+ cookie_hash.add_cookies(cookie)
277
+ options = {headers: @@user_agent_headers.dup.merge(
278
+ { 'cookie' => cookie_hash.to_cookie_string }
279
+ )}
280
+
281
+ crumb_response = ::HTTParty.get('https://query1.finance.yahoo.com/v1/test/getcrumb', options)
282
+ @@crumb = crumb_response.parsed_response
283
+
284
+ # Validate crumb: must be short, alphanumeric, no spaces, not an error message
285
+ if crumb_valid?(@@crumb)
286
+ # warn "[yf_as_dataframe] Successfully fetched valid crumb on attempt #{attempt + 1}"
287
+ return @@crumb
288
+ else
289
+ # warn "[yf_as_dataframe] Invalid crumb received on attempt #{attempt + 1}: '#{@@crumb.inspect}'"
290
+ @@crumb = nil
291
+ end
292
+ rescue => e
293
+ # warn "[yf_as_dataframe] Error fetching crumb on attempt #{attempt + 1}: #{e.message}"
294
+ @@crumb = nil
295
+ end
296
+ end
297
+
298
+ # All attempts failed
299
+ # warn "[yf_as_dataframe] Failed to fetch valid crumb after 3 attempts"
300
+ raise "Could not fetch a valid Yahoo Finance crumb after 3 attempts"
301
+ end
242
302
 
243
- return (@@crumb.nil? || '<html>'.in?(@@crumb)) ? nil : @@crumb
303
+ def crumb_valid?(crumb)
304
+ return false if crumb.nil?
305
+ return false if crumb.include?('<html>')
306
+ return false if crumb.include?('Too Many Requests')
307
+ return false if crumb.strip.empty?
308
+ return false if crumb.length < 8 || crumb.length > 20
309
+ return false if crumb =~ /\s/
310
+ true
244
311
  end
245
312
 
246
313
  def _get_cookie_csrf()
@@ -310,7 +377,8 @@ class YfAsDataframe
310
377
  # puts 'reusing crumb'
311
378
  return @@crumb unless @@crumb.nil?
312
379
  # This cookie stored in session
313
- return nil unless _get_cookie_csrf().present?
380
+ cookie_csrf = _get_cookie_csrf()
381
+ return nil if cookie_csrf.nil? || (cookie_csrf.respond_to?(:empty?) && cookie_csrf.empty?)
314
382
 
315
383
  get_args = {
316
384
  url: 'https://query2.finance.yahoo.com/v1/test/getcrumb',
@@ -323,7 +391,7 @@ class YfAsDataframe
323
391
  @@crumb = r.text
324
392
 
325
393
  # puts "Didn't receive crumb"
326
- return nil if @@crumb.nil? || '<html>'.in?(@@crumb) || @@crumb.length.zero?
394
+ return nil if @@crumb.nil? || @@crumb.include?('<html>') || @@crumb.length.zero?
327
395
  return @@crumb
328
396
  end
329
397
 
@@ -358,5 +426,124 @@ class YfAsDataframe
358
426
  @@zache.put(:basic, nil, lifetime: 1) unless @@zache.exists?(:basic, dirty: false)
359
427
  return @@zache.expired?(:basic) ? nil : @@zache.get(:basic)
360
428
  end
429
+
430
+ def throttle_request
431
+ # Random delay between 0.1 and 0.5 seconds to be respectful of rate limits
432
+ # Similar to yfinance's approach
433
+ sleep(rand(0.1..0.5))
434
+ end
435
+
436
+ def track_session_usage
437
+ @@request_count += 1
438
+ @@last_request_time = Time.now
439
+ end
440
+
441
+ def refresh_session_if_needed
442
+ return unless session_needs_refresh?
443
+
444
+ # warn "[yf_as_dataframe] Refreshing session (age: #{session_age} seconds, requests: #{@@request_count})"
445
+ refresh_session
446
+ end
447
+
448
+ def session_needs_refresh?
449
+ return true if session_age > @@session_refresh_interval
450
+ return true if @@request_count > 100 # Refresh after 100 requests
451
+ return true if @@cookie.nil? || @@crumb.nil?
452
+ false
453
+ end
454
+
455
+ def session_age
456
+ Time.now - @@session_created_at
457
+ end
458
+
459
+ def refresh_session
460
+ @@cookie = nil
461
+ @@crumb = nil
462
+ @@session_created_at = Time.now
463
+ @@request_count = 0
464
+ # warn "[yf_as_dataframe] Session refreshed"
465
+ end
466
+
467
+ # Circuit breaker methods
468
+ def circuit_breaker_allow_request?
469
+ case @@circuit_breaker_state
470
+ when :closed
471
+ true
472
+ when :open
473
+ if Time.now - @@last_failure_time > @@circuit_breaker_timeout
474
+ @@circuit_breaker_state = :half_open
475
+ # warn "[yf_as_dataframe] Circuit breaker transitioning to half-open"
476
+ true
477
+ else
478
+ false
479
+ end
480
+ when :half_open
481
+ true
482
+ end
483
+ end
484
+
485
+ def circuit_breaker_record_failure
486
+ @@failure_count += 1
487
+ @@last_failure_time = Time.now
488
+
489
+ if @@failure_count >= @@circuit_breaker_threshold && @@circuit_breaker_state != :open
490
+ @@circuit_breaker_state = :open
491
+ # Exponential backoff: 60s, 120s, 240s, 480s, etc.
492
+ @@circuit_breaker_timeout = @@circuit_breaker_base_timeout * (2 ** (@@failure_count - @@circuit_breaker_threshold))
493
+ # warn "[yf_as_dataframe] Circuit breaker opened after #{@@failure_count} failures (timeout: #{@@circuit_breaker_timeout}s)"
494
+ end
495
+ end
496
+
497
+ def circuit_breaker_record_success
498
+ if @@circuit_breaker_state == :half_open
499
+ @@circuit_breaker_state = :closed
500
+ @@failure_count = 0
501
+ @@circuit_breaker_timeout = @@circuit_breaker_base_timeout
502
+ # warn "[yf_as_dataframe] Circuit breaker closed after successful request"
503
+ elsif @@circuit_breaker_state == :closed
504
+ # Reset failure count on success
505
+ @@failure_count = 0
506
+ @@circuit_breaker_timeout = @@circuit_breaker_base_timeout
507
+ end
508
+ end
509
+
510
+ def response_failure?(response)
511
+ return true if response.nil?
512
+ return true if response.code >= 400
513
+ return true if response.body.to_s.include?("Too Many Requests")
514
+ return true if response.body.to_s.include?("Will be right back")
515
+ return true if response.body.to_s.include?("<html>")
516
+ false
517
+ end
518
+
519
+ def circuit_breaker_status
520
+ {
521
+ state: @@circuit_breaker_state,
522
+ failure_count: @@failure_count,
523
+ last_failure_time: @@last_failure_time,
524
+ timeout: @@circuit_breaker_timeout,
525
+ threshold: @@circuit_breaker_threshold
526
+ }
527
+ end
528
+
529
+ # For /v7/finance/download, scrape crumb from quote page
530
+ def get_crumb_scrape_quote_page(symbol)
531
+ return nil if symbol.nil?
532
+ url = "https://finance.yahoo.com/quote/#{symbol}"
533
+ response = ::HTTParty.get(url, headers: @@user_agent_headers)
534
+ # Look for root.App.main = { ... };
535
+ m = response.body.match(/root\.App\.main\s*=\s*(\{.*?\});/m)
536
+ return nil unless m
537
+ json_blob = m[1]
538
+ begin
539
+ data = JSON.parse(json_blob)
540
+ crumb = data.dig('context', 'dispatcher', 'stores', 'CrumbStore', 'crumb')
541
+ # warn "[yf_as_dataframe] Scraped crumb from quote page: #{crumb.inspect}"
542
+ return crumb
543
+ rescue => e
544
+ # warn "[yf_as_dataframe] Failed to parse crumb from quote page: #{e.message}"
545
+ return nil
546
+ end
547
+ end
361
548
  end
362
549
  end
@@ -0,0 +1,129 @@
1
+ # Minimal patch to make curl-impersonate the default behavior
2
+ # This file should be required after the main YfConnection class
3
+
4
+ require_relative 'curl_impersonate_integration'
5
+
6
+ class YfAsDataframe
7
+ module YfConnection
8
+ # Store original methods
9
+ alias_method :get_original, :get
10
+ alias_method :get_raw_json_original, :get_raw_json
11
+
12
+ # Override get method to use curl-impersonate by default
13
+ def get(url, headers=nil, params=nil)
14
+ # Debug output
15
+ puts "DEBUG: curl_impersonate_enabled = #{CurlImpersonateIntegration.curl_impersonate_enabled}"
16
+ puts "DEBUG: curl_impersonate_fallback = #{CurlImpersonateIntegration.curl_impersonate_fallback}"
17
+
18
+ # Try curl-impersonate first if enabled
19
+ if CurlImpersonateIntegration.curl_impersonate_enabled
20
+ puts "DEBUG: Trying curl-impersonate..."
21
+ begin
22
+ # Prepare headers and params as in original method
23
+ headers ||= {}
24
+ params ||= {}
25
+
26
+ # Only fetch crumb for /v7/finance/download endpoint
27
+ crumb_needed = url.include?('/v7/finance/download')
28
+ if crumb_needed
29
+ crumb = get_crumb_scrape_quote_page(params[:symbol] || params['symbol'])
30
+ params.merge!(crumb: crumb) unless crumb.nil?
31
+ end
32
+
33
+ cookie, _, strategy = _get_cookie_and_crumb(crumb_needed)
34
+ crumbs = {} # crumb logic handled above if needed
35
+
36
+ # Prepare headers for curl-impersonate
37
+ curl_headers = headers.dup.merge(@@user_agent_headers)
38
+
39
+ # Add cookie if available
40
+ if cookie
41
+ cookie_hash = ::HTTParty::CookieHash.new
42
+ cookie_hash.add_cookies(cookie)
43
+ curl_headers['Cookie'] = cookie_hash.to_cookie_string
44
+ end
45
+
46
+ # Add crumb if available
47
+ curl_headers['crumb'] = crumb if crumb
48
+
49
+ # Make curl-impersonate request with improved timeout handling
50
+ response = CurlImpersonateIntegration.make_request(
51
+ url,
52
+ headers: curl_headers,
53
+ params: params.merge(crumbs),
54
+ timeout: CurlImpersonateIntegration.curl_impersonate_timeout,
55
+ retries: CurlImpersonateIntegration.curl_impersonate_retries
56
+ )
57
+
58
+ if response && response.success?
59
+ puts "DEBUG: curl-impersonate succeeded"
60
+ return response
61
+ else
62
+ puts "DEBUG: curl-impersonate returned nil or failed"
63
+ end
64
+ rescue => e
65
+ # Log error but continue to fallback
66
+ puts "DEBUG: curl-impersonate exception: #{e.message}"
67
+ # warn "curl-impersonate request failed: #{e.message}" if $VERBOSE
68
+ end
69
+ else
70
+ puts "DEBUG: curl-impersonate is disabled, skipping to fallback"
71
+ end
72
+
73
+ # Fallback to original HTTParty method
74
+ if CurlImpersonateIntegration.curl_impersonate_fallback
75
+ puts "DEBUG: Using HTTParty fallback"
76
+ get_original(url, headers, params)
77
+ else
78
+ puts "DEBUG: Fallback is disabled, but forcing fallback anyway"
79
+ get_original(url, headers, params)
80
+ end
81
+ end
82
+
83
+ # get_raw_json uses get, so it automatically gets curl-impersonate behavior
84
+ # No need to override it separately
85
+
86
+ # Class-level configuration methods
87
+ class << self
88
+ def enable_curl_impersonate(enabled: true)
89
+ CurlImpersonateIntegration.curl_impersonate_enabled = enabled
90
+ end
91
+
92
+ def enable_curl_impersonate_fallback(enabled: true)
93
+ CurlImpersonateIntegration.curl_impersonate_fallback = enabled
94
+ end
95
+
96
+ def set_curl_impersonate_timeout(timeout)
97
+ CurlImpersonateIntegration.curl_impersonate_timeout = timeout
98
+ end
99
+
100
+ def set_curl_impersonate_connect_timeout(timeout)
101
+ CurlImpersonateIntegration.curl_impersonate_connect_timeout = timeout
102
+ end
103
+
104
+ def set_curl_impersonate_process_timeout(timeout)
105
+ CurlImpersonateIntegration.curl_impersonate_process_timeout = timeout
106
+ end
107
+
108
+ def set_curl_impersonate_retries(retries)
109
+ CurlImpersonateIntegration.curl_impersonate_retries = retries
110
+ end
111
+
112
+ def get_available_curl_impersonate_executables
113
+ CurlImpersonateIntegration.available_executables
114
+ end
115
+
116
+ def get_curl_impersonate_config
117
+ {
118
+ enabled: CurlImpersonateIntegration.curl_impersonate_enabled,
119
+ fallback: CurlImpersonateIntegration.curl_impersonate_fallback,
120
+ timeout: CurlImpersonateIntegration.curl_impersonate_timeout,
121
+ connect_timeout: CurlImpersonateIntegration.curl_impersonate_connect_timeout,
122
+ process_timeout: CurlImpersonateIntegration.curl_impersonate_process_timeout,
123
+ retries: CurlImpersonateIntegration.curl_impersonate_retries,
124
+ retry_delay: CurlImpersonateIntegration.curl_impersonate_retry_delay
125
+ }
126
+ end
127
+ end
128
+ end
129
+ end
@@ -1,3 +1,5 @@
1
+ require 'logger'
2
+
1
3
  class YfAsDataframe
2
4
  class YfinanceException < StandardError
3
5
  attr_reader :msg
@@ -9,7 +11,7 @@ class YfAsDataframe
9
11
  class YFNotImplementedError < NotImplementedError
10
12
  def initialize(str)
11
13
  @msg = "Have not implemented fetching \"#{str}\" from Yahoo API"
12
- Rails.logger.warn { @msg }
14
+ # Logger.new(STDOUT).warn { @msg }
13
15
  end
14
16
  end
15
17
  end
@@ -7,6 +7,8 @@ require_relative 'yf_as_dataframe/version'
7
7
  require_relative 'yf_as_dataframe/utils'
8
8
  require_relative 'yf_as_dataframe/yfinance_exception'
9
9
  require_relative 'yf_as_dataframe/yf_connection'
10
+ require_relative 'yf_as_dataframe/curl_impersonate_integration'
11
+ require_relative 'yf_as_dataframe/yf_connection_minimal_patch'
10
12
  require_relative 'yf_as_dataframe/price_technical'
11
13
  require_relative 'yf_as_dataframe/price_history'
12
14
  require_relative 'yf_as_dataframe/quote'
data/smoke_test.rb ADDED
@@ -0,0 +1,64 @@
1
+ # require "bundler/setup"
2
+ require "yf_as_dataframe"
3
+
4
+ def print_section(title)
5
+ puts "\n=== #{title} ==="
6
+ end
7
+
8
+ begin
9
+ print_section("Ticker Creation")
10
+ msft = YfAsDataframe::Ticker.new("MSFT")
11
+ puts "Ticker created: #{msft.ticker}"
12
+
13
+ print_section("Price History")
14
+ hist = msft.history(period: "1mo")
15
+ puts "History DataFrame shape: #{hist.shape}" if hist
16
+
17
+ print_section("Meta Information")
18
+ meta = msft.history_metadata
19
+ puts "Meta: #{meta.inspect}"
20
+
21
+ print_section("Actions")
22
+ puts "Dividends: #{msft.dividends.inspect}"
23
+ puts "Splits: #{msft.splits.inspect}"
24
+
25
+ print_section("Share Count")
26
+ shares = msft.shares_full(start: "2022-01-01", fin: nil)
27
+ puts "Shares DataFrame shape: #{shares.shape}" if shares
28
+
29
+ print_section("Financials")
30
+ puts "Income Statement: #{msft.income_stmt.inspect}"
31
+ puts "Balance Sheet: #{msft.balance_sheet.inspect}"
32
+ puts "Cash Flow: #{msft.cashflow.inspect}"
33
+
34
+ print_section("Holders")
35
+ puts "Major Holders: #{msft.major_holders.inspect}"
36
+ puts "Institutional Holders: #{msft.institutional_holders.inspect}"
37
+
38
+ print_section("Recommendations")
39
+ puts "Recommendations: #{msft.recommendations.inspect}"
40
+
41
+ print_section("Earnings Dates")
42
+ puts "Earnings Dates: #{msft.earnings_dates.inspect}"
43
+
44
+ print_section("ISIN")
45
+ puts "ISIN: #{msft.isin.inspect}"
46
+
47
+ print_section("Options")
48
+ puts "Options: #{msft.options.inspect}"
49
+
50
+ print_section("News")
51
+ puts "News: #{msft.news.inspect}"
52
+
53
+ print_section("Technical Indicator Example")
54
+ if hist
55
+ ad = YfAsDataframe.ad(hist)
56
+ puts "AD indicator: #{ad.inspect}"
57
+ end
58
+
59
+ puts "\nAll tests completed successfully!"
60
+
61
+ rescue => e
62
+ puts "\nTest failed: #{e.class} - #{e.message}"
63
+ puts e.backtrace.first(10)
64
+ end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yf_as_dataframe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bill McKinnon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-05 00:00:00.000000000 Z
11
+ date: 2025-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: tzinfo-data
14
+ name: tzinfo
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: polars-df
28
+ name: tzinfo-data
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: polars-df
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.12.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.12.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: zache
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,34 @@ dependencies:
80
94
  - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: nokogiri
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: activesupport
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
83
125
  description: Download market data from Yahoo! Finance's API
84
126
  email:
85
127
  - bill@bmck.org
@@ -91,12 +133,15 @@ files:
91
133
  - CHANGELOG.rst
92
134
  - CODE_OF_CONDUCT.md
93
135
  - Gemfile
136
+ - Gemfile.lock
94
137
  - LICENSE.txt
138
+ - MINIMAL_INTEGRATION.md
95
139
  - README.md
96
140
  - Rakefile
97
141
  - chart.png
98
142
  - lib/yf_as_dataframe.rb
99
143
  - lib/yf_as_dataframe/analysis.rb
144
+ - lib/yf_as_dataframe/curl_impersonate_integration.rb
100
145
  - lib/yf_as_dataframe/financials.rb
101
146
  - lib/yf_as_dataframe/fundamentals.rb
102
147
  - lib/yf_as_dataframe/holders.rb
@@ -109,7 +154,9 @@ files:
109
154
  - lib/yf_as_dataframe/utils.rb
110
155
  - lib/yf_as_dataframe/version.rb
111
156
  - lib/yf_as_dataframe/yf_connection.rb
157
+ - lib/yf_as_dataframe/yf_connection_minimal_patch.rb
112
158
  - lib/yf_as_dataframe/yfinance_exception.rb
159
+ - smoke_test.rb
113
160
  homepage: https://www.github.com/bmck/yf_as_dataframe
114
161
  licenses:
115
162
  - MIT
@@ -132,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
179
  - !ruby/object:Gem::Version
133
180
  version: '0'
134
181
  requirements: []
135
- rubygems_version: 3.2.33
182
+ rubygems_version: 3.5.22
136
183
  signing_key:
137
184
  specification_version: 4
138
185
  summary: A shameless port of python's yfinance module to ruby