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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module SqaDemo
|
|
6
|
+
module Sinatra
|
|
7
|
+
module Routes
|
|
8
|
+
module Pages
|
|
9
|
+
def self.registered(app)
|
|
10
|
+
# Home / Dashboard
|
|
11
|
+
app.get '/' do
|
|
12
|
+
erb :index
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Dashboard for specific ticker
|
|
16
|
+
app.get '/dashboard/:ticker' do
|
|
17
|
+
begin
|
|
18
|
+
data = load_stock(params[:ticker])
|
|
19
|
+
@stock = data[:stock]
|
|
20
|
+
@ticker = data[:ticker]
|
|
21
|
+
@company_name = data[:company_name]
|
|
22
|
+
@show_period_selector = true
|
|
23
|
+
erb :dashboard
|
|
24
|
+
rescue => e
|
|
25
|
+
@error = "Failed to load data for #{params[:ticker].upcase}: #{e.message}"
|
|
26
|
+
erb :error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Analysis page
|
|
31
|
+
app.get '/analyze/:ticker' do
|
|
32
|
+
begin
|
|
33
|
+
data = load_stock(params[:ticker])
|
|
34
|
+
@stock = data[:stock]
|
|
35
|
+
@ticker = data[:ticker]
|
|
36
|
+
@company_name = data[:company_name]
|
|
37
|
+
erb :analyze
|
|
38
|
+
rescue => e
|
|
39
|
+
@error = "Failed to load data for #{params[:ticker].upcase}: #{e.message}"
|
|
40
|
+
erb :error
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Backtest page
|
|
45
|
+
app.get '/backtest/:ticker' do
|
|
46
|
+
begin
|
|
47
|
+
data = load_stock(params[:ticker])
|
|
48
|
+
@stock = data[:stock]
|
|
49
|
+
@ticker = data[:ticker]
|
|
50
|
+
@company_name = data[:company_name]
|
|
51
|
+
erb :backtest
|
|
52
|
+
rescue => e
|
|
53
|
+
@error = "Failed to load data for #{params[:ticker].upcase}: #{e.message}"
|
|
54
|
+
erb :error
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Portfolio optimizer
|
|
59
|
+
app.get '/portfolio' do
|
|
60
|
+
erb :portfolio
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Stock comparison page (compare multiple tickers)
|
|
64
|
+
app.get '/compare' do
|
|
65
|
+
tickers_param = params[:tickers] || ''
|
|
66
|
+
@tickers = tickers_param.split(/\s+/).map(&:upcase).uniq.first(5)
|
|
67
|
+
|
|
68
|
+
if @tickers.empty?
|
|
69
|
+
@error = "No tickers provided. Enter up to 5 tickers separated by spaces."
|
|
70
|
+
return erb :error
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if tickers_param.split(/\s+/).map(&:upcase).uniq.length > 5
|
|
74
|
+
@error = "Maximum of 5 tickers allowed for comparison. Please reduce your selection."
|
|
75
|
+
return erb :error
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@stocks_data = {}
|
|
79
|
+
@errors = {}
|
|
80
|
+
|
|
81
|
+
# Fetch data for each ticker in parallel using threads
|
|
82
|
+
threads = @tickers.map do |ticker|
|
|
83
|
+
Thread.new(ticker) do |t|
|
|
84
|
+
fetch_comparison_data(t)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Wait for all threads to complete
|
|
89
|
+
threads.each do |thread|
|
|
90
|
+
ticker, data = thread.value
|
|
91
|
+
if data[:error]
|
|
92
|
+
@errors[ticker] = data[:error]
|
|
93
|
+
else
|
|
94
|
+
@stocks_data[ticker] = data
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
erb :compare
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Company details page
|
|
102
|
+
app.get '/company/:ticker' do
|
|
103
|
+
begin
|
|
104
|
+
data = load_stock_with_overview(params[:ticker])
|
|
105
|
+
@stock = data[:stock]
|
|
106
|
+
@ticker = data[:ticker]
|
|
107
|
+
@company_name = data[:company_name]
|
|
108
|
+
@overview = data[:overview]
|
|
109
|
+
@exchange = data[:exchange]
|
|
110
|
+
|
|
111
|
+
ohlcv = extract_ohlcv(@stock)
|
|
112
|
+
prices = ohlcv[:closes]
|
|
113
|
+
volumes = ohlcv[:volumes]
|
|
114
|
+
dates = ohlcv[:dates]
|
|
115
|
+
|
|
116
|
+
@data_start_date = dates.first
|
|
117
|
+
@data_end_date = dates.last
|
|
118
|
+
@total_trading_days = dates.length
|
|
119
|
+
@current_price = prices.last
|
|
120
|
+
@all_time_high = prices.max
|
|
121
|
+
@all_time_low = prices.min
|
|
122
|
+
@avg_volume = (volumes.sum.to_f / volumes.length).round
|
|
123
|
+
@max_volume = volumes.max
|
|
124
|
+
@price_range = @all_time_high - @all_time_low
|
|
125
|
+
@ytd_return = calculate_ytd_return(dates, prices)
|
|
126
|
+
|
|
127
|
+
erb :company
|
|
128
|
+
rescue => e
|
|
129
|
+
@error = "Failed to load data for #{params[:ticker].upcase}: #{e.message}"
|
|
130
|
+
erb :error
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -118,6 +118,47 @@
|
|
|
118
118
|
background: var(--light-bg);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
.fpop-table small {
|
|
122
|
+
color: var(--text-secondary);
|
|
123
|
+
font-size: 0.8rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.fpop-future {
|
|
127
|
+
background: rgba(0, 212, 255, 0.05);
|
|
128
|
+
border-left: 3px solid var(--primary-color);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.verification-correct {
|
|
132
|
+
color: var(--success-color);
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.verification-correct i {
|
|
137
|
+
margin-right: 0.25rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.verification-wrong {
|
|
141
|
+
color: var(--danger-color);
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.verification-wrong i {
|
|
146
|
+
margin-right: 0.25rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.fpop-summary {
|
|
150
|
+
margin-top: 1rem;
|
|
151
|
+
padding: 0.75rem 1rem;
|
|
152
|
+
background: var(--light-bg);
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
font-size: 0.9rem;
|
|
155
|
+
color: var(--text-secondary);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.fpop-summary strong {
|
|
159
|
+
color: var(--primary-color);
|
|
160
|
+
}
|
|
161
|
+
|
|
121
162
|
.chart-subtitle {
|
|
122
163
|
color: var(--text-secondary);
|
|
123
164
|
font-size: 0.875rem;
|
|
@@ -243,10 +284,11 @@ function renderFPOPAnalysis(fpop) {
|
|
|
243
284
|
<table class="fpop-table">
|
|
244
285
|
<thead>
|
|
245
286
|
<tr>
|
|
246
|
-
<th>
|
|
247
|
-
<th>
|
|
287
|
+
<th>Date</th>
|
|
288
|
+
<th>Predicted</th>
|
|
289
|
+
<th>Actual</th>
|
|
290
|
+
<th>Result</th>
|
|
248
291
|
<th>Risk</th>
|
|
249
|
-
<th>Interpretation</th>
|
|
250
292
|
</tr>
|
|
251
293
|
</thead>
|
|
252
294
|
<tbody>
|
|
@@ -256,18 +298,51 @@ function renderFPOPAnalysis(fpop) {
|
|
|
256
298
|
const dirClass = f.direction === 'UP' ? 'signal-buy' :
|
|
257
299
|
f.direction === 'DOWN' ? 'signal-sell' : 'signal-neutral';
|
|
258
300
|
|
|
301
|
+
let actualCell, resultCell;
|
|
302
|
+
|
|
303
|
+
if (f.is_future) {
|
|
304
|
+
// Future prediction - no actual data yet
|
|
305
|
+
actualCell = '<span style="color: var(--text-secondary);">—</span>';
|
|
306
|
+
resultCell = '<span style="color: var(--text-secondary);">Pending</span>';
|
|
307
|
+
} else {
|
|
308
|
+
// Historical - show actual change and verification
|
|
309
|
+
const actualClass = f.actual_change > 0 ? 'signal-buy' : (f.actual_change < 0 ? 'signal-sell' : 'signal-neutral');
|
|
310
|
+
const sign = f.actual_change > 0 ? '+' : '';
|
|
311
|
+
actualCell = `<span class="${actualClass}">${sign}${f.actual_change}%</span>`;
|
|
312
|
+
|
|
313
|
+
if (f.correct) {
|
|
314
|
+
resultCell = '<span class="verification-correct"><i class="fas fa-check-circle"></i> Correct</span>';
|
|
315
|
+
} else {
|
|
316
|
+
resultCell = '<span class="verification-wrong"><i class="fas fa-times-circle"></i> Wrong</span>';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const rowClass = f.is_future ? 'fpop-future' : '';
|
|
321
|
+
|
|
259
322
|
tableHTML += `
|
|
260
|
-
<tr>
|
|
261
|
-
<td
|
|
262
|
-
<td>${f.magnitude.toFixed(
|
|
263
|
-
<td>${
|
|
264
|
-
<td>${
|
|
323
|
+
<tr class="${rowClass}">
|
|
324
|
+
<td>${f.date}</td>
|
|
325
|
+
<td><span class="${dirClass}">${f.direction}</span> <small>(${f.magnitude.toFixed(1)}%)</small></td>
|
|
326
|
+
<td>${actualCell}</td>
|
|
327
|
+
<td>${resultCell}</td>
|
|
328
|
+
<td>±${f.risk.toFixed(1)}%</td>
|
|
265
329
|
</tr>
|
|
266
330
|
`;
|
|
267
331
|
});
|
|
268
332
|
|
|
269
333
|
tableHTML += '</tbody></table>';
|
|
270
334
|
|
|
335
|
+
// Calculate accuracy for historical predictions
|
|
336
|
+
const historical = fpop.filter(f => !f.is_future);
|
|
337
|
+
const correct = historical.filter(f => f.correct).length;
|
|
338
|
+
const accuracy = historical.length > 0 ? ((correct / historical.length) * 100).toFixed(0) : 0;
|
|
339
|
+
|
|
340
|
+
tableHTML += `
|
|
341
|
+
<div class="fpop-summary">
|
|
342
|
+
<span>Historical Accuracy: <strong>${correct}/${historical.length}</strong> (${accuracy}%)</span>
|
|
343
|
+
</div>
|
|
344
|
+
`;
|
|
345
|
+
|
|
271
346
|
document.getElementById('fpopAnalysis').innerHTML = tableHTML;
|
|
272
347
|
}
|
|
273
348
|
|