sqa 0.0.24 → 0.0.31
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/.goose/memory/development.txt +3 -0
- data/.semver +6 -0
- data/ARCHITECTURE.md +648 -0
- data/CHANGELOG.md +82 -0
- data/CLAUDE.md +653 -0
- data/COMMITS.md +196 -0
- data/DATAFRAME_ARCHITECTURE_REVIEW.md +421 -0
- data/NEXT-STEPS.md +154 -0
- data/README.md +812 -262
- data/TASKS.md +358 -0
- data/TEST_RESULTS.md +140 -0
- data/TODO.md +42 -0
- data/_notes.txt +25 -0
- data/bin/sqa-console +11 -0
- data/data/talk_talk.json +103284 -0
- data/develop_summary.md +313 -0
- data/docs/advanced/backtesting.md +206 -0
- data/docs/advanced/ensemble.md +68 -0
- data/docs/advanced/fpop.md +153 -0
- data/docs/advanced/index.md +112 -0
- data/docs/advanced/multi-timeframe.md +67 -0
- data/docs/advanced/pattern-matcher.md +75 -0
- data/docs/advanced/portfolio-optimizer.md +79 -0
- data/docs/advanced/portfolio.md +166 -0
- data/docs/advanced/risk-management.md +210 -0
- data/docs/advanced/strategy-generator.md +158 -0
- data/docs/advanced/streaming.md +209 -0
- data/docs/ai_and_ml.md +80 -0
- data/docs/api/dataframe.md +1115 -0
- data/docs/api/index.md +126 -0
- data/docs/assets/css/custom.css +88 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/concepts/index.md +68 -0
- data/docs/contributing/index.md +60 -0
- data/docs/data-sources/index.md +66 -0
- data/docs/data_frame.md +317 -97
- data/docs/factors_that_impact_price.md +26 -0
- data/docs/finviz.md +11 -0
- data/docs/fx_pro_bit.md +25 -0
- data/docs/genetic_programming.md +104 -0
- data/docs/getting-started/index.md +123 -0
- data/docs/getting-started/installation.md +229 -0
- data/docs/getting-started/quick-start.md +244 -0
- data/docs/i_gotta_an_idea.md +22 -0
- data/docs/index.md +163 -0
- data/docs/indicators/index.md +97 -0
- data/docs/indicators.md +110 -24
- data/docs/options.md +8 -0
- data/docs/strategies/bollinger-bands.md +146 -0
- data/docs/strategies/consensus.md +64 -0
- data/docs/strategies/custom.md +310 -0
- data/docs/strategies/ema.md +53 -0
- data/docs/strategies/index.md +92 -0
- data/docs/strategies/kbs.md +164 -0
- data/docs/strategies/macd.md +96 -0
- data/docs/strategies/market-profile.md +54 -0
- data/docs/strategies/mean-reversion.md +58 -0
- data/docs/strategies/rsi.md +95 -0
- data/docs/strategies/sma.md +55 -0
- data/docs/strategies/stochastic.md +63 -0
- data/docs/strategies/volume-breakout.md +54 -0
- data/docs/tags.md +7 -0
- data/docs/true_strength_index.md +46 -0
- data/docs/weighted_moving_average.md +48 -0
- data/examples/README.md +354 -0
- data/examples/advanced_features_example.rb +350 -0
- data/examples/fpop_analysis_example.rb +191 -0
- data/examples/genetic_programming_example.rb +148 -0
- data/examples/kbs_strategy_example.rb +208 -0
- data/examples/pattern_context_example.rb +300 -0
- data/examples/rails_app/Gemfile +34 -0
- data/examples/rails_app/README.md +416 -0
- data/examples/rails_app/app/assets/javascripts/application.js +107 -0
- data/examples/rails_app/app/assets/stylesheets/application.css +659 -0
- data/examples/rails_app/app/controllers/analysis_controller.rb +11 -0
- data/examples/rails_app/app/controllers/api/v1/stocks_controller.rb +227 -0
- data/examples/rails_app/app/controllers/application_controller.rb +22 -0
- data/examples/rails_app/app/controllers/backtest_controller.rb +11 -0
- data/examples/rails_app/app/controllers/dashboard_controller.rb +21 -0
- data/examples/rails_app/app/controllers/portfolio_controller.rb +7 -0
- data/examples/rails_app/app/views/analysis/show.html.erb +209 -0
- data/examples/rails_app/app/views/backtest/show.html.erb +171 -0
- data/examples/rails_app/app/views/dashboard/index.html.erb +118 -0
- data/examples/rails_app/app/views/dashboard/show.html.erb +408 -0
- data/examples/rails_app/app/views/errors/show.html.erb +17 -0
- data/examples/rails_app/app/views/layouts/application.html.erb +60 -0
- data/examples/rails_app/app/views/portfolio/index.html.erb +33 -0
- data/examples/rails_app/bin/rails +6 -0
- data/examples/rails_app/config/application.rb +45 -0
- data/examples/rails_app/config/boot.rb +5 -0
- data/examples/rails_app/config/database.yml +18 -0
- data/examples/rails_app/config/environment.rb +11 -0
- data/examples/rails_app/config/routes.rb +26 -0
- data/examples/rails_app/config.ru +8 -0
- data/examples/realtime_stream_example.rb +274 -0
- data/examples/sinatra_app/Gemfile +22 -0
- data/examples/sinatra_app/QUICKSTART.md +159 -0
- data/examples/sinatra_app/README.md +461 -0
- data/examples/sinatra_app/app.rb +344 -0
- data/examples/sinatra_app/config.ru +5 -0
- data/examples/sinatra_app/public/css/style.css +659 -0
- data/examples/sinatra_app/public/js/app.js +107 -0
- data/examples/sinatra_app/views/analyze.erb +306 -0
- data/examples/sinatra_app/views/backtest.erb +325 -0
- data/examples/sinatra_app/views/dashboard.erb +419 -0
- data/examples/sinatra_app/views/error.erb +58 -0
- data/examples/sinatra_app/views/index.erb +118 -0
- data/examples/sinatra_app/views/layout.erb +61 -0
- data/examples/sinatra_app/views/portfolio.erb +43 -0
- data/examples/strategy_generator_example.rb +346 -0
- data/hsa_portfolio.csv +11 -0
- data/justfile +0 -0
- data/lib/api/alpha_vantage_api.rb +462 -0
- data/lib/sqa/backtest.rb +329 -0
- data/lib/sqa/data_frame/alpha_vantage.rb +43 -65
- data/lib/sqa/data_frame/data.rb +92 -0
- data/lib/sqa/data_frame/yahoo_finance.rb +35 -43
- data/lib/sqa/data_frame.rb +148 -243
- data/lib/sqa/ensemble.rb +359 -0
- data/lib/sqa/fpop.rb +199 -0
- data/lib/sqa/gp.rb +259 -0
- data/lib/sqa/indicator.rb +5 -8
- data/lib/sqa/init.rb +15 -8
- data/lib/sqa/market_regime.rb +240 -0
- data/lib/sqa/multi_timeframe.rb +379 -0
- data/lib/sqa/pattern_matcher.rb +497 -0
- data/lib/sqa/portfolio.rb +260 -6
- data/lib/sqa/portfolio_optimizer.rb +377 -0
- data/lib/sqa/risk_manager.rb +442 -0
- data/lib/sqa/seasonal_analyzer.rb +209 -0
- data/lib/sqa/sector_analyzer.rb +300 -0
- data/lib/sqa/stock.rb +67 -125
- data/lib/sqa/strategy/bollinger_bands.rb +42 -0
- data/lib/sqa/strategy/consensus.rb +5 -2
- data/lib/sqa/strategy/kbs_strategy.rb +470 -0
- data/lib/sqa/strategy/macd.rb +46 -0
- data/lib/sqa/strategy/mp.rb +1 -1
- data/lib/sqa/strategy/stochastic.rb +60 -0
- data/lib/sqa/strategy/volume_breakout.rb +57 -0
- data/lib/sqa/strategy.rb +5 -0
- data/lib/sqa/strategy_generator.rb +947 -0
- data/lib/sqa/stream.rb +361 -0
- data/lib/sqa/version.rb +1 -7
- data/lib/sqa.rb +23 -16
- data/main.just +81 -0
- data/mkdocs.yml +288 -0
- data/trace.log +0 -0
- metadata +261 -51
- data/bin/sqa +0 -6
- data/lib/patches/dry-cli.rb +0 -228
- data/lib/sqa/activity.rb +0 -10
- data/lib/sqa/cli.rb +0 -62
- data/lib/sqa/commands/analysis.rb +0 -309
- data/lib/sqa/commands/base.rb +0 -139
- data/lib/sqa/commands/web.rb +0 -199
- data/lib/sqa/commands.rb +0 -22
- data/lib/sqa/constants.rb +0 -23
- data/lib/sqa/indicator/average_true_range.rb +0 -33
- data/lib/sqa/indicator/bollinger_bands.rb +0 -28
- data/lib/sqa/indicator/candlestick_pattern_recognizer.rb +0 -60
- data/lib/sqa/indicator/donchian_channel.rb +0 -29
- data/lib/sqa/indicator/double_top_bottom_pattern.rb +0 -34
- data/lib/sqa/indicator/elliott_wave_theory.rb +0 -57
- data/lib/sqa/indicator/exponential_moving_average.rb +0 -25
- data/lib/sqa/indicator/exponential_moving_average_trend.rb +0 -36
- data/lib/sqa/indicator/fibonacci_retracement.rb +0 -23
- data/lib/sqa/indicator/head_and_shoulders_pattern.rb +0 -26
- data/lib/sqa/indicator/market_profile.rb +0 -32
- data/lib/sqa/indicator/mean_reversion.rb +0 -37
- data/lib/sqa/indicator/momentum.rb +0 -28
- data/lib/sqa/indicator/moving_average_convergence_divergence.rb +0 -29
- data/lib/sqa/indicator/peaks_and_valleys.rb +0 -29
- data/lib/sqa/indicator/predict_next_value.rb +0 -202
- data/lib/sqa/indicator/relative_strength_index.rb +0 -47
- data/lib/sqa/indicator/simple_moving_average.rb +0 -24
- data/lib/sqa/indicator/simple_moving_average_trend.rb +0 -32
- data/lib/sqa/indicator/stochastic_oscillator.rb +0 -68
- data/lib/sqa/indicator/true_range.rb +0 -39
- data/lib/sqa/trade.rb +0 -26
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
# DataFrame API Reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`SQA::DataFrame` is a high-performance wrapper around the Polars DataFrame library, specifically optimized for time series financial data manipulation. Polars is a Rust-backed library that provides blazingly fast columnar data operations.
|
|
6
|
+
|
|
7
|
+
The DataFrame system consists of two main components:
|
|
8
|
+
|
|
9
|
+
1. **SQA::DataFrame** - The main wrapper class with financial data convenience methods
|
|
10
|
+
2. **SQA::DataFrame::Data** - Metadata storage for stock information (separate from price data)
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
### Why Polars?
|
|
15
|
+
|
|
16
|
+
Polars provides:
|
|
17
|
+
- **Blazing Speed**: Rust-backed implementation with zero-copy operations
|
|
18
|
+
- **Memory Efficiency**: Columnar storage format optimized for analytics
|
|
19
|
+
- **Lazy Evaluation**: Query optimization before execution
|
|
20
|
+
- **Type Safety**: Strong typing with automatic type inference
|
|
21
|
+
|
|
22
|
+
### Wrapper Benefits
|
|
23
|
+
|
|
24
|
+
`SQA::DataFrame` wraps Polars to provide:
|
|
25
|
+
- Financial data-specific convenience methods
|
|
26
|
+
- Consistent column naming across data sources
|
|
27
|
+
- FPL (Future Period Loss/Profit) analysis methods
|
|
28
|
+
- Seamless integration with SQA workflows
|
|
29
|
+
- Method delegation for full Polars API access
|
|
30
|
+
|
|
31
|
+
## Class: SQA::DataFrame
|
|
32
|
+
|
|
33
|
+
**Location**: `lib/sqa/data_frame.rb`
|
|
34
|
+
|
|
35
|
+
### Instance Attributes
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
df.data # => Polars::DataFrame - The underlying Polars DataFrame
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Direct access to the Polars DataFrame for advanced operations.
|
|
42
|
+
|
|
43
|
+
### Class Methods
|
|
44
|
+
|
|
45
|
+
#### `.new(raw_data = nil, mapping: {}, transformers: {})`
|
|
46
|
+
|
|
47
|
+
Creates a new DataFrame instance with optional column mapping and transformations.
|
|
48
|
+
|
|
49
|
+
**Parameters:**
|
|
50
|
+
- `raw_data` (Hash, Array, Polars::DataFrame, nil) - Initial data
|
|
51
|
+
- `mapping` (Hash) - Column name mappings `{ "source_name" => :target_name }`
|
|
52
|
+
- `transformers` (Hash) - Value transformation lambdas `{ column: ->(v) { transform(v) } }`
|
|
53
|
+
|
|
54
|
+
**Returns:** `SQA::DataFrame` instance
|
|
55
|
+
|
|
56
|
+
**Important:** Columns are renamed FIRST, then transformers are applied. Transformers receive renamed column names.
|
|
57
|
+
|
|
58
|
+
**Example:**
|
|
59
|
+
```ruby
|
|
60
|
+
# From array of hashes
|
|
61
|
+
data = [
|
|
62
|
+
{ 'Date' => '2024-01-01', 'Close' => '150.5' },
|
|
63
|
+
{ 'Date' => '2024-01-02', 'Close' => '152.3' }
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
mapping = { 'Date' => :timestamp, 'Close' => :close_price }
|
|
67
|
+
transformers = { close_price: ->(v) { v.to_f } }
|
|
68
|
+
|
|
69
|
+
df = SQA::DataFrame.new(data, mapping: mapping, transformers: transformers)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### `.load(source:, transformers: {}, mapping: {})`
|
|
73
|
+
|
|
74
|
+
Loads a DataFrame from a CSV file with optional transformations.
|
|
75
|
+
|
|
76
|
+
**Parameters:**
|
|
77
|
+
- `source` (String, Pathname) - Path to CSV file
|
|
78
|
+
- `mapping` (Hash) - Column name mappings (usually empty for cached data)
|
|
79
|
+
- `transformers` (Hash) - Value transformations (usually empty for cached data)
|
|
80
|
+
|
|
81
|
+
**Returns:** `SQA::DataFrame` instance
|
|
82
|
+
|
|
83
|
+
**Note:** For cached CSV files, transformers and mapping should typically be empty since transformations were already applied when the data was first fetched.
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
```ruby
|
|
87
|
+
# Load from cached CSV (no transformations needed)
|
|
88
|
+
df = SQA::DataFrame.load(source: "~/sqa_data/aapl.csv")
|
|
89
|
+
|
|
90
|
+
# Load with migration transformations
|
|
91
|
+
df = SQA::DataFrame.load(
|
|
92
|
+
source: "old_data.csv",
|
|
93
|
+
mapping: { 'date' => :timestamp },
|
|
94
|
+
transformers: { volume: ->(v) { v.to_i } }
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `.from_csv_file(source, mapping: {}, transformers: {})`
|
|
99
|
+
|
|
100
|
+
Alias for `.load()` - loads DataFrame from CSV file.
|
|
101
|
+
|
|
102
|
+
**Example:**
|
|
103
|
+
```ruby
|
|
104
|
+
df = SQA::DataFrame.from_csv_file('stock_data.csv')
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### `.from_json_file(source, mapping: {}, transformers: {})`
|
|
108
|
+
|
|
109
|
+
Loads DataFrame from JSON file containing array of hashes.
|
|
110
|
+
|
|
111
|
+
**Parameters:**
|
|
112
|
+
- `source` (String, Pathname) - Path to JSON file
|
|
113
|
+
- `mapping` (Hash) - Column name mappings
|
|
114
|
+
- `transformers` (Hash) - Value transformations
|
|
115
|
+
|
|
116
|
+
**Returns:** `SQA::DataFrame` instance
|
|
117
|
+
|
|
118
|
+
**Example:**
|
|
119
|
+
```ruby
|
|
120
|
+
# JSON format: [{"date": "2024-01-01", "price": 150.5}, ...]
|
|
121
|
+
df = SQA::DataFrame.from_json_file('prices.json')
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### `.from_aofh(aofh, mapping: {}, transformers: {})`
|
|
125
|
+
|
|
126
|
+
Creates DataFrame from Array of Hashes (AOFH).
|
|
127
|
+
|
|
128
|
+
**Parameters:**
|
|
129
|
+
- `aofh` (Array<Hash>) - Array of hash records
|
|
130
|
+
- `mapping` (Hash) - Column name mappings
|
|
131
|
+
- `transformers` (Hash) - Value transformations
|
|
132
|
+
|
|
133
|
+
**Returns:** `SQA::DataFrame` instance
|
|
134
|
+
|
|
135
|
+
**Example:**
|
|
136
|
+
```ruby
|
|
137
|
+
data = [
|
|
138
|
+
{ date: '2024-01-01', price: 150.5, volume: 1000000 },
|
|
139
|
+
{ date: '2024-01-02', price: 152.0, volume: 1100000 }
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
df = SQA::DataFrame.from_aofh(data)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Instance Methods
|
|
146
|
+
|
|
147
|
+
#### Column Operations
|
|
148
|
+
|
|
149
|
+
##### `#columns`
|
|
150
|
+
|
|
151
|
+
Returns array of column names.
|
|
152
|
+
|
|
153
|
+
**Returns:** `Array<String>`
|
|
154
|
+
|
|
155
|
+
**Example:**
|
|
156
|
+
```ruby
|
|
157
|
+
df.columns
|
|
158
|
+
# => ["timestamp", "open_price", "high_price", "low_price",
|
|
159
|
+
# "close_price", "adj_close_price", "volume"]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
##### `#keys`
|
|
163
|
+
|
|
164
|
+
Alias for `#columns`.
|
|
165
|
+
|
|
166
|
+
##### `#vectors`
|
|
167
|
+
|
|
168
|
+
Alias for `#columns`.
|
|
169
|
+
|
|
170
|
+
##### `#[](column_name)`
|
|
171
|
+
|
|
172
|
+
Access column data (returns Polars::Series).
|
|
173
|
+
|
|
174
|
+
**Parameters:**
|
|
175
|
+
- `column_name` (String) - Name of column
|
|
176
|
+
|
|
177
|
+
**Returns:** `Polars::Series`
|
|
178
|
+
|
|
179
|
+
**Example:**
|
|
180
|
+
```ruby
|
|
181
|
+
# Get close prices
|
|
182
|
+
close_series = df["close_price"]
|
|
183
|
+
|
|
184
|
+
# Convert to array
|
|
185
|
+
prices = df["close_price"].to_a
|
|
186
|
+
# => [150.5, 152.0, 151.5, ...]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
##### `#rename_columns!(mapping)`
|
|
190
|
+
|
|
191
|
+
Renames columns in place according to mapping.
|
|
192
|
+
|
|
193
|
+
**Parameters:**
|
|
194
|
+
- `mapping` (Hash) - Hash of old_name => new_name mappings
|
|
195
|
+
|
|
196
|
+
**Returns:** `nil` (modifies in place)
|
|
197
|
+
|
|
198
|
+
**Important:**
|
|
199
|
+
- Normalizes symbol keys to strings
|
|
200
|
+
- Tries exact match first, then lowercase match
|
|
201
|
+
- Polars requires both keys and values to be strings
|
|
202
|
+
|
|
203
|
+
**Example:**
|
|
204
|
+
```ruby
|
|
205
|
+
mapping = { 'Open' => :open_price, 'Close' => :close_price }
|
|
206
|
+
df.rename_columns!(mapping)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
##### `#apply_transformers!(transformers)`
|
|
210
|
+
|
|
211
|
+
Applies transformation functions to columns in place.
|
|
212
|
+
|
|
213
|
+
**Parameters:**
|
|
214
|
+
- `transformers` (Hash) - Hash of column_name => lambda mappings
|
|
215
|
+
|
|
216
|
+
**Returns:** `nil` (modifies in place)
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
```ruby
|
|
220
|
+
transformers = {
|
|
221
|
+
volume: ->(v) { v.to_i },
|
|
222
|
+
close_price: ->(v) { v.to_f.round(2) }
|
|
223
|
+
}
|
|
224
|
+
df.apply_transformers!(transformers)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### Dimension Methods
|
|
228
|
+
|
|
229
|
+
##### `#size`
|
|
230
|
+
|
|
231
|
+
Returns number of rows.
|
|
232
|
+
|
|
233
|
+
**Returns:** `Integer`
|
|
234
|
+
|
|
235
|
+
**Aliases:** `#nrows`, `#length`
|
|
236
|
+
|
|
237
|
+
**Example:**
|
|
238
|
+
```ruby
|
|
239
|
+
df.size # => 250
|
|
240
|
+
df.nrows # => 250
|
|
241
|
+
df.length # => 250
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
##### `#ncols`
|
|
245
|
+
|
|
246
|
+
Returns number of columns.
|
|
247
|
+
|
|
248
|
+
**Returns:** `Integer`
|
|
249
|
+
|
|
250
|
+
**Example:**
|
|
251
|
+
```ruby
|
|
252
|
+
df.ncols # => 7
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### Data Combination
|
|
256
|
+
|
|
257
|
+
##### `#append!(other_df)`
|
|
258
|
+
|
|
259
|
+
Appends another DataFrame's rows to this one.
|
|
260
|
+
|
|
261
|
+
**Parameters:**
|
|
262
|
+
- `other_df` (SQA::DataFrame) - DataFrame to append
|
|
263
|
+
|
|
264
|
+
**Returns:** `nil` (modifies in place)
|
|
265
|
+
|
|
266
|
+
**Raises:** `RuntimeError` if row count doesn't match expected
|
|
267
|
+
|
|
268
|
+
**Aliases:** `#concat!`
|
|
269
|
+
|
|
270
|
+
**Example:**
|
|
271
|
+
```ruby
|
|
272
|
+
# Combine two DataFrames
|
|
273
|
+
df1.append!(df2)
|
|
274
|
+
|
|
275
|
+
# Verify
|
|
276
|
+
puts df1.size # => original size + df2 size
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### Export Methods
|
|
280
|
+
|
|
281
|
+
##### `#to_csv(path_to_file)`
|
|
282
|
+
|
|
283
|
+
Writes DataFrame to CSV file.
|
|
284
|
+
|
|
285
|
+
**Parameters:**
|
|
286
|
+
- `path_to_file` (String, Pathname) - Destination file path
|
|
287
|
+
|
|
288
|
+
**Returns:** `nil`
|
|
289
|
+
|
|
290
|
+
**Example:**
|
|
291
|
+
```ruby
|
|
292
|
+
df.to_csv("~/sqa_data/aapl_backup.csv")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
##### `#to_h`
|
|
296
|
+
|
|
297
|
+
Converts DataFrame to Hash with symbolized keys.
|
|
298
|
+
|
|
299
|
+
**Returns:** `Hash` - Column name symbols to arrays
|
|
300
|
+
|
|
301
|
+
**Example:**
|
|
302
|
+
```ruby
|
|
303
|
+
df.to_h
|
|
304
|
+
# => {
|
|
305
|
+
# timestamp: ["2024-01-01", "2024-01-02", ...],
|
|
306
|
+
# close_price: [150.5, 152.0, ...],
|
|
307
|
+
# volume: [1000000, 1100000, ...]
|
|
308
|
+
# }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### FPL Analysis Methods
|
|
312
|
+
|
|
313
|
+
##### `#fpl(column: "adj_close_price", fpop: 14)`
|
|
314
|
+
|
|
315
|
+
Calculates Future Period Loss/Profit for each data point.
|
|
316
|
+
|
|
317
|
+
**Parameters:**
|
|
318
|
+
- `column` (String, Symbol) - Price column name
|
|
319
|
+
- `fpop` (Integer) - Future Period of Performance (days to look ahead)
|
|
320
|
+
|
|
321
|
+
**Returns:** `Array<Array<Float, Float>>` - Array of [min_delta, max_delta] pairs
|
|
322
|
+
|
|
323
|
+
**Example:**
|
|
324
|
+
```ruby
|
|
325
|
+
# Look 10 days into the future
|
|
326
|
+
fpl_data = df.fpl(column: "adj_close_price", fpop: 10)
|
|
327
|
+
# => [[-2.5, 5.3], [-1.2, 3.8], ...]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
##### `#fpl_analysis(column: "adj_close_price", fpop: 14)`
|
|
331
|
+
|
|
332
|
+
Comprehensive FPL analysis with risk metrics and direction classification.
|
|
333
|
+
|
|
334
|
+
**Parameters:**
|
|
335
|
+
- `column` (String, Symbol) - Price column name
|
|
336
|
+
- `fpop` (Integer) - Future Period of Performance
|
|
337
|
+
|
|
338
|
+
**Returns:** `Array<Hash>` - Array of analysis hashes
|
|
339
|
+
|
|
340
|
+
**Hash Keys:**
|
|
341
|
+
- `:min_delta` - Minimum future price change %
|
|
342
|
+
- `:max_delta` - Maximum future price change %
|
|
343
|
+
- `:magnitude` - Average expected movement %
|
|
344
|
+
- `:risk` - Volatility range
|
|
345
|
+
- `:direction` - `:UP`, `:DOWN`, `:UNCERTAIN`, or `:FLAT`
|
|
346
|
+
|
|
347
|
+
**Example:**
|
|
348
|
+
```ruby
|
|
349
|
+
analysis = df.fpl_analysis(column: "adj_close_price", fpop: 10)
|
|
350
|
+
|
|
351
|
+
analysis.first
|
|
352
|
+
# => {
|
|
353
|
+
# min_delta: -2.5,
|
|
354
|
+
# max_delta: 5.3,
|
|
355
|
+
# magnitude: 3.9,
|
|
356
|
+
# risk: 7.8,
|
|
357
|
+
# direction: :UP
|
|
358
|
+
# }
|
|
359
|
+
|
|
360
|
+
# Filter high-quality opportunities
|
|
361
|
+
filtered = SQA::FPOP.filter_by_quality(
|
|
362
|
+
analysis,
|
|
363
|
+
min_magnitude: 5.0,
|
|
364
|
+
max_risk: 25.0,
|
|
365
|
+
directions: [:UP]
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### Delegation to Polars
|
|
370
|
+
|
|
371
|
+
Any method not defined on `SQA::DataFrame` is automatically delegated to the underlying `Polars::DataFrame`.
|
|
372
|
+
|
|
373
|
+
**Example:**
|
|
374
|
+
```ruby
|
|
375
|
+
# These call Polars methods directly
|
|
376
|
+
df.head(10) # First 10 rows
|
|
377
|
+
df.tail(5) # Last 5 rows
|
|
378
|
+
df.describe # Statistical summary
|
|
379
|
+
df.filter(...) # Polars filter expression
|
|
380
|
+
df.select(...) # Select columns
|
|
381
|
+
df.with_column(...) # Add computed column
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
See [Polars documentation](https://pola-rs.github.io/polars-book/) for full API.
|
|
385
|
+
|
|
386
|
+
### Class Helper Methods
|
|
387
|
+
|
|
388
|
+
These utility methods are primarily used internally by data source adapters.
|
|
389
|
+
|
|
390
|
+
#### `.generate_mapping(keys)`
|
|
391
|
+
|
|
392
|
+
Generates column name mapping from source keys to underscored symbols.
|
|
393
|
+
|
|
394
|
+
**Parameters:**
|
|
395
|
+
- `keys` (Array) - Array of column names
|
|
396
|
+
|
|
397
|
+
**Returns:** `Hash` - Mapping hash
|
|
398
|
+
|
|
399
|
+
**Example:**
|
|
400
|
+
```ruby
|
|
401
|
+
keys = ['Open', 'High', 'Low', 'Close']
|
|
402
|
+
mapping = SQA::DataFrame.generate_mapping(keys)
|
|
403
|
+
# => { "Open" => :open, "High" => :high, "Low" => :low, "Close" => :close }
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### `.underscore_key(key)`
|
|
407
|
+
|
|
408
|
+
Converts a key to underscored, lowercase symbol.
|
|
409
|
+
|
|
410
|
+
**Parameters:**
|
|
411
|
+
- `key` (String, Symbol) - Key to convert
|
|
412
|
+
|
|
413
|
+
**Returns:** `Symbol`
|
|
414
|
+
|
|
415
|
+
**Example:**
|
|
416
|
+
```ruby
|
|
417
|
+
SQA::DataFrame.underscore_key('AdjClose') # => :adj_close
|
|
418
|
+
SQA::DataFrame.underscore_key('OpenPrice') # => :open_price
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Alias:** `.sanitize_key`
|
|
422
|
+
|
|
423
|
+
#### `.normalize_keys(hash, adapter_mapping: {})`
|
|
424
|
+
|
|
425
|
+
Normalizes hash keys to underscored symbols.
|
|
426
|
+
|
|
427
|
+
**Parameters:**
|
|
428
|
+
- `hash` (Hash) - Hash to normalize
|
|
429
|
+
- `adapter_mapping` (Hash) - Optional custom mappings
|
|
430
|
+
|
|
431
|
+
**Returns:** `Hash` - Hash with normalized keys
|
|
432
|
+
|
|
433
|
+
#### `.rename(hash, mapping)`
|
|
434
|
+
|
|
435
|
+
Renames hash keys according to mapping.
|
|
436
|
+
|
|
437
|
+
**Parameters:**
|
|
438
|
+
- `hash` (Hash) - Hash to modify
|
|
439
|
+
- `mapping` (Hash) - Key mappings
|
|
440
|
+
|
|
441
|
+
**Returns:** `Hash` - Modified hash
|
|
442
|
+
|
|
443
|
+
#### `.is_date?(value)`
|
|
444
|
+
|
|
445
|
+
Checks if value looks like a date string (YYYY-MM-DD format).
|
|
446
|
+
|
|
447
|
+
**Parameters:**
|
|
448
|
+
- `value` (String) - Value to check
|
|
449
|
+
|
|
450
|
+
**Returns:** `Boolean`
|
|
451
|
+
|
|
452
|
+
**Example:**
|
|
453
|
+
```ruby
|
|
454
|
+
SQA::DataFrame.is_date?("2024-01-01") # => true
|
|
455
|
+
SQA::DataFrame.is_date?("150.5") # => false
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Class: SQA::DataFrame::Data
|
|
461
|
+
|
|
462
|
+
**Location**: `lib/sqa/data_frame/data.rb`
|
|
463
|
+
|
|
464
|
+
A metadata storage class for stock information, completely separate from the price/volume DataFrame.
|
|
465
|
+
|
|
466
|
+
### Attributes
|
|
467
|
+
|
|
468
|
+
All attributes are read/write accessible via `attr_accessor`:
|
|
469
|
+
|
|
470
|
+
- `ticker` (String) - Stock symbol (e.g., 'AAPL', 'MSFT')
|
|
471
|
+
- `name` (String) - Company name
|
|
472
|
+
- `exchange` (String) - Exchange symbol (NASDAQ, NYSE, etc.)
|
|
473
|
+
- `source` (Symbol) - Data source (`:alpha_vantage`, `:yahoo_finance`)
|
|
474
|
+
- `indicators` (Hash) - Technical indicators configuration
|
|
475
|
+
- `overview` (Hash) - Company overview data from Alpha Vantage
|
|
476
|
+
|
|
477
|
+
### Instance Methods
|
|
478
|
+
|
|
479
|
+
#### `.new(data_hash = nil, ticker: nil, name: nil, exchange: nil, source: :alpha_vantage, indicators: {}, overview: {})`
|
|
480
|
+
|
|
481
|
+
Dual initialization constructor supporting both hash and keyword arguments.
|
|
482
|
+
|
|
483
|
+
**From Hash (JSON deserialization):**
|
|
484
|
+
```ruby
|
|
485
|
+
json_data = JSON.parse(File.read('aapl.json'))
|
|
486
|
+
data = SQA::DataFrame::Data.new(json_data)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**From Keyword Arguments:**
|
|
490
|
+
```ruby
|
|
491
|
+
data = SQA::DataFrame::Data.new(
|
|
492
|
+
ticker: 'AAPL',
|
|
493
|
+
source: :alpha_vantage,
|
|
494
|
+
indicators: { rsi: 14, sma: [20, 50] }
|
|
495
|
+
)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### `#to_json(*args)`
|
|
499
|
+
|
|
500
|
+
Serializes metadata to JSON string.
|
|
501
|
+
|
|
502
|
+
**Returns:** `String` - JSON representation
|
|
503
|
+
|
|
504
|
+
**Example:**
|
|
505
|
+
```ruby
|
|
506
|
+
json_string = data.to_json
|
|
507
|
+
File.write('aapl.json', json_string)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### `#to_h`
|
|
511
|
+
|
|
512
|
+
Converts metadata to Hash.
|
|
513
|
+
|
|
514
|
+
**Returns:** `Hash` - Hash representation
|
|
515
|
+
|
|
516
|
+
**Example:**
|
|
517
|
+
```ruby
|
|
518
|
+
hash = data.to_h
|
|
519
|
+
# => {
|
|
520
|
+
# ticker: 'AAPL',
|
|
521
|
+
# name: 'Apple Inc.',
|
|
522
|
+
# exchange: 'NASDAQ',
|
|
523
|
+
# source: :alpha_vantage,
|
|
524
|
+
# indicators: { rsi: 14 },
|
|
525
|
+
# overview: { ... }
|
|
526
|
+
# }
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Usage in SQA::Stock
|
|
530
|
+
|
|
531
|
+
`SQA::Stock` uses `DataFrame::Data` to persist metadata separately from price data:
|
|
532
|
+
|
|
533
|
+
**Persistence Pattern:**
|
|
534
|
+
```ruby
|
|
535
|
+
# In SQA::Stock
|
|
536
|
+
@data_path = SQA.data_dir + "#{@ticker}.json" # Metadata file
|
|
537
|
+
@df_path = SQA.data_dir + "#{@ticker}.csv" # Price data file
|
|
538
|
+
|
|
539
|
+
# Save metadata
|
|
540
|
+
@data_path.write(@data.to_json)
|
|
541
|
+
|
|
542
|
+
# Load metadata
|
|
543
|
+
@data = SQA::DataFrame::Data.new(JSON.parse(@data_path.read))
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Data Source Adapters
|
|
549
|
+
|
|
550
|
+
### Alpha Vantage Adapter
|
|
551
|
+
|
|
552
|
+
**Location**: `lib/sqa/data_frame/alpha_vantage.rb`
|
|
553
|
+
|
|
554
|
+
**Class:** `SQA::DataFrame::AlphaVantage`
|
|
555
|
+
|
|
556
|
+
#### Constants
|
|
557
|
+
|
|
558
|
+
```ruby
|
|
559
|
+
# Standard column headers
|
|
560
|
+
HEADERS = [
|
|
561
|
+
:timestamp, # 0
|
|
562
|
+
:open_price, # 1
|
|
563
|
+
:high_price, # 2
|
|
564
|
+
:low_price, # 3
|
|
565
|
+
:close_price, # 4
|
|
566
|
+
:adj_close_price, # 5
|
|
567
|
+
:volume # 6
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
# Maps Alpha Vantage CSV columns to standard headers
|
|
571
|
+
HEADER_MAPPING = {
|
|
572
|
+
"timestamp" => HEADERS[0], # :timestamp
|
|
573
|
+
"open" => HEADERS[1], # :open_price
|
|
574
|
+
"high" => HEADERS[2], # :high_price
|
|
575
|
+
"low" => HEADERS[3], # :low_price
|
|
576
|
+
"close" => HEADERS[4], # :close_price
|
|
577
|
+
"volume" => HEADERS[6] # :volume
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
# Value transformations applied after renaming
|
|
581
|
+
TRANSFORMERS = {
|
|
582
|
+
HEADERS[1] => ->(v) { v.to_f.round(3) }, # :open_price
|
|
583
|
+
HEADERS[2] => ->(v) { v.to_f.round(3) }, # :high_price
|
|
584
|
+
HEADERS[3] => ->(v) { v.to_f.round(3) }, # :low_price
|
|
585
|
+
HEADERS[4] => ->(v) { v.to_f.round(3) }, # :close_price
|
|
586
|
+
HEADERS[6] => ->(v) { v.to_i } # :volume
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
#### `.recent(ticker, full: false, from_date: nil)`
|
|
591
|
+
|
|
592
|
+
Fetches recent price data from Alpha Vantage API.
|
|
593
|
+
|
|
594
|
+
**Parameters:**
|
|
595
|
+
- `ticker` (String) - Stock symbol
|
|
596
|
+
- `full` (Boolean) - If true, fetches full history; otherwise last 100 days
|
|
597
|
+
- `from_date` (Date, String, nil) - Optional date filter
|
|
598
|
+
|
|
599
|
+
**Returns:** `SQA::DataFrame` - Wrapped DataFrame with standardized columns
|
|
600
|
+
|
|
601
|
+
**Requirements:**
|
|
602
|
+
- Environment variable: `AV_API_KEY` or `ALPHAVANTAGE_API_KEY`
|
|
603
|
+
- Free tier: 5 calls/minute, 500 calls/day
|
|
604
|
+
|
|
605
|
+
**Important:** Alpha Vantage doesn't provide separate adjusted close, so `close_price` is duplicated as `adj_close_price` for compatibility.
|
|
606
|
+
|
|
607
|
+
**Example:**
|
|
608
|
+
```ruby
|
|
609
|
+
# Fetch recent 100 days
|
|
610
|
+
df = SQA::DataFrame::AlphaVantage.recent('AAPL')
|
|
611
|
+
|
|
612
|
+
# Fetch full history
|
|
613
|
+
df = SQA::DataFrame::AlphaVantage.recent('AAPL', full: true)
|
|
614
|
+
|
|
615
|
+
# Fetch from specific date
|
|
616
|
+
df = SQA::DataFrame::AlphaVantage.recent('AAPL', from_date: '2024-01-01')
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Yahoo Finance Adapter
|
|
620
|
+
|
|
621
|
+
**Location**: `lib/sqa/data_frame/yahoo_finance.rb`
|
|
622
|
+
|
|
623
|
+
**Class:** `SQA::DataFrame::YahooFinance`
|
|
624
|
+
|
|
625
|
+
#### Constants
|
|
626
|
+
|
|
627
|
+
```ruby
|
|
628
|
+
HEADERS = [
|
|
629
|
+
:timestamp, # 0
|
|
630
|
+
:open_price, # 1
|
|
631
|
+
:high_price, # 2
|
|
632
|
+
:low_price, # 3
|
|
633
|
+
:close_price, # 4
|
|
634
|
+
:adj_close_price, # 5
|
|
635
|
+
:volume # 6
|
|
636
|
+
]
|
|
637
|
+
|
|
638
|
+
HEADER_MAPPING = {
|
|
639
|
+
"Date" => HEADERS[0],
|
|
640
|
+
"Open" => HEADERS[1],
|
|
641
|
+
"High" => HEADERS[2],
|
|
642
|
+
"Low" => HEADERS[3],
|
|
643
|
+
"Close" => HEADERS[4],
|
|
644
|
+
"Adj Close" => HEADERS[5],
|
|
645
|
+
"Volume" => HEADERS[6]
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
#### `.recent(ticker)`
|
|
650
|
+
|
|
651
|
+
Scrapes recent price data from Yahoo Finance website.
|
|
652
|
+
|
|
653
|
+
**Parameters:**
|
|
654
|
+
- `ticker` (String) - Stock symbol
|
|
655
|
+
|
|
656
|
+
**Returns:** `SQA::DataFrame` - Wrapped DataFrame with standardized columns
|
|
657
|
+
|
|
658
|
+
**Note:** Web scraping based, less reliable than API but requires no API key.
|
|
659
|
+
|
|
660
|
+
**Example:**
|
|
661
|
+
```ruby
|
|
662
|
+
df = SQA::DataFrame::YahooFinance.recent('AAPL')
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Creating Custom Adapters
|
|
666
|
+
|
|
667
|
+
To add a new data source:
|
|
668
|
+
|
|
669
|
+
1. Create `lib/sqa/data_frame/my_source.rb`
|
|
670
|
+
2. Define constants: `HEADERS`, `HEADER_MAPPING`, `TRANSFORMERS`
|
|
671
|
+
3. Implement `.recent(ticker, **options)` class method
|
|
672
|
+
4. **MUST** return `SQA::DataFrame`, not raw Polars::DataFrame
|
|
673
|
+
|
|
674
|
+
**Example Template:**
|
|
675
|
+
```ruby
|
|
676
|
+
class SQA::DataFrame::MySource
|
|
677
|
+
HEADERS = [
|
|
678
|
+
:timestamp,
|
|
679
|
+
:open_price,
|
|
680
|
+
:high_price,
|
|
681
|
+
:low_price,
|
|
682
|
+
:close_price,
|
|
683
|
+
:adj_close_price,
|
|
684
|
+
:volume
|
|
685
|
+
]
|
|
686
|
+
|
|
687
|
+
HEADER_MAPPING = {
|
|
688
|
+
"date" => HEADERS[0],
|
|
689
|
+
"open" => HEADERS[1],
|
|
690
|
+
# ... map source columns to standard headers
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
TRANSFORMERS = {
|
|
694
|
+
HEADERS[1] => ->(v) { v.to_f.round(3) },
|
|
695
|
+
HEADERS[6] => ->(v) { v.to_i }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
def self.recent(ticker, **options)
|
|
699
|
+
# 1. Fetch data from API/source
|
|
700
|
+
raw_data = fetch_from_source(ticker)
|
|
701
|
+
|
|
702
|
+
# 2. Convert to Polars DataFrame
|
|
703
|
+
polars_df = Polars.read_csv(StringIO.new(raw_data))
|
|
704
|
+
|
|
705
|
+
# 3. MUST wrap in SQA::DataFrame with mapping and transformers
|
|
706
|
+
sqa_df = SQA::DataFrame.new(
|
|
707
|
+
polars_df,
|
|
708
|
+
mapping: HEADER_MAPPING,
|
|
709
|
+
transformers: TRANSFORMERS
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# 4. Add any missing columns if needed
|
|
713
|
+
# Example: Alpha Vantage doesn't have adj_close_price
|
|
714
|
+
# sqa_df.data = sqa_df.data.with_column(
|
|
715
|
+
# sqa_df.data["close_price"].alias("adj_close_price")
|
|
716
|
+
# )
|
|
717
|
+
|
|
718
|
+
# 5. Return wrapped DataFrame
|
|
719
|
+
sqa_df
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## Usage Examples
|
|
727
|
+
|
|
728
|
+
### Basic Workflow
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
require 'sqa'
|
|
732
|
+
|
|
733
|
+
SQA.init
|
|
734
|
+
|
|
735
|
+
# Load stock (fetches from Alpha Vantage by default)
|
|
736
|
+
stock = SQA::Stock.new(ticker: 'AAPL')
|
|
737
|
+
|
|
738
|
+
# Access DataFrame
|
|
739
|
+
df = stock.df
|
|
740
|
+
|
|
741
|
+
# Get dimensions
|
|
742
|
+
puts "Rows: #{df.size}, Columns: #{df.ncols}"
|
|
743
|
+
# => Rows: 250, Columns: 7
|
|
744
|
+
|
|
745
|
+
# Get column names
|
|
746
|
+
puts df.columns.join(", ")
|
|
747
|
+
# => timestamp, open_price, high_price, low_price, close_price, adj_close_price, volume
|
|
748
|
+
|
|
749
|
+
# Extract price array for indicators
|
|
750
|
+
prices = df["adj_close_price"].to_a
|
|
751
|
+
|
|
752
|
+
# Calculate technical indicators (via sqa-tai gem)
|
|
753
|
+
sma_20 = SQAI.sma(prices, period: 20)
|
|
754
|
+
rsi_14 = SQAI.rsi(prices, period: 14)
|
|
755
|
+
|
|
756
|
+
puts "Current Price: #{prices.last}"
|
|
757
|
+
puts "20-day SMA: #{sma_20.last}"
|
|
758
|
+
puts "14-day RSI: #{rsi_14.last}"
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Working with Polars Directly
|
|
762
|
+
|
|
763
|
+
```ruby
|
|
764
|
+
# Access underlying Polars DataFrame
|
|
765
|
+
polars_df = df.data
|
|
766
|
+
|
|
767
|
+
# Filter using Polars expressions
|
|
768
|
+
high_volume = polars_df.filter(
|
|
769
|
+
Polars.col("volume") > 10_000_000
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# Calculate statistics
|
|
773
|
+
avg_close = polars_df["close_price"].mean
|
|
774
|
+
max_high = polars_df["high_price"].max
|
|
775
|
+
total_volume = polars_df["volume"].sum
|
|
776
|
+
|
|
777
|
+
# Add computed columns
|
|
778
|
+
polars_df = polars_df.with_column(
|
|
779
|
+
(Polars.col("close_price") - Polars.col("open_price"))
|
|
780
|
+
.alias("daily_change")
|
|
781
|
+
)
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### FPL Analysis Workflow
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
# Get FPL analysis
|
|
788
|
+
fpl_analysis = df.fpl_analysis(column: "adj_close_price", fpop: 10)
|
|
789
|
+
|
|
790
|
+
# Find high-quality opportunities
|
|
791
|
+
opportunities = SQA::FPOP.filter_by_quality(
|
|
792
|
+
fpl_analysis,
|
|
793
|
+
min_magnitude: 5.0, # At least 5% expected move
|
|
794
|
+
max_risk: 25.0, # Max 25% risk range
|
|
795
|
+
directions: [:UP] # Only upward moves
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
puts "Found #{opportunities.size} high-quality opportunities"
|
|
799
|
+
|
|
800
|
+
opportunities.each_with_index do |opp, idx|
|
|
801
|
+
puts "\nOpportunity ##{idx + 1}:"
|
|
802
|
+
puts " Expected Move: #{opp[:magnitude].round(2)}%"
|
|
803
|
+
puts " Risk: #{opp[:risk].round(2)}%"
|
|
804
|
+
puts " Direction: #{opp[:direction]}"
|
|
805
|
+
puts " Range: #{opp[:min_delta].round(2)}% to #{opp[:max_delta].round(2)}%"
|
|
806
|
+
end
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Data Export and Import
|
|
810
|
+
|
|
811
|
+
```ruby
|
|
812
|
+
# Export to CSV
|
|
813
|
+
df.to_csv("aapl_prices.csv")
|
|
814
|
+
|
|
815
|
+
# Export to Hash
|
|
816
|
+
hash = df.to_h
|
|
817
|
+
File.write("aapl_prices.json", hash.to_json)
|
|
818
|
+
|
|
819
|
+
# Load from CSV
|
|
820
|
+
df = SQA::DataFrame.load(source: "aapl_prices.csv")
|
|
821
|
+
|
|
822
|
+
# Load from JSON
|
|
823
|
+
df = SQA::DataFrame.from_json_file("aapl_prices.json")
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Combining DataFrames
|
|
827
|
+
|
|
828
|
+
```ruby
|
|
829
|
+
# Load historical data
|
|
830
|
+
historical_df = SQA::DataFrame.load(source: "aapl_historical.csv")
|
|
831
|
+
|
|
832
|
+
# Fetch recent updates
|
|
833
|
+
recent_df = SQA::DataFrame::AlphaVantage.recent('AAPL')
|
|
834
|
+
|
|
835
|
+
# Combine
|
|
836
|
+
historical_df.append!(recent_df)
|
|
837
|
+
|
|
838
|
+
# Save updated dataset
|
|
839
|
+
historical_df.to_csv("aapl_updated.csv")
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Performance Considerations
|
|
845
|
+
|
|
846
|
+
### 1. Use Column Operations
|
|
847
|
+
|
|
848
|
+
**Good:**
|
|
849
|
+
```ruby
|
|
850
|
+
# Vectorized operation (fast)
|
|
851
|
+
avg = df.data["close_price"].mean
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
**Bad:**
|
|
855
|
+
```ruby
|
|
856
|
+
# Ruby loop (slow)
|
|
857
|
+
prices = df["close_price"].to_a
|
|
858
|
+
avg = prices.sum / prices.size.to_f
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### 2. Minimize Array Conversions
|
|
862
|
+
|
|
863
|
+
Only convert to arrays when necessary (e.g., passing to external functions):
|
|
864
|
+
|
|
865
|
+
```ruby
|
|
866
|
+
# Only convert for indicators
|
|
867
|
+
prices = df["adj_close_price"].to_a
|
|
868
|
+
rsi = SQAI.rsi(prices, period: 14)
|
|
869
|
+
|
|
870
|
+
# Use Polars for everything else
|
|
871
|
+
avg = df.data["adj_close_price"].mean # No conversion needed
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### 3. Batch Operations
|
|
875
|
+
|
|
876
|
+
Combine operations when possible:
|
|
877
|
+
|
|
878
|
+
```ruby
|
|
879
|
+
# Apply all transformations at once
|
|
880
|
+
df = SQA::DataFrame.new(
|
|
881
|
+
raw_data,
|
|
882
|
+
mapping: mapping,
|
|
883
|
+
transformers: transformers
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Instead of separate calls
|
|
887
|
+
df.rename_columns!(mapping)
|
|
888
|
+
df.apply_transformers!(transformers)
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### 4. Use Polars Native Operations
|
|
892
|
+
|
|
893
|
+
Leverage Polars' lazy evaluation and query optimization:
|
|
894
|
+
|
|
895
|
+
```ruby
|
|
896
|
+
# Polars can optimize this entire chain
|
|
897
|
+
result = df.data
|
|
898
|
+
.filter(Polars.col("volume") > 1_000_000)
|
|
899
|
+
.select(["timestamp", "close_price"])
|
|
900
|
+
.head(100)
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
### 5. Avoid Repeated Column Access
|
|
904
|
+
|
|
905
|
+
Cache column data if used multiple times:
|
|
906
|
+
|
|
907
|
+
```ruby
|
|
908
|
+
# Good: cache the series
|
|
909
|
+
close_prices = df["close_price"]
|
|
910
|
+
avg = close_prices.mean
|
|
911
|
+
max = close_prices.max
|
|
912
|
+
min = close_prices.min
|
|
913
|
+
|
|
914
|
+
# Bad: repeated access
|
|
915
|
+
avg = df["close_price"].mean
|
|
916
|
+
max = df["close_price"].max
|
|
917
|
+
min = df["close_price"].min
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
---
|
|
921
|
+
|
|
922
|
+
## Common Gotchas
|
|
923
|
+
|
|
924
|
+
### 1. DataFrame vs Polars
|
|
925
|
+
|
|
926
|
+
```ruby
|
|
927
|
+
df # => SQA::DataFrame (wrapper)
|
|
928
|
+
df.data # => Polars::DataFrame (underlying data)
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
Use `df.data` for direct Polars operations.
|
|
932
|
+
|
|
933
|
+
### 2. Column Names are Strings
|
|
934
|
+
|
|
935
|
+
```ruby
|
|
936
|
+
# Correct
|
|
937
|
+
df["close_price"]
|
|
938
|
+
|
|
939
|
+
# Wrong
|
|
940
|
+
df[:close_price] # Polars uses strings, not symbols
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### 3. Transformers Expect Renamed Columns
|
|
944
|
+
|
|
945
|
+
Order matters in initialization:
|
|
946
|
+
1. Columns are renamed FIRST
|
|
947
|
+
2. Then transformers are applied
|
|
948
|
+
|
|
949
|
+
Transformers receive the NEW column names, not the original names.
|
|
950
|
+
|
|
951
|
+
```ruby
|
|
952
|
+
mapping = { 'Close' => :close_price }
|
|
953
|
+
transformers = { close_price: ->(v) { v.to_f } } # Use renamed name
|
|
954
|
+
|
|
955
|
+
df = SQA::DataFrame.new(data, mapping: mapping, transformers: transformers)
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
### 4. Indicators Need Arrays
|
|
959
|
+
|
|
960
|
+
All SQAI/TAI indicator functions require Ruby arrays:
|
|
961
|
+
|
|
962
|
+
```ruby
|
|
963
|
+
# Correct
|
|
964
|
+
prices = df["adj_close_price"].to_a
|
|
965
|
+
rsi = SQAI.rsi(prices, period: 14)
|
|
966
|
+
|
|
967
|
+
# Wrong
|
|
968
|
+
rsi = SQAI.rsi(df["adj_close_price"], period: 14) # Series not supported
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### 5. Method Delegation
|
|
972
|
+
|
|
973
|
+
Unknown methods are delegated to `Polars::DataFrame`:
|
|
974
|
+
|
|
975
|
+
```ruby
|
|
976
|
+
# These work via delegation
|
|
977
|
+
df.head(10)
|
|
978
|
+
df.describe
|
|
979
|
+
|
|
980
|
+
# Check Polars docs for advanced features
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### 6. CSV Round-Trip Considerations
|
|
984
|
+
|
|
985
|
+
When loading cached CSV files, don't reapply transformers:
|
|
986
|
+
|
|
987
|
+
```ruby
|
|
988
|
+
# First time: apply transformers
|
|
989
|
+
df = SQA::DataFrame::AlphaVantage.recent('AAPL')
|
|
990
|
+
df.to_csv("aapl.csv")
|
|
991
|
+
|
|
992
|
+
# Later: don't reapply transformers (already applied)
|
|
993
|
+
df = SQA::DataFrame.load(source: "aapl.csv")
|
|
994
|
+
# NOT: load(source: "aapl.csv", transformers: TRANSFORMERS)
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
### 7. Data Source Return Types
|
|
998
|
+
|
|
999
|
+
All data source adapters MUST return `SQA::DataFrame`, not raw `Polars::DataFrame`:
|
|
1000
|
+
|
|
1001
|
+
```ruby
|
|
1002
|
+
# Correct
|
|
1003
|
+
def self.recent(ticker)
|
|
1004
|
+
polars_df = fetch_data(ticker)
|
|
1005
|
+
SQA::DataFrame.new(polars_df, mapping: MAPPING) # Wrap it!
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
# Wrong
|
|
1009
|
+
def self.recent(ticker)
|
|
1010
|
+
fetch_data(ticker) # Returns Polars::DataFrame
|
|
1011
|
+
end
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
---
|
|
1015
|
+
|
|
1016
|
+
## Recent Fixes (2024-11)
|
|
1017
|
+
|
|
1018
|
+
The DataFrame architecture underwent significant fixes to resolve type safety issues:
|
|
1019
|
+
|
|
1020
|
+
### Issue 1: Missing DataFrame::Data Class
|
|
1021
|
+
**Problem:** Stock metadata class didn't exist
|
|
1022
|
+
**Fix:** Created `SQA::DataFrame::Data` with dual initialization
|
|
1023
|
+
|
|
1024
|
+
### Issue 2: Type Mismatches
|
|
1025
|
+
**Problem:** Adapters returned `Polars::DataFrame` instead of `SQA::DataFrame`
|
|
1026
|
+
**Fix:** All adapters now wrap DataFrames before returning
|
|
1027
|
+
|
|
1028
|
+
### Issue 3: Missing .load() Method
|
|
1029
|
+
**Problem:** Stock tried to call non-existent `.load()` method
|
|
1030
|
+
**Fix:** Added class method with proper signature
|
|
1031
|
+
|
|
1032
|
+
### Issue 4: Column Mapping Order
|
|
1033
|
+
**Problem:** Transformers applied before column renaming
|
|
1034
|
+
**Fix:** Renamed columns FIRST, then apply transformers
|
|
1035
|
+
|
|
1036
|
+
### Issue 5: Key Type Mismatches
|
|
1037
|
+
**Problem:** Symbol keys used where Polars expects strings
|
|
1038
|
+
**Fix:** Convert all keys to strings in `rename_columns!()`
|
|
1039
|
+
|
|
1040
|
+
### Issue 6: Incorrect Polars API Usage
|
|
1041
|
+
**Problem:** Used `Polars::DataFrame.read_csv()` and `df["col"].gt_eq()`
|
|
1042
|
+
**Fix:** Use `Polars.read_csv()` and `Polars.col("col") >=`
|
|
1043
|
+
|
|
1044
|
+
See `DATAFRAME_ARCHITECTURE_REVIEW.md` for detailed analysis.
|
|
1045
|
+
|
|
1046
|
+
---
|
|
1047
|
+
|
|
1048
|
+
## Related Documentation
|
|
1049
|
+
|
|
1050
|
+
- [Stock Class](stock.md) - Using DataFrames with Stock objects
|
|
1051
|
+
- [FPL Analysis](../advanced/fpop.md) - Future Period Loss/Profit utilities
|
|
1052
|
+
- [Technical Indicators](../indicators/index.md) - SQAI/TAI integration
|
|
1053
|
+
- [Polars Documentation](https://pola-rs.github.io/polars-book/) - Underlying library
|
|
1054
|
+
- [Data Sources](../data-sources/index.md) - Alpha Vantage and Yahoo Finance
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## Complete Example
|
|
1059
|
+
|
|
1060
|
+
```ruby
|
|
1061
|
+
require 'sqa'
|
|
1062
|
+
|
|
1063
|
+
# Initialize
|
|
1064
|
+
SQA.init
|
|
1065
|
+
|
|
1066
|
+
# Load stock data
|
|
1067
|
+
stock = SQA::Stock.new(ticker: 'AAPL')
|
|
1068
|
+
df = stock.df
|
|
1069
|
+
|
|
1070
|
+
puts "=== Stock Information ==="
|
|
1071
|
+
puts "Ticker: #{stock.ticker}"
|
|
1072
|
+
puts "Exchange: #{stock.exchange}"
|
|
1073
|
+
puts "Source: #{stock.source}"
|
|
1074
|
+
puts "Data Points: #{df.size}"
|
|
1075
|
+
|
|
1076
|
+
puts "\n=== Price Data ==="
|
|
1077
|
+
prices = df["adj_close_price"].to_a
|
|
1078
|
+
puts "Current Price: $#{prices.last.round(2)}"
|
|
1079
|
+
puts "52-Week High: $#{prices.max.round(2)}"
|
|
1080
|
+
puts "52-Week Low: $#{prices.min.round(2)}"
|
|
1081
|
+
|
|
1082
|
+
puts "\n=== Technical Indicators ==="
|
|
1083
|
+
sma_20 = SQAI.sma(prices, period: 20)
|
|
1084
|
+
sma_50 = SQAI.sma(prices, period: 50)
|
|
1085
|
+
rsi_14 = SQAI.rsi(prices, period: 14)
|
|
1086
|
+
|
|
1087
|
+
puts "20-day SMA: $#{sma_20.last.round(2)}"
|
|
1088
|
+
puts "50-day SMA: $#{sma_50.last.round(2)}"
|
|
1089
|
+
puts "14-day RSI: #{rsi_14.last.round(2)}"
|
|
1090
|
+
|
|
1091
|
+
puts "\n=== FPL Analysis ==="
|
|
1092
|
+
fpl_analysis = df.fpl_analysis(column: "adj_close_price", fpop: 10)
|
|
1093
|
+
latest = fpl_analysis.last
|
|
1094
|
+
|
|
1095
|
+
puts "10-Day Forecast:"
|
|
1096
|
+
puts " Direction: #{latest[:direction]}"
|
|
1097
|
+
puts " Expected Move: #{latest[:magnitude].round(2)}%"
|
|
1098
|
+
puts " Risk: #{latest[:risk].round(2)}%"
|
|
1099
|
+
puts " Range: #{latest[:min_delta].round(2)}% to #{latest[:max_delta].round(2)}%"
|
|
1100
|
+
|
|
1101
|
+
# Export data
|
|
1102
|
+
df.to_csv("aapl_export.csv")
|
|
1103
|
+
File.write("aapl_metadata.json", stock.data.to_json)
|
|
1104
|
+
|
|
1105
|
+
puts "\n=== Export Complete ==="
|
|
1106
|
+
puts "Data saved to aapl_export.csv"
|
|
1107
|
+
puts "Metadata saved to aapl_metadata.json"
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
**See Also:**
|
|
1113
|
+
- [Getting Started Guide](../getting-started/quick-start.md)
|
|
1114
|
+
- [Examples Directory](https://github.com/MadBomber/sqa/tree/main/examples)
|
|
1115
|
+
- [Contributing Guide](../contributing/index.md)
|