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,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ module V1
5
+ class StocksController < ActionController::API
6
+ before_action :set_ticker, except: []
7
+
8
+ # GET /api/v1/stock/:ticker
9
+ def show
10
+ stock = SQA::Stock.new(ticker: @ticker)
11
+ df = stock.df
12
+
13
+ # Get price data
14
+ dates = df['date'].to_a.map(&:to_s)
15
+ opens = df['open_price'].to_a
16
+ highs = df['high_price'].to_a
17
+ lows = df['low_price'].to_a
18
+ closes = df['adj_close_price'].to_a
19
+ volumes = df['volume'].to_a
20
+
21
+ # Calculate basic stats
22
+ current_price = closes.last
23
+ prev_price = closes[-2]
24
+ change = current_price - prev_price
25
+ change_pct = (change / prev_price) * 100
26
+
27
+ render json: {
28
+ ticker: @ticker,
29
+ current_price: current_price,
30
+ change: change,
31
+ change_percent: change_pct,
32
+ high_52w: closes.last(252).max,
33
+ low_52w: closes.last(252).min,
34
+ dates: dates,
35
+ open: opens,
36
+ high: highs,
37
+ low: lows,
38
+ close: closes,
39
+ volume: volumes
40
+ }
41
+ rescue => e
42
+ render json: { error: e.message }, status: :internal_server_error
43
+ end
44
+
45
+ # GET /api/v1/indicators/:ticker
46
+ def indicators
47
+ stock = SQA::Stock.new(ticker: @ticker)
48
+ df = stock.df
49
+
50
+ prices = df['adj_close_price'].to_a
51
+ highs = df['high_price'].to_a
52
+ lows = df['low_price'].to_a
53
+ dates = df['date'].to_a.map(&:to_s)
54
+
55
+ # Calculate indicators
56
+ rsi = SQAI.rsi(prices, period: 14)
57
+ macd_result = SQAI.macd(prices)
58
+ bb_result = SQAI.bbands(prices)
59
+ sma_20 = SQAI.sma(prices, period: 20)
60
+ sma_50 = SQAI.sma(prices, period: 50)
61
+ ema_20 = SQAI.ema(prices, period: 20)
62
+
63
+ render json: {
64
+ dates: dates,
65
+ rsi: rsi,
66
+ macd: macd_result[0],
67
+ macd_signal: macd_result[1],
68
+ macd_hist: macd_result[2],
69
+ bb_upper: bb_result[0],
70
+ bb_middle: bb_result[1],
71
+ bb_lower: bb_result[2],
72
+ sma_20: sma_20,
73
+ sma_50: sma_50,
74
+ ema_20: ema_20
75
+ }
76
+ rescue => e
77
+ render json: { error: e.message }, status: :internal_server_error
78
+ end
79
+
80
+ # POST /api/v1/backtest/:ticker
81
+ def backtest
82
+ stock = SQA::Stock.new(ticker: @ticker)
83
+ strategy_name = params[:strategy] || 'RSI'
84
+
85
+ strategy = resolve_strategy(strategy_name)
86
+
87
+ backtest_runner = SQA::Backtest.new(
88
+ stock: stock,
89
+ strategy: strategy,
90
+ initial_capital: 10_000.0,
91
+ commission: 1.0
92
+ )
93
+
94
+ results = backtest_runner.run
95
+
96
+ render json: {
97
+ total_return: results.total_return,
98
+ annualized_return: results.annualized_return,
99
+ sharpe_ratio: results.sharpe_ratio,
100
+ max_drawdown: results.max_drawdown,
101
+ win_rate: results.win_rate,
102
+ total_trades: results.total_trades,
103
+ profit_factor: results.profit_factor,
104
+ avg_win: results.avg_win,
105
+ avg_loss: results.avg_loss
106
+ }
107
+ rescue => e
108
+ render json: { error: e.message }, status: :internal_server_error
109
+ end
110
+
111
+ # GET /api/v1/analyze/:ticker
112
+ def analyze
113
+ stock = SQA::Stock.new(ticker: @ticker)
114
+ prices = stock.df['adj_close_price'].to_a
115
+
116
+ # Market regime
117
+ regime = SQA::MarketRegime.detect(stock)
118
+
119
+ # Seasonal analysis
120
+ seasonal = SQA::SeasonalAnalyzer.analyze(stock)
121
+
122
+ # FPOP analysis
123
+ fpop_data = SQA::FPOP.fpl_analysis(prices, fpop: 10)
124
+ recent_fpop = fpop_data.last(10).map do |f|
125
+ {
126
+ direction: f[:direction],
127
+ magnitude: f[:magnitude],
128
+ risk: f[:risk],
129
+ interpretation: f[:interpretation]
130
+ }
131
+ end
132
+
133
+ # Risk metrics
134
+ returns = prices.each_cons(2).map { |a, b| (b - a) / a }
135
+ var_95 = SQA::RiskManager.var(returns, confidence: 0.95)
136
+ sharpe = SQA::RiskManager.sharpe_ratio(returns)
137
+ max_dd = SQA::RiskManager.max_drawdown(prices)
138
+
139
+ render json: {
140
+ regime: {
141
+ type: regime[:type],
142
+ volatility: regime[:volatility],
143
+ strength: regime[:strength],
144
+ trend: regime[:trend]
145
+ },
146
+ seasonal: {
147
+ best_months: seasonal[:best_months],
148
+ worst_months: seasonal[:worst_months],
149
+ best_quarters: seasonal[:best_quarters],
150
+ has_pattern: seasonal[:has_seasonal_pattern]
151
+ },
152
+ fpop: recent_fpop,
153
+ risk: {
154
+ var_95: var_95,
155
+ sharpe_ratio: sharpe,
156
+ max_drawdown: max_dd[:max_drawdown]
157
+ }
158
+ }
159
+ rescue => e
160
+ render json: { error: e.message }, status: :internal_server_error
161
+ end
162
+
163
+ # POST /api/v1/compare/:ticker
164
+ def compare
165
+ stock = SQA::Stock.new(ticker: @ticker)
166
+
167
+ strategies = {
168
+ 'RSI' => SQA::Strategy::RSI,
169
+ 'SMA' => SQA::Strategy::SMA,
170
+ 'EMA' => SQA::Strategy::EMA,
171
+ 'MACD' => SQA::Strategy::MACD,
172
+ 'BollingerBands' => SQA::Strategy::BollingerBands
173
+ }
174
+
175
+ results = strategies.map do |name, strategy_class|
176
+ backtest_runner = SQA::Backtest.new(
177
+ stock: stock,
178
+ strategy: strategy_class,
179
+ initial_capital: 10_000.0,
180
+ commission: 1.0
181
+ )
182
+
183
+ result = backtest_runner.run
184
+
185
+ {
186
+ strategy: name,
187
+ return: result.total_return,
188
+ sharpe: result.sharpe_ratio,
189
+ drawdown: result.max_drawdown,
190
+ win_rate: result.win_rate,
191
+ trades: result.total_trades
192
+ }
193
+ rescue => e
194
+ nil
195
+ end.compact
196
+
197
+ results.sort_by! { |r| -r[:return] }
198
+ render json: results
199
+ rescue => e
200
+ render json: { error: e.message }, status: :internal_server_error
201
+ end
202
+
203
+ private
204
+
205
+ def set_ticker
206
+ @ticker = params[:ticker].upcase
207
+ end
208
+
209
+ def resolve_strategy(name)
210
+ case name.upcase
211
+ when 'RSI' then SQA::Strategy::RSI
212
+ when 'SMA' then SQA::Strategy::SMA
213
+ when 'EMA' then SQA::Strategy::EMA
214
+ when 'MACD' then SQA::Strategy::MACD
215
+ when 'BOLLINGERBANDS' then SQA::Strategy::BollingerBands
216
+ when 'STOCHASTIC' then SQA::Strategy::Stochastic
217
+ when 'VOLUMEBREAKOUT' then SQA::Strategy::VolumeBreakout
218
+ when 'KBS' then SQA::Strategy::KBS
219
+ when 'CONSENSUS' then SQA::Strategy::Consensus
220
+ when 'RANDOM' then SQA::Strategy::Random
221
+ else
222
+ SQA::Strategy::RSI
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationController < ActionController::Base
4
+ # Prevent CSRF attacks by raising an exception
5
+ protect_from_forgery with: :exception
6
+
7
+ helper_method :format_percent, :format_currency, :format_number
8
+
9
+ private
10
+
11
+ def format_percent(value)
12
+ format('%.2f%%', value)
13
+ end
14
+
15
+ def format_currency(value)
16
+ format('$%.2f', value)
17
+ end
18
+
19
+ def format_number(value)
20
+ value.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BacktestController < ApplicationController
4
+ def show
5
+ @ticker = params[:ticker].upcase
6
+ @stock = SQA::Stock.new(ticker: @ticker)
7
+ rescue => e
8
+ @error = "Failed to load data for #{@ticker}: #{e.message}"
9
+ render 'errors/show', status: :unprocessable_entity
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DashboardController < ApplicationController
4
+ def index
5
+ # Landing page
6
+ end
7
+
8
+ def show
9
+ @ticker = params[:ticker].upcase
10
+ load_stock_data
11
+ rescue => e
12
+ @error = "Failed to load data for #{@ticker}: #{e.message}"
13
+ render 'errors/show', status: :unprocessable_entity
14
+ end
15
+
16
+ private
17
+
18
+ def load_stock_data
19
+ @stock = SQA::Stock.new(ticker: @ticker)
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PortfolioController < ApplicationController
4
+ def index
5
+ # Portfolio optimization page
6
+ end
7
+ end
@@ -0,0 +1,209 @@
1
+ <div class="dashboard">
2
+ <div class="dashboard-header">
3
+ <div class="ticker-info">
4
+ <h1><%= @ticker %> - Market Analysis</h1>
5
+ </div>
6
+ <div class="header-actions">
7
+ <%= link_to stock_dashboard_path(@ticker), class: "btn btn-secondary" do %>
8
+ <i class="fas fa-chart-line"></i> Dashboard
9
+ <% end %>
10
+ <%= link_to stock_backtest_path(@ticker), class: "btn btn-secondary" do %>
11
+ <i class="fas fa-history"></i> Backtest
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+
16
+ <!-- Market Regime Analysis -->
17
+ <div class="chart-container">
18
+ <div class="chart-header">
19
+ <h2><i class="fas fa-chart-area"></i> Market Regime Analysis</h2>
20
+ </div>
21
+ <div id="regimeAnalysis" class="analysis-content">
22
+ <p class="loading"><i class="fas fa-spinner fa-spin"></i> Loading market regime data...</p>
23
+ </div>
24
+ </div>
25
+
26
+ <!-- Seasonal Pattern Analysis -->
27
+ <div class="chart-container">
28
+ <div class="chart-header">
29
+ <h2><i class="fas fa-calendar-alt"></i> Seasonal Pattern Analysis</h2>
30
+ </div>
31
+ <div id="seasonalAnalysis" class="analysis-content">
32
+ <p class="loading"><i class="fas fa-spinner fa-spin"></i> Loading seasonal data...</p>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- FPOP Analysis -->
37
+ <div class="chart-container">
38
+ <div class="chart-header">
39
+ <h2><i class="fas fa-crystal-ball"></i> Future Period Analysis (FPOP)</h2>
40
+ <p class="chart-subtitle">Potential future price movements based on historical patterns</p>
41
+ </div>
42
+ <div id="fpopAnalysis" class="analysis-content">
43
+ <p class="loading"><i class="fas fa-spinner fa-spin"></i> Loading FPOP analysis...</p>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- Risk Metrics -->
48
+ <div class="chart-container">
49
+ <div class="chart-header">
50
+ <h2><i class="fas fa-shield-alt"></i> Risk Metrics</h2>
51
+ </div>
52
+ <div id="riskMetrics" class="analysis-content">
53
+ <p class="loading"><i class="fas fa-spinner fa-spin"></i> Loading risk metrics...</p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <%= javascript_tag do %>
59
+ const ticker = '<%= @ticker %>';
60
+
61
+ document.addEventListener('DOMContentLoaded', async function() {
62
+ await loadAnalysisData();
63
+ });
64
+
65
+ async function loadAnalysisData() {
66
+ try {
67
+ const response = await fetch(`/api/v1/analyze/${ticker}`);
68
+ const data = await response.json();
69
+
70
+ renderRegimeAnalysis(data.regime);
71
+ renderSeasonalAnalysis(data.seasonal);
72
+ renderFPOPAnalysis(data.fpop);
73
+ renderRiskMetrics(data.risk);
74
+ } catch (error) {
75
+ console.error('Error loading analysis data:', error);
76
+ }
77
+ }
78
+
79
+ function renderRegimeAnalysis(regime) {
80
+ const regimeType = regime.type.toUpperCase();
81
+ const regimeClass = regimeType === 'BULL' ? 'signal-buy' :
82
+ regimeType === 'BEAR' ? 'signal-sell' : 'signal-neutral';
83
+
84
+ const html = `
85
+ <div class="analysis-grid">
86
+ <div class="analysis-card">
87
+ <h3>Current Market Regime</h3>
88
+ <div class="analysis-stat">
89
+ <span class="stat-label">Type</span>
90
+ <span class="stat-value ${regimeClass}">${regimeType}</span>
91
+ </div>
92
+ <div class="analysis-stat">
93
+ <span class="stat-label">Volatility</span>
94
+ <span class="stat-value">${regime.volatility.toUpperCase()}</span>
95
+ </div>
96
+ <div class="analysis-stat">
97
+ <span class="stat-label">Strength</span>
98
+ <span class="stat-value">${regime.strength.toFixed(2)}</span>
99
+ </div>
100
+ <div class="analysis-stat">
101
+ <span class="stat-label">Trend</span>
102
+ <span class="stat-value">${regime.trend.toFixed(2)}%</span>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ `;
107
+
108
+ document.getElementById('regimeAnalysis').innerHTML = html;
109
+ }
110
+
111
+ function renderSeasonalAnalysis(seasonal) {
112
+ const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
113
+ const bestMonths = seasonal.best_months.map(m => monthNames[m]).join(', ');
114
+ const worstMonths = seasonal.worst_months.map(m => monthNames[m]).join(', ');
115
+
116
+ const html = `
117
+ <div class="analysis-grid">
118
+ <div class="analysis-card">
119
+ <h3>Monthly Patterns</h3>
120
+ <div class="analysis-stat">
121
+ <span class="stat-label">Best Months</span>
122
+ <span class="stat-value signal-buy">${bestMonths}</span>
123
+ </div>
124
+ <div class="analysis-stat">
125
+ <span class="stat-label">Worst Months</span>
126
+ <span class="stat-value signal-sell">${worstMonths}</span>
127
+ </div>
128
+ </div>
129
+ <div class="analysis-card">
130
+ <h3>Quarterly Patterns</h3>
131
+ <div class="analysis-stat">
132
+ <span class="stat-label">Best Quarters</span>
133
+ <span class="stat-value signal-buy">Q${seasonal.best_quarters.join(', Q')}</span>
134
+ </div>
135
+ <div class="analysis-stat">
136
+ <span class="stat-label">Has Seasonal Pattern</span>
137
+ <span class="stat-value">${seasonal.has_pattern ? 'Yes' : 'No'}</span>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ `;
142
+
143
+ document.getElementById('seasonalAnalysis').innerHTML = html;
144
+ }
145
+
146
+ function renderFPOPAnalysis(fpop) {
147
+ if (!fpop || fpop.length === 0) {
148
+ document.getElementById('fpopAnalysis').innerHTML = '<p class="hint">No FPOP data available.</p>';
149
+ return;
150
+ }
151
+
152
+ let tableHTML = `
153
+ <table class="fpop-table">
154
+ <thead>
155
+ <tr>
156
+ <th>Direction</th>
157
+ <th>Magnitude</th>
158
+ <th>Risk</th>
159
+ <th>Interpretation</th>
160
+ </tr>
161
+ </thead>
162
+ <tbody>
163
+ `;
164
+
165
+ fpop.forEach(f => {
166
+ const dirClass = f.direction === 'UP' ? 'signal-buy' :
167
+ f.direction === 'DOWN' ? 'signal-sell' : 'signal-neutral';
168
+
169
+ tableHTML += `
170
+ <tr>
171
+ <td><span class="${dirClass}">${f.direction}</span></td>
172
+ <td>${f.magnitude.toFixed(2)}%</td>
173
+ <td>${f.risk.toFixed(2)}%</td>
174
+ <td>${f.interpretation}</td>
175
+ </tr>
176
+ `;
177
+ });
178
+
179
+ tableHTML += '</tbody></table>';
180
+ document.getElementById('fpopAnalysis').innerHTML = tableHTML;
181
+ }
182
+
183
+ function renderRiskMetrics(risk) {
184
+ const html = `
185
+ <div class="analysis-grid">
186
+ <div class="analysis-card">
187
+ <h3>Value at Risk (VaR)</h3>
188
+ <div class="analysis-stat">
189
+ <span class="stat-label">95% Confidence</span>
190
+ <span class="stat-value signal-sell">${(risk.var_95 * 100).toFixed(2)}%</span>
191
+ </div>
192
+ </div>
193
+ <div class="analysis-card">
194
+ <h3>Performance Metrics</h3>
195
+ <div class="analysis-stat">
196
+ <span class="stat-label">Sharpe Ratio</span>
197
+ <span class="stat-value">${risk.sharpe_ratio.toFixed(2)}</span>
198
+ </div>
199
+ <div class="analysis-stat">
200
+ <span class="stat-label">Max Drawdown</span>
201
+ <span class="stat-value signal-sell">${(risk.max_drawdown * 100).toFixed(2)}%</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ `;
206
+
207
+ document.getElementById('riskMetrics').innerHTML = html;
208
+ }
209
+ <% end %>
@@ -0,0 +1,171 @@
1
+ <div class="dashboard">
2
+ <div class="dashboard-header">
3
+ <div class="ticker-info">
4
+ <h1><%= @ticker %> - Strategy Backtesting</h1>
5
+ </div>
6
+ <div class="header-actions">
7
+ <%= link_to stock_dashboard_path(@ticker), class: "btn btn-secondary" do %>
8
+ <i class="fas fa-chart-line"></i> Dashboard
9
+ <% end %>
10
+ <%= link_to stock_analysis_path(@ticker), class: "btn btn-secondary" do %>
11
+ <i class="fas fa-analytics"></i> Analysis
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+
16
+ <!-- Strategy Selection -->
17
+ <div class="chart-container">
18
+ <div class="chart-header">
19
+ <h2><i class="fas fa-cogs"></i> Select Strategy</h2>
20
+ </div>
21
+ <div class="strategy-selector">
22
+ <div class="strategy-grid">
23
+ <div class="strategy-option" onclick="selectStrategy('RSI')">
24
+ <div class="strategy-icon"><i class="fas fa-wave-square"></i></div>
25
+ <h3>RSI</h3>
26
+ <p>Relative Strength Index</p>
27
+ </div>
28
+ <div class="strategy-option" onclick="selectStrategy('MACD')">
29
+ <div class="strategy-icon"><i class="fas fa-signal"></i></div>
30
+ <h3>MACD</h3>
31
+ <p>Moving Average Convergence Divergence</p>
32
+ </div>
33
+ <div class="strategy-option" onclick="selectStrategy('SMA')">
34
+ <div class="strategy-icon"><i class="fas fa-chart-line"></i></div>
35
+ <h3>SMA</h3>
36
+ <p>Simple Moving Average</p>
37
+ </div>
38
+ <div class="strategy-option" onclick="selectStrategy('EMA')">
39
+ <div class="strategy-icon"><i class="fas fa-chart-area"></i></div>
40
+ <h3>EMA</h3>
41
+ <p>Exponential Moving Average</p>
42
+ </div>
43
+ <div class="strategy-option" onclick="selectStrategy('BollingerBands')">
44
+ <div class="strategy-icon"><i class="fas fa-chart-candlestick"></i></div>
45
+ <h3>Bollinger Bands</h3>
46
+ <p>Volatility-based bands</p>
47
+ </div>
48
+ <div class="strategy-option" onclick="selectStrategy('KBS')">
49
+ <div class="strategy-icon"><i class="fas fa-brain"></i></div>
50
+ <h3>KBS</h3>
51
+ <p>Knowledge-Based Strategy</p>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- Backtest Results -->
58
+ <div class="chart-container">
59
+ <div class="chart-header">
60
+ <h2><i class="fas fa-chart-bar"></i> Backtest Results</h2>
61
+ </div>
62
+ <div id="backtestResults" class="backtest-results">
63
+ <p class="hint">Select a strategy above to run backtest</p>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Strategy Comparison -->
68
+ <div class="chart-container">
69
+ <div class="chart-header">
70
+ <h2><i class="fas fa-trophy"></i> Compare All Strategies</h2>
71
+ <button onclick="compareStrategies()" class="btn btn-primary">
72
+ <i class="fas fa-play"></i> Run Comparison
73
+ </button>
74
+ </div>
75
+ <div id="comparisonResults" class="comparison-results">
76
+ <p class="hint">Click "Run Comparison" to backtest all strategies</p>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <%= javascript_tag do %>
82
+ const ticker = '<%= @ticker %>';
83
+
84
+ async function selectStrategy(strategy) {
85
+ event.currentTarget.style.borderColor = 'var(--primary-color)';
86
+ await runBacktest(strategy);
87
+ }
88
+
89
+ async function runBacktest(strategy) {
90
+ const resultsDiv = document.getElementById('backtestResults');
91
+ resultsDiv.innerHTML = '<p class="loading"><i class="fas fa-spinner fa-spin"></i> Running backtest...</p>';
92
+
93
+ try {
94
+ const response = await fetch(`/api/v1/backtest/${ticker}`, {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ 'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
99
+ },
100
+ body: JSON.stringify({ strategy: strategy })
101
+ });
102
+
103
+ const results = await response.json();
104
+
105
+ const html = `
106
+ <h3 style="margin-bottom: 1.5rem">${strategy} Strategy Results</h3>
107
+ <div class="results-metrics">
108
+ <div class="result-metric">
109
+ <div class="result-metric-label">Total Return</div>
110
+ <div class="result-metric-value ${results.total_return >= 0 ? 'positive' : 'negative'}">
111
+ ${results.total_return.toFixed(2)}%
112
+ </div>
113
+ </div>
114
+ <div class="result-metric">
115
+ <div class="result-metric-label">Sharpe Ratio</div>
116
+ <div class="result-metric-value">${results.sharpe_ratio.toFixed(2)}</div>
117
+ </div>
118
+ <div class="result-metric">
119
+ <div class="result-metric-label">Max Drawdown</div>
120
+ <div class="result-metric-value negative">${results.max_drawdown.toFixed(2)}%</div>
121
+ </div>
122
+ <div class="result-metric">
123
+ <div class="result-metric-label">Win Rate</div>
124
+ <div class="result-metric-value">${results.win_rate.toFixed(2)}%</div>
125
+ </div>
126
+ </div>
127
+ `;
128
+
129
+ resultsDiv.innerHTML = html;
130
+ } catch (error) {
131
+ console.error('Error running backtest:', error);
132
+ resultsDiv.innerHTML = '<p class="error">Failed to run backtest.</p>';
133
+ }
134
+ }
135
+
136
+ async function compareStrategies() {
137
+ const resultsDiv = document.getElementById('comparisonResults');
138
+ resultsDiv.innerHTML = '<p class="loading"><i class="fas fa-spinner fa-spin"></i> Comparing strategies...</p>';
139
+
140
+ try {
141
+ const response = await fetch(`/api/v1/compare/${ticker}`, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
145
+ }
146
+ });
147
+ const results = await response.json();
148
+
149
+ let html = '<table class="results-table"><thead><tr>';
150
+ html += '<th>Strategy</th><th>Return</th><th>Sharpe</th><th>Drawdown</th><th>Win Rate</th>';
151
+ html += '</tr></thead><tbody>';
152
+
153
+ results.forEach((result, i) => {
154
+ const rank = i === 0 ? '<i class="fas fa-trophy" style="color: #FFD700;"></i> ' : '';
155
+ html += `<tr>
156
+ <td>${rank}${result.strategy}</td>
157
+ <td class="${result.return >= 0 ? 'positive' : 'negative'}">${result.return.toFixed(2)}%</td>
158
+ <td>${result.sharpe.toFixed(2)}</td>
159
+ <td>${result.drawdown.toFixed(2)}%</td>
160
+ <td>${result.win_rate.toFixed(2)}%</td>
161
+ </tr>`;
162
+ });
163
+
164
+ html += '</tbody></table>';
165
+ resultsDiv.innerHTML = html;
166
+ } catch (error) {
167
+ console.error('Error comparing strategies:', error);
168
+ resultsDiv.innerHTML = '<p class="error">Failed to compare strategies.</p>';
169
+ }
170
+ }
171
+ <% end %>