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,500 @@
|
|
|
1
|
+
<div class="compare-page">
|
|
2
|
+
<div class="compare-header">
|
|
3
|
+
<h1>
|
|
4
|
+
<i class="fas fa-balance-scale"></i>
|
|
5
|
+
Stock Comparison
|
|
6
|
+
</h1>
|
|
7
|
+
<p class="compare-subtitle">
|
|
8
|
+
Comparing <%= @tickers.join(', ') %>
|
|
9
|
+
</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if @errors.any? %>
|
|
13
|
+
<div class="compare-warnings">
|
|
14
|
+
<% @errors.each do |ticker, error| %>
|
|
15
|
+
<div class="warning-alert">
|
|
16
|
+
<i class="fas fa-exclamation-triangle"></i>
|
|
17
|
+
<strong><%= ticker %>:</strong> <%= error %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% if @stocks_data.any? %>
|
|
24
|
+
<%
|
|
25
|
+
# Define indicator configurations with direction (higher_is_better)
|
|
26
|
+
# nil means neutral (no best/worst highlighting)
|
|
27
|
+
indicators = [
|
|
28
|
+
{ section: 'Price & Performance', items: [
|
|
29
|
+
{ key: :current_price, label: 'Current Price', format: :currency, higher_is_better: nil },
|
|
30
|
+
{ key: :change_pct, label: 'Daily Change', format: :percent_sign, higher_is_better: true },
|
|
31
|
+
{ key: :ytd_return, label: 'YTD Return', format: :percent_sign, higher_is_better: true },
|
|
32
|
+
{ key: :high_52w, label: '52-Week High', format: :currency, higher_is_better: nil },
|
|
33
|
+
{ key: :low_52w, label: '52-Week Low', format: :currency, higher_is_better: nil },
|
|
34
|
+
{ key: :avg_volume, label: 'Avg Volume', format: :number, higher_is_better: nil },
|
|
35
|
+
]},
|
|
36
|
+
{ section: 'Momentum Indicators', items: [
|
|
37
|
+
{ key: :rsi, label: 'RSI (14)', format: :decimal2, higher_is_better: nil, note: '30-70 neutral' },
|
|
38
|
+
{ key: :macd, label: 'MACD Line', format: :decimal3, higher_is_better: true },
|
|
39
|
+
{ key: :macd_hist, label: 'MACD Histogram', format: :decimal3, higher_is_better: true },
|
|
40
|
+
{ key: :stoch_k, label: 'Stochastic %K', format: :decimal2, higher_is_better: nil, note: '20-80 neutral' },
|
|
41
|
+
{ key: :stoch_d, label: 'Stochastic %D', format: :decimal2, higher_is_better: nil },
|
|
42
|
+
{ key: :mom, label: 'Momentum (10)', format: :decimal2, higher_is_better: true },
|
|
43
|
+
{ key: :roc, label: 'Rate of Change', format: :percent, higher_is_better: true },
|
|
44
|
+
{ key: :cci, label: 'CCI (14)', format: :decimal2, higher_is_better: nil, note: '-100 to +100 neutral' },
|
|
45
|
+
{ key: :willr, label: "Williams %R", format: :decimal2, higher_is_better: nil, note: '-80 to -20 neutral' },
|
|
46
|
+
]},
|
|
47
|
+
{ section: 'Trend Indicators', items: [
|
|
48
|
+
{ key: :adx, label: 'ADX (14)', format: :decimal2, higher_is_better: true, note: 'Trend strength' },
|
|
49
|
+
{ key: :sma_50, label: 'SMA (50)', format: :currency, higher_is_better: nil },
|
|
50
|
+
{ key: :sma_200, label: 'SMA (200)', format: :currency, higher_is_better: nil },
|
|
51
|
+
{ key: :ema_20, label: 'EMA (20)', format: :currency, higher_is_better: nil },
|
|
52
|
+
]},
|
|
53
|
+
{ section: 'Volatility', items: [
|
|
54
|
+
{ key: :atr, label: 'ATR (14)', format: :decimal2, higher_is_better: false, note: 'Lower = less volatile' },
|
|
55
|
+
{ key: :bb_upper, label: 'Bollinger Upper', format: :currency, higher_is_better: nil },
|
|
56
|
+
{ key: :bb_middle, label: 'Bollinger Middle', format: :currency, higher_is_better: nil },
|
|
57
|
+
{ key: :bb_lower, label: 'Bollinger Lower', format: :currency, higher_is_better: nil },
|
|
58
|
+
{ key: :beta, label: 'Beta', format: :decimal3, higher_is_better: nil, note: '1.0 = market' },
|
|
59
|
+
]},
|
|
60
|
+
{ section: 'Valuation', items: [
|
|
61
|
+
{ key: :pe_ratio, label: 'P/E Ratio', format: :decimal2, higher_is_better: false },
|
|
62
|
+
{ key: :forward_pe, label: 'Forward P/E', format: :decimal2, higher_is_better: false },
|
|
63
|
+
{ key: :peg_ratio, label: 'PEG Ratio', format: :decimal2, higher_is_better: false },
|
|
64
|
+
{ key: :price_to_book, label: 'Price/Book', format: :decimal2, higher_is_better: false },
|
|
65
|
+
{ key: :market_cap, label: 'Market Cap', format: :currency_billions, higher_is_better: nil },
|
|
66
|
+
]},
|
|
67
|
+
{ section: 'Profitability', items: [
|
|
68
|
+
{ key: :eps, label: 'EPS', format: :currency, higher_is_better: true },
|
|
69
|
+
{ key: :profit_margin, label: 'Profit Margin', format: :percent_from_decimal, higher_is_better: true },
|
|
70
|
+
{ key: :operating_margin, label: 'Operating Margin', format: :percent_from_decimal, higher_is_better: true },
|
|
71
|
+
{ key: :roe, label: 'Return on Equity', format: :percent_from_decimal, higher_is_better: true },
|
|
72
|
+
{ key: :roa, label: 'Return on Assets', format: :percent_from_decimal, higher_is_better: true },
|
|
73
|
+
{ key: :dividend_yield, label: 'Dividend Yield', format: :percent_from_decimal, higher_is_better: true },
|
|
74
|
+
]},
|
|
75
|
+
{ section: 'Risk Metrics', items: [
|
|
76
|
+
{ key: :sharpe_ratio, label: 'Sharpe Ratio', format: :decimal2, higher_is_better: true },
|
|
77
|
+
{ key: :max_drawdown, label: 'Max Drawdown', format: :percent_from_decimal, higher_is_better: false, note: 'Closer to 0 is better' },
|
|
78
|
+
{ key: :analyst_target, label: 'Analyst Target', format: :currency, higher_is_better: nil },
|
|
79
|
+
]},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Calculate best count for each ticker (needed for sorting columns)
|
|
83
|
+
best_counts = {}
|
|
84
|
+
@tickers.each { |t| best_counts[t] = 0 if @stocks_data[t] }
|
|
85
|
+
|
|
86
|
+
indicators.each do |section|
|
|
87
|
+
section[:items].each do |item|
|
|
88
|
+
best_ticker, _ = find_extremes(@stocks_data, item[:key], item[:higher_is_better])
|
|
89
|
+
best_counts[best_ticker] += 1 if best_ticker
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Sort tickers by best count (descending), then alphabetically for ties
|
|
94
|
+
sorted_tickers = @tickers.select { |t| @stocks_data[t] }.sort_by { |t| [-best_counts[t], t] }
|
|
95
|
+
|
|
96
|
+
# Find the ticker with the most "best" results
|
|
97
|
+
max_best_count = best_counts.values.max || 0
|
|
98
|
+
%>
|
|
99
|
+
|
|
100
|
+
<div class="compare-table-container">
|
|
101
|
+
<table class="compare-table">
|
|
102
|
+
<thead>
|
|
103
|
+
<tr>
|
|
104
|
+
<th class="metric-header">Metric</th>
|
|
105
|
+
<% sorted_tickers.each do |ticker| %>
|
|
106
|
+
<th class="ticker-header">
|
|
107
|
+
<div class="ticker-name"><%= ticker %></div>
|
|
108
|
+
<% if @stocks_data[ticker][:company_name] %>
|
|
109
|
+
<div class="company-name"><%= @stocks_data[ticker][:company_name] %></div>
|
|
110
|
+
<% end %>
|
|
111
|
+
</th>
|
|
112
|
+
<% end %>
|
|
113
|
+
</tr>
|
|
114
|
+
</thead>
|
|
115
|
+
<tbody>
|
|
116
|
+
<tr class="best-count-row">
|
|
117
|
+
<td class="metric-label best-count-label">Best Count</td>
|
|
118
|
+
<% sorted_tickers.each do |ticker| %>
|
|
119
|
+
<%
|
|
120
|
+
count = best_counts[ticker] || 0
|
|
121
|
+
is_leader = count == max_best_count && count > 0
|
|
122
|
+
%>
|
|
123
|
+
<td class="metric-value best-count-value <%= 'best-value' if is_leader %>"><%= count %></td>
|
|
124
|
+
<% end %>
|
|
125
|
+
</tr>
|
|
126
|
+
<% indicators.each do |section| %>
|
|
127
|
+
<tr class="section-row">
|
|
128
|
+
<td colspan="<%= @stocks_data.length + 1 %>" class="section-header">
|
|
129
|
+
<%= section[:section] %>
|
|
130
|
+
</td>
|
|
131
|
+
</tr>
|
|
132
|
+
<% section[:items].each do |item| %>
|
|
133
|
+
<%
|
|
134
|
+
best_ticker, worst_ticker = find_extremes(@stocks_data, item[:key], item[:higher_is_better])
|
|
135
|
+
%>
|
|
136
|
+
<tr>
|
|
137
|
+
<td class="metric-label">
|
|
138
|
+
<%= item[:label] %>
|
|
139
|
+
<% if item[:note] %>
|
|
140
|
+
<span class="metric-note"><%= item[:note] %></span>
|
|
141
|
+
<% end %>
|
|
142
|
+
</td>
|
|
143
|
+
<% sorted_tickers.each do |ticker| %>
|
|
144
|
+
<%
|
|
145
|
+
value = @stocks_data[ticker][item[:key]]
|
|
146
|
+
formatted = format_compare_value(value, item[:format])
|
|
147
|
+
cell_class = ''
|
|
148
|
+
if value && item[:higher_is_better] != nil
|
|
149
|
+
if ticker == best_ticker
|
|
150
|
+
cell_class = 'best-value'
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
# Special coloring for percent changes
|
|
154
|
+
if [:percent_sign].include?(item[:format]) && value
|
|
155
|
+
cell_class += value >= 0 ? ' positive' : ' negative'
|
|
156
|
+
end
|
|
157
|
+
%>
|
|
158
|
+
<td class="metric-value <%= cell_class %>"><%= formatted %></td>
|
|
159
|
+
<% end %>
|
|
160
|
+
</tr>
|
|
161
|
+
<% end %>
|
|
162
|
+
<% end %>
|
|
163
|
+
</tbody>
|
|
164
|
+
</table>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="compare-legend">
|
|
168
|
+
<h3>Legend</h3>
|
|
169
|
+
<div class="legend-items">
|
|
170
|
+
<div class="legend-item">
|
|
171
|
+
<span class="legend-color best"></span>
|
|
172
|
+
<span>Best value in comparison</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="legend-item">
|
|
175
|
+
<span class="legend-color neutral"></span>
|
|
176
|
+
<span>Neutral / context-dependent</span>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="compare-actions">
|
|
182
|
+
<h3>Individual Analysis</h3>
|
|
183
|
+
<div class="action-buttons">
|
|
184
|
+
<% sorted_tickers.each do |ticker| %>
|
|
185
|
+
<div class="ticker-actions">
|
|
186
|
+
<span class="ticker-label"><%= ticker %></span>
|
|
187
|
+
<a href="/dashboard/<%= ticker %>" class="btn btn-small">Dashboard</a>
|
|
188
|
+
<a href="/analyze/<%= ticker %>" class="btn btn-small">Analysis</a>
|
|
189
|
+
<a href="/company/<%= ticker %>" class="btn btn-small">Company</a>
|
|
190
|
+
</div>
|
|
191
|
+
<% end %>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<% else %>
|
|
195
|
+
<div class="no-data">
|
|
196
|
+
<i class="fas fa-exclamation-circle"></i>
|
|
197
|
+
<p>No stock data could be loaded. Please check the ticker symbols and try again.</p>
|
|
198
|
+
</div>
|
|
199
|
+
<% end %>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<style>
|
|
203
|
+
.compare-page {
|
|
204
|
+
max-width: 1400px;
|
|
205
|
+
margin: 0 auto;
|
|
206
|
+
padding: 20px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.compare-header {
|
|
210
|
+
text-align: center;
|
|
211
|
+
margin-bottom: 30px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.compare-header h1 {
|
|
215
|
+
color: #e8eaf6;
|
|
216
|
+
font-size: 2rem;
|
|
217
|
+
margin: 0 0 10px 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.compare-header h1 i {
|
|
221
|
+
color: #00d4ff;
|
|
222
|
+
margin-right: 10px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.compare-subtitle {
|
|
226
|
+
color: #9fa8da;
|
|
227
|
+
font-size: 1.1rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.compare-warnings {
|
|
231
|
+
margin-bottom: 20px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.warning-alert {
|
|
235
|
+
background: rgba(255, 152, 0, 0.15);
|
|
236
|
+
border: 1px solid #ff9800;
|
|
237
|
+
border-radius: 8px;
|
|
238
|
+
padding: 12px 16px;
|
|
239
|
+
margin-bottom: 10px;
|
|
240
|
+
color: #ffb74d;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.warning-alert i {
|
|
244
|
+
margin-right: 8px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.compare-table-container {
|
|
248
|
+
overflow-x: auto;
|
|
249
|
+
margin-bottom: 30px;
|
|
250
|
+
border-radius: 10px;
|
|
251
|
+
border: 1px solid #2a3154;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.compare-table {
|
|
255
|
+
width: 100%;
|
|
256
|
+
border-collapse: collapse;
|
|
257
|
+
background: #1a2248;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.compare-table th,
|
|
261
|
+
.compare-table td {
|
|
262
|
+
padding: 12px 16px;
|
|
263
|
+
text-align: right;
|
|
264
|
+
border-bottom: 1px solid #2a3154;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.compare-table th:first-child,
|
|
268
|
+
.compare-table td:first-child {
|
|
269
|
+
text-align: left;
|
|
270
|
+
position: sticky;
|
|
271
|
+
left: 0;
|
|
272
|
+
background: #1a2248;
|
|
273
|
+
z-index: 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.metric-header {
|
|
277
|
+
background: #232b52;
|
|
278
|
+
color: #9fa8da;
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
font-size: 0.8rem;
|
|
282
|
+
letter-spacing: 0.5px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.ticker-header {
|
|
286
|
+
background: #232b52;
|
|
287
|
+
color: #e8eaf6;
|
|
288
|
+
min-width: 150px;
|
|
289
|
+
text-align: right;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.ticker-header .ticker-name {
|
|
293
|
+
font-size: 1.1rem;
|
|
294
|
+
font-weight: bold;
|
|
295
|
+
color: #00d4ff;
|
|
296
|
+
text-align: right;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.ticker-header .company-name {
|
|
300
|
+
font-size: 0.8rem;
|
|
301
|
+
color: #9fa8da;
|
|
302
|
+
font-weight: normal;
|
|
303
|
+
margin-top: 4px;
|
|
304
|
+
white-space: nowrap;
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
text-overflow: ellipsis;
|
|
307
|
+
text-align: right;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.best-count-row {
|
|
311
|
+
background: linear-gradient(135deg, #1a2248, #232b52);
|
|
312
|
+
border-bottom: 2px solid #00d4ff;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.best-count-row .best-count-label {
|
|
316
|
+
font-weight: bold;
|
|
317
|
+
color: #00d4ff;
|
|
318
|
+
text-transform: uppercase;
|
|
319
|
+
font-size: 0.9rem;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.best-count-row .best-count-value {
|
|
323
|
+
font-size: 1.3rem;
|
|
324
|
+
font-weight: bold;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.section-row td {
|
|
328
|
+
background: #141a36 !important;
|
|
329
|
+
font-weight: bold;
|
|
330
|
+
color: #00d4ff;
|
|
331
|
+
font-size: 0.9rem;
|
|
332
|
+
text-transform: uppercase;
|
|
333
|
+
letter-spacing: 1px;
|
|
334
|
+
padding: 10px 16px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.metric-label {
|
|
338
|
+
color: #b0b8d9;
|
|
339
|
+
font-size: 0.9rem;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.metric-note {
|
|
343
|
+
display: block;
|
|
344
|
+
font-size: 0.75rem;
|
|
345
|
+
color: #7986cb;
|
|
346
|
+
font-weight: normal;
|
|
347
|
+
margin-top: 2px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.metric-value {
|
|
351
|
+
color: #e8eaf6;
|
|
352
|
+
font-family: 'Roboto Mono', monospace;
|
|
353
|
+
font-size: 0.9rem;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.metric-value.best-value {
|
|
357
|
+
background: rgba(0, 255, 136, 0.15);
|
|
358
|
+
color: #00ff88;
|
|
359
|
+
font-weight: bold;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.metric-value.worst-value {
|
|
363
|
+
background: rgba(255, 51, 102, 0.15);
|
|
364
|
+
color: #ff3366;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.metric-value.positive {
|
|
368
|
+
color: #00ff88;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.metric-value.negative {
|
|
372
|
+
color: #ff3366;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.compare-legend {
|
|
376
|
+
background: #1a2248;
|
|
377
|
+
border-radius: 10px;
|
|
378
|
+
padding: 20px;
|
|
379
|
+
margin-bottom: 30px;
|
|
380
|
+
border: 1px solid #2a3154;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.compare-legend h3 {
|
|
384
|
+
color: #e8eaf6;
|
|
385
|
+
margin: 0 0 15px 0;
|
|
386
|
+
font-size: 1rem;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.legend-items {
|
|
390
|
+
display: flex;
|
|
391
|
+
gap: 30px;
|
|
392
|
+
flex-wrap: wrap;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.legend-item {
|
|
396
|
+
display: flex;
|
|
397
|
+
align-items: center;
|
|
398
|
+
gap: 10px;
|
|
399
|
+
color: #9fa8da;
|
|
400
|
+
font-size: 0.9rem;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.legend-color {
|
|
404
|
+
width: 20px;
|
|
405
|
+
height: 20px;
|
|
406
|
+
border-radius: 4px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.legend-color.best {
|
|
410
|
+
background: rgba(0, 255, 136, 0.3);
|
|
411
|
+
border: 2px solid #00ff88;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.legend-color.worst {
|
|
415
|
+
background: rgba(255, 51, 102, 0.3);
|
|
416
|
+
border: 2px solid #ff3366;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.legend-color.neutral {
|
|
420
|
+
background: #2a3154;
|
|
421
|
+
border: 2px solid #5c6bc0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.compare-actions {
|
|
425
|
+
background: #1a2248;
|
|
426
|
+
border-radius: 10px;
|
|
427
|
+
padding: 20px;
|
|
428
|
+
border: 1px solid #2a3154;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.compare-actions h3 {
|
|
432
|
+
color: #e8eaf6;
|
|
433
|
+
margin: 0 0 15px 0;
|
|
434
|
+
font-size: 1rem;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.action-buttons {
|
|
438
|
+
display: flex;
|
|
439
|
+
flex-direction: column;
|
|
440
|
+
gap: 12px;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.ticker-actions {
|
|
444
|
+
display: flex;
|
|
445
|
+
align-items: center;
|
|
446
|
+
gap: 10px;
|
|
447
|
+
flex-wrap: wrap;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.ticker-label {
|
|
451
|
+
color: #00d4ff;
|
|
452
|
+
font-weight: bold;
|
|
453
|
+
min-width: 60px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.btn-small {
|
|
457
|
+
padding: 6px 12px;
|
|
458
|
+
font-size: 0.85rem;
|
|
459
|
+
background: #2a3154;
|
|
460
|
+
color: #e8eaf6;
|
|
461
|
+
border: 1px solid #3d4a7a;
|
|
462
|
+
border-radius: 6px;
|
|
463
|
+
text-decoration: none;
|
|
464
|
+
transition: all 0.2s;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.btn-small:hover {
|
|
468
|
+
background: #3d4a7a;
|
|
469
|
+
border-color: #00d4ff;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.no-data {
|
|
473
|
+
text-align: center;
|
|
474
|
+
padding: 60px 20px;
|
|
475
|
+
color: #9fa8da;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.no-data i {
|
|
479
|
+
font-size: 3rem;
|
|
480
|
+
color: #ff9800;
|
|
481
|
+
margin-bottom: 20px;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@media (max-width: 768px) {
|
|
485
|
+
.compare-table th,
|
|
486
|
+
.compare-table td {
|
|
487
|
+
padding: 8px 10px;
|
|
488
|
+
font-size: 0.85rem;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.ticker-header .company-name {
|
|
492
|
+
display: none;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.legend-items {
|
|
496
|
+
flex-direction: column;
|
|
497
|
+
gap: 10px;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
</style>
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
<div class="dashboard">
|
|
2
2
|
<div class="dashboard-header">
|
|
3
3
|
<div class="ticker-info">
|
|
4
|
-
<h1
|
|
4
|
+
<h1>
|
|
5
|
+
<%= @ticker %>
|
|
6
|
+
<% if @company_name %>
|
|
7
|
+
<span class="company-name">- <%= @company_name %></span>
|
|
8
|
+
<% end %>
|
|
9
|
+
</h1>
|
|
10
|
+
<div class="company-link">
|
|
11
|
+
<a href="/company/<%= @ticker %>" class="btn btn-link">
|
|
12
|
+
<i class="fas fa-building"></i> View Company Details
|
|
13
|
+
</a>
|
|
14
|
+
</div>
|
|
5
15
|
<div id="priceInfo" class="price-info">
|
|
6
16
|
<span class="current-price">Loading...</span>
|
|
7
17
|
<span class="price-change">--</span>
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
<input
|
|
14
14
|
type="text"
|
|
15
15
|
id="mainTickerInput"
|
|
16
|
-
placeholder="Enter
|
|
16
|
+
placeholder="Enter ticker(s) separated by spaces (e.g., AAPL MSFT GOOGL)"
|
|
17
17
|
autocomplete="off"
|
|
18
18
|
>
|
|
19
19
|
<button type="submit" class="btn btn-primary btn-large">
|
|
20
20
|
<i class="fas fa-search"></i> Analyze
|
|
21
21
|
</button>
|
|
22
22
|
</form>
|
|
23
|
+
<p class="ticker-hint">Enter multiple tickers (up to 5) to compare stocks side-by-side</p>
|
|
23
24
|
</div>
|
|
24
25
|
|
|
25
26
|
<div class="quick-links">
|
|
@@ -15,20 +15,111 @@
|
|
|
15
15
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
|
-
<!--
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
18
|
+
<!-- Sticky Header Container -->
|
|
19
|
+
<header class="site-header">
|
|
20
|
+
<!-- Primary Navigation -->
|
|
21
|
+
<nav class="navbar">
|
|
22
|
+
<div class="nav-container">
|
|
23
|
+
<a href="/" class="nav-brand">
|
|
24
|
+
<i class="fas fa-chart-line"></i>
|
|
25
|
+
<span>SQA Analytics</span>
|
|
26
|
+
</a>
|
|
27
|
+
<ul class="nav-menu">
|
|
28
|
+
<li><a href="/" class="nav-link"><i class="fas fa-home"></i> Home</a></li>
|
|
29
|
+
<li class="nav-dropdown">
|
|
30
|
+
<a href="#" class="nav-link nav-dropdown-toggle"><i class="fas fa-chart-area"></i> Dashboard <i class="fas fa-caret-down"></i></a>
|
|
31
|
+
<div class="nav-dropdown-menu">
|
|
32
|
+
<a href="#" onclick="navigateToStock('dashboard'); return false;">Enter Ticker...</a>
|
|
33
|
+
<div class="nav-dropdown-divider"></div>
|
|
34
|
+
<span class="nav-dropdown-label">Popular</span>
|
|
35
|
+
<a href="/dashboard/AAPL">AAPL</a>
|
|
36
|
+
<a href="/dashboard/MSFT">MSFT</a>
|
|
37
|
+
<a href="/dashboard/GOOGL">GOOGL</a>
|
|
38
|
+
<a href="/dashboard/TSLA">TSLA</a>
|
|
39
|
+
</div>
|
|
40
|
+
</li>
|
|
41
|
+
<li class="nav-dropdown">
|
|
42
|
+
<a href="#" class="nav-link nav-dropdown-toggle"><i class="fas fa-microscope"></i> Analysis <i class="fas fa-caret-down"></i></a>
|
|
43
|
+
<div class="nav-dropdown-menu">
|
|
44
|
+
<a href="#" onclick="navigateToStock('analyze'); return false;">Enter Ticker...</a>
|
|
45
|
+
<div class="nav-dropdown-divider"></div>
|
|
46
|
+
<span class="nav-dropdown-label">Popular</span>
|
|
47
|
+
<a href="/analyze/AAPL">AAPL</a>
|
|
48
|
+
<a href="/analyze/MSFT">MSFT</a>
|
|
49
|
+
<a href="/analyze/GOOGL">GOOGL</a>
|
|
50
|
+
<a href="/analyze/TSLA">TSLA</a>
|
|
51
|
+
</div>
|
|
52
|
+
</li>
|
|
53
|
+
<li class="nav-dropdown">
|
|
54
|
+
<a href="#" class="nav-link nav-dropdown-toggle"><i class="fas fa-flask"></i> Backtest <i class="fas fa-caret-down"></i></a>
|
|
55
|
+
<div class="nav-dropdown-menu">
|
|
56
|
+
<a href="#" onclick="navigateToStock('backtest'); return false;">Enter Ticker...</a>
|
|
57
|
+
<div class="nav-dropdown-divider"></div>
|
|
58
|
+
<span class="nav-dropdown-label">Popular</span>
|
|
59
|
+
<a href="/backtest/AAPL">AAPL</a>
|
|
60
|
+
<a href="/backtest/MSFT">MSFT</a>
|
|
61
|
+
<a href="/backtest/GOOGL">GOOGL</a>
|
|
62
|
+
<a href="/backtest/TSLA">TSLA</a>
|
|
63
|
+
</div>
|
|
64
|
+
</li>
|
|
65
|
+
<li class="nav-dropdown">
|
|
66
|
+
<a href="#" class="nav-link nav-dropdown-toggle"><i class="fas fa-balance-scale"></i> Compare <i class="fas fa-caret-down"></i></a>
|
|
67
|
+
<div class="nav-dropdown-menu">
|
|
68
|
+
<a href="#" onclick="showCompareModal(); return false;">Enter Tickers...</a>
|
|
69
|
+
<div class="nav-dropdown-divider"></div>
|
|
70
|
+
<span class="nav-dropdown-label">Popular Comparisons</span>
|
|
71
|
+
<a href="/compare?tickers=AAPL+MSFT+GOOGL">AAPL vs MSFT vs GOOGL</a>
|
|
72
|
+
<a href="/compare?tickers=TSLA+F+GM">TSLA vs F vs GM</a>
|
|
73
|
+
<a href="/compare?tickers=AMZN+WMT+TGT">AMZN vs WMT vs TGT</a>
|
|
74
|
+
</div>
|
|
75
|
+
</li>
|
|
76
|
+
<li><a href="/portfolio" class="nav-link"><i class="fas fa-briefcase"></i> Portfolio</a></li>
|
|
77
|
+
</ul>
|
|
78
|
+
<button class="nav-search-btn" onclick="showTickerModal()" title="Search Stock">
|
|
79
|
+
<i class="fas fa-search"></i>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</nav>
|
|
83
|
+
|
|
84
|
+
<!-- Context Bar (shown on stock-specific pages) -->
|
|
85
|
+
<% if defined?(@ticker) && @ticker %>
|
|
86
|
+
<div class="context-bar">
|
|
87
|
+
<div class="context-container">
|
|
88
|
+
<div class="context-stock">
|
|
89
|
+
<span class="context-ticker"><%= @ticker %></span>
|
|
90
|
+
<span class="context-name"><%= @company_name || @ticker %></span>
|
|
91
|
+
</div>
|
|
92
|
+
<nav class="context-nav">
|
|
93
|
+
<a href="/dashboard/<%= @ticker %>" class="context-link <%= 'active' if request.path_info.start_with?('/dashboard') %>">
|
|
94
|
+
<i class="fas fa-chart-area"></i> Dashboard
|
|
95
|
+
</a>
|
|
96
|
+
<a href="/analyze/<%= @ticker %>" class="context-link <%= 'active' if request.path_info.start_with?('/analyze') %>">
|
|
97
|
+
<i class="fas fa-microscope"></i> Analysis
|
|
98
|
+
</a>
|
|
99
|
+
<a href="/backtest/<%= @ticker %>" class="context-link <%= 'active' if request.path_info.start_with?('/backtest') %>">
|
|
100
|
+
<i class="fas fa-flask"></i> Backtest
|
|
101
|
+
</a>
|
|
102
|
+
<a href="/company/<%= @ticker %>" class="context-link <%= 'active' if request.path_info.start_with?('/company') %>">
|
|
103
|
+
<i class="fas fa-building"></i> Company
|
|
104
|
+
</a>
|
|
105
|
+
</nav>
|
|
106
|
+
<% if defined?(@show_period_selector) && @show_period_selector %>
|
|
107
|
+
<div class="context-period">
|
|
108
|
+
<select id="headerPeriodSelect" onchange="changePeriod(this.value)">
|
|
109
|
+
<option value="30">30 Days</option>
|
|
110
|
+
<option value="60">60 Days</option>
|
|
111
|
+
<option value="90" selected>90 Days</option>
|
|
112
|
+
<option value="1q">1 Quarter</option>
|
|
113
|
+
<option value="2q">2 Quarters</option>
|
|
114
|
+
<option value="1y">1 Year</option>
|
|
115
|
+
<option value="all">All Data</option>
|
|
116
|
+
</select>
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
24
119
|
</div>
|
|
25
|
-
<ul class="nav-menu">
|
|
26
|
-
<li><a href="/" class="nav-link"><i class="fas fa-home"></i> Home</a></li>
|
|
27
|
-
<li><a href="#" class="nav-link" onclick="showTickerModal()"><i class="fas fa-search"></i> Analyze Stock</a></li>
|
|
28
|
-
<li><a href="/portfolio" class="nav-link"><i class="fas fa-briefcase"></i> Portfolio</a></li>
|
|
29
|
-
</ul>
|
|
30
120
|
</div>
|
|
31
|
-
|
|
121
|
+
<% end %>
|
|
122
|
+
</header>
|
|
32
123
|
|
|
33
124
|
<!-- Main Content -->
|
|
34
125
|
<main class="main-content">
|
|
@@ -47,11 +138,12 @@
|
|
|
47
138
|
<div id="tickerModal" class="modal">
|
|
48
139
|
<div class="modal-content">
|
|
49
140
|
<span class="close" onclick="closeTickerModal()">×</span>
|
|
50
|
-
<h2>Enter Stock Ticker</h2>
|
|
141
|
+
<h2>Enter Stock Ticker(s)</h2>
|
|
51
142
|
<form onsubmit="return searchTicker(event)">
|
|
52
|
-
<input type="text" id="tickerInput" placeholder="e.g., AAPL" autocomplete="off" autofocus>
|
|
143
|
+
<input type="text" id="tickerInput" placeholder="e.g., AAPL or AAPL MSFT GOOGL" autocomplete="off" autofocus>
|
|
53
144
|
<button type="submit" class="btn btn-primary">Analyze</button>
|
|
54
145
|
</form>
|
|
146
|
+
<p class="modal-hint">Enter multiple tickers (up to 5) separated by spaces to compare</p>
|
|
55
147
|
</div>
|
|
56
148
|
</div>
|
|
57
149
|
|