sqa_demo-sinatra 0.1.0 → 0.2.2
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/CHANGELOG.md +141 -0
- data/README.md +18 -0
- data/lib/sqa_demo/sinatra/app.rb +18 -549
- data/lib/sqa_demo/sinatra/helpers/api_helpers.rb +221 -0
- data/lib/sqa_demo/sinatra/helpers/filters.rb +50 -0
- data/lib/sqa_demo/sinatra/helpers/formatting.rb +63 -0
- data/lib/sqa_demo/sinatra/helpers/stock_loader.rb +215 -0
- data/lib/sqa_demo/sinatra/public/css/style.css +290 -13
- data/lib/sqa_demo/sinatra/public/js/app.js +55 -8
- data/lib/sqa_demo/sinatra/routes/api.rb +235 -0
- data/lib/sqa_demo/sinatra/routes/pages.rb +137 -0
- data/lib/sqa_demo/sinatra/version.rb +1 -1
- data/lib/sqa_demo/sinatra/views/analyze.erb +83 -8
- data/lib/sqa_demo/sinatra/views/company.erb +682 -0
- data/lib/sqa_demo/sinatra/views/compare.erb +500 -0
- data/lib/sqa_demo/sinatra/views/dashboard.erb +11 -1
- data/lib/sqa_demo/sinatra/views/index.erb +2 -1
- data/lib/sqa_demo/sinatra/views/layout.erb +106 -14
- metadata +10 -1
data/lib/sqa_demo/sinatra/app.rb
CHANGED
|
@@ -5,6 +5,16 @@ require 'sinatra/json'
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'sqa'
|
|
7
7
|
|
|
8
|
+
# Load helpers
|
|
9
|
+
require_relative 'helpers/formatting'
|
|
10
|
+
require_relative 'helpers/filters'
|
|
11
|
+
require_relative 'helpers/stock_loader'
|
|
12
|
+
require_relative 'helpers/api_helpers'
|
|
13
|
+
|
|
14
|
+
# Load routes
|
|
15
|
+
require_relative 'routes/pages'
|
|
16
|
+
require_relative 'routes/api'
|
|
17
|
+
|
|
8
18
|
module SqaDemo
|
|
9
19
|
module Sinatra
|
|
10
20
|
class App < ::Sinatra::Base
|
|
@@ -26,556 +36,15 @@ module SqaDemo
|
|
|
26
36
|
SQA.init
|
|
27
37
|
end
|
|
28
38
|
|
|
29
|
-
#
|
|
30
|
-
helpers
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def format_currency(value)
|
|
36
|
-
sprintf("$%.2f", value)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def format_number(value)
|
|
40
|
-
value.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Filter data arrays by time period
|
|
44
|
-
# period can be: "30d", "60d", "90d", "1q", "2q", "3q", "4q", "all"
|
|
45
|
-
def filter_by_period(dates, *data_arrays, period: 'all')
|
|
46
|
-
return [dates, *data_arrays] if period == 'all' || dates.empty?
|
|
47
|
-
|
|
48
|
-
require 'date'
|
|
49
|
-
|
|
50
|
-
# Parse dates (they're strings in YYYY-MM-DD format)
|
|
51
|
-
parsed_dates = dates.map { |d| Date.parse(d) }
|
|
52
|
-
latest_date = parsed_dates.max
|
|
53
|
-
|
|
54
|
-
# Calculate cutoff date based on period
|
|
55
|
-
cutoff_date = case period
|
|
56
|
-
when '30d'
|
|
57
|
-
latest_date - 30
|
|
58
|
-
when '60d'
|
|
59
|
-
latest_date - 60
|
|
60
|
-
when '90d'
|
|
61
|
-
latest_date - 90
|
|
62
|
-
when '1q'
|
|
63
|
-
latest_date - 63 # ~3 months = 1 quarter (63 trading days)
|
|
64
|
-
when '2q'
|
|
65
|
-
latest_date - 126 # ~6 months = 2 quarters
|
|
66
|
-
when '3q'
|
|
67
|
-
latest_date - 189 # ~9 months = 3 quarters
|
|
68
|
-
when '4q'
|
|
69
|
-
latest_date - 252 # ~12 months = 4 quarters
|
|
70
|
-
else
|
|
71
|
-
parsed_dates.min # "all" - keep everything
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Find indices where date >= cutoff_date
|
|
75
|
-
indices = parsed_dates.each_with_index.select { |d, _i| d >= cutoff_date }.map(&:last)
|
|
76
|
-
|
|
77
|
-
# Filter all arrays by the same indices
|
|
78
|
-
filtered_dates = indices.map { |i| dates[i] }
|
|
79
|
-
filtered_data = data_arrays.map { |arr| indices.map { |i| arr[i] } }
|
|
80
|
-
|
|
81
|
-
[filtered_dates, *filtered_data]
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Routes
|
|
86
|
-
|
|
87
|
-
# Home / Dashboard
|
|
88
|
-
get '/' do
|
|
89
|
-
erb :index
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Dashboard for specific ticker
|
|
93
|
-
get '/dashboard/:ticker' do
|
|
94
|
-
ticker = params[:ticker].upcase
|
|
95
|
-
|
|
96
|
-
begin
|
|
97
|
-
@stock = SQA::Stock.new(ticker: ticker)
|
|
98
|
-
@ticker = ticker
|
|
99
|
-
erb :dashboard
|
|
100
|
-
rescue => e
|
|
101
|
-
@error = "Failed to load data for #{ticker}: #{e.message}"
|
|
102
|
-
erb :error
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Analysis page
|
|
107
|
-
get '/analyze/:ticker' do
|
|
108
|
-
ticker = params[:ticker].upcase
|
|
109
|
-
|
|
110
|
-
begin
|
|
111
|
-
@stock = SQA::Stock.new(ticker: ticker)
|
|
112
|
-
@ticker = ticker
|
|
113
|
-
erb :analyze
|
|
114
|
-
rescue => e
|
|
115
|
-
@error = "Failed to load data for #{ticker}: #{e.message}"
|
|
116
|
-
erb :error
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Backtest page
|
|
121
|
-
get '/backtest/:ticker' do
|
|
122
|
-
ticker = params[:ticker].upcase
|
|
123
|
-
|
|
124
|
-
begin
|
|
125
|
-
@stock = SQA::Stock.new(ticker: ticker)
|
|
126
|
-
@ticker = ticker
|
|
127
|
-
erb :backtest
|
|
128
|
-
rescue => e
|
|
129
|
-
@error = "Failed to load data for #{ticker}: #{e.message}"
|
|
130
|
-
erb :error
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Portfolio optimizer
|
|
135
|
-
get '/portfolio' do
|
|
136
|
-
erb :portfolio
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# API Endpoints
|
|
140
|
-
|
|
141
|
-
# Get stock data
|
|
142
|
-
get '/api/stock/:ticker' do
|
|
143
|
-
content_type :json
|
|
144
|
-
|
|
145
|
-
ticker = params[:ticker].upcase
|
|
146
|
-
period = params[:period] || 'all'
|
|
147
|
-
|
|
148
|
-
begin
|
|
149
|
-
stock = SQA::Stock.new(ticker: ticker)
|
|
150
|
-
df = stock.df
|
|
151
|
-
|
|
152
|
-
# Get price data (all data first)
|
|
153
|
-
dates = df["timestamp"].to_a.map(&:to_s)
|
|
154
|
-
opens = df["open_price"].to_a
|
|
155
|
-
highs = df["high_price"].to_a
|
|
156
|
-
lows = df["low_price"].to_a
|
|
157
|
-
closes = df["adj_close_price"].to_a
|
|
158
|
-
volumes = df["volume"].to_a
|
|
159
|
-
|
|
160
|
-
# Filter by period
|
|
161
|
-
filtered_dates, filtered_opens, filtered_highs, filtered_lows, filtered_closes, filtered_volumes =
|
|
162
|
-
filter_by_period(dates, opens, highs, lows, closes, volumes, period: period)
|
|
163
|
-
|
|
164
|
-
# Calculate basic stats
|
|
165
|
-
current_price = filtered_closes.last
|
|
166
|
-
prev_price = filtered_closes[-2]
|
|
167
|
-
change = current_price - prev_price
|
|
168
|
-
change_pct = (change / prev_price) * 100
|
|
169
|
-
|
|
170
|
-
# 52-week high/low uses full data for reference
|
|
171
|
-
high_52w = closes.last(252).max rescue closes.max
|
|
172
|
-
low_52w = closes.last(252).min rescue closes.min
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
ticker: ticker,
|
|
176
|
-
period: period,
|
|
177
|
-
current_price: current_price,
|
|
178
|
-
change: change,
|
|
179
|
-
change_percent: change_pct,
|
|
180
|
-
high_52w: high_52w,
|
|
181
|
-
low_52w: low_52w,
|
|
182
|
-
dates: filtered_dates,
|
|
183
|
-
open: filtered_opens,
|
|
184
|
-
high: filtered_highs,
|
|
185
|
-
low: filtered_lows,
|
|
186
|
-
close: filtered_closes,
|
|
187
|
-
volume: filtered_volumes
|
|
188
|
-
}.to_json
|
|
189
|
-
rescue => e
|
|
190
|
-
status 500
|
|
191
|
-
{ error: e.message }.to_json
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# Get technical indicators
|
|
196
|
-
get '/api/indicators/:ticker' do
|
|
197
|
-
content_type :json
|
|
198
|
-
|
|
199
|
-
ticker = params[:ticker].upcase
|
|
200
|
-
period = params[:period] || 'all'
|
|
201
|
-
|
|
202
|
-
begin
|
|
203
|
-
stock = SQA::Stock.new(ticker: ticker)
|
|
204
|
-
df = stock.df
|
|
205
|
-
|
|
206
|
-
prices = df["adj_close_price"].to_a
|
|
207
|
-
opens = df["open_price"].to_a
|
|
208
|
-
highs = df["high_price"].to_a
|
|
209
|
-
lows = df["low_price"].to_a
|
|
210
|
-
volumes = df["volume"].to_a
|
|
211
|
-
dates = df["timestamp"].to_a.map(&:to_s)
|
|
212
|
-
n = prices.length
|
|
213
|
-
|
|
214
|
-
# Calculate price indicators on full dataset (they need historical context)
|
|
215
|
-
rsi = SQAI.rsi(prices, period: 14)
|
|
216
|
-
macd_result = SQAI.macd(prices)
|
|
217
|
-
bb_result = SQAI.bbands(prices)
|
|
218
|
-
sma_12 = SQAI.sma(prices, period: 12)
|
|
219
|
-
sma_20 = SQAI.sma(prices, period: 20)
|
|
220
|
-
sma_50 = SQAI.sma(prices, period: 50)
|
|
221
|
-
ema_20 = SQAI.ema(prices, period: 20)
|
|
222
|
-
|
|
223
|
-
# Additional moving averages for price overlay
|
|
224
|
-
wma_20 = SQAI.wma(prices, period: 20)
|
|
225
|
-
dema_20 = SQAI.dema(prices, period: 20)
|
|
226
|
-
tema_20 = SQAI.tema(prices, period: 20)
|
|
227
|
-
kama_30 = SQAI.kama(prices, period: 30)
|
|
228
|
-
|
|
229
|
-
# Momentum indicators (require high/low/close)
|
|
230
|
-
stoch_result = SQAI.stoch(highs, lows, prices)
|
|
231
|
-
mom_10 = SQAI.mom(prices, period: 10)
|
|
232
|
-
cci_14 = SQAI.cci(highs, lows, prices, period: 14)
|
|
233
|
-
willr_14 = SQAI.willr(highs, lows, prices, period: 14)
|
|
234
|
-
roc_10 = SQAI.roc(prices, period: 10)
|
|
235
|
-
adx_14 = SQAI.adx(highs, lows, prices, period: 14)
|
|
236
|
-
|
|
237
|
-
# Volatility indicators
|
|
238
|
-
atr_14 = SQAI.atr(highs, lows, prices, period: 14)
|
|
239
|
-
|
|
240
|
-
# Volume indicators
|
|
241
|
-
obv = SQAI.obv(prices, volumes)
|
|
242
|
-
ad = SQAI.ad(highs, lows, prices, volumes)
|
|
243
|
-
|
|
244
|
-
# Calculate volume moving averages
|
|
245
|
-
vol_sma_12 = SQAI.sma(volumes, period: 12)
|
|
246
|
-
vol_sma_20 = SQAI.sma(volumes, period: 20)
|
|
247
|
-
vol_sma_50 = SQAI.sma(volumes, period: 50)
|
|
248
|
-
vol_ema_12 = SQAI.ema(volumes, period: 12)
|
|
249
|
-
vol_ema_20 = SQAI.ema(volumes, period: 20)
|
|
250
|
-
|
|
251
|
-
# Candlestick pattern recognition (high-priority patterns)
|
|
252
|
-
cdl_doji = SQAI.cdl_doji(opens, highs, lows, prices)
|
|
253
|
-
cdl_hammer = SQAI.cdl_hammer(opens, highs, lows, prices)
|
|
254
|
-
cdl_shootingstar = SQAI.cdl_shootingstar(opens, highs, lows, prices)
|
|
255
|
-
cdl_engulfing = SQAI.cdl_engulfing(opens, highs, lows, prices)
|
|
256
|
-
cdl_morningstar = SQAI.cdl_morningstar(opens, highs, lows, prices)
|
|
257
|
-
cdl_eveningstar = SQAI.cdl_eveningstar(opens, highs, lows, prices)
|
|
258
|
-
cdl_harami = SQAI.cdl_harami(opens, highs, lows, prices)
|
|
259
|
-
cdl_3whitesoldiers = SQAI.cdl_3whitesoldiers(opens, highs, lows, prices)
|
|
260
|
-
cdl_3blackcrows = SQAI.cdl_3blackcrows(opens, highs, lows, prices)
|
|
261
|
-
cdl_piercing = SQAI.cdl_piercing(opens, highs, lows, prices)
|
|
262
|
-
cdl_darkcloudcover = SQAI.cdl_darkcloudcover(opens, highs, lows, prices)
|
|
263
|
-
cdl_marubozu = SQAI.cdl_marubozu(opens, highs, lows, prices)
|
|
264
|
-
|
|
265
|
-
# Pad indicator arrays with nil at the beginning to align with dates
|
|
266
|
-
# Indicators return shorter arrays due to warmup periods
|
|
267
|
-
pad_array = ->(arr) { Array.new(n - arr.length, nil) + arr }
|
|
268
|
-
|
|
269
|
-
rsi = pad_array.call(rsi)
|
|
270
|
-
macd_line = pad_array.call(macd_result[0])
|
|
271
|
-
macd_signal = pad_array.call(macd_result[1])
|
|
272
|
-
macd_hist = pad_array.call(macd_result[2])
|
|
273
|
-
bb_upper = pad_array.call(bb_result[0])
|
|
274
|
-
bb_middle = pad_array.call(bb_result[1])
|
|
275
|
-
bb_lower = pad_array.call(bb_result[2])
|
|
276
|
-
sma_12 = pad_array.call(sma_12)
|
|
277
|
-
sma_20 = pad_array.call(sma_20)
|
|
278
|
-
sma_50 = pad_array.call(sma_50)
|
|
279
|
-
ema_20 = pad_array.call(ema_20)
|
|
280
|
-
|
|
281
|
-
# Pad additional moving averages
|
|
282
|
-
wma_20 = pad_array.call(wma_20)
|
|
283
|
-
dema_20 = pad_array.call(dema_20)
|
|
284
|
-
tema_20 = pad_array.call(tema_20)
|
|
285
|
-
kama_30 = pad_array.call(kama_30)
|
|
286
|
-
|
|
287
|
-
# Pad momentum indicators
|
|
288
|
-
stoch_slowk = pad_array.call(stoch_result[0])
|
|
289
|
-
stoch_slowd = pad_array.call(stoch_result[1])
|
|
290
|
-
mom_10 = pad_array.call(mom_10)
|
|
291
|
-
cci_14 = pad_array.call(cci_14)
|
|
292
|
-
willr_14 = pad_array.call(willr_14)
|
|
293
|
-
roc_10 = pad_array.call(roc_10)
|
|
294
|
-
adx_14 = pad_array.call(adx_14)
|
|
295
|
-
|
|
296
|
-
# Pad volatility indicators
|
|
297
|
-
atr_14 = pad_array.call(atr_14)
|
|
298
|
-
|
|
299
|
-
# Pad volume indicators
|
|
300
|
-
obv = pad_array.call(obv)
|
|
301
|
-
ad = pad_array.call(ad)
|
|
302
|
-
vol_sma_12 = pad_array.call(vol_sma_12)
|
|
303
|
-
vol_sma_20 = pad_array.call(vol_sma_20)
|
|
304
|
-
vol_sma_50 = pad_array.call(vol_sma_50)
|
|
305
|
-
vol_ema_12 = pad_array.call(vol_ema_12)
|
|
306
|
-
vol_ema_20 = pad_array.call(vol_ema_20)
|
|
307
|
-
|
|
308
|
-
# Pad pattern arrays
|
|
309
|
-
cdl_doji = pad_array.call(cdl_doji)
|
|
310
|
-
cdl_hammer = pad_array.call(cdl_hammer)
|
|
311
|
-
cdl_shootingstar = pad_array.call(cdl_shootingstar)
|
|
312
|
-
cdl_engulfing = pad_array.call(cdl_engulfing)
|
|
313
|
-
cdl_morningstar = pad_array.call(cdl_morningstar)
|
|
314
|
-
cdl_eveningstar = pad_array.call(cdl_eveningstar)
|
|
315
|
-
cdl_harami = pad_array.call(cdl_harami)
|
|
316
|
-
cdl_3whitesoldiers = pad_array.call(cdl_3whitesoldiers)
|
|
317
|
-
cdl_3blackcrows = pad_array.call(cdl_3blackcrows)
|
|
318
|
-
cdl_piercing = pad_array.call(cdl_piercing)
|
|
319
|
-
cdl_darkcloudcover = pad_array.call(cdl_darkcloudcover)
|
|
320
|
-
cdl_marubozu = pad_array.call(cdl_marubozu)
|
|
321
|
-
|
|
322
|
-
# Detect patterns from the data
|
|
323
|
-
# Pattern types:
|
|
324
|
-
# :neutral - always neutral signal (Doji)
|
|
325
|
-
# :fixed - predetermined signal regardless of value sign
|
|
326
|
-
# :directional - sign of value determines bullish (+) or bearish (-)
|
|
327
|
-
pattern_defs = {
|
|
328
|
-
doji: { data: cdl_doji, name: 'Doji', type: :neutral },
|
|
329
|
-
hammer: { data: cdl_hammer, name: 'Hammer', type: :fixed, signal: 'bullish' },
|
|
330
|
-
shootingstar: { data: cdl_shootingstar, name: 'Shooting Star', type: :fixed, signal: 'bearish' },
|
|
331
|
-
engulfing: { data: cdl_engulfing, name: 'Engulfing', type: :directional },
|
|
332
|
-
morningstar: { data: cdl_morningstar, name: 'Morning Star', type: :fixed, signal: 'bullish' },
|
|
333
|
-
eveningstar: { data: cdl_eveningstar, name: 'Evening Star', type: :fixed, signal: 'bearish' },
|
|
334
|
-
harami: { data: cdl_harami, name: 'Harami', type: :directional },
|
|
335
|
-
whitesoldiers: { data: cdl_3whitesoldiers, name: 'Three White Soldiers', type: :fixed, signal: 'bullish' },
|
|
336
|
-
blackcrows: { data: cdl_3blackcrows, name: 'Three Black Crows', type: :fixed, signal: 'bearish' },
|
|
337
|
-
piercing: { data: cdl_piercing, name: 'Piercing', type: :fixed, signal: 'bullish' },
|
|
338
|
-
darkcloudcover: { data: cdl_darkcloudcover, name: 'Dark Cloud Cover', type: :fixed, signal: 'bearish' },
|
|
339
|
-
marubozu: { data: cdl_marubozu, name: 'Marubozu', type: :directional }
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
detected_patterns = []
|
|
343
|
-
pattern_defs.each do |_key, pdef|
|
|
344
|
-
pdef[:data].each_with_index do |val, i|
|
|
345
|
-
next if val.nil? || val == 0
|
|
346
|
-
|
|
347
|
-
signal = case pdef[:type]
|
|
348
|
-
when :neutral then 'neutral'
|
|
349
|
-
when :fixed then pdef[:signal]
|
|
350
|
-
when :directional then val > 0 ? 'bullish' : 'bearish'
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
detected_patterns << {
|
|
354
|
-
date: dates[i],
|
|
355
|
-
pattern: pdef[:name],
|
|
356
|
-
signal: signal,
|
|
357
|
-
strength: val.abs
|
|
358
|
-
}
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
# Sort by date descending and keep only 20 most recent
|
|
362
|
-
detected_patterns.sort_by! { |p| p[:date] }.reverse!
|
|
363
|
-
detected_patterns = detected_patterns.first(20)
|
|
364
|
-
|
|
365
|
-
# Filter results by period (keep indicators aligned with dates)
|
|
366
|
-
filtered_dates, filtered_rsi, filtered_macd, filtered_macd_signal, filtered_macd_hist,
|
|
367
|
-
filtered_bb_upper, filtered_bb_middle, filtered_bb_lower,
|
|
368
|
-
filtered_sma_12, filtered_sma_20, filtered_sma_50, filtered_ema_20,
|
|
369
|
-
filtered_wma_20, filtered_dema_20, filtered_tema_20, filtered_kama_30,
|
|
370
|
-
filtered_stoch_slowk, filtered_stoch_slowd, filtered_mom_10,
|
|
371
|
-
filtered_cci_14, filtered_willr_14, filtered_roc_10, filtered_adx_14,
|
|
372
|
-
filtered_atr_14, filtered_obv, filtered_ad,
|
|
373
|
-
filtered_vol_sma_12, filtered_vol_sma_20, filtered_vol_sma_50,
|
|
374
|
-
filtered_vol_ema_12, filtered_vol_ema_20 =
|
|
375
|
-
filter_by_period(dates, rsi, macd_line, macd_signal, macd_hist,
|
|
376
|
-
bb_upper, bb_middle, bb_lower,
|
|
377
|
-
sma_12, sma_20, sma_50, ema_20,
|
|
378
|
-
wma_20, dema_20, tema_20, kama_30,
|
|
379
|
-
stoch_slowk, stoch_slowd, mom_10,
|
|
380
|
-
cci_14, willr_14, roc_10, adx_14,
|
|
381
|
-
atr_14, obv, ad,
|
|
382
|
-
vol_sma_12, vol_sma_20, vol_sma_50,
|
|
383
|
-
vol_ema_12, vol_ema_20, period: period)
|
|
384
|
-
|
|
385
|
-
{
|
|
386
|
-
period: period,
|
|
387
|
-
dates: filtered_dates,
|
|
388
|
-
rsi: filtered_rsi,
|
|
389
|
-
macd: filtered_macd,
|
|
390
|
-
macd_signal: filtered_macd_signal,
|
|
391
|
-
macd_hist: filtered_macd_hist,
|
|
392
|
-
bb_upper: filtered_bb_upper,
|
|
393
|
-
bb_middle: filtered_bb_middle,
|
|
394
|
-
bb_lower: filtered_bb_lower,
|
|
395
|
-
sma_12: filtered_sma_12,
|
|
396
|
-
sma_20: filtered_sma_20,
|
|
397
|
-
sma_50: filtered_sma_50,
|
|
398
|
-
ema_20: filtered_ema_20,
|
|
399
|
-
wma_20: filtered_wma_20,
|
|
400
|
-
dema_20: filtered_dema_20,
|
|
401
|
-
tema_20: filtered_tema_20,
|
|
402
|
-
kama_30: filtered_kama_30,
|
|
403
|
-
stoch_slowk: filtered_stoch_slowk,
|
|
404
|
-
stoch_slowd: filtered_stoch_slowd,
|
|
405
|
-
mom_10: filtered_mom_10,
|
|
406
|
-
cci_14: filtered_cci_14,
|
|
407
|
-
willr_14: filtered_willr_14,
|
|
408
|
-
roc_10: filtered_roc_10,
|
|
409
|
-
adx_14: filtered_adx_14,
|
|
410
|
-
atr_14: filtered_atr_14,
|
|
411
|
-
obv: filtered_obv,
|
|
412
|
-
ad: filtered_ad,
|
|
413
|
-
vol_sma_12: filtered_vol_sma_12,
|
|
414
|
-
vol_sma_20: filtered_vol_sma_20,
|
|
415
|
-
vol_sma_50: filtered_vol_sma_50,
|
|
416
|
-
vol_ema_12: filtered_vol_ema_12,
|
|
417
|
-
vol_ema_20: filtered_vol_ema_20,
|
|
418
|
-
patterns: detected_patterns || []
|
|
419
|
-
}.to_json
|
|
420
|
-
rescue => e
|
|
421
|
-
status 500
|
|
422
|
-
{ error: e.message }.to_json
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# Run backtest
|
|
427
|
-
post '/api/backtest/:ticker' do
|
|
428
|
-
content_type :json
|
|
429
|
-
|
|
430
|
-
ticker = params[:ticker].upcase
|
|
431
|
-
strategy_name = params[:strategy] || 'RSI'
|
|
432
|
-
|
|
433
|
-
begin
|
|
434
|
-
stock = SQA::Stock.new(ticker: ticker)
|
|
435
|
-
|
|
436
|
-
# Resolve strategy
|
|
437
|
-
strategy = case strategy_name.upcase
|
|
438
|
-
when 'RSI' then SQA::Strategy::RSI
|
|
439
|
-
when 'SMA' then SQA::Strategy::SMA
|
|
440
|
-
when 'EMA' then SQA::Strategy::EMA
|
|
441
|
-
when 'MACD' then SQA::Strategy::MACD
|
|
442
|
-
when 'BOLLINGERBANDS' then SQA::Strategy::BollingerBands
|
|
443
|
-
when 'KBS' then SQA::Strategy::KBS
|
|
444
|
-
else SQA::Strategy::RSI
|
|
445
|
-
end
|
|
39
|
+
# Register helpers
|
|
40
|
+
helpers Helpers::Formatting
|
|
41
|
+
helpers Helpers::Filters
|
|
42
|
+
helpers Helpers::StockLoader
|
|
43
|
+
helpers Helpers::ApiHelpers
|
|
446
44
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
strategy: strategy,
|
|
451
|
-
initial_capital: 10_000.0,
|
|
452
|
-
commission: 1.0
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
results = backtest.run
|
|
456
|
-
|
|
457
|
-
{
|
|
458
|
-
total_return: results.total_return,
|
|
459
|
-
annualized_return: results.annualized_return,
|
|
460
|
-
sharpe_ratio: results.sharpe_ratio,
|
|
461
|
-
max_drawdown: results.max_drawdown,
|
|
462
|
-
win_rate: results.win_rate,
|
|
463
|
-
total_trades: results.total_trades,
|
|
464
|
-
profit_factor: results.profit_factor,
|
|
465
|
-
avg_win: results.avg_win,
|
|
466
|
-
avg_loss: results.avg_loss
|
|
467
|
-
}.to_json
|
|
468
|
-
rescue => e
|
|
469
|
-
status 500
|
|
470
|
-
{ error: e.message }.to_json
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
# Run market analysis
|
|
475
|
-
get '/api/analyze/:ticker' do
|
|
476
|
-
content_type :json
|
|
477
|
-
|
|
478
|
-
ticker = params[:ticker].upcase
|
|
479
|
-
|
|
480
|
-
begin
|
|
481
|
-
stock = SQA::Stock.new(ticker: ticker)
|
|
482
|
-
prices = stock.df["adj_close_price"].to_a
|
|
483
|
-
|
|
484
|
-
# Market regime
|
|
485
|
-
regime = SQA::MarketRegime.detect(stock)
|
|
486
|
-
|
|
487
|
-
# Seasonal analysis
|
|
488
|
-
seasonal = SQA::SeasonalAnalyzer.analyze(stock)
|
|
489
|
-
|
|
490
|
-
# FPOP analysis
|
|
491
|
-
fpop_data = SQA::FPOP.fpl_analysis(prices, fpop: 10)
|
|
492
|
-
recent_fpop = fpop_data.last(10).map do |f|
|
|
493
|
-
{
|
|
494
|
-
direction: f[:direction],
|
|
495
|
-
magnitude: f[:magnitude],
|
|
496
|
-
risk: f[:risk],
|
|
497
|
-
interpretation: f[:interpretation]
|
|
498
|
-
}
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# Risk metrics
|
|
502
|
-
returns = prices.each_cons(2).map { |a, b| (b - a) / a }
|
|
503
|
-
var_95 = SQA::RiskManager.var(returns, confidence: 0.95)
|
|
504
|
-
sharpe = SQA::RiskManager.sharpe_ratio(returns)
|
|
505
|
-
max_dd = SQA::RiskManager.max_drawdown(prices)
|
|
506
|
-
|
|
507
|
-
{
|
|
508
|
-
regime: {
|
|
509
|
-
type: regime[:type],
|
|
510
|
-
volatility: regime[:volatility],
|
|
511
|
-
strength: regime[:strength],
|
|
512
|
-
trend: regime[:trend]
|
|
513
|
-
},
|
|
514
|
-
seasonal: {
|
|
515
|
-
best_months: seasonal[:best_months],
|
|
516
|
-
worst_months: seasonal[:worst_months],
|
|
517
|
-
best_quarters: seasonal[:best_quarters],
|
|
518
|
-
has_pattern: seasonal[:has_seasonal_pattern]
|
|
519
|
-
},
|
|
520
|
-
fpop: recent_fpop,
|
|
521
|
-
risk: {
|
|
522
|
-
var_95: var_95,
|
|
523
|
-
sharpe_ratio: sharpe,
|
|
524
|
-
max_drawdown: max_dd[:max_drawdown]
|
|
525
|
-
}
|
|
526
|
-
}.to_json
|
|
527
|
-
rescue => e
|
|
528
|
-
status 500
|
|
529
|
-
{ error: e.message }.to_json
|
|
530
|
-
end
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
# Compare strategies
|
|
534
|
-
post '/api/compare/:ticker' do
|
|
535
|
-
content_type :json
|
|
536
|
-
|
|
537
|
-
ticker = params[:ticker].upcase
|
|
538
|
-
|
|
539
|
-
begin
|
|
540
|
-
stock = SQA::Stock.new(ticker: ticker)
|
|
541
|
-
|
|
542
|
-
strategies = {
|
|
543
|
-
'RSI' => SQA::Strategy::RSI,
|
|
544
|
-
'SMA' => SQA::Strategy::SMA,
|
|
545
|
-
'EMA' => SQA::Strategy::EMA,
|
|
546
|
-
'MACD' => SQA::Strategy::MACD,
|
|
547
|
-
'BollingerBands' => SQA::Strategy::BollingerBands
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
results = strategies.map do |name, strategy_class|
|
|
551
|
-
backtest = SQA::Backtest.new(
|
|
552
|
-
stock: stock,
|
|
553
|
-
strategy: strategy_class,
|
|
554
|
-
initial_capital: 10_000.0,
|
|
555
|
-
commission: 1.0
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
result = backtest.run
|
|
559
|
-
|
|
560
|
-
{
|
|
561
|
-
strategy: name,
|
|
562
|
-
return: result.total_return,
|
|
563
|
-
sharpe: result.sharpe_ratio,
|
|
564
|
-
drawdown: result.max_drawdown,
|
|
565
|
-
win_rate: result.win_rate,
|
|
566
|
-
trades: result.total_trades
|
|
567
|
-
}
|
|
568
|
-
rescue => e
|
|
569
|
-
nil
|
|
570
|
-
end.compact
|
|
571
|
-
|
|
572
|
-
results.sort_by! { |r| -r[:return] }
|
|
573
|
-
results.to_json
|
|
574
|
-
rescue => e
|
|
575
|
-
status 500
|
|
576
|
-
{ error: e.message }.to_json
|
|
577
|
-
end
|
|
578
|
-
end
|
|
45
|
+
# Register routes
|
|
46
|
+
register Routes::Pages
|
|
47
|
+
register Routes::Api
|
|
579
48
|
end
|
|
580
49
|
end
|
|
581
50
|
end
|