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.
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SqaDemo
4
4
  module Sinatra
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.2'
6
6
  end
7
7
  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>Direction</th>
247
- <th>Magnitude</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><span class="${dirClass}">${f.direction}</span></td>
262
- <td>${f.magnitude.toFixed(2)}%</td>
263
- <td>${f.risk.toFixed(2)}%</td>
264
- <td>${f.interpretation}</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