sqa 0.0.32 → 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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -1
  3. data/README.md +4 -0
  4. data/Rakefile +52 -10
  5. data/docs/IMPROVEMENT_PLAN.md +531 -0
  6. data/docs/advanced/index.md +1 -13
  7. data/docs/api/index.md +547 -61
  8. data/docs/api-reference/alphavantageapi.md +1057 -0
  9. data/docs/api-reference/apierror.md +31 -0
  10. data/docs/api-reference/index.md +221 -0
  11. data/docs/api-reference/notimplemented.md +27 -0
  12. data/docs/api-reference/sqa.md +267 -0
  13. data/docs/api-reference/sqa_backtest.md +137 -0
  14. data/docs/api-reference/sqa_backtest_results.md +530 -0
  15. data/docs/api-reference/sqa_badparametererror.md +13 -0
  16. data/docs/api-reference/sqa_config.md +538 -0
  17. data/docs/api-reference/sqa_configurationerror.md +13 -0
  18. data/docs/api-reference/sqa_datafetcherror.md +56 -0
  19. data/docs/api-reference/sqa_dataframe.md +752 -0
  20. data/docs/api-reference/sqa_dataframe_alphavantage.md +30 -0
  21. data/docs/api-reference/sqa_dataframe_data.md +325 -0
  22. data/docs/api-reference/sqa_dataframe_yahoofinance.md +25 -0
  23. data/docs/api-reference/sqa_ensemble.md +413 -0
  24. data/docs/api-reference/sqa_fpop.md +211 -0
  25. data/docs/api-reference/sqa_geneticprogram.md +325 -0
  26. data/docs/api-reference/sqa_geneticprogram_individual.md +114 -0
  27. data/docs/api-reference/sqa_marketregime.md +212 -0
  28. data/docs/api-reference/sqa_multitimeframe.md +227 -0
  29. data/docs/api-reference/sqa_patternmatcher.md +195 -0
  30. data/docs/api-reference/sqa_pluginmanager.md +55 -0
  31. data/docs/api-reference/sqa_portfolio.md +455 -0
  32. data/docs/api-reference/sqa_portfolio_position.md +220 -0
  33. data/docs/api-reference/sqa_portfolio_trade.md +332 -0
  34. data/docs/api-reference/sqa_portfoliooptimizer.md +248 -0
  35. data/docs/api-reference/sqa_riskmanager.md +388 -0
  36. data/docs/api-reference/sqa_seasonalanalyzer.md +121 -0
  37. data/docs/api-reference/sqa_sectoranalyzer.md +163 -0
  38. data/docs/api-reference/sqa_stock.md +649 -0
  39. data/docs/api-reference/sqa_strategy.md +178 -0
  40. data/docs/api-reference/sqa_strategy_bollingerbands.md +26 -0
  41. data/docs/api-reference/sqa_strategy_common.md +29 -0
  42. data/docs/api-reference/sqa_strategy_consensus.md +129 -0
  43. data/docs/api-reference/sqa_strategy_ema.md +41 -0
  44. data/docs/api-reference/sqa_strategy_kbs.md +154 -0
  45. data/docs/api-reference/sqa_strategy_macd.md +26 -0
  46. data/docs/api-reference/sqa_strategy_mp.md +41 -0
  47. data/docs/api-reference/sqa_strategy_mr.md +41 -0
  48. data/docs/api-reference/sqa_strategy_random.md +41 -0
  49. data/docs/api-reference/sqa_strategy_rsi.md +41 -0
  50. data/docs/api-reference/sqa_strategy_sma.md +41 -0
  51. data/docs/api-reference/sqa_strategy_stochastic.md +26 -0
  52. data/docs/api-reference/sqa_strategy_volumebreakout.md +26 -0
  53. data/docs/api-reference/sqa_strategygenerator.md +298 -0
  54. data/docs/api-reference/sqa_strategygenerator_pattern.md +264 -0
  55. data/docs/api-reference/sqa_strategygenerator_patterncontext.md +326 -0
  56. data/docs/api-reference/sqa_strategygenerator_profitablepoint.md +424 -0
  57. data/docs/api-reference/sqa_stream.md +256 -0
  58. data/docs/api-reference/sqa_ticker.md +175 -0
  59. data/docs/api-reference/string.md +135 -0
  60. data/docs/assets/images/advanced-workflow.svg +89 -0
  61. data/docs/assets/images/architecture.svg +107 -0
  62. data/docs/assets/images/data-flow.svg +138 -0
  63. data/docs/assets/images/getting-started-workflow.svg +88 -0
  64. data/docs/assets/images/strategy-flow.svg +78 -0
  65. data/docs/assets/images/system-architecture.svg +150 -0
  66. data/docs/concepts/index.md +292 -19
  67. data/docs/getting-started/index.md +1 -14
  68. data/docs/index.md +26 -23
  69. data/docs/llms.txt +109 -0
  70. data/docs/strategies/kbs.md +15 -14
  71. data/docs/strategy.md +381 -3
  72. data/docs/terms_of_use.md +1 -1
  73. data/examples/README.md +10 -0
  74. data/lib/api/alpha_vantage_api.rb +3 -7
  75. data/lib/sqa/config.rb +109 -28
  76. data/lib/sqa/data_frame/data.rb +13 -1
  77. data/lib/sqa/data_frame.rb +168 -26
  78. data/lib/sqa/errors.rb +79 -17
  79. data/lib/sqa/init.rb +70 -15
  80. data/lib/sqa/pattern_matcher.rb +4 -4
  81. data/lib/sqa/portfolio.rb +1 -1
  82. data/lib/sqa/sector_analyzer.rb +3 -11
  83. data/lib/sqa/stock.rb +169 -15
  84. data/lib/sqa/strategy.rb +62 -4
  85. data/lib/sqa/ticker.rb +106 -48
  86. data/lib/sqa/version.rb +1 -1
  87. data/lib/sqa.rb +4 -4
  88. data/mkdocs.yml +68 -81
  89. metadata +89 -21
  90. data/docs/README.md +0 -43
  91. data/examples/sinatra_app/Gemfile +0 -42
  92. data/examples/sinatra_app/Gemfile.lock +0 -268
  93. data/examples/sinatra_app/QUICKSTART.md +0 -169
  94. data/examples/sinatra_app/README.md +0 -471
  95. data/examples/sinatra_app/RUNNING_WITHOUT_TALIB.md +0 -90
  96. data/examples/sinatra_app/TROUBLESHOOTING.md +0 -95
  97. data/examples/sinatra_app/app.rb +0 -404
  98. data/examples/sinatra_app/config.ru +0 -5
  99. data/examples/sinatra_app/public/css/style.css +0 -723
  100. data/examples/sinatra_app/public/debug_macd.html +0 -82
  101. data/examples/sinatra_app/public/js/app.js +0 -107
  102. data/examples/sinatra_app/start.sh +0 -53
  103. data/examples/sinatra_app/views/analyze.erb +0 -306
  104. data/examples/sinatra_app/views/backtest.erb +0 -325
  105. data/examples/sinatra_app/views/dashboard.erb +0 -831
  106. data/examples/sinatra_app/views/error.erb +0 -58
  107. data/examples/sinatra_app/views/index.erb +0 -118
  108. data/examples/sinatra_app/views/layout.erb +0 -61
  109. data/examples/sinatra_app/views/portfolio.erb +0 -43
@@ -1,404 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'sinatra'
5
- require 'sinatra/json'
6
- require 'json'
7
-
8
- # Add SQA lib to load path
9
- $LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
10
-
11
- require 'sqa'
12
-
13
- # Initialize SQA
14
- SQA.init
15
-
16
- # Configure Sinatra
17
- set :port, 4567
18
- set :bind, '0.0.0.0'
19
- set :public_folder, File.dirname(__FILE__) + '/public'
20
- set :views, File.dirname(__FILE__) + '/views'
21
-
22
- # Enable sessions for flash messages
23
- enable :sessions
24
-
25
- # Helpers
26
- helpers do
27
- def format_percent(value)
28
- sprintf("%.2f%%", value)
29
- end
30
-
31
- def format_currency(value)
32
- sprintf("$%.2f", value)
33
- end
34
-
35
- def format_number(value)
36
- value.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
37
- end
38
-
39
- # Filter data arrays by time period
40
- # period can be: "30d", "60d", "90d", "1q", "2q", "3q", "4q", "all"
41
- def filter_by_period(dates, *data_arrays, period: 'all')
42
- return [dates, *data_arrays] if period == 'all' || dates.empty?
43
-
44
- require 'date'
45
-
46
- # Parse dates (they're strings in YYYY-MM-DD format)
47
- parsed_dates = dates.map { |d| Date.parse(d) }
48
- latest_date = parsed_dates.max
49
-
50
- # Calculate cutoff date based on period
51
- cutoff_date = case period
52
- when '30d'
53
- latest_date - 30
54
- when '60d'
55
- latest_date - 60
56
- when '90d'
57
- latest_date - 90
58
- when '1q'
59
- latest_date - 63 # ~3 months = 1 quarter (63 trading days)
60
- when '2q'
61
- latest_date - 126 # ~6 months = 2 quarters
62
- when '3q'
63
- latest_date - 189 # ~9 months = 3 quarters
64
- when '4q'
65
- latest_date - 252 # ~12 months = 4 quarters
66
- else
67
- parsed_dates.min # "all" - keep everything
68
- end
69
-
70
- # Find indices where date >= cutoff_date
71
- indices = parsed_dates.each_with_index.select { |d, i| d >= cutoff_date }.map(&:last)
72
-
73
- # Filter all arrays by the same indices
74
- filtered_dates = indices.map { |i| dates[i] }
75
- filtered_data = data_arrays.map { |arr| indices.map { |i| arr[i] } }
76
-
77
- [filtered_dates, *filtered_data]
78
- end
79
- end
80
-
81
- # Routes
82
-
83
- # Home / Dashboard
84
- get '/' do
85
- erb :index
86
- end
87
-
88
- # Dashboard for specific ticker
89
- get '/dashboard/:ticker' do
90
- ticker = params[:ticker].upcase
91
-
92
- begin
93
- @stock = SQA::Stock.new(ticker: ticker)
94
- @ticker = ticker
95
- erb :dashboard
96
- rescue => e
97
- @error = "Failed to load data for #{ticker}: #{e.message}"
98
- erb :error
99
- end
100
- end
101
-
102
- # Analysis page
103
- get '/analyze/:ticker' do
104
- ticker = params[:ticker].upcase
105
-
106
- begin
107
- @stock = SQA::Stock.new(ticker: ticker)
108
- @ticker = ticker
109
- erb :analyze
110
- rescue => e
111
- @error = "Failed to load data for #{ticker}: #{e.message}"
112
- erb :error
113
- end
114
- end
115
-
116
- # Backtest page
117
- get '/backtest/:ticker' do
118
- ticker = params[:ticker].upcase
119
-
120
- begin
121
- @stock = SQA::Stock.new(ticker: ticker)
122
- @ticker = ticker
123
- erb :backtest
124
- rescue => e
125
- @error = "Failed to load data for #{ticker}: #{e.message}"
126
- erb :error
127
- end
128
- end
129
-
130
- # Portfolio optimizer
131
- get '/portfolio' do
132
- erb :portfolio
133
- end
134
-
135
- # API Endpoints
136
-
137
- # Get stock data
138
- get '/api/stock/:ticker' do
139
- content_type :json
140
-
141
- ticker = params[:ticker].upcase
142
- period = params[:period] || 'all'
143
-
144
- begin
145
- stock = SQA::Stock.new(ticker: ticker)
146
- df = stock.df
147
-
148
- # Get price data (all data first)
149
- dates = df["timestamp"].to_a.map(&:to_s)
150
- opens = df["open_price"].to_a
151
- highs = df["high_price"].to_a
152
- lows = df["low_price"].to_a
153
- closes = df["adj_close_price"].to_a
154
- volumes = df["volume"].to_a
155
-
156
- # Filter by period
157
- filtered_dates, filtered_opens, filtered_highs, filtered_lows, filtered_closes, filtered_volumes =
158
- filter_by_period(dates, opens, highs, lows, closes, volumes, period: period)
159
-
160
- # Calculate basic stats
161
- current_price = filtered_closes.last
162
- prev_price = filtered_closes[-2]
163
- change = current_price - prev_price
164
- change_pct = (change / prev_price) * 100
165
-
166
- # 52-week high/low uses full data for reference
167
- high_52w = closes.last(252).max rescue closes.max
168
- low_52w = closes.last(252).min rescue closes.min
169
-
170
- {
171
- ticker: ticker,
172
- period: period,
173
- current_price: current_price,
174
- change: change,
175
- change_percent: change_pct,
176
- high_52w: high_52w,
177
- low_52w: low_52w,
178
- dates: filtered_dates,
179
- open: filtered_opens,
180
- high: filtered_highs,
181
- low: filtered_lows,
182
- close: filtered_closes,
183
- volume: filtered_volumes
184
- }.to_json
185
- rescue => e
186
- status 500
187
- { error: e.message }.to_json
188
- end
189
- end
190
-
191
- # Get technical indicators
192
- get '/api/indicators/:ticker' do
193
- content_type :json
194
-
195
- ticker = params[:ticker].upcase
196
- period = params[:period] || 'all'
197
-
198
- begin
199
- stock = SQA::Stock.new(ticker: ticker)
200
- df = stock.df
201
-
202
- prices = df["adj_close_price"].to_a
203
- highs = df["high_price"].to_a
204
- lows = df["low_price"].to_a
205
- dates = df["timestamp"].to_a.map(&:to_s)
206
-
207
- # Calculate indicators on full dataset (they need historical context)
208
- rsi = SQAI.rsi(prices, period: 14)
209
- macd_result = SQAI.macd(prices)
210
- bb_result = SQAI.bbands(prices)
211
- sma_20 = SQAI.sma(prices, period: 20)
212
- sma_50 = SQAI.sma(prices, period: 50)
213
- ema_20 = SQAI.ema(prices, period: 20)
214
-
215
- # Filter results by period (keep indicators aligned with dates)
216
- filtered_dates, filtered_rsi, filtered_macd, filtered_macd_signal, filtered_macd_hist,
217
- filtered_bb_upper, filtered_bb_middle, filtered_bb_lower, filtered_sma_20, filtered_sma_50, filtered_ema_20 =
218
- filter_by_period(dates, rsi, macd_result[0], macd_result[1], macd_result[2],
219
- bb_result[0], bb_result[1], bb_result[2],
220
- sma_20, sma_50, ema_20, period: period)
221
-
222
- {
223
- period: period,
224
- dates: filtered_dates,
225
- rsi: filtered_rsi,
226
- macd: filtered_macd,
227
- macd_signal: filtered_macd_signal,
228
- macd_hist: filtered_macd_hist,
229
- bb_upper: filtered_bb_upper,
230
- bb_middle: filtered_bb_middle,
231
- bb_lower: filtered_bb_lower,
232
- sma_20: filtered_sma_20,
233
- sma_50: filtered_sma_50,
234
- ema_20: filtered_ema_20
235
- }.to_json
236
- rescue => e
237
- status 500
238
- { error: e.message }.to_json
239
- end
240
- end
241
-
242
- # Run backtest
243
- post '/api/backtest/:ticker' do
244
- content_type :json
245
-
246
- ticker = params[:ticker].upcase
247
- strategy_name = params[:strategy] || 'RSI'
248
-
249
- begin
250
- stock = SQA::Stock.new(ticker: ticker)
251
-
252
- # Resolve strategy
253
- strategy = case strategy_name.upcase
254
- when 'RSI' then SQA::Strategy::RSI
255
- when 'SMA' then SQA::Strategy::SMA
256
- when 'EMA' then SQA::Strategy::EMA
257
- when 'MACD' then SQA::Strategy::MACD
258
- when 'BOLLINGERBANDS' then SQA::Strategy::BollingerBands
259
- when 'KBS' then SQA::Strategy::KBS
260
- else SQA::Strategy::RSI
261
- end
262
-
263
- # Run backtest
264
- backtest = SQA::Backtest.new(
265
- stock: stock,
266
- strategy: strategy,
267
- initial_capital: 10_000.0,
268
- commission: 1.0
269
- )
270
-
271
- results = backtest.run
272
-
273
- {
274
- total_return: results.total_return,
275
- annualized_return: results.annualized_return,
276
- sharpe_ratio: results.sharpe_ratio,
277
- max_drawdown: results.max_drawdown,
278
- win_rate: results.win_rate,
279
- total_trades: results.total_trades,
280
- profit_factor: results.profit_factor,
281
- avg_win: results.avg_win,
282
- avg_loss: results.avg_loss
283
- }.to_json
284
- rescue => e
285
- status 500
286
- { error: e.message }.to_json
287
- end
288
- end
289
-
290
- # Run market analysis
291
- get '/api/analyze/:ticker' do
292
- content_type :json
293
-
294
- ticker = params[:ticker].upcase
295
-
296
- begin
297
- stock = SQA::Stock.new(ticker: ticker)
298
- prices = stock.df["adj_close_price"].to_a
299
-
300
- # Market regime
301
- regime = SQA::MarketRegime.detect(stock)
302
-
303
- # Seasonal analysis
304
- seasonal = SQA::SeasonalAnalyzer.analyze(stock)
305
-
306
- # FPOP analysis
307
- fpop_data = SQA::FPOP.fpl_analysis(prices, fpop: 10)
308
- recent_fpop = fpop_data.last(10).map do |f|
309
- {
310
- direction: f[:direction],
311
- magnitude: f[:magnitude],
312
- risk: f[:risk],
313
- interpretation: f[:interpretation]
314
- }
315
- end
316
-
317
- # Risk metrics
318
- returns = prices.each_cons(2).map { |a, b| (b - a) / a }
319
- var_95 = SQA::RiskManager.var(returns, confidence: 0.95)
320
- sharpe = SQA::RiskManager.sharpe_ratio(returns)
321
- max_dd = SQA::RiskManager.max_drawdown(prices)
322
-
323
- {
324
- regime: {
325
- type: regime[:type],
326
- volatility: regime[:volatility],
327
- strength: regime[:strength],
328
- trend: regime[:trend]
329
- },
330
- seasonal: {
331
- best_months: seasonal[:best_months],
332
- worst_months: seasonal[:worst_months],
333
- best_quarters: seasonal[:best_quarters],
334
- has_pattern: seasonal[:has_seasonal_pattern]
335
- },
336
- fpop: recent_fpop,
337
- risk: {
338
- var_95: var_95,
339
- sharpe_ratio: sharpe,
340
- max_drawdown: max_dd[:max_drawdown]
341
- }
342
- }.to_json
343
- rescue => e
344
- status 500
345
- { error: e.message }.to_json
346
- end
347
- end
348
-
349
- # Compare strategies
350
- post '/api/compare/:ticker' do
351
- content_type :json
352
-
353
- ticker = params[:ticker].upcase
354
-
355
- begin
356
- stock = SQA::Stock.new(ticker: ticker)
357
-
358
- strategies = {
359
- 'RSI' => SQA::Strategy::RSI,
360
- 'SMA' => SQA::Strategy::SMA,
361
- 'EMA' => SQA::Strategy::EMA,
362
- 'MACD' => SQA::Strategy::MACD,
363
- 'BollingerBands' => SQA::Strategy::BollingerBands
364
- }
365
-
366
- results = strategies.map do |name, strategy_class|
367
- backtest = SQA::Backtest.new(
368
- stock: stock,
369
- strategy: strategy_class,
370
- initial_capital: 10_000.0,
371
- commission: 1.0
372
- )
373
-
374
- result = backtest.run
375
-
376
- {
377
- strategy: name,
378
- return: result.total_return,
379
- sharpe: result.sharpe_ratio,
380
- drawdown: result.max_drawdown,
381
- win_rate: result.win_rate,
382
- trades: result.total_trades
383
- }
384
- rescue => e
385
- nil
386
- end.compact
387
-
388
- results.sort_by! { |r| -r[:return] }
389
- results.to_json
390
- rescue => e
391
- status 500
392
- { error: e.message }.to_json
393
- end
394
- end
395
-
396
- # Start server
397
- if __FILE__ == $0
398
- puts "=" * 60
399
- puts "SQA Web Application"
400
- puts "=" * 60
401
- puts "Starting server on http://localhost:4567"
402
- puts "Press Ctrl+C to stop"
403
- puts "=" * 60
404
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'app'
4
-
5
- run Sinatra::Application