yf_as_dataframe 0.3.1 → 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 +235 -48
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13496d1eaf3e5ce09c9477de83957acb3d2183214eec2b0dedabdf8b612bd80b
|
4
|
+
data.tar.gz: 604c01e5bba073e9350146c6636c6ce29f1ac62a7d2a9a4e6e9338a33f1a65e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e758fafbc0396c7582a9ad0654c94d7721afd2457a403408376be07fa7ace874887a83499c4507a122f1d027a0126da31c9447cb5b61a3d9fc3ab190e7e36f96
|
7
|
+
data.tar.gz: 74a1263cd148add7c56417b4912052471ae6c49fc4163daacc5d5ce3988e53135802f4ec7912aeef372e735fec470072921f15f536d628e5449315fb31ea3142
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
yf_as_dataframe (0.4.0)
|
5
|
+
activesupport
|
6
|
+
httparty
|
7
|
+
nokogiri
|
8
|
+
polars-df (~> 0.12.0)
|
9
|
+
tulirb
|
10
|
+
tzinfo
|
11
|
+
tzinfo-data
|
12
|
+
zache
|
13
|
+
|
14
|
+
GEM
|
15
|
+
remote: https://rubygems.org/
|
16
|
+
specs:
|
17
|
+
activesupport (7.2.2.1)
|
18
|
+
base64
|
19
|
+
benchmark (>= 0.3)
|
20
|
+
bigdecimal
|
21
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
22
|
+
connection_pool (>= 2.2.5)
|
23
|
+
drb
|
24
|
+
i18n (>= 1.6, < 2)
|
25
|
+
logger (>= 1.4.2)
|
26
|
+
minitest (>= 5.1)
|
27
|
+
securerandom (>= 0.3)
|
28
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
29
|
+
ast (2.4.3)
|
30
|
+
base64 (0.3.0)
|
31
|
+
benchmark (0.4.1)
|
32
|
+
bigdecimal (3.2.2)
|
33
|
+
concurrent-ruby (1.3.5)
|
34
|
+
connection_pool (2.5.3)
|
35
|
+
csv (3.3.5)
|
36
|
+
drb (2.2.3)
|
37
|
+
httparty (0.23.1)
|
38
|
+
csv
|
39
|
+
mini_mime (>= 1.0.0)
|
40
|
+
multi_xml (>= 0.5.2)
|
41
|
+
i18n (1.14.7)
|
42
|
+
concurrent-ruby (~> 1.0)
|
43
|
+
json (2.12.2)
|
44
|
+
language_server-protocol (3.17.0.5)
|
45
|
+
lint_roller (1.1.0)
|
46
|
+
logger (1.7.0)
|
47
|
+
mini_mime (1.1.5)
|
48
|
+
minitest (5.25.5)
|
49
|
+
multi_xml (0.7.1)
|
50
|
+
bigdecimal (~> 3.1)
|
51
|
+
nokogiri (1.18.8-arm64-darwin)
|
52
|
+
racc (~> 1.4)
|
53
|
+
parallel (1.27.0)
|
54
|
+
parser (3.3.8.0)
|
55
|
+
ast (~> 2.4.1)
|
56
|
+
racc
|
57
|
+
polars-df (0.12.0-arm64-darwin)
|
58
|
+
bigdecimal
|
59
|
+
prism (1.4.0)
|
60
|
+
racc (1.8.1)
|
61
|
+
rainbow (3.1.1)
|
62
|
+
rake (13.3.0)
|
63
|
+
regexp_parser (2.10.0)
|
64
|
+
rubocop (1.77.0)
|
65
|
+
json (~> 2.3)
|
66
|
+
language_server-protocol (~> 3.17.0.2)
|
67
|
+
lint_roller (~> 1.1.0)
|
68
|
+
parallel (~> 1.10)
|
69
|
+
parser (>= 3.3.0.2)
|
70
|
+
rainbow (>= 2.2.2, < 4.0)
|
71
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
72
|
+
rubocop-ast (>= 1.45.1, < 2.0)
|
73
|
+
ruby-progressbar (~> 1.7)
|
74
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
75
|
+
rubocop-ast (1.45.1)
|
76
|
+
parser (>= 3.3.7.2)
|
77
|
+
prism (~> 1.4)
|
78
|
+
ruby-progressbar (1.13.0)
|
79
|
+
securerandom (0.4.1)
|
80
|
+
tulirb (1.0.0)
|
81
|
+
tzinfo (2.0.6)
|
82
|
+
concurrent-ruby (~> 1.0)
|
83
|
+
tzinfo-data (1.2025.2)
|
84
|
+
tzinfo (>= 1.0.0)
|
85
|
+
unicode-display_width (3.1.4)
|
86
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
87
|
+
unicode-emoji (4.0.4)
|
88
|
+
zache (0.15.0)
|
89
|
+
|
90
|
+
PLATFORMS
|
91
|
+
arm64-darwin-23
|
92
|
+
|
93
|
+
DEPENDENCIES
|
94
|
+
rake (~> 13.0)
|
95
|
+
rubocop (~> 1.21)
|
96
|
+
yf_as_dataframe!
|
97
|
+
|
98
|
+
BUNDLED WITH
|
99
|
+
2.6.9
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# Minimal Curl-Impersonate Integration
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
This is a minimal integration that makes curl-impersonate the **default behavior** for all Yahoo Finance requests. No changes to your existing code are required - curl-impersonate is used automatically to bypass TLS fingerprinting.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
### 1. Install curl-impersonate
|
10
|
+
|
11
|
+
```bash
|
12
|
+
# macOS
|
13
|
+
brew tap shakacode/brew
|
14
|
+
brew install curl-impersonate
|
15
|
+
|
16
|
+
# Verify installation
|
17
|
+
ls -la /usr/local/bin/curl_*
|
18
|
+
```
|
19
|
+
|
20
|
+
### 2. Custom Installation Directory (Optional)
|
21
|
+
|
22
|
+
If you have curl-impersonate installed in a different directory, you can set the `CURL_IMPERSONATE_DIR` environment variable:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
# Set custom directory
|
26
|
+
export CURL_IMPERSONATE_DIR="/opt/curl-impersonate/bin"
|
27
|
+
|
28
|
+
# Or set it for a single command
|
29
|
+
CURL_IMPERSONATE_DIR="/opt/curl-impersonate/bin" ruby your_script.rb
|
30
|
+
```
|
31
|
+
|
32
|
+
The default directory is `/usr/local/bin` if the environment variable is not set.
|
33
|
+
|
34
|
+
### 3. Add Integration Files
|
35
|
+
|
36
|
+
Copy these two files to your project's `lib/yf_as_dataframe/` directory:
|
37
|
+
|
38
|
+
1. `lib/yf_as_dataframe/curl_impersonate_integration.rb`
|
39
|
+
2. `lib/yf_as_dataframe/yf_connection_minimal_patch.rb`
|
40
|
+
|
41
|
+
### 4. Enable Integration
|
42
|
+
|
43
|
+
Add this single line to your code **before** any Yahoo Finance requests:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'yf_as_dataframe/curl_impersonate_integration'
|
47
|
+
require 'yf_as_dataframe/yf_connection_minimal_patch'
|
48
|
+
```
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
### Default Behavior (Recommended)
|
53
|
+
|
54
|
+
Your existing code works exactly as before, but now uses curl-impersonate automatically:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
require 'yf_as_dataframe'
|
58
|
+
require 'yf_as_dataframe/curl_impersonate_integration'
|
59
|
+
require 'yf_as_dataframe/yf_connection_minimal_patch'
|
60
|
+
|
61
|
+
# Your existing code - no changes needed!
|
62
|
+
msft = YfAsDataframe::Ticker.new("MSFT")
|
63
|
+
hist = msft.history(period: "1mo") # Uses curl-impersonate automatically
|
64
|
+
puts "Retrieved #{hist.length} data points"
|
65
|
+
```
|
66
|
+
|
67
|
+
### Configuration (Optional)
|
68
|
+
|
69
|
+
You can configure the behavior if needed:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# Disable curl-impersonate (use HTTParty only)
|
73
|
+
YfAsDataframe::YfConnection.enable_curl_impersonate(false)
|
74
|
+
|
75
|
+
# Disable fallback (fail if curl-impersonate fails)
|
76
|
+
YfAsDataframe::YfConnection.enable_curl_impersonate_fallback(false)
|
77
|
+
|
78
|
+
# Set timeout
|
79
|
+
YfAsDataframe::YfConnection.set_curl_impersonate_timeout(45)
|
80
|
+
|
81
|
+
# Check available executables
|
82
|
+
executables = YfAsDataframe::YfConnection.get_available_curl_impersonate_executables
|
83
|
+
puts "Available: #{executables.length} executables"
|
84
|
+
|
85
|
+
# Check which directory is being used
|
86
|
+
puts "Using directory: #{YfAsDataframe::CurlImpersonateIntegration.executable_directory}"
|
87
|
+
```
|
88
|
+
|
89
|
+
## How It Works
|
90
|
+
|
91
|
+
1. **Automatic Detection**: Dynamically finds curl-impersonate executables in the configured directory
|
92
|
+
2. **Default Behavior**: Uses curl-impersonate for all requests by default
|
93
|
+
3. **Seamless Fallback**: Falls back to HTTParty if curl-impersonate fails
|
94
|
+
4. **Zero Interface Changes**: All existing method signatures remain the same
|
95
|
+
|
96
|
+
## Key Features
|
97
|
+
|
98
|
+
### ✅ **Zero Code Changes**
|
99
|
+
- Your existing code works exactly as before
|
100
|
+
- No new method names to learn
|
101
|
+
- No changes to method signatures
|
102
|
+
|
103
|
+
### ✅ **Automatic Browser Rotation**
|
104
|
+
- Randomly selects from available curl-impersonate executables
|
105
|
+
- Supports Chrome, Firefox, Edge, and Safari configurations
|
106
|
+
- Automatically adapts to new browser versions
|
107
|
+
|
108
|
+
### ✅ **Robust Fallback**
|
109
|
+
- Falls back to HTTParty if curl-impersonate fails
|
110
|
+
- Configurable fallback behavior
|
111
|
+
- Maintains compatibility with existing code
|
112
|
+
|
113
|
+
### ✅ **Dynamic Discovery**
|
114
|
+
- Automatically finds curl-impersonate executables
|
115
|
+
- Configurable directory via environment variable
|
116
|
+
- Works with any curl-impersonate installation
|
117
|
+
|
118
|
+
### ✅ **Environment Variable Support**
|
119
|
+
- Set `CURL_IMPERSONATE_DIR` to customize installation directory
|
120
|
+
- Defaults to `/usr/local/bin` if not set
|
121
|
+
- Supports both persistent and per-command configuration
|
122
|
+
|
123
|
+
## Example
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
require 'yf_as_dataframe'
|
127
|
+
require 'yf_as_dataframe/curl_impersonate_integration'
|
128
|
+
require 'yf_as_dataframe/yf_connection_minimal_patch'
|
129
|
+
|
130
|
+
# Check what's available
|
131
|
+
executables = YfAsDataframe::YfConnection.get_available_curl_impersonate_executables
|
132
|
+
puts "Found #{executables.length} curl-impersonate executables"
|
133
|
+
|
134
|
+
# Check which directory is being used
|
135
|
+
puts "Using directory: #{YfAsDataframe::CurlImpersonateIntegration.executable_directory}"
|
136
|
+
|
137
|
+
# Use as normal - curl-impersonate is used automatically
|
138
|
+
msft = YfAsDataframe::Ticker.new("MSFT")
|
139
|
+
|
140
|
+
begin
|
141
|
+
# These all use curl-impersonate automatically
|
142
|
+
hist = msft.history(period: "1mo")
|
143
|
+
info = msft.info
|
144
|
+
actions = msft.actions
|
145
|
+
|
146
|
+
puts "✅ All requests successful using curl-impersonate"
|
147
|
+
puts "History: #{hist.length} data points"
|
148
|
+
puts "Company: #{info['longName']}"
|
149
|
+
puts "Actions: #{actions.length} items"
|
150
|
+
rescue => e
|
151
|
+
puts "❌ Error: #{e.message}"
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
## Troubleshooting
|
156
|
+
|
157
|
+
### "No curl-impersonate executables found"
|
158
|
+
```bash
|
159
|
+
# Check if executables exist in default location
|
160
|
+
ls -la /usr/local/bin/curl_*
|
161
|
+
|
162
|
+
# Check if executables exist in custom location
|
163
|
+
ls -la $CURL_IMPERSONATE_DIR/curl_*
|
164
|
+
|
165
|
+
# If not found, reinstall curl-impersonate
|
166
|
+
brew reinstall curl-impersonate
|
167
|
+
```
|
168
|
+
|
169
|
+
### Permission errors
|
170
|
+
```bash
|
171
|
+
sudo chmod +x /usr/local/bin/curl_*
|
172
|
+
# or
|
173
|
+
sudo chmod +x $CURL_IMPERSONATE_DIR/curl_*
|
174
|
+
```
|
175
|
+
|
176
|
+
### Still getting blocked
|
177
|
+
```ruby
|
178
|
+
# Try disabling fallback to see if curl-impersonate is working
|
179
|
+
YfAsDataframe::YfConnection.enable_curl_impersonate_fallback(false)
|
180
|
+
|
181
|
+
# Check available executables
|
182
|
+
executables = YfAsDataframe::YfConnection.get_available_curl_impersonate_executables
|
183
|
+
puts executables.map { |e| "#{e[:browser]} #{e[:executable]}" }
|
184
|
+
|
185
|
+
# Check which directory is being used
|
186
|
+
puts "Directory: #{YfAsDataframe::CurlImpersonateIntegration.executable_directory}"
|
187
|
+
```
|
188
|
+
|
189
|
+
## Configuration Options
|
190
|
+
|
191
|
+
| Option | Default | Description |
|
192
|
+
|--------|---------|-------------|
|
193
|
+
| `curl_impersonate_enabled` | `true` | Use curl-impersonate for requests |
|
194
|
+
| `curl_impersonate_fallback` | `true` | Fall back to HTTParty if curl-impersonate fails |
|
195
|
+
| `curl_impersonate_timeout` | `30` | Timeout in seconds for curl-impersonate requests |
|
196
|
+
| `CURL_IMPERSONATE_DIR` | `/usr/local/bin` | Directory containing curl-impersonate executables |
|
197
|
+
|
198
|
+
## Benefits
|
199
|
+
|
200
|
+
1. **Immediate Solution**: Bypasses TLS fingerprinting immediately
|
201
|
+
2. **Zero Learning Curve**: No new APIs or methods to learn
|
202
|
+
3. **Future-Proof**: Automatically adapts to new curl-impersonate versions
|
203
|
+
4. **Robust**: Multiple fallback strategies ensure reliability
|
204
|
+
5. **Minimal**: Only two small files to add
|
205
|
+
6. **Flexible**: Configurable installation directory via environment variable
|
206
|
+
|
207
|
+
## Comparison with Previous Approach
|
208
|
+
|
209
|
+
| Aspect | Previous Approach | Minimal Approach |
|
210
|
+
|--------|------------------|------------------|
|
211
|
+
| Interface Changes | New method names | No changes |
|
212
|
+
| Learning Curve | High | Zero |
|
213
|
+
| Integration | Complex | Simple |
|
214
|
+
| Default Behavior | HTTParty | curl-impersonate |
|
215
|
+
| Configuration | Required | Optional |
|
216
|
+
| Files to Add | 3 files | 2 files |
|
217
|
+
| Directory Config | Hardcoded | Environment variable |
|
218
|
+
|
219
|
+
## Next Steps
|
220
|
+
|
221
|
+
1. **Install curl-impersonate** following the instructions above
|
222
|
+
2. **Set CURL_IMPERSONATE_DIR** if using a custom installation directory
|
223
|
+
3. **Add the two integration files** to your project
|
224
|
+
4. **Add the require statements** to your code
|
225
|
+
5. **Test with your existing code** - it should work immediately
|
226
|
+
|
227
|
+
That's it! Your existing Yahoo Finance scraping code will now automatically use curl-impersonate to bypass TLS fingerprinting.
|
data/README.md
CHANGED
@@ -197,6 +197,71 @@ YfAsDataframe.zlema(df, column: 'Adj Close', window: 5)
|
|
197
197
|
|
198
198
|
---
|
199
199
|
|
200
|
+
## TLS Fingerprinting Protection
|
201
|
+
|
202
|
+
**New in v0.4.0**: This gem now includes built-in support for [curl-impersonate](https://github.com/lwthiker/curl-impersonate). The curl-impersonate integration is **enabled by default** in v0.4.0+. Existing code will automatically use curl-impersonate to bypass TLS fingerprinting:
|
203
|
+
|
204
|
+
### Installation Requirements
|
205
|
+
|
206
|
+
To use the TLS fingerprinting protection, you need to install curl-impersonate:
|
207
|
+
|
208
|
+
```bash
|
209
|
+
# macOS
|
210
|
+
brew tap shakacode/brew
|
211
|
+
brew install curl-impersonate
|
212
|
+
|
213
|
+
# Verify installation
|
214
|
+
ls -la /usr/local/bin/curl_*
|
215
|
+
```
|
216
|
+
|
217
|
+
### Custom Installation Directory
|
218
|
+
|
219
|
+
If you have curl-impersonate installed in a different directory, you can set the `CURL_IMPERSONATE_DIR` environment variable:
|
220
|
+
|
221
|
+
```bash
|
222
|
+
# Set custom directory
|
223
|
+
export CURL_IMPERSONATE_DIR="/opt/curl-impersonate/bin"
|
224
|
+
|
225
|
+
# Or set it for a single command
|
226
|
+
CURL_IMPERSONATE_DIR="/opt/curl-impersonate/bin" ruby your_script.rb
|
227
|
+
```
|
228
|
+
|
229
|
+
The default directory is `/usr/local/bin` if the environment variable is not set.
|
230
|
+
|
231
|
+
### Configuration (Optional)
|
232
|
+
|
233
|
+
You can configure the curl-impersonate behavior if needed:
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
# Disable curl-impersonate (use HTTParty only)
|
237
|
+
YfAsDataframe::YfConnection.enable_curl_impersonate(false)
|
238
|
+
|
239
|
+
# Disable fallback (fail if curl-impersonate fails)
|
240
|
+
YfAsDataframe::YfConnection.enable_curl_impersonate_fallback(false)
|
241
|
+
|
242
|
+
# Set timeout
|
243
|
+
YfAsDataframe::YfConnection.set_curl_impersonate_timeout(45)
|
244
|
+
|
245
|
+
# Check available executables
|
246
|
+
executables = YfAsDataframe::YfConnection.get_available_curl_impersonate_executables
|
247
|
+
puts "Available: #{executables.length} executables"
|
248
|
+
|
249
|
+
# Check which directory is being used
|
250
|
+
puts "Using directory: #{YfAsDataframe::CurlImpersonateIntegration.executable_directory}"
|
251
|
+
```
|
252
|
+
|
253
|
+
### How It Works
|
254
|
+
|
255
|
+
1. **Automatic Detection**: Dynamically finds curl-impersonate executables in the configured directory
|
256
|
+
2. **Default Behavior**: Uses curl-impersonate for all requests by default
|
257
|
+
3. **Seamless Fallback**: Falls back to HTTParty if curl-impersonate fails
|
258
|
+
4. **Browser Rotation**: Randomly selects from Chrome, Firefox, Edge, and Safari configurations
|
259
|
+
5. **Zero Interface Changes**: All existing method signatures remain the same
|
260
|
+
|
261
|
+
For more detailed information, see [MINIMAL_INTEGRATION.md](MINIMAL_INTEGRATION.md).
|
262
|
+
|
263
|
+
---
|
264
|
+
|
200
265
|
## Graphing
|
201
266
|
|
202
267
|
To graph any of the series using [Vega](https://github.com/ankane/vega-ruby), per the information [here](https://github.com/ankane/vega-ruby#exporting-charts-experimental), you will need to run
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'json'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
class YfAsDataframe
|
6
|
+
module CurlImpersonateIntegration
|
7
|
+
# Configuration
|
8
|
+
@curl_impersonate_enabled = true
|
9
|
+
@curl_impersonate_fallback = true
|
10
|
+
@curl_impersonate_timeout = 5
|
11
|
+
@curl_impersonate_retries = 2
|
12
|
+
@curl_impersonate_retry_delay = 1
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_accessor :curl_impersonate_enabled, :curl_impersonate_fallback,
|
16
|
+
:curl_impersonate_timeout, :curl_impersonate_retries,
|
17
|
+
:curl_impersonate_retry_delay
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the curl-impersonate executable directory from environment variable or default
|
21
|
+
def self.executable_directory
|
22
|
+
ENV['CURL_IMPERSONATE_DIR'] || '/usr/local/bin'
|
23
|
+
end
|
24
|
+
|
25
|
+
# Find available curl-impersonate executables
|
26
|
+
def self.available_executables
|
27
|
+
@available_executables ||= begin
|
28
|
+
executables = []
|
29
|
+
Dir.glob(File.join(executable_directory, "curl_*")).each do |path|
|
30
|
+
executable = File.basename(path)
|
31
|
+
if executable.start_with?('curl_')
|
32
|
+
browser_type = case executable
|
33
|
+
when /^curl_chrome/ then :chrome
|
34
|
+
when /^curl_ff/ then :firefox
|
35
|
+
when /^curl_edge/ then :edge
|
36
|
+
when /^curl_safari/ then :safari
|
37
|
+
else :unknown
|
38
|
+
end
|
39
|
+
executables << { path: path, executable: executable, browser: browser_type }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
executables
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get a random executable
|
47
|
+
def self.get_random_executable
|
48
|
+
available = available_executables
|
49
|
+
return nil if available.empty?
|
50
|
+
available.sample
|
51
|
+
end
|
52
|
+
|
53
|
+
# Make a curl-impersonate request
|
54
|
+
def self.make_request(url, headers: {}, params: {}, timeout: nil)
|
55
|
+
executable_info = get_random_executable
|
56
|
+
return nil unless executable_info
|
57
|
+
|
58
|
+
timeout ||= @curl_impersonate_timeout
|
59
|
+
|
60
|
+
# Build command
|
61
|
+
cmd = [executable_info[:path], "--max-time", timeout.to_s]
|
62
|
+
|
63
|
+
# Add headers
|
64
|
+
headers.each do |key, value|
|
65
|
+
cmd.concat(["-H", "#{key}: #{value}"])
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add query parameters
|
69
|
+
unless params.empty?
|
70
|
+
query_string = params.map { |k, v| "#{k}=#{v}" }.join('&')
|
71
|
+
separator = url.include?('?') ? '&' : '?'
|
72
|
+
url = "#{url}#{separator}#{query_string}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Add URL
|
76
|
+
cmd << url
|
77
|
+
|
78
|
+
# Debug output
|
79
|
+
puts "DEBUG: curl-impersonate command: #{cmd.join(' ')}"
|
80
|
+
puts "DEBUG: curl-impersonate timeout: #{timeout} seconds"
|
81
|
+
|
82
|
+
# Execute
|
83
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
84
|
+
|
85
|
+
puts "DEBUG: curl-impersonate stdout: #{stdout[0..200]}..." if stdout && !stdout.empty?
|
86
|
+
puts "DEBUG: curl-impersonate stderr: #{stderr}" if stderr && !stderr.empty?
|
87
|
+
puts "DEBUG: curl-impersonate status: #{status.exitstatus}"
|
88
|
+
|
89
|
+
if status.success?
|
90
|
+
# Create a response object similar to HTTParty
|
91
|
+
response = OpenStruct.new
|
92
|
+
response.body = stdout
|
93
|
+
response.code = 200
|
94
|
+
response.define_singleton_method(:success?) { true }
|
95
|
+
response.parsed_response = parse_json_if_possible(stdout)
|
96
|
+
response
|
97
|
+
else
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def self.parse_json_if_possible(response_body)
|
105
|
+
JSON.parse(response_body)
|
106
|
+
rescue JSON::ParserError
|
107
|
+
response_body
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'polars-df'
|
2
|
+
require 'logger'
|
2
3
|
|
3
4
|
class YfAsDataframe
|
4
5
|
module Financials
|
@@ -111,7 +112,7 @@ class YfAsDataframe
|
|
111
112
|
ts_url_base = "https://query2.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/#{symbol}?symbol=#{symbol}"
|
112
113
|
url = ts_url_base + "&type=" + ts_keys.map { |k| "#{timescale}#{k}" }.join(",")
|
113
114
|
start_dt = DateTime.new(2016, 12, 31)
|
114
|
-
end_dt =
|
115
|
+
end_dt = Time.now.tomorrow.midnight
|
115
116
|
url += "&period1=#{start_dt.to_i}&period2=#{end_dt.to_i}"
|
116
117
|
|
117
118
|
json_str = get(url).parsed_response
|
@@ -160,7 +161,7 @@ class YfAsDataframe
|
|
160
161
|
statement = _create_financials_table(nam, timescale)
|
161
162
|
return statement unless statement.nil?
|
162
163
|
rescue Yfin::YfinDataException => e
|
163
|
-
|
164
|
+
Logger.new(STDOUT).error {"#{@symbol}: Failed to create #{nam} financials table for reason: #{e}"}
|
164
165
|
end
|
165
166
|
Polars::DataFrame.new()
|
166
167
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
1
3
|
class YfAsDataframe
|
2
4
|
module Holders
|
3
5
|
extend ActiveSupport::Concern
|
@@ -97,7 +99,7 @@ class YfAsDataframe
|
|
97
99
|
result = get_raw_json(QUOTE_SUMMARY_URL + "/#{symbol}", user_agent_headers=user_agent_headers, params=params_dict)
|
98
100
|
# Rails.logger.info { "#{__FILE__}:#{__LINE__} result = #{result.inspect}" }
|
99
101
|
rescue Exception => e
|
100
|
-
|
102
|
+
Logger.new(STDOUT).error("ERROR: #{e.message}")
|
101
103
|
return nil
|
102
104
|
end
|
103
105
|
return result
|
@@ -133,7 +135,7 @@ class YfAsDataframe
|
|
133
135
|
|
134
136
|
def _parse_result(result)
|
135
137
|
data = result.parsed_response['quoteSummary']['result'].first #.dig('quoteSummary', 'result', 0)
|
136
|
-
|
138
|
+
Logger.new(STDOUT).info { "#{__FILE__}:#{__LINE__} data = #{data.inspect}" }
|
137
139
|
_parse_institution_ownership(data['institutionOwnership'])
|
138
140
|
_parse_fund_ownership(data['fundOwnership'])
|
139
141
|
_parse_major_holders_breakdown(data['majorHoldersBreakdown'])
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'polars-df'
|
2
|
+
require 'logger'
|
2
3
|
|
3
4
|
class YfAsDataframe
|
4
5
|
class Multi
|
@@ -59,7 +60,7 @@ class YfAsDataframe
|
|
59
60
|
# session: None or Session
|
60
61
|
# Optional. Pass your own session object to be used for all requests
|
61
62
|
# """
|
62
|
-
logger =
|
63
|
+
logger = Logger.new(STDOUT)
|
63
64
|
|
64
65
|
if show_errors
|
65
66
|
YfAsDataframe::Utils.print_once("yfinance: download(show_errors=#{show_errors}) argument is deprecated and will be removed in future version. Do this instead: logging.getLogger('yfinance').setLevel(logging.ERROR)")
|