sqa 0.0.31 → 0.0.37

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/CLAUDE.md +21 -0
  4. data/README.md +60 -32
  5. data/Rakefile +52 -10
  6. data/docs/IMPROVEMENT_PLAN.md +531 -0
  7. data/docs/advanced/index.md +1 -13
  8. data/docs/api/dataframe.md +0 -1
  9. data/docs/api/index.md +547 -61
  10. data/docs/api-reference/alphavantageapi.md +1057 -0
  11. data/docs/api-reference/apierror.md +31 -0
  12. data/docs/api-reference/index.md +221 -0
  13. data/docs/api-reference/notimplemented.md +27 -0
  14. data/docs/api-reference/sqa.md +267 -0
  15. data/docs/api-reference/sqa_backtest.md +137 -0
  16. data/docs/api-reference/sqa_backtest_results.md +530 -0
  17. data/docs/api-reference/sqa_badparametererror.md +13 -0
  18. data/docs/api-reference/sqa_config.md +538 -0
  19. data/docs/api-reference/sqa_configurationerror.md +13 -0
  20. data/docs/api-reference/sqa_datafetcherror.md +56 -0
  21. data/docs/api-reference/sqa_dataframe.md +752 -0
  22. data/docs/api-reference/sqa_dataframe_alphavantage.md +30 -0
  23. data/docs/api-reference/sqa_dataframe_data.md +325 -0
  24. data/docs/api-reference/sqa_dataframe_yahoofinance.md +25 -0
  25. data/docs/api-reference/sqa_ensemble.md +413 -0
  26. data/docs/api-reference/sqa_fpop.md +211 -0
  27. data/docs/api-reference/sqa_geneticprogram.md +325 -0
  28. data/docs/api-reference/sqa_geneticprogram_individual.md +114 -0
  29. data/docs/api-reference/sqa_marketregime.md +212 -0
  30. data/docs/api-reference/sqa_multitimeframe.md +227 -0
  31. data/docs/api-reference/sqa_patternmatcher.md +195 -0
  32. data/docs/api-reference/sqa_pluginmanager.md +55 -0
  33. data/docs/api-reference/sqa_portfolio.md +455 -0
  34. data/docs/api-reference/sqa_portfolio_position.md +220 -0
  35. data/docs/api-reference/sqa_portfolio_trade.md +332 -0
  36. data/docs/api-reference/sqa_portfoliooptimizer.md +248 -0
  37. data/docs/api-reference/sqa_riskmanager.md +388 -0
  38. data/docs/api-reference/sqa_seasonalanalyzer.md +121 -0
  39. data/docs/api-reference/sqa_sectoranalyzer.md +163 -0
  40. data/docs/api-reference/sqa_stock.md +649 -0
  41. data/docs/api-reference/sqa_strategy.md +178 -0
  42. data/docs/api-reference/sqa_strategy_bollingerbands.md +26 -0
  43. data/docs/api-reference/sqa_strategy_common.md +29 -0
  44. data/docs/api-reference/sqa_strategy_consensus.md +129 -0
  45. data/docs/api-reference/sqa_strategy_ema.md +41 -0
  46. data/docs/api-reference/sqa_strategy_kbs.md +154 -0
  47. data/docs/api-reference/sqa_strategy_macd.md +26 -0
  48. data/docs/api-reference/sqa_strategy_mp.md +41 -0
  49. data/docs/api-reference/sqa_strategy_mr.md +41 -0
  50. data/docs/api-reference/sqa_strategy_random.md +41 -0
  51. data/docs/api-reference/sqa_strategy_rsi.md +41 -0
  52. data/docs/api-reference/sqa_strategy_sma.md +41 -0
  53. data/docs/api-reference/sqa_strategy_stochastic.md +26 -0
  54. data/docs/api-reference/sqa_strategy_volumebreakout.md +26 -0
  55. data/docs/api-reference/sqa_strategygenerator.md +298 -0
  56. data/docs/api-reference/sqa_strategygenerator_pattern.md +264 -0
  57. data/docs/api-reference/sqa_strategygenerator_patterncontext.md +326 -0
  58. data/docs/api-reference/sqa_strategygenerator_profitablepoint.md +424 -0
  59. data/docs/api-reference/sqa_stream.md +256 -0
  60. data/docs/api-reference/sqa_ticker.md +175 -0
  61. data/docs/api-reference/string.md +135 -0
  62. data/docs/assets/images/advanced-workflow.svg +89 -0
  63. data/docs/assets/images/architecture.svg +107 -0
  64. data/docs/assets/images/data-flow.svg +138 -0
  65. data/docs/assets/images/getting-started-workflow.svg +88 -0
  66. data/docs/assets/images/sqa.jpg +0 -0
  67. data/docs/assets/images/strategy-flow.svg +78 -0
  68. data/docs/assets/images/system-architecture.svg +150 -0
  69. data/docs/concepts/index.md +292 -27
  70. data/docs/data_frame.md +0 -1
  71. data/docs/getting-started/index.md +1 -30
  72. data/docs/getting-started/installation.md +2 -2
  73. data/docs/getting-started/quick-start.md +4 -4
  74. data/docs/index.md +26 -25
  75. data/docs/llms.txt +109 -0
  76. data/docs/strategies/bollinger-bands.md +1 -1
  77. data/docs/strategies/kbs.md +15 -14
  78. data/docs/strategies/rsi.md +1 -1
  79. data/docs/strategy.md +381 -3
  80. data/docs/terms_of_use.md +1 -1
  81. data/examples/README.md +10 -0
  82. data/lib/api/alpha_vantage_api.rb +3 -7
  83. data/lib/sqa/config.rb +109 -28
  84. data/lib/sqa/data_frame/alpha_vantage.rb +13 -3
  85. data/lib/sqa/data_frame/data.rb +13 -1
  86. data/lib/sqa/data_frame.rb +189 -41
  87. data/lib/sqa/errors.rb +79 -17
  88. data/lib/sqa/indicator.rb +17 -4
  89. data/lib/sqa/init.rb +70 -15
  90. data/lib/sqa/pattern_matcher.rb +4 -4
  91. data/lib/sqa/portfolio.rb +1 -1
  92. data/lib/sqa/sector_analyzer.rb +3 -11
  93. data/lib/sqa/stock.rb +236 -20
  94. data/lib/sqa/strategy.rb +62 -4
  95. data/lib/sqa/ticker.rb +107 -42
  96. data/lib/sqa/version.rb +1 -1
  97. data/lib/sqa.rb +16 -8
  98. data/mkdocs.yml +68 -117
  99. metadata +90 -36
  100. data/docs/README.md +0 -43
  101. data/docs/alpha_vantage_technical_indicators.md +0 -62
  102. data/docs/average_true_range.md +0 -9
  103. data/docs/bollinger_bands.md +0 -15
  104. data/docs/candlestick_pattern_recognizer.md +0 -4
  105. data/docs/donchian_channel.md +0 -5
  106. data/docs/double_top_bottom_pattern.md +0 -3
  107. data/docs/exponential_moving_average.md +0 -19
  108. data/docs/fibonacci_retracement.md +0 -30
  109. data/docs/head_and_shoulders_pattern.md +0 -3
  110. data/docs/market_profile.md +0 -4
  111. data/docs/momentum.md +0 -19
  112. data/docs/moving_average_convergence_divergence.md +0 -23
  113. data/docs/peaks_and_valleys.md +0 -11
  114. data/docs/relative_strength_index.md +0 -6
  115. data/docs/simple_moving_average.md +0 -8
  116. data/docs/stochastic_oscillator.md +0 -4
  117. data/docs/ta_lib.md +0 -160
  118. data/docs/true_range.md +0 -12
  119. data/docs/true_strength_index.md +0 -46
  120. data/docs/weighted_moving_average.md +0 -48
  121. data/examples/sinatra_app/Gemfile +0 -22
  122. data/examples/sinatra_app/QUICKSTART.md +0 -159
  123. data/examples/sinatra_app/README.md +0 -461
  124. data/examples/sinatra_app/app.rb +0 -344
  125. data/examples/sinatra_app/config.ru +0 -5
  126. data/examples/sinatra_app/public/css/style.css +0 -659
  127. data/examples/sinatra_app/public/js/app.js +0 -107
  128. data/examples/sinatra_app/views/analyze.erb +0 -306
  129. data/examples/sinatra_app/views/backtest.erb +0 -325
  130. data/examples/sinatra_app/views/dashboard.erb +0 -419
  131. data/examples/sinatra_app/views/error.erb +0 -58
  132. data/examples/sinatra_app/views/index.erb +0 -118
  133. data/examples/sinatra_app/views/layout.erb +0 -61
  134. data/examples/sinatra_app/views/portfolio.erb +0 -43
data/docs/strategy.md CHANGED
@@ -1,5 +1,383 @@
1
- # Strategy
1
+ # Strategy Framework
2
2
 
3
- A strategy is a recipe that cooks all the indicators together to make a decision on a potential trade. The SQA::Strategy class provides the framework for executing multiple strategies.
3
+ A comprehensive guide to SQA's trading strategy architecture.
4
4
 
5
- You can also think of a strategy as a set of rules like in the old days of rule-based forward/backward chaining engines. The rules are evaluated to determine whether a specific decision to trade is good or bad.
5
+ ## Overview
6
+
7
+ A strategy in SQA is a set of rules that analyze technical indicators and market data to generate trading signals. The `SQA::Strategy` class provides the framework for managing and executing multiple strategies.
8
+
9
+ ## Architecture
10
+
11
+ ![Strategy Flow](assets/images/strategy-flow.svg)
12
+
13
+ ## The Strategy Pattern
14
+
15
+ All SQA strategies follow a common interface:
16
+
17
+ ```ruby
18
+ class SQA::Strategy::YourStrategy
19
+ def self.trade(vector)
20
+ # Analyze the vector (OpenStruct with indicator data)
21
+ # Return :buy, :sell, or :hold
22
+ end
23
+ end
24
+ ```
25
+
26
+ ### The Vector
27
+
28
+ The vector is an `OpenStruct` containing all the data a strategy needs to make a decision:
29
+
30
+ ```ruby
31
+ require 'ostruct'
32
+
33
+ vector = OpenStruct.new(
34
+ rsi: { trend: :over_sold, value: 28.5 },
35
+ macd: { crossover: :bullish, histogram: 0.5 },
36
+ prices: prices_array,
37
+ sma_20: sma_20.last,
38
+ sma_50: sma_50.last
39
+ )
40
+ ```
41
+
42
+ ## Using the Strategy Framework
43
+
44
+ ### Managing Multiple Strategies
45
+
46
+ ```ruby
47
+ # Create a strategy manager
48
+ strategy = SQA::Strategy.new
49
+
50
+ # Add strategies
51
+ strategy.add SQA::Strategy::RSI
52
+ strategy.add SQA::Strategy::MACD
53
+ strategy.add SQA::Strategy::BollingerBands
54
+
55
+ # Execute all strategies
56
+ signals = strategy.execute(vector)
57
+ # => [:buy, :hold, :sell]
58
+
59
+ # Count votes for consensus
60
+ buy_votes = signals.count(:buy)
61
+ sell_votes = signals.count(:sell)
62
+ ```
63
+
64
+ ### Auto-Loading Strategies
65
+
66
+ ```ruby
67
+ # Load all built-in strategies
68
+ strategy = SQA::Strategy.new
69
+ strategy.auto_load
70
+
71
+ # Load specific strategies only
72
+ strategy.auto_load(only: [:rsi, :macd])
73
+
74
+ # Load all except certain strategies
75
+ strategy.auto_load(except: [:random, :common])
76
+ ```
77
+
78
+ ### Listing Available Strategies
79
+
80
+ ```ruby
81
+ strategy = SQA::Strategy.new
82
+ available = strategy.available
83
+ # => [SQA::Strategy::RSI, SQA::Strategy::MACD, ...]
84
+ ```
85
+
86
+ ## Built-in Strategies
87
+
88
+ SQA includes 13+ built-in trading strategies:
89
+
90
+ ### Trend-Following
91
+
92
+ | Strategy | Description | Best For |
93
+ |----------|-------------|----------|
94
+ | **SMA** | Simple Moving Average crossovers | Trending markets |
95
+ | **EMA** | Exponential Moving Average crossovers | Faster trend detection |
96
+ | **MACD** | Moving Average Convergence Divergence | Momentum + trend |
97
+
98
+ ### Momentum
99
+
100
+ | Strategy | Description | Best For |
101
+ |----------|-------------|----------|
102
+ | **RSI** | Relative Strength Index (oversold/overbought) | Range-bound markets |
103
+ | **Stochastic** | Stochastic oscillator crossovers | Short-term reversals |
104
+
105
+ ### Volatility
106
+
107
+ | Strategy | Description | Best For |
108
+ |----------|-------------|----------|
109
+ | **Bollinger Bands** | Price touching volatility bands | Volatile markets |
110
+ | **Volume Breakout** | High volume price breakouts | Breakout trading |
111
+
112
+ ### Advanced
113
+
114
+ | Strategy | Description | Best For |
115
+ |----------|-------------|----------|
116
+ | **KBS** | Knowledge-Based System with RETE engine | Complex rule combinations |
117
+ | **Consensus** | Aggregates multiple strategy signals | Reducing noise |
118
+ | **Mean Reversion** | Statistical mean reversion | Range-bound markets |
119
+
120
+ ## Creating Custom Strategies
121
+
122
+ ### Basic Template
123
+
124
+ ```ruby
125
+ # lib/my_strategies/awesome_strategy.rb
126
+
127
+ class SQA::Strategy::AwesomeStrategy
128
+ def self.trade(vector)
129
+ # Your trading logic here
130
+ if buy_condition?(vector)
131
+ :buy
132
+ elsif sell_condition?(vector)
133
+ :sell
134
+ else
135
+ :hold
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def self.buy_condition?(vector)
142
+ vector.rsi[:value] < 30 &&
143
+ vector.macd[:crossover] == :bullish
144
+ end
145
+
146
+ def self.sell_condition?(vector)
147
+ vector.rsi[:value] > 70 &&
148
+ vector.macd[:crossover] == :bearish
149
+ end
150
+ end
151
+ ```
152
+
153
+ ### Using Your Strategy
154
+
155
+ ```ruby
156
+ require_relative 'my_strategies/awesome_strategy'
157
+
158
+ # Execute directly
159
+ signal = SQA::Strategy::AwesomeStrategy.trade(vector)
160
+
161
+ # Or add to strategy manager
162
+ strategy = SQA::Strategy.new
163
+ strategy.add SQA::Strategy::AwesomeStrategy
164
+ ```
165
+
166
+ ### Strategy with Configuration
167
+
168
+ ```ruby
169
+ class SQA::Strategy::ConfigurableRSI
170
+ @oversold_threshold = 30
171
+ @overbought_threshold = 70
172
+
173
+ class << self
174
+ attr_accessor :oversold_threshold, :overbought_threshold
175
+
176
+ def trade(vector)
177
+ rsi_value = vector.rsi[:value] || vector.rsi
178
+
179
+ if rsi_value < oversold_threshold
180
+ :buy
181
+ elsif rsi_value > overbought_threshold
182
+ :sell
183
+ else
184
+ :hold
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # Configure before using
191
+ SQA::Strategy::ConfigurableRSI.oversold_threshold = 25
192
+ SQA::Strategy::ConfigurableRSI.overbought_threshold = 75
193
+ ```
194
+
195
+ ## Combining Strategies
196
+
197
+ ### Consensus Strategy
198
+
199
+ The built-in Consensus strategy aggregates signals from multiple strategies:
200
+
201
+ ```ruby
202
+ # Simple majority vote
203
+ signals = strategy.execute(vector)
204
+ # => [:buy, :buy, :hold, :sell]
205
+
206
+ consensus = signals.group_by(&:itself)
207
+ .transform_values(&:count)
208
+ .max_by { |_, v| v }
209
+ .first
210
+ # => :buy (2 votes vs 1 each for hold/sell)
211
+ ```
212
+
213
+ ### Weighted Voting
214
+
215
+ ```ruby
216
+ # Assign weights to strategies
217
+ weights = {
218
+ SQA::Strategy::RSI => 2.0, # RSI gets double weight
219
+ SQA::Strategy::MACD => 1.5, # MACD gets 1.5x weight
220
+ SQA::Strategy::SMA => 1.0 # SMA gets normal weight
221
+ }
222
+
223
+ # Calculate weighted consensus
224
+ weighted_votes = { buy: 0.0, sell: 0.0, hold: 0.0 }
225
+ signals.each_with_index do |signal, i|
226
+ weight = weights.values[i]
227
+ weighted_votes[signal] += weight
228
+ end
229
+
230
+ final_signal = weighted_votes.max_by { |_, v| v }.first
231
+ ```
232
+
233
+ ## Testing Strategies
234
+
235
+ ### Unit Testing
236
+
237
+ ```ruby
238
+ # test/strategy/awesome_strategy_test.rb
239
+ require 'minitest/autorun'
240
+ require 'ostruct'
241
+
242
+ class AwesomeStrategyTest < Minitest::Test
243
+ def test_buy_signal_on_oversold
244
+ vector = OpenStruct.new(
245
+ rsi: { value: 25, trend: :over_sold },
246
+ macd: { crossover: :bullish }
247
+ )
248
+
249
+ assert_equal :buy, SQA::Strategy::AwesomeStrategy.trade(vector)
250
+ end
251
+
252
+ def test_sell_signal_on_overbought
253
+ vector = OpenStruct.new(
254
+ rsi: { value: 75, trend: :over_bought },
255
+ macd: { crossover: :bearish }
256
+ )
257
+
258
+ assert_equal :sell, SQA::Strategy::AwesomeStrategy.trade(vector)
259
+ end
260
+ end
261
+ ```
262
+
263
+ ### Backtesting
264
+
265
+ ```ruby
266
+ backtest = SQA::Backtest.new(
267
+ stock: stock,
268
+ strategy: SQA::Strategy::AwesomeStrategy,
269
+ initial_cash: 10_000
270
+ )
271
+
272
+ results = backtest.run
273
+ puts "Return: #{results.total_return}%"
274
+ puts "Sharpe: #{results.sharpe_ratio}"
275
+ ```
276
+
277
+ ## Strategy Internals
278
+
279
+ ### How Strategies Process Data
280
+
281
+ 1. **Data Preparation**: Raw price data is converted to indicators
282
+ 2. **Vector Creation**: Indicators are packaged into an OpenStruct
283
+ 3. **Strategy Execution**: Each strategy analyzes the vector
284
+ 4. **Signal Generation**: Strategy returns `:buy`, `:sell`, or `:hold`
285
+ 5. **Aggregation**: Signals can be combined for consensus
286
+
287
+ ### Data Ordering
288
+
289
+ All indicator data in SQA follows **ascending chronological order** (oldest first):
290
+
291
+ ```ruby
292
+ # prices[0] = oldest price
293
+ # prices[-1] = most recent price
294
+
295
+ prices = stock.df["adj_close_price"].to_a
296
+ rsi = SQAI.rsi(prices, period: 14)
297
+
298
+ # rsi.last is the most recent RSI value
299
+ current_rsi = rsi.last
300
+ ```
301
+
302
+ ## Best Practices
303
+
304
+ ### 1. Keep Strategies Simple
305
+
306
+ ```ruby
307
+ # GOOD: Clear, single-purpose logic
308
+ def self.trade(vector)
309
+ vector.rsi[:value] < 30 ? :buy : :hold
310
+ end
311
+
312
+ # BAD: Complex nested conditions
313
+ def self.trade(vector)
314
+ if vector.rsi[:value] < 30
315
+ if vector.macd[:histogram] > 0
316
+ if vector.volume > vector.avg_volume * 1.5
317
+ # ... more nesting ...
318
+ end
319
+ end
320
+ end
321
+ end
322
+ ```
323
+
324
+ ### 2. Use Named Parameters in Vectors
325
+
326
+ ```ruby
327
+ # GOOD: Self-documenting
328
+ vector = OpenStruct.new(
329
+ rsi_value: rsi.last,
330
+ rsi_trend: rsi.last < 30 ? :oversold : :neutral,
331
+ sma_20: sma_20.last,
332
+ sma_50: sma_50.last
333
+ )
334
+
335
+ # BAD: Magic numbers
336
+ vector = OpenStruct.new(
337
+ val1: 28.5,
338
+ val2: 150.0,
339
+ val3: 148.0
340
+ )
341
+ ```
342
+
343
+ ### 3. Handle Edge Cases
344
+
345
+ ```ruby
346
+ def self.trade(vector)
347
+ return :hold if vector.rsi.nil?
348
+ return :hold if vector.prices.empty?
349
+
350
+ # Main logic here
351
+ end
352
+ ```
353
+
354
+ ### 4. Document Your Strategy
355
+
356
+ ```ruby
357
+ # Dual Moving Average Crossover Strategy
358
+ #
359
+ # Generates buy signals when short MA crosses above long MA,
360
+ # sell signals when short MA crosses below long MA.
361
+ #
362
+ # Parameters:
363
+ # vector.sma_short - Short-term SMA (e.g., 20-day)
364
+ # vector.sma_long - Long-term SMA (e.g., 50-day)
365
+ #
366
+ # Returns:
367
+ # :buy - Short MA > Long MA (bullish crossover)
368
+ # :sell - Short MA < Long MA (bearish crossover)
369
+ # :hold - No clear signal
370
+ #
371
+ class SQA::Strategy::DualMA
372
+ def self.trade(vector)
373
+ # ...
374
+ end
375
+ end
376
+ ```
377
+
378
+ ## Related Documentation
379
+
380
+ - [Trading Strategies Reference](strategies/index.md) - Details on each built-in strategy
381
+ - [Custom Strategies](strategies/custom.md) - Guide to creating your own
382
+ - [Backtesting](advanced/backtesting.md) - Test strategies on historical data
383
+ - [Technical Indicators](indicators/index.md) - Calculate indicator values
data/docs/terms_of_use.md CHANGED
@@ -32,7 +32,7 @@ Your affirmative act of using our Ruby Gem ("library") located at https://github
32
32
  - [20. House rules](#20-house-rules)
33
33
  - [21. Third Party Software](#21-third-party-software)
34
34
  - [22. Scripts](#22-scripts)
35
- - [23. Publications - No Recommendation or Advice Status](#23-publications---no-recommendation-or-advice-status)
35
+ - [23. Publications - No Recommendation or Advice Status](#23-publications-no-recommendation-or-advice-status)
36
36
 
37
37
  <!-- Tocer[finish]: Auto-generated, don't remove. -->
38
38
 
data/examples/README.md CHANGED
@@ -335,6 +335,15 @@ Examples may create output files in `/tmp/`:
335
335
  - `/tmp/sqa_evolution_history.csv` - GP evolution history
336
336
  - `/tmp/sqa_backtest_results.csv` - Backtest results
337
337
 
338
+ ## Web Demo Application
339
+
340
+ For a complete web-based demonstration of SQA's capabilities, see the **[sqa_demo-sinatra](https://github.com/MadBomber/sqa_demo-sinatra)** gem. This Sinatra application provides a visual interface for:
341
+
342
+ - Stock analysis dashboard
343
+ - Technical indicator visualization
344
+ - Strategy backtesting
345
+ - Portfolio management
346
+
338
347
  ## Next Steps
339
348
 
340
349
  After running these examples:
@@ -344,6 +353,7 @@ After running these examples:
344
353
  3. **Walk-Forward Validation**: Test on out-of-sample data
345
354
  4. **Combine Techniques**: Use strategy generator + GP + KBS together
346
355
  5. **Production Deployment**: Connect to real WebSocket data feeds
356
+ 6. **Try the Web Demo**: Install [sqa_demo-sinatra](https://github.com/MadBomber/sqa_demo-sinatra) for a visual interface
347
357
 
348
358
  ## Educational Disclaimer
349
359
 
@@ -3,13 +3,9 @@
3
3
  require 'faraday'
4
4
  require 'json'
5
5
 
6
- # TODO: Reorganize the methods by category
7
- # Market Data
8
- # Technical Indicators
9
- # Trading
10
- # Economic Indicators
11
- # Digital and Forex
12
- #
6
+ # Alpha Vantage API wrapper
7
+ # Categories: Market Data, Technical Indicators, Trading, Economic Indicators, Digital/Forex
8
+ # See: https://www.alphavantage.co/documentation/
13
9
 
14
10
 
15
11
  class AlphaVantageAPI
data/lib/sqa/config.rb CHANGED
@@ -1,30 +1,63 @@
1
1
  # lib/sqa/config.rb
2
2
 
3
- # The hierarchies of values should be:
4
- # default
5
- # envar ..... overrides default
6
- # config file ..... overrides envar
7
- # command line parameters ...... overrides config file
8
-
3
+ # Configuration management for SQA with hierarchical value resolution.
4
+ # Values are resolved in this order (later overrides earlier):
5
+ # 1. default values
6
+ # 2. environment variables (SQA_ prefix)
7
+ # 3. config file (YAML, TOML, or JSON)
8
+ # 4. command line parameters
9
+ #
10
+ # @example Basic configuration
11
+ # SQA.init
12
+ # SQA.config.data_dir = "~/my_data"
13
+ # SQA.config.debug = true
14
+ #
15
+ # @example Using config file
16
+ # SQA.config.config_file = "~/.sqa.yml"
17
+ # SQA.config.from_file
18
+ #
19
+ # @example Environment variables
20
+ # # Set SQA_DATA_DIR, SQA_DEBUG, etc. before requiring sqa
21
+ #
22
+
23
+ require 'fileutils'
9
24
  require 'yaml'
10
25
  require 'toml-rb'
11
26
 
12
27
  module SQA
13
- # class Config < Hashie::Trash
14
- # include Hashie::Extensions::IgnoreUndeclared
15
- # include Hashie::Extensions::Coercion
16
-
17
-
28
+ # Configuration class for SQA settings.
29
+ # Extends Hashie::Dash for property-based configuration with coercion.
30
+ #
31
+ # @!attribute [rw] command
32
+ # @return [String, nil] Current command (nil, 'analysis', or 'web')
33
+ # @!attribute [rw] config_file
34
+ # @return [String, nil] Path to configuration file
35
+ # @!attribute [rw] dump_config
36
+ # @return [String, nil] Path to dump current configuration
37
+ # @!attribute [rw] data_dir
38
+ # @return [String] Directory for data storage (default: ~/sqa_data)
39
+ # @!attribute [rw] portfolio_filename
40
+ # @return [String] Portfolio CSV filename (default: portfolio.csv)
41
+ # @!attribute [rw] trades_filename
42
+ # @return [String] Trades CSV filename (default: trades.csv)
43
+ # @!attribute [rw] log_level
44
+ # @return [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
45
+ # @!attribute [rw] debug
46
+ # @return [Boolean] Enable debug mode
47
+ # @!attribute [rw] verbose
48
+ # @return [Boolean] Enable verbose output
49
+ # @!attribute [rw] plotting_library
50
+ # @return [Symbol] Plotting library to use (:gruff)
51
+ # @!attribute [rw] lazy_update
52
+ # @return [Boolean] Skip API updates if cached data exists
53
+ #
18
54
  class Config < Hashie::Dash
19
55
  include Hashie::Extensions::Dash::PropertyTranslation
20
56
  include Hashie::Extensions::MethodAccess
21
57
  include Hashie::Extensions::Coercion
22
58
 
23
- # FIXME: Getting undefined error PredefinedValues
24
- # I'm thinking that Ruby is dropping it from the ObjectSpace
25
- # Looks like it is only used for the log level. Should
26
- # able to work around that.
27
- #
59
+ # NOTE: PredefinedValues extension disabled due to compatibility issues.
60
+ # Log level validation is handled via the `values:` option on the property instead.
28
61
  # include Hashie::Extensions::Dash::PredefinedValues
29
62
 
30
63
  property :command # a String currently, nil, analysis or web
@@ -33,18 +66,17 @@ module SQA
33
66
 
34
67
  property :data_dir, default: Nenv.home + "/sqa_data"
35
68
 
36
- # TODO: If no path is given, these files will be in
37
- # data directory, otherwise, use the given path
69
+ # Relative filenames are resolved against data_dir; absolute paths used as-is
38
70
  property :portfolio_filename, from: :portfolio, default: "portfolio.csv"
39
71
  property :trades_filename, from: :trades, default: "trades.csv"
40
72
 
41
73
  property :log_level, default: :info, coerce: Symbol, values: %i[debug info warn error fatal]
42
74
 
43
- # TODO: need a custom proc since there is no Boolean class in Ruby
44
- property :debug, default: false #, coerce: Boolean
45
- property :verbose, default: false #, coerce: Boolean
75
+ # Boolean coercion handled via coerce_key blocks below (no Boolean class in Ruby)
76
+ property :debug, default: false
77
+ property :verbose, default: false
46
78
 
47
- # TODO: use svggraph
79
+ # Plotting library - gruff is default; svggraph support could be added in future
48
80
  property :plotting_library, from: :plot_lib, default: :gruff, coerce: Symbol
49
81
  property :lazy_update, from: :lazy, default: false
50
82
 
@@ -71,17 +103,41 @@ module SQA
71
103
  end
72
104
  end
73
105
 
106
+ coerce_key :log_level, ->(v) do
107
+ v.is_a?(String) ? v.to_sym : v
108
+ end
109
+
110
+ coerce_key :plotting_library, ->(v) do
111
+ v.is_a?(String) ? v.to_sym : v
112
+ end
113
+
74
114
  ########################################################
115
+
116
+ # Creates a new Config instance with optional initial values.
117
+ # Automatically applies environment variable overrides.
118
+ #
119
+ # @param a_hash [Hash] Initial configuration values
75
120
  def initialize(a_hash={})
76
121
  super(a_hash)
77
122
  override_with_envars
78
123
  end
79
124
 
125
+ # Returns whether debug mode is enabled.
126
+ # @return [Boolean] true if debug mode is on
80
127
  def debug? = debug
128
+
129
+ # Returns whether verbose mode is enabled.
130
+ # @return [Boolean] true if verbose mode is on
81
131
  def verbose? = verbose
82
132
 
83
133
 
84
134
  ########################################################
135
+
136
+ # Loads configuration from a file.
137
+ # Supports YAML (.yml, .yaml), TOML (.toml), and JSON (.json) formats.
138
+ #
139
+ # @return [void]
140
+ # @raise [BadParameterError] If config file is invalid or unsupported format
85
141
  def from_file
86
142
  return if config_file.nil?
87
143
 
@@ -93,8 +149,7 @@ module SQA
93
149
  type = "invalid"
94
150
  end
95
151
 
96
- # TODO: arrange order in mostly often used
97
-
152
+ # Config file format detection (YAML is most common)
98
153
  if ".json" == type
99
154
  incoming = form_json
100
155
 
@@ -108,19 +163,24 @@ module SQA
108
163
  raise BadParameterError, "Invalid Config File: #{config_file}"
109
164
  end
110
165
 
111
- if incoming.has_key? :data_dir
166
+ if incoming.key?(:data_dir)
112
167
  incoming[:data_dir] = incoming[:data_dir].gsub(/^~/, Nenv.home)
113
168
  end
114
169
 
115
170
  merge! incoming
116
171
  end
117
172
 
173
+ # Writes current configuration to a file.
174
+ # Format is determined by file extension.
175
+ #
176
+ # @return [void]
177
+ # @raise [BadParameterError] If config file is not set or unsupported format
118
178
  def dump_file
119
179
  if config_file.nil?
120
180
  raise BadParameterError, "No config file given"
121
181
  end
122
182
 
123
- `touch #{config_file}`
183
+ FileUtils.touch(config_file)
124
184
  # unless File.exist?(config_file)
125
185
 
126
186
  type = File.extname(config_file).downcase
@@ -139,7 +199,10 @@ module SQA
139
199
  end
140
200
  end
141
201
 
142
- # Method to dynamically extend properties from external sources (e.g., plugins)
202
+ # Injects additional properties from plugins.
203
+ # Allows external code to register new configuration options.
204
+ #
205
+ # @return [void]
143
206
  def inject_additional_properties
144
207
  SQA::PluginManager.registered_properties.each do |prop, options|
145
208
  self.class.property(prop, options)
@@ -185,11 +248,29 @@ module SQA
185
248
 
186
249
  #####################################
187
250
  class << self
251
+ # Resets the configuration to default values.
252
+ # Creates a new Config instance and assigns it to SQA.config.
253
+ #
254
+ # @return [SQA::Config] The new config instance
188
255
  def reset
256
+ @initialized = true
189
257
  SQA.config = new
190
258
  end
259
+
260
+ # Returns whether the configuration has been initialized.
261
+ #
262
+ # @return [Boolean] true if reset has been called
263
+ def initialized?
264
+ @initialized ||= false
265
+ end
191
266
  end
192
267
  end
193
268
  end
194
269
 
195
- SQA::Config.reset
270
+ # Auto-initialization with deprecation warning
271
+ # This will be removed in v1.0.0 - applications should call SQA.init explicitly
272
+ unless SQA::Config.initialized?
273
+ warn "[SQA DEPRECATION] Auto-initialization at require time will be removed in v1.0. " \
274
+ "Please call SQA.init explicitly in your application startup." if $VERBOSE
275
+ SQA::Config.reset
276
+ end
@@ -35,9 +35,14 @@ class SQA::DataFrame
35
35
 
36
36
  ################################################################
37
37
 
38
- # Get recent data from JSON API
38
+ # Get recent data from Alpha Vantage API
39
+ #
39
40
  # ticker String the security to retrieve
40
- # returns a Polars DataFrame
41
+ # full Boolean whether to fetch full history or compact (last 100 days)
42
+ # from_date Date optional date to fetch data after (for incremental updates)
43
+ #
44
+ # Returns: SQA::DataFrame sorted in ASCENDING order (oldest to newest)
45
+ # Note: Alpha Vantage returns data newest-first, but we sort ascending for TA-Lib compatibility
41
46
  def self.recent(ticker, full: false, from_date: nil)
42
47
  response = CONNECTION.get(
43
48
  "/query?" +
@@ -67,7 +72,8 @@ class SQA::DataFrame
67
72
  # Handle date criteria if applicable
68
73
  if from_date
69
74
  # Use Polars.col() to create an expression for filtering
70
- df = df.filter(Polars.col("timestamp") >= from_date.to_s)
75
+ # Use > (not >=) to exclude the from_date itself and prevent duplicates
76
+ df = df.filter(Polars.col("timestamp") > from_date.to_s)
71
77
  end
72
78
 
73
79
  # Wrap in SQA::DataFrame with proper transformers
@@ -80,6 +86,10 @@ class SQA::DataFrame
80
86
  sqa_df.data["close_price"].alias("adj_close_price")
81
87
  )
82
88
 
89
+ # Sort data in ascending chronological order (oldest to newest) for TA-Lib compatibility
90
+ # Alpha Vantage returns data newest-first, but TA-Lib expects oldest-first
91
+ sqa_df.data = sqa_df.data.sort("timestamp", reverse: false)
92
+
83
93
  sqa_df
84
94
  end
85
95
  end