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.
- checksums.yaml +4 -4
- data/.goose/memory/development.txt +3 -0
- data/.semver +6 -0
- data/ARCHITECTURE.md +648 -0
- data/CHANGELOG.md +95 -0
- data/CLAUDE.md +674 -0
- data/COMMITS.md +196 -0
- data/DATAFRAME_ARCHITECTURE_REVIEW.md +421 -0
- data/NEXT-STEPS.md +154 -0
- data/README.md +839 -265
- 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 +1114 -0
- data/docs/api/index.md +126 -0
- data/docs/assets/css/custom.css +88 -0
- data/docs/assets/images/sqa.jpg +0 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/concepts/index.md +60 -0
- data/docs/contributing/index.md +60 -0
- data/docs/data-sources/index.md +66 -0
- data/docs/data_frame.md +316 -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 +107 -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 +161 -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/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 +42 -0
- data/examples/sinatra_app/Gemfile.lock +268 -0
- data/examples/sinatra_app/QUICKSTART.md +169 -0
- data/examples/sinatra_app/README.md +471 -0
- data/examples/sinatra_app/RUNNING_WITHOUT_TALIB.md +90 -0
- data/examples/sinatra_app/TROUBLESHOOTING.md +95 -0
- data/examples/sinatra_app/app.rb +404 -0
- data/examples/sinatra_app/config.ru +5 -0
- data/examples/sinatra_app/public/css/style.css +723 -0
- data/examples/sinatra_app/public/debug_macd.html +82 -0
- data/examples/sinatra_app/public/js/app.js +107 -0
- data/examples/sinatra_app/start.sh +53 -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 +831 -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 +51 -63
- 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 +154 -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 +16 -6
- 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 +131 -127
- 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/ticker.rb +9 -2
- data/lib/sqa/version.rb +1 -7
- data/lib/sqa.rb +35 -20
- data/main.just +81 -0
- data/mkdocs.yml +252 -0
- data/trace.log +0 -0
- metadata +265 -69
- data/bin/sqa +0 -6
- data/docs/alpha_vantage_technical_indicators.md +0 -62
- data/docs/average_true_range.md +0 -9
- data/docs/bollinger_bands.md +0 -15
- data/docs/candlestick_pattern_recognizer.md +0 -4
- data/docs/donchian_channel.md +0 -5
- data/docs/double_top_bottom_pattern.md +0 -3
- data/docs/exponential_moving_average.md +0 -19
- data/docs/fibonacci_retracement.md +0 -30
- data/docs/head_and_shoulders_pattern.md +0 -3
- data/docs/market_profile.md +0 -4
- data/docs/momentum.md +0 -19
- data/docs/moving_average_convergence_divergence.md +0 -23
- data/docs/peaks_and_valleys.md +0 -11
- data/docs/relative_strength_index.md +0 -6
- data/docs/simple_moving_average.md +0 -8
- data/docs/stochastic_oscillator.md +0 -4
- data/docs/ta_lib.md +0 -160
- data/docs/true_range.md +0 -12
- 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,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
|