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,379 @@
1
+ # lib/sqa/multi_timeframe.rb
2
+ # frozen_string_literal: true
3
+
4
+ module SQA
5
+ ##
6
+ # MultiTimeframe - Analyze patterns across multiple timeframes
7
+ #
8
+ # Provides methods for:
9
+ # - Timeframe conversion (daily → weekly → monthly)
10
+ # - Multi-timeframe signal confirmation
11
+ # - Trend alignment across timeframes
12
+ # - Support/resistance across timeframes
13
+ #
14
+ # Common timeframe strategy:
15
+ # - Use higher timeframe for trend direction
16
+ # - Use lower timeframe for entry timing
17
+ #
18
+ # @example Multi-timeframe trend alignment
19
+ # mta = SQA::MultiTimeframe.new(stock: stock)
20
+ # alignment = mta.trend_alignment
21
+ # # => { daily: :up, weekly: :up, monthly: :up, aligned: true }
22
+ #
23
+ class MultiTimeframe
24
+ attr_reader :stock, :timeframes
25
+
26
+ ##
27
+ # Initialize multi-timeframe analyzer
28
+ #
29
+ # @param stock [SQA::Stock] Stock object with daily data
30
+ #
31
+ def initialize(stock:)
32
+ @stock = stock
33
+ @timeframes = {}
34
+
35
+ # Convert daily data to other timeframes
36
+ convert_timeframes
37
+ end
38
+
39
+ ##
40
+ # Convert daily data to weekly and monthly
41
+ #
42
+ def convert_timeframes
43
+ daily_data = @stock.df.data
44
+
45
+ @timeframes[:daily] = daily_data
46
+ @timeframes[:weekly] = resample(daily_data, period: 5)
47
+ @timeframes[:monthly] = resample(daily_data, period: 21)
48
+ end
49
+
50
+ ##
51
+ # Check trend alignment across timeframes
52
+ #
53
+ # @param lookback [Integer] Periods to look back for trend
54
+ # @return [Hash] Trend direction for each timeframe
55
+ #
56
+ def trend_alignment(lookback: 20)
57
+ trends = {}
58
+
59
+ @timeframes.each do |timeframe, data|
60
+ prices = data["adj_close_price"].to_a
61
+ recent = prices.last([lookback, prices.size].min)
62
+
63
+ # Simple trend: compare current to average
64
+ current = recent.last
65
+ avg = recent.sum / recent.size.to_f
66
+
67
+ trends[timeframe] = if current > avg * 1.02
68
+ :up
69
+ elsif current < avg * 0.98
70
+ :down
71
+ else
72
+ :sideways
73
+ end
74
+ end
75
+
76
+ # Check if all timeframes aligned
77
+ all_up = trends.values.all? { |t| t == :up }
78
+ all_down = trends.values.all? { |t| t == :down }
79
+
80
+ trends[:aligned] = all_up || all_down
81
+ trends[:direction] = if all_up
82
+ :bullish
83
+ elsif all_down
84
+ :bearish
85
+ else
86
+ :mixed
87
+ end
88
+
89
+ trends
90
+ end
91
+
92
+ ##
93
+ # Generate multi-timeframe signal
94
+ #
95
+ # Uses higher timeframe for trend, lower for timing.
96
+ #
97
+ # @param strategy_class [Class] Strategy to apply
98
+ # @param higher_timeframe [Symbol] Timeframe for trend (:weekly, :monthly)
99
+ # @param lower_timeframe [Symbol] Timeframe for entry (:daily, :weekly)
100
+ # @return [Symbol] :buy, :sell, or :hold
101
+ #
102
+ def signal(strategy_class:, higher_timeframe: :weekly, lower_timeframe: :daily)
103
+ # Get trend from higher timeframe
104
+ higher_data = @timeframes[higher_timeframe]
105
+ higher_prices = higher_data["adj_close_price"].to_a
106
+
107
+ higher_trend = if higher_prices.last > higher_prices[-10..-1].sum / 10.0
108
+ :up
109
+ else
110
+ :down
111
+ end
112
+
113
+ # Get signal from lower timeframe
114
+ lower_data = @timeframes[lower_timeframe]
115
+ lower_vector = create_vector(lower_data)
116
+
117
+ lower_signal = strategy_class.trade(lower_vector)
118
+
119
+ # Combine: only take trades aligned with higher timeframe
120
+ case higher_trend
121
+ when :up
122
+ lower_signal == :buy ? :buy : :hold
123
+ when :down
124
+ lower_signal == :sell ? :sell : :hold
125
+ else
126
+ :hold
127
+ end
128
+ end
129
+
130
+ ##
131
+ # Find support/resistance levels across timeframes
132
+ #
133
+ # Levels that appear on multiple timeframes are stronger.
134
+ #
135
+ # @param tolerance [Float] Price tolerance for matching levels (default: 0.02 for 2%)
136
+ # @return [Hash] Support and resistance levels
137
+ #
138
+ def support_resistance(tolerance: 0.02)
139
+ all_levels = { support: [], resistance: [] }
140
+
141
+ @timeframes.each do |timeframe, data|
142
+ prices = data["adj_close_price"].to_a
143
+ levels = find_levels(prices)
144
+
145
+ all_levels[:support] += levels[:support].map { |l| { price: l, timeframe: timeframe } }
146
+ all_levels[:resistance] += levels[:resistance].map { |l| { price: l, timeframe: timeframe } }
147
+ end
148
+
149
+ # Find levels that appear on multiple timeframes
150
+ strong_support = cluster_levels(all_levels[:support], tolerance)
151
+ strong_resistance = cluster_levels(all_levels[:resistance], tolerance)
152
+
153
+ {
154
+ support: strong_support,
155
+ resistance: strong_resistance,
156
+ current_price: @stock.df.data["adj_close_price"].to_a.last
157
+ }
158
+ end
159
+
160
+ ##
161
+ # Calculate indicators for each timeframe
162
+ #
163
+ # @param indicator [Symbol] Indicator to calculate
164
+ # @param options [Hash] Indicator options
165
+ # @return [Hash] Indicator values for each timeframe
166
+ #
167
+ def indicators(indicator:, **options)
168
+ results = {}
169
+
170
+ @timeframes.each do |timeframe, data|
171
+ prices = data["adj_close_price"].to_a
172
+
173
+ results[timeframe] = case indicator
174
+ when :sma
175
+ SQAI.sma(prices, **options)
176
+ when :ema
177
+ SQAI.ema(prices, **options)
178
+ when :rsi
179
+ SQAI.rsi(prices, **options)
180
+ when :macd
181
+ SQAI.macd(prices, **options)
182
+ else
183
+ nil
184
+ end
185
+ end
186
+
187
+ results
188
+ end
189
+
190
+ ##
191
+ # Detect divergence across timeframes
192
+ #
193
+ # Divergence occurs when price and indicator move in opposite directions.
194
+ #
195
+ # @return [Hash] Divergence information
196
+ #
197
+ def detect_divergence
198
+ divergences = {}
199
+
200
+ @timeframes.each do |timeframe, data|
201
+ prices = data["adj_close_price"].to_a
202
+ rsi = SQAI.rsi(prices, period: 14)
203
+
204
+ next if prices.size < 20 || rsi.size < 20
205
+
206
+ # Compare last 10 periods
207
+ price_trend = prices[-1] > prices[-10]
208
+ rsi_trend = rsi[-1] > rsi[-10]
209
+
210
+ divergences[timeframe] = if price_trend && !rsi_trend
211
+ :bearish_divergence
212
+ elsif !price_trend && rsi_trend
213
+ :bullish_divergence
214
+ else
215
+ :no_divergence
216
+ end
217
+ end
218
+
219
+ divergences
220
+ end
221
+
222
+ ##
223
+ # Check if timeframes confirm each other
224
+ #
225
+ # @param strategy_class [Class] Strategy to use
226
+ # @return [Hash] Confirmation status
227
+ #
228
+ def confirmation(strategy_class:)
229
+ signals = {}
230
+
231
+ @timeframes.each do |timeframe, data|
232
+ vector = create_vector(data)
233
+ signals[timeframe] = strategy_class.trade(vector)
234
+ end
235
+
236
+ # Check agreement
237
+ buy_count = signals.values.count(:buy)
238
+ sell_count = signals.values.count(:sell)
239
+
240
+ {
241
+ signals: signals,
242
+ confirmed: buy_count >= 2 || sell_count >= 2,
243
+ consensus: if buy_count >= 2
244
+ :buy
245
+ elsif sell_count >= 2
246
+ :sell
247
+ else
248
+ :hold
249
+ end
250
+ }
251
+ end
252
+
253
+ private
254
+
255
+ ##
256
+ # Resample daily data to longer timeframes
257
+ #
258
+ # @param daily_data [Polars::DataFrame] Daily data
259
+ # @param period [Integer] Number of days per period
260
+ # @return [Polars::DataFrame] Resampled data
261
+ #
262
+ def resample(daily_data, period:)
263
+ prices = daily_data["adj_close_price"].to_a
264
+ volumes = daily_data["volume"].to_a
265
+
266
+ resampled_prices = []
267
+ resampled_volumes = []
268
+
269
+ (0...prices.size).step(period) do |i|
270
+ chunk_prices = prices[i, period]
271
+ chunk_volumes = volumes[i, period]
272
+
273
+ next if chunk_prices.nil? || chunk_prices.empty?
274
+
275
+ # OHLC would be better, but we'll use close for simplicity
276
+ resampled_prices << chunk_prices.last
277
+ resampled_volumes << chunk_volumes.sum
278
+ end
279
+
280
+ # Create new DataFrame
281
+ SQA::DataFrame.new(
282
+ {
283
+ "adj_close_price" => resampled_prices,
284
+ "volume" => resampled_volumes
285
+ }
286
+ ).data
287
+ end
288
+
289
+ ##
290
+ # Find support/resistance levels using local extrema
291
+ #
292
+ # @param prices [Array<Float>] Price array
293
+ # @return [Hash] Support and resistance levels
294
+ #
295
+ def find_levels(prices)
296
+ return { support: [], resistance: [] } if prices.size < 20
297
+
298
+ support = []
299
+ resistance = []
300
+
301
+ window = 5
302
+
303
+ (window...(prices.size - window)).each do |idx|
304
+ current = prices[idx]
305
+ left = prices[(idx - window)...idx]
306
+ right = prices[(idx + 1)..(idx + window)]
307
+
308
+ # Local minimum (support)
309
+ if left.all? { |p| current <= p } && right.all? { |p| current <= p }
310
+ support << current
311
+ end
312
+
313
+ # Local maximum (resistance)
314
+ if left.all? { |p| current >= p } && right.all? { |p| current >= p }
315
+ resistance << current
316
+ end
317
+ end
318
+
319
+ { support: support, resistance: resistance }
320
+ end
321
+
322
+ ##
323
+ # Cluster similar levels across timeframes
324
+ #
325
+ # @param levels [Array<Hash>] Levels with { price:, timeframe: }
326
+ # @param tolerance [Float] Price tolerance
327
+ # @return [Array<Hash>] Clustered levels
328
+ #
329
+ def cluster_levels(levels, tolerance)
330
+ return [] if levels.empty?
331
+
332
+ clusters = []
333
+
334
+ levels.each do |level|
335
+ # Find existing cluster
336
+ cluster = clusters.find do |c|
337
+ (c[:price] - level[:price]).abs / c[:price] < tolerance
338
+ end
339
+
340
+ if cluster
341
+ # Add to existing cluster
342
+ cluster[:timeframes] << level[:timeframe]
343
+ cluster[:count] += 1
344
+ # Update average price
345
+ cluster[:price] = (cluster[:price] * (cluster[:count] - 1) + level[:price]) / cluster[:count]
346
+ else
347
+ # Create new cluster
348
+ clusters << {
349
+ price: level[:price],
350
+ timeframes: [level[:timeframe]],
351
+ count: 1
352
+ }
353
+ end
354
+ end
355
+
356
+ # Sort by strength (number of timeframes)
357
+ clusters.sort_by { |c| -c[:count] }
358
+ end
359
+
360
+ ##
361
+ # Create vector for strategy from timeframe data
362
+ #
363
+ # @param data [Polars::DataFrame] Timeframe data
364
+ # @return [OpenStruct] Vector for strategy
365
+ #
366
+ def create_vector(data)
367
+ prices = data["adj_close_price"].to_a
368
+
369
+ require 'ostruct'
370
+ OpenStruct.new(
371
+ prices: prices,
372
+ close: prices.last,
373
+ rsi: SQAI.rsi(prices, period: 14).last,
374
+ sma_20: SQAI.sma(prices, period: 20).last,
375
+ ema_12: SQAI.ema(prices, period: 12).last
376
+ )
377
+ end
378
+ end
379
+ end