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.
- checksums.yaml +4 -4
- data/.goose/memory/development.txt +3 -0
- data/.semver +6 -0
- data/ARCHITECTURE.md +648 -0
- data/CHANGELOG.md +82 -0
- data/CLAUDE.md +653 -0
- data/COMMITS.md +196 -0
- data/DATAFRAME_ARCHITECTURE_REVIEW.md +421 -0
- data/NEXT-STEPS.md +154 -0
- data/README.md +812 -262
- data/TASKS.md +358 -0
- data/TEST_RESULTS.md +140 -0
- data/TODO.md +42 -0
- data/_notes.txt +25 -0
- data/bin/sqa-console +11 -0
- data/data/talk_talk.json +103284 -0
- data/develop_summary.md +313 -0
- data/docs/advanced/backtesting.md +206 -0
- data/docs/advanced/ensemble.md +68 -0
- data/docs/advanced/fpop.md +153 -0
- data/docs/advanced/index.md +112 -0
- data/docs/advanced/multi-timeframe.md +67 -0
- data/docs/advanced/pattern-matcher.md +75 -0
- data/docs/advanced/portfolio-optimizer.md +79 -0
- data/docs/advanced/portfolio.md +166 -0
- data/docs/advanced/risk-management.md +210 -0
- data/docs/advanced/strategy-generator.md +158 -0
- data/docs/advanced/streaming.md +209 -0
- data/docs/ai_and_ml.md +80 -0
- data/docs/api/dataframe.md +1115 -0
- data/docs/api/index.md +126 -0
- data/docs/assets/css/custom.css +88 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/concepts/index.md +68 -0
- data/docs/contributing/index.md +60 -0
- data/docs/data-sources/index.md +66 -0
- data/docs/data_frame.md +317 -97
- data/docs/factors_that_impact_price.md +26 -0
- data/docs/finviz.md +11 -0
- data/docs/fx_pro_bit.md +25 -0
- data/docs/genetic_programming.md +104 -0
- data/docs/getting-started/index.md +123 -0
- data/docs/getting-started/installation.md +229 -0
- data/docs/getting-started/quick-start.md +244 -0
- data/docs/i_gotta_an_idea.md +22 -0
- data/docs/index.md +163 -0
- data/docs/indicators/index.md +97 -0
- data/docs/indicators.md +110 -24
- data/docs/options.md +8 -0
- data/docs/strategies/bollinger-bands.md +146 -0
- data/docs/strategies/consensus.md +64 -0
- data/docs/strategies/custom.md +310 -0
- data/docs/strategies/ema.md +53 -0
- data/docs/strategies/index.md +92 -0
- data/docs/strategies/kbs.md +164 -0
- data/docs/strategies/macd.md +96 -0
- data/docs/strategies/market-profile.md +54 -0
- data/docs/strategies/mean-reversion.md +58 -0
- data/docs/strategies/rsi.md +95 -0
- data/docs/strategies/sma.md +55 -0
- data/docs/strategies/stochastic.md +63 -0
- data/docs/strategies/volume-breakout.md +54 -0
- data/docs/tags.md +7 -0
- data/docs/true_strength_index.md +46 -0
- data/docs/weighted_moving_average.md +48 -0
- data/examples/README.md +354 -0
- data/examples/advanced_features_example.rb +350 -0
- data/examples/fpop_analysis_example.rb +191 -0
- data/examples/genetic_programming_example.rb +148 -0
- data/examples/kbs_strategy_example.rb +208 -0
- data/examples/pattern_context_example.rb +300 -0
- data/examples/rails_app/Gemfile +34 -0
- data/examples/rails_app/README.md +416 -0
- data/examples/rails_app/app/assets/javascripts/application.js +107 -0
- data/examples/rails_app/app/assets/stylesheets/application.css +659 -0
- data/examples/rails_app/app/controllers/analysis_controller.rb +11 -0
- data/examples/rails_app/app/controllers/api/v1/stocks_controller.rb +227 -0
- data/examples/rails_app/app/controllers/application_controller.rb +22 -0
- data/examples/rails_app/app/controllers/backtest_controller.rb +11 -0
- data/examples/rails_app/app/controllers/dashboard_controller.rb +21 -0
- data/examples/rails_app/app/controllers/portfolio_controller.rb +7 -0
- data/examples/rails_app/app/views/analysis/show.html.erb +209 -0
- data/examples/rails_app/app/views/backtest/show.html.erb +171 -0
- data/examples/rails_app/app/views/dashboard/index.html.erb +118 -0
- data/examples/rails_app/app/views/dashboard/show.html.erb +408 -0
- data/examples/rails_app/app/views/errors/show.html.erb +17 -0
- data/examples/rails_app/app/views/layouts/application.html.erb +60 -0
- data/examples/rails_app/app/views/portfolio/index.html.erb +33 -0
- data/examples/rails_app/bin/rails +6 -0
- data/examples/rails_app/config/application.rb +45 -0
- data/examples/rails_app/config/boot.rb +5 -0
- data/examples/rails_app/config/database.yml +18 -0
- data/examples/rails_app/config/environment.rb +11 -0
- data/examples/rails_app/config/routes.rb +26 -0
- data/examples/rails_app/config.ru +8 -0
- data/examples/realtime_stream_example.rb +274 -0
- data/examples/sinatra_app/Gemfile +22 -0
- data/examples/sinatra_app/QUICKSTART.md +159 -0
- data/examples/sinatra_app/README.md +461 -0
- data/examples/sinatra_app/app.rb +344 -0
- data/examples/sinatra_app/config.ru +5 -0
- data/examples/sinatra_app/public/css/style.css +659 -0
- data/examples/sinatra_app/public/js/app.js +107 -0
- data/examples/sinatra_app/views/analyze.erb +306 -0
- data/examples/sinatra_app/views/backtest.erb +325 -0
- data/examples/sinatra_app/views/dashboard.erb +419 -0
- data/examples/sinatra_app/views/error.erb +58 -0
- data/examples/sinatra_app/views/index.erb +118 -0
- data/examples/sinatra_app/views/layout.erb +61 -0
- data/examples/sinatra_app/views/portfolio.erb +43 -0
- data/examples/strategy_generator_example.rb +346 -0
- data/hsa_portfolio.csv +11 -0
- data/justfile +0 -0
- data/lib/api/alpha_vantage_api.rb +462 -0
- data/lib/sqa/backtest.rb +329 -0
- data/lib/sqa/data_frame/alpha_vantage.rb +43 -65
- data/lib/sqa/data_frame/data.rb +92 -0
- data/lib/sqa/data_frame/yahoo_finance.rb +35 -43
- data/lib/sqa/data_frame.rb +148 -243
- data/lib/sqa/ensemble.rb +359 -0
- data/lib/sqa/fpop.rb +199 -0
- data/lib/sqa/gp.rb +259 -0
- data/lib/sqa/indicator.rb +5 -8
- data/lib/sqa/init.rb +15 -8
- data/lib/sqa/market_regime.rb +240 -0
- data/lib/sqa/multi_timeframe.rb +379 -0
- data/lib/sqa/pattern_matcher.rb +497 -0
- data/lib/sqa/portfolio.rb +260 -6
- data/lib/sqa/portfolio_optimizer.rb +377 -0
- data/lib/sqa/risk_manager.rb +442 -0
- data/lib/sqa/seasonal_analyzer.rb +209 -0
- data/lib/sqa/sector_analyzer.rb +300 -0
- data/lib/sqa/stock.rb +67 -125
- data/lib/sqa/strategy/bollinger_bands.rb +42 -0
- data/lib/sqa/strategy/consensus.rb +5 -2
- data/lib/sqa/strategy/kbs_strategy.rb +470 -0
- data/lib/sqa/strategy/macd.rb +46 -0
- data/lib/sqa/strategy/mp.rb +1 -1
- data/lib/sqa/strategy/stochastic.rb +60 -0
- data/lib/sqa/strategy/volume_breakout.rb +57 -0
- data/lib/sqa/strategy.rb +5 -0
- data/lib/sqa/strategy_generator.rb +947 -0
- data/lib/sqa/stream.rb +361 -0
- data/lib/sqa/version.rb +1 -7
- data/lib/sqa.rb +23 -16
- data/main.just +81 -0
- data/mkdocs.yml +288 -0
- data/trace.log +0 -0
- metadata +261 -51
- data/bin/sqa +0 -6
- data/lib/patches/dry-cli.rb +0 -228
- data/lib/sqa/activity.rb +0 -10
- data/lib/sqa/cli.rb +0 -62
- data/lib/sqa/commands/analysis.rb +0 -309
- data/lib/sqa/commands/base.rb +0 -139
- data/lib/sqa/commands/web.rb +0 -199
- data/lib/sqa/commands.rb +0 -22
- data/lib/sqa/constants.rb +0 -23
- data/lib/sqa/indicator/average_true_range.rb +0 -33
- data/lib/sqa/indicator/bollinger_bands.rb +0 -28
- data/lib/sqa/indicator/candlestick_pattern_recognizer.rb +0 -60
- data/lib/sqa/indicator/donchian_channel.rb +0 -29
- data/lib/sqa/indicator/double_top_bottom_pattern.rb +0 -34
- data/lib/sqa/indicator/elliott_wave_theory.rb +0 -57
- data/lib/sqa/indicator/exponential_moving_average.rb +0 -25
- data/lib/sqa/indicator/exponential_moving_average_trend.rb +0 -36
- data/lib/sqa/indicator/fibonacci_retracement.rb +0 -23
- data/lib/sqa/indicator/head_and_shoulders_pattern.rb +0 -26
- data/lib/sqa/indicator/market_profile.rb +0 -32
- data/lib/sqa/indicator/mean_reversion.rb +0 -37
- data/lib/sqa/indicator/momentum.rb +0 -28
- data/lib/sqa/indicator/moving_average_convergence_divergence.rb +0 -29
- data/lib/sqa/indicator/peaks_and_valleys.rb +0 -29
- data/lib/sqa/indicator/predict_next_value.rb +0 -202
- data/lib/sqa/indicator/relative_strength_index.rb +0 -47
- data/lib/sqa/indicator/simple_moving_average.rb +0 -24
- data/lib/sqa/indicator/simple_moving_average_trend.rb +0 -32
- data/lib/sqa/indicator/stochastic_oscillator.rb +0 -68
- data/lib/sqa/indicator/true_range.rb +0 -39
- 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,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 %>
|