sqa 0.0.24 → 0.0.32

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 (203) 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 +95 -0
  6. data/CLAUDE.md +674 -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 +839 -265
  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 +1114 -0
  31. data/docs/api/index.md +126 -0
  32. data/docs/assets/css/custom.css +88 -0
  33. data/docs/assets/images/sqa.jpg +0 -0
  34. data/docs/assets/js/mathjax.js +18 -0
  35. data/docs/concepts/index.md +60 -0
  36. data/docs/contributing/index.md +60 -0
  37. data/docs/data-sources/index.md +66 -0
  38. data/docs/data_frame.md +316 -97
  39. data/docs/factors_that_impact_price.md +26 -0
  40. data/docs/finviz.md +11 -0
  41. data/docs/fx_pro_bit.md +25 -0
  42. data/docs/genetic_programming.md +104 -0
  43. data/docs/getting-started/index.md +107 -0
  44. data/docs/getting-started/installation.md +229 -0
  45. data/docs/getting-started/quick-start.md +244 -0
  46. data/docs/i_gotta_an_idea.md +22 -0
  47. data/docs/index.md +161 -0
  48. data/docs/indicators/index.md +97 -0
  49. data/docs/indicators.md +110 -24
  50. data/docs/options.md +8 -0
  51. data/docs/strategies/bollinger-bands.md +146 -0
  52. data/docs/strategies/consensus.md +64 -0
  53. data/docs/strategies/custom.md +310 -0
  54. data/docs/strategies/ema.md +53 -0
  55. data/docs/strategies/index.md +92 -0
  56. data/docs/strategies/kbs.md +164 -0
  57. data/docs/strategies/macd.md +96 -0
  58. data/docs/strategies/market-profile.md +54 -0
  59. data/docs/strategies/mean-reversion.md +58 -0
  60. data/docs/strategies/rsi.md +95 -0
  61. data/docs/strategies/sma.md +55 -0
  62. data/docs/strategies/stochastic.md +63 -0
  63. data/docs/strategies/volume-breakout.md +54 -0
  64. data/docs/tags.md +7 -0
  65. data/examples/README.md +354 -0
  66. data/examples/advanced_features_example.rb +350 -0
  67. data/examples/fpop_analysis_example.rb +191 -0
  68. data/examples/genetic_programming_example.rb +148 -0
  69. data/examples/kbs_strategy_example.rb +208 -0
  70. data/examples/pattern_context_example.rb +300 -0
  71. data/examples/rails_app/Gemfile +34 -0
  72. data/examples/rails_app/README.md +416 -0
  73. data/examples/rails_app/app/assets/javascripts/application.js +107 -0
  74. data/examples/rails_app/app/assets/stylesheets/application.css +659 -0
  75. data/examples/rails_app/app/controllers/analysis_controller.rb +11 -0
  76. data/examples/rails_app/app/controllers/api/v1/stocks_controller.rb +227 -0
  77. data/examples/rails_app/app/controllers/application_controller.rb +22 -0
  78. data/examples/rails_app/app/controllers/backtest_controller.rb +11 -0
  79. data/examples/rails_app/app/controllers/dashboard_controller.rb +21 -0
  80. data/examples/rails_app/app/controllers/portfolio_controller.rb +7 -0
  81. data/examples/rails_app/app/views/analysis/show.html.erb +209 -0
  82. data/examples/rails_app/app/views/backtest/show.html.erb +171 -0
  83. data/examples/rails_app/app/views/dashboard/index.html.erb +118 -0
  84. data/examples/rails_app/app/views/dashboard/show.html.erb +408 -0
  85. data/examples/rails_app/app/views/errors/show.html.erb +17 -0
  86. data/examples/rails_app/app/views/layouts/application.html.erb +60 -0
  87. data/examples/rails_app/app/views/portfolio/index.html.erb +33 -0
  88. data/examples/rails_app/bin/rails +6 -0
  89. data/examples/rails_app/config/application.rb +45 -0
  90. data/examples/rails_app/config/boot.rb +5 -0
  91. data/examples/rails_app/config/database.yml +18 -0
  92. data/examples/rails_app/config/environment.rb +11 -0
  93. data/examples/rails_app/config/routes.rb +26 -0
  94. data/examples/rails_app/config.ru +8 -0
  95. data/examples/realtime_stream_example.rb +274 -0
  96. data/examples/sinatra_app/Gemfile +42 -0
  97. data/examples/sinatra_app/Gemfile.lock +268 -0
  98. data/examples/sinatra_app/QUICKSTART.md +169 -0
  99. data/examples/sinatra_app/README.md +471 -0
  100. data/examples/sinatra_app/RUNNING_WITHOUT_TALIB.md +90 -0
  101. data/examples/sinatra_app/TROUBLESHOOTING.md +95 -0
  102. data/examples/sinatra_app/app.rb +404 -0
  103. data/examples/sinatra_app/config.ru +5 -0
  104. data/examples/sinatra_app/public/css/style.css +723 -0
  105. data/examples/sinatra_app/public/debug_macd.html +82 -0
  106. data/examples/sinatra_app/public/js/app.js +107 -0
  107. data/examples/sinatra_app/start.sh +53 -0
  108. data/examples/sinatra_app/views/analyze.erb +306 -0
  109. data/examples/sinatra_app/views/backtest.erb +325 -0
  110. data/examples/sinatra_app/views/dashboard.erb +831 -0
  111. data/examples/sinatra_app/views/error.erb +58 -0
  112. data/examples/sinatra_app/views/index.erb +118 -0
  113. data/examples/sinatra_app/views/layout.erb +61 -0
  114. data/examples/sinatra_app/views/portfolio.erb +43 -0
  115. data/examples/strategy_generator_example.rb +346 -0
  116. data/hsa_portfolio.csv +11 -0
  117. data/justfile +0 -0
  118. data/lib/api/alpha_vantage_api.rb +462 -0
  119. data/lib/sqa/backtest.rb +329 -0
  120. data/lib/sqa/data_frame/alpha_vantage.rb +51 -63
  121. data/lib/sqa/data_frame/data.rb +92 -0
  122. data/lib/sqa/data_frame/yahoo_finance.rb +35 -43
  123. data/lib/sqa/data_frame.rb +154 -243
  124. data/lib/sqa/ensemble.rb +359 -0
  125. data/lib/sqa/fpop.rb +199 -0
  126. data/lib/sqa/gp.rb +259 -0
  127. data/lib/sqa/indicator.rb +16 -6
  128. data/lib/sqa/init.rb +15 -8
  129. data/lib/sqa/market_regime.rb +240 -0
  130. data/lib/sqa/multi_timeframe.rb +379 -0
  131. data/lib/sqa/pattern_matcher.rb +497 -0
  132. data/lib/sqa/portfolio.rb +260 -6
  133. data/lib/sqa/portfolio_optimizer.rb +377 -0
  134. data/lib/sqa/risk_manager.rb +442 -0
  135. data/lib/sqa/seasonal_analyzer.rb +209 -0
  136. data/lib/sqa/sector_analyzer.rb +300 -0
  137. data/lib/sqa/stock.rb +131 -127
  138. data/lib/sqa/strategy/bollinger_bands.rb +42 -0
  139. data/lib/sqa/strategy/consensus.rb +5 -2
  140. data/lib/sqa/strategy/kbs_strategy.rb +470 -0
  141. data/lib/sqa/strategy/macd.rb +46 -0
  142. data/lib/sqa/strategy/mp.rb +1 -1
  143. data/lib/sqa/strategy/stochastic.rb +60 -0
  144. data/lib/sqa/strategy/volume_breakout.rb +57 -0
  145. data/lib/sqa/strategy.rb +5 -0
  146. data/lib/sqa/strategy_generator.rb +947 -0
  147. data/lib/sqa/stream.rb +361 -0
  148. data/lib/sqa/ticker.rb +9 -2
  149. data/lib/sqa/version.rb +1 -7
  150. data/lib/sqa.rb +35 -20
  151. data/main.just +81 -0
  152. data/mkdocs.yml +252 -0
  153. data/trace.log +0 -0
  154. metadata +265 -69
  155. data/bin/sqa +0 -6
  156. data/docs/alpha_vantage_technical_indicators.md +0 -62
  157. data/docs/average_true_range.md +0 -9
  158. data/docs/bollinger_bands.md +0 -15
  159. data/docs/candlestick_pattern_recognizer.md +0 -4
  160. data/docs/donchian_channel.md +0 -5
  161. data/docs/double_top_bottom_pattern.md +0 -3
  162. data/docs/exponential_moving_average.md +0 -19
  163. data/docs/fibonacci_retracement.md +0 -30
  164. data/docs/head_and_shoulders_pattern.md +0 -3
  165. data/docs/market_profile.md +0 -4
  166. data/docs/momentum.md +0 -19
  167. data/docs/moving_average_convergence_divergence.md +0 -23
  168. data/docs/peaks_and_valleys.md +0 -11
  169. data/docs/relative_strength_index.md +0 -6
  170. data/docs/simple_moving_average.md +0 -8
  171. data/docs/stochastic_oscillator.md +0 -4
  172. data/docs/ta_lib.md +0 -160
  173. data/docs/true_range.md +0 -12
  174. data/lib/patches/dry-cli.rb +0 -228
  175. data/lib/sqa/activity.rb +0 -10
  176. data/lib/sqa/cli.rb +0 -62
  177. data/lib/sqa/commands/analysis.rb +0 -309
  178. data/lib/sqa/commands/base.rb +0 -139
  179. data/lib/sqa/commands/web.rb +0 -199
  180. data/lib/sqa/commands.rb +0 -22
  181. data/lib/sqa/constants.rb +0 -23
  182. data/lib/sqa/indicator/average_true_range.rb +0 -33
  183. data/lib/sqa/indicator/bollinger_bands.rb +0 -28
  184. data/lib/sqa/indicator/candlestick_pattern_recognizer.rb +0 -60
  185. data/lib/sqa/indicator/donchian_channel.rb +0 -29
  186. data/lib/sqa/indicator/double_top_bottom_pattern.rb +0 -34
  187. data/lib/sqa/indicator/elliott_wave_theory.rb +0 -57
  188. data/lib/sqa/indicator/exponential_moving_average.rb +0 -25
  189. data/lib/sqa/indicator/exponential_moving_average_trend.rb +0 -36
  190. data/lib/sqa/indicator/fibonacci_retracement.rb +0 -23
  191. data/lib/sqa/indicator/head_and_shoulders_pattern.rb +0 -26
  192. data/lib/sqa/indicator/market_profile.rb +0 -32
  193. data/lib/sqa/indicator/mean_reversion.rb +0 -37
  194. data/lib/sqa/indicator/momentum.rb +0 -28
  195. data/lib/sqa/indicator/moving_average_convergence_divergence.rb +0 -29
  196. data/lib/sqa/indicator/peaks_and_valleys.rb +0 -29
  197. data/lib/sqa/indicator/predict_next_value.rb +0 -202
  198. data/lib/sqa/indicator/relative_strength_index.rb +0 -47
  199. data/lib/sqa/indicator/simple_moving_average.rb +0 -24
  200. data/lib/sqa/indicator/simple_moving_average_trend.rb +0 -32
  201. data/lib/sqa/indicator/stochastic_oscillator.rb +0 -68
  202. data/lib/sqa/indicator/true_range.rb +0 -39
  203. data/lib/sqa/trade.rb +0 -26
@@ -0,0 +1,95 @@
1
+ # Troubleshooting the Sinatra App
2
+
3
+ ## Dashboard page loads but buttons/charts don't work
4
+
5
+ This usually means JavaScript errors are preventing the page from functioning.
6
+
7
+ ### Quick Fix Checklist
8
+
9
+ 1. **Install dependencies first:**
10
+ ```bash
11
+ cd examples/sinatra_app
12
+ bundle install
13
+ ```
14
+
15
+ 2. **Start the server with bundle exec:**
16
+ ```bash
17
+ bundle exec ruby app.rb
18
+ # OR
19
+ bundle exec rackup
20
+ ```
21
+
22
+ 3. **Check browser console for errors:**
23
+ - Open browser DevTools (F12 or Ctrl+Shift+I)
24
+ - Go to "Console" tab
25
+ - Look for red error messages
26
+ - Take a screenshot and check what's failing
27
+
28
+ 4. **Check Network tab:**
29
+ - Open DevTools → Network tab
30
+ - Reload the dashboard page
31
+ - Look for failed requests (red status codes)
32
+ - Check if `/api/stock/AAPL`, `/api/indicators/AAPL`, `/api/analyze/AAPL` are returning 200 or errors
33
+
34
+ ### Common Issues
35
+
36
+ #### Issue: "ApexCharts is not defined"
37
+ **Symptom:** Charts don't render, console shows `Uncaught ReferenceError: ApexCharts is not defined`
38
+
39
+ **Fix:** The CDN link for ApexCharts is missing or blocked. Check `views/layout.erb`:
40
+ ```html
41
+ <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
42
+ ```
43
+
44
+ #### Issue: API calls failing with 500 errors
45
+ **Symptom:** Network tab shows `/api/indicators/AAPL` returns 500
46
+
47
+ **Fix:** This is expected if TA-Lib isn't installed. The app should handle this gracefully.
48
+
49
+ #### Issue: Buttons don't respond
50
+ **Symptom:** Clicking buttons does nothing
51
+
52
+ **Possible causes:**
53
+ 1. JavaScript not loaded (check console for errors)
54
+ 2. Event handlers not attached (check if page finished loading)
55
+ 3. JavaScript errors earlier in the file preventing execution
56
+
57
+ ### Debug Mode
58
+
59
+ Run the server with verbose logging:
60
+ ```bash
61
+ RACK_ENV=development bundle exec ruby app.rb
62
+ ```
63
+
64
+ ### Test API Endpoints Manually
65
+
66
+ While server is running, test in another terminal:
67
+
68
+ ```bash
69
+ # Test stock data
70
+ curl -s "http://localhost:4567/api/stock/AAPL?period=30d" | python3 -m json.tool
71
+
72
+ # Test indicators (may fail without TA-Lib - that's OK)
73
+ curl -s "http://localhost:4567/api/indicators/AAPL?period=30d"
74
+
75
+ # Test analysis
76
+ curl -s "http://localhost:4567/api/analyze/AAPL" | python3 -m json.tool
77
+ ```
78
+
79
+ All should return JSON. If you get HTML error pages, there's a server-side issue.
80
+
81
+ ### Check if all gems are installed
82
+
83
+ ```bash
84
+ bundle check
85
+ ```
86
+
87
+ If it says "The Gemfile's dependencies are satisfied", you're good.
88
+
89
+ ### Still not working?
90
+
91
+ Please provide:
92
+ 1. Screenshot of browser console (DevTools → Console tab)
93
+ 2. Screenshot of network tab showing failed requests
94
+ 3. Output of `bundle check`
95
+ 4. Any errors from server console
@@ -0,0 +1,404 @@
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'app'
4
+
5
+ run Sinatra::Application