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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.goose/memory/development.txt +3 -0
  3. data/.semver +6 -0
  4. data/ARCHITECTURE.md +648 -0
  5. data/CHANGELOG.md +82 -0
  6. data/CLAUDE.md +653 -0
  7. data/COMMITS.md +196 -0
  8. data/DATAFRAME_ARCHITECTURE_REVIEW.md +421 -0
  9. data/NEXT-STEPS.md +154 -0
  10. data/README.md +812 -262
  11. data/TASKS.md +358 -0
  12. data/TEST_RESULTS.md +140 -0
  13. data/TODO.md +42 -0
  14. data/_notes.txt +25 -0
  15. data/bin/sqa-console +11 -0
  16. data/data/talk_talk.json +103284 -0
  17. data/develop_summary.md +313 -0
  18. data/docs/advanced/backtesting.md +206 -0
  19. data/docs/advanced/ensemble.md +68 -0
  20. data/docs/advanced/fpop.md +153 -0
  21. data/docs/advanced/index.md +112 -0
  22. data/docs/advanced/multi-timeframe.md +67 -0
  23. data/docs/advanced/pattern-matcher.md +75 -0
  24. data/docs/advanced/portfolio-optimizer.md +79 -0
  25. data/docs/advanced/portfolio.md +166 -0
  26. data/docs/advanced/risk-management.md +210 -0
  27. data/docs/advanced/strategy-generator.md +158 -0
  28. data/docs/advanced/streaming.md +209 -0
  29. data/docs/ai_and_ml.md +80 -0
  30. data/docs/api/dataframe.md +1115 -0
  31. data/docs/api/index.md +126 -0
  32. data/docs/assets/css/custom.css +88 -0
  33. data/docs/assets/js/mathjax.js +18 -0
  34. data/docs/concepts/index.md +68 -0
  35. data/docs/contributing/index.md +60 -0
  36. data/docs/data-sources/index.md +66 -0
  37. data/docs/data_frame.md +317 -97
  38. data/docs/factors_that_impact_price.md +26 -0
  39. data/docs/finviz.md +11 -0
  40. data/docs/fx_pro_bit.md +25 -0
  41. data/docs/genetic_programming.md +104 -0
  42. data/docs/getting-started/index.md +123 -0
  43. data/docs/getting-started/installation.md +229 -0
  44. data/docs/getting-started/quick-start.md +244 -0
  45. data/docs/i_gotta_an_idea.md +22 -0
  46. data/docs/index.md +163 -0
  47. data/docs/indicators/index.md +97 -0
  48. data/docs/indicators.md +110 -24
  49. data/docs/options.md +8 -0
  50. data/docs/strategies/bollinger-bands.md +146 -0
  51. data/docs/strategies/consensus.md +64 -0
  52. data/docs/strategies/custom.md +310 -0
  53. data/docs/strategies/ema.md +53 -0
  54. data/docs/strategies/index.md +92 -0
  55. data/docs/strategies/kbs.md +164 -0
  56. data/docs/strategies/macd.md +96 -0
  57. data/docs/strategies/market-profile.md +54 -0
  58. data/docs/strategies/mean-reversion.md +58 -0
  59. data/docs/strategies/rsi.md +95 -0
  60. data/docs/strategies/sma.md +55 -0
  61. data/docs/strategies/stochastic.md +63 -0
  62. data/docs/strategies/volume-breakout.md +54 -0
  63. data/docs/tags.md +7 -0
  64. data/docs/true_strength_index.md +46 -0
  65. data/docs/weighted_moving_average.md +48 -0
  66. data/examples/README.md +354 -0
  67. data/examples/advanced_features_example.rb +350 -0
  68. data/examples/fpop_analysis_example.rb +191 -0
  69. data/examples/genetic_programming_example.rb +148 -0
  70. data/examples/kbs_strategy_example.rb +208 -0
  71. data/examples/pattern_context_example.rb +300 -0
  72. data/examples/rails_app/Gemfile +34 -0
  73. data/examples/rails_app/README.md +416 -0
  74. data/examples/rails_app/app/assets/javascripts/application.js +107 -0
  75. data/examples/rails_app/app/assets/stylesheets/application.css +659 -0
  76. data/examples/rails_app/app/controllers/analysis_controller.rb +11 -0
  77. data/examples/rails_app/app/controllers/api/v1/stocks_controller.rb +227 -0
  78. data/examples/rails_app/app/controllers/application_controller.rb +22 -0
  79. data/examples/rails_app/app/controllers/backtest_controller.rb +11 -0
  80. data/examples/rails_app/app/controllers/dashboard_controller.rb +21 -0
  81. data/examples/rails_app/app/controllers/portfolio_controller.rb +7 -0
  82. data/examples/rails_app/app/views/analysis/show.html.erb +209 -0
  83. data/examples/rails_app/app/views/backtest/show.html.erb +171 -0
  84. data/examples/rails_app/app/views/dashboard/index.html.erb +118 -0
  85. data/examples/rails_app/app/views/dashboard/show.html.erb +408 -0
  86. data/examples/rails_app/app/views/errors/show.html.erb +17 -0
  87. data/examples/rails_app/app/views/layouts/application.html.erb +60 -0
  88. data/examples/rails_app/app/views/portfolio/index.html.erb +33 -0
  89. data/examples/rails_app/bin/rails +6 -0
  90. data/examples/rails_app/config/application.rb +45 -0
  91. data/examples/rails_app/config/boot.rb +5 -0
  92. data/examples/rails_app/config/database.yml +18 -0
  93. data/examples/rails_app/config/environment.rb +11 -0
  94. data/examples/rails_app/config/routes.rb +26 -0
  95. data/examples/rails_app/config.ru +8 -0
  96. data/examples/realtime_stream_example.rb +274 -0
  97. data/examples/sinatra_app/Gemfile +22 -0
  98. data/examples/sinatra_app/QUICKSTART.md +159 -0
  99. data/examples/sinatra_app/README.md +461 -0
  100. data/examples/sinatra_app/app.rb +344 -0
  101. data/examples/sinatra_app/config.ru +5 -0
  102. data/examples/sinatra_app/public/css/style.css +659 -0
  103. data/examples/sinatra_app/public/js/app.js +107 -0
  104. data/examples/sinatra_app/views/analyze.erb +306 -0
  105. data/examples/sinatra_app/views/backtest.erb +325 -0
  106. data/examples/sinatra_app/views/dashboard.erb +419 -0
  107. data/examples/sinatra_app/views/error.erb +58 -0
  108. data/examples/sinatra_app/views/index.erb +118 -0
  109. data/examples/sinatra_app/views/layout.erb +61 -0
  110. data/examples/sinatra_app/views/portfolio.erb +43 -0
  111. data/examples/strategy_generator_example.rb +346 -0
  112. data/hsa_portfolio.csv +11 -0
  113. data/justfile +0 -0
  114. data/lib/api/alpha_vantage_api.rb +462 -0
  115. data/lib/sqa/backtest.rb +329 -0
  116. data/lib/sqa/data_frame/alpha_vantage.rb +43 -65
  117. data/lib/sqa/data_frame/data.rb +92 -0
  118. data/lib/sqa/data_frame/yahoo_finance.rb +35 -43
  119. data/lib/sqa/data_frame.rb +148 -243
  120. data/lib/sqa/ensemble.rb +359 -0
  121. data/lib/sqa/fpop.rb +199 -0
  122. data/lib/sqa/gp.rb +259 -0
  123. data/lib/sqa/indicator.rb +5 -8
  124. data/lib/sqa/init.rb +15 -8
  125. data/lib/sqa/market_regime.rb +240 -0
  126. data/lib/sqa/multi_timeframe.rb +379 -0
  127. data/lib/sqa/pattern_matcher.rb +497 -0
  128. data/lib/sqa/portfolio.rb +260 -6
  129. data/lib/sqa/portfolio_optimizer.rb +377 -0
  130. data/lib/sqa/risk_manager.rb +442 -0
  131. data/lib/sqa/seasonal_analyzer.rb +209 -0
  132. data/lib/sqa/sector_analyzer.rb +300 -0
  133. data/lib/sqa/stock.rb +67 -125
  134. data/lib/sqa/strategy/bollinger_bands.rb +42 -0
  135. data/lib/sqa/strategy/consensus.rb +5 -2
  136. data/lib/sqa/strategy/kbs_strategy.rb +470 -0
  137. data/lib/sqa/strategy/macd.rb +46 -0
  138. data/lib/sqa/strategy/mp.rb +1 -1
  139. data/lib/sqa/strategy/stochastic.rb +60 -0
  140. data/lib/sqa/strategy/volume_breakout.rb +57 -0
  141. data/lib/sqa/strategy.rb +5 -0
  142. data/lib/sqa/strategy_generator.rb +947 -0
  143. data/lib/sqa/stream.rb +361 -0
  144. data/lib/sqa/version.rb +1 -7
  145. data/lib/sqa.rb +23 -16
  146. data/main.just +81 -0
  147. data/mkdocs.yml +288 -0
  148. data/trace.log +0 -0
  149. metadata +261 -51
  150. data/bin/sqa +0 -6
  151. data/lib/patches/dry-cli.rb +0 -228
  152. data/lib/sqa/activity.rb +0 -10
  153. data/lib/sqa/cli.rb +0 -62
  154. data/lib/sqa/commands/analysis.rb +0 -309
  155. data/lib/sqa/commands/base.rb +0 -139
  156. data/lib/sqa/commands/web.rb +0 -199
  157. data/lib/sqa/commands.rb +0 -22
  158. data/lib/sqa/constants.rb +0 -23
  159. data/lib/sqa/indicator/average_true_range.rb +0 -33
  160. data/lib/sqa/indicator/bollinger_bands.rb +0 -28
  161. data/lib/sqa/indicator/candlestick_pattern_recognizer.rb +0 -60
  162. data/lib/sqa/indicator/donchian_channel.rb +0 -29
  163. data/lib/sqa/indicator/double_top_bottom_pattern.rb +0 -34
  164. data/lib/sqa/indicator/elliott_wave_theory.rb +0 -57
  165. data/lib/sqa/indicator/exponential_moving_average.rb +0 -25
  166. data/lib/sqa/indicator/exponential_moving_average_trend.rb +0 -36
  167. data/lib/sqa/indicator/fibonacci_retracement.rb +0 -23
  168. data/lib/sqa/indicator/head_and_shoulders_pattern.rb +0 -26
  169. data/lib/sqa/indicator/market_profile.rb +0 -32
  170. data/lib/sqa/indicator/mean_reversion.rb +0 -37
  171. data/lib/sqa/indicator/momentum.rb +0 -28
  172. data/lib/sqa/indicator/moving_average_convergence_divergence.rb +0 -29
  173. data/lib/sqa/indicator/peaks_and_valleys.rb +0 -29
  174. data/lib/sqa/indicator/predict_next_value.rb +0 -202
  175. data/lib/sqa/indicator/relative_strength_index.rb +0 -47
  176. data/lib/sqa/indicator/simple_moving_average.rb +0 -24
  177. data/lib/sqa/indicator/simple_moving_average_trend.rb +0 -32
  178. data/lib/sqa/indicator/stochastic_oscillator.rb +0 -68
  179. data/lib/sqa/indicator/true_range.rb +0 -39
  180. 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)