sqa_demo-sinatra 0.1.0
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 +7 -0
- data/COMMITS.md +196 -0
- data/LICENSE +21 -0
- data/README.md +199 -0
- data/Rakefile +12 -0
- data/bin/sqa_sinatra +72 -0
- data/config.ru +5 -0
- data/lib/sqa_demo/sinatra/app.rb +581 -0
- data/lib/sqa_demo/sinatra/public/css/style.css +962 -0
- data/lib/sqa_demo/sinatra/public/debug_macd.html +82 -0
- data/lib/sqa_demo/sinatra/public/js/app.js +107 -0
- data/lib/sqa_demo/sinatra/version.rb +7 -0
- data/lib/sqa_demo/sinatra/views/analyze.erb +306 -0
- data/lib/sqa_demo/sinatra/views/backtest.erb +325 -0
- data/lib/sqa_demo/sinatra/views/dashboard.erb +1890 -0
- data/lib/sqa_demo/sinatra/views/error.erb +58 -0
- data/lib/sqa_demo/sinatra/views/index.erb +118 -0
- data/lib/sqa_demo/sinatra/views/layout.erb +61 -0
- data/lib/sqa_demo/sinatra/views/portfolio.erb +43 -0
- data/lib/sqa_demo/sinatra.rb +10 -0
- data/start.sh +47 -0
- metadata +219 -0
|
@@ -0,0 +1,1890 @@
|
|
|
1
|
+
<div class="dashboard">
|
|
2
|
+
<div class="dashboard-header">
|
|
3
|
+
<div class="ticker-info">
|
|
4
|
+
<h1><%= @ticker %></h1>
|
|
5
|
+
<div id="priceInfo" class="price-info">
|
|
6
|
+
<span class="current-price">Loading...</span>
|
|
7
|
+
<span class="price-change">--</span>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="header-actions">
|
|
11
|
+
<button onclick="location.href='/analyze/<%= @ticker %>'" class="btn btn-secondary">
|
|
12
|
+
<i class="fas fa-analytics"></i> Analysis
|
|
13
|
+
</button>
|
|
14
|
+
<button onclick="location.href='/backtest/<%= @ticker %>'" class="btn btn-secondary">
|
|
15
|
+
<i class="fas fa-history"></i> Backtest
|
|
16
|
+
</button>
|
|
17
|
+
<button onclick="refreshData()" class="btn btn-secondary">
|
|
18
|
+
<i class="fas fa-sync-alt"></i> Refresh
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Key Metrics Cards -->
|
|
24
|
+
<div class="metrics-grid">
|
|
25
|
+
<div class="metric-card">
|
|
26
|
+
<div class="metric-label">52-Week High</div>
|
|
27
|
+
<div id="high52w" class="metric-value">--</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="metric-card">
|
|
30
|
+
<div class="metric-label">52-Week Low</div>
|
|
31
|
+
<div id="low52w" class="metric-value">--</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="metric-card">
|
|
34
|
+
<div class="metric-label">Current RSI</div>
|
|
35
|
+
<div id="currentRSI" class="metric-value">--</div>
|
|
36
|
+
<div id="rsiSignal" class="metric-signal"></div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="metric-card">
|
|
39
|
+
<div class="metric-label">Market Regime</div>
|
|
40
|
+
<div id="marketRegime" class="metric-value">--</div>
|
|
41
|
+
<div id="regimeDetail" class="metric-signal"></div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Time Period Selector -->
|
|
46
|
+
<div class="period-selector">
|
|
47
|
+
<label>Time Period:</label>
|
|
48
|
+
<div class="period-buttons">
|
|
49
|
+
<button onclick="updatePeriod('30d')" class="btn-period" data-period="30d">30 Days</button>
|
|
50
|
+
<button onclick="updatePeriod('60d')" class="btn-period" data-period="60d">60 Days</button>
|
|
51
|
+
<button onclick="updatePeriod('90d')" class="btn-period active" data-period="90d">90 Days</button>
|
|
52
|
+
<button onclick="updatePeriod('1q')" class="btn-period" data-period="1q">1 Quarter</button>
|
|
53
|
+
<button onclick="updatePeriod('2q')" class="btn-period" data-period="2q">2 Quarters</button>
|
|
54
|
+
<button onclick="updatePeriod('3q')" class="btn-period" data-period="3q">3 Quarters</button>
|
|
55
|
+
<button onclick="updatePeriod('4q')" class="btn-period" data-period="4q">4 Quarters</button>
|
|
56
|
+
<button onclick="updatePeriod('all')" class="btn-period" data-period="all">All Data</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Main Price Chart -->
|
|
61
|
+
<div class="chart-container">
|
|
62
|
+
<div class="chart-header">
|
|
63
|
+
<h2><i class="fas fa-chart-candlestick"></i> Price Chart</h2>
|
|
64
|
+
<div class="chart-controls">
|
|
65
|
+
<button onclick="updateChartType('candlestick')" class="btn-small active" data-chart="candlestick">
|
|
66
|
+
Candlestick
|
|
67
|
+
</button>
|
|
68
|
+
<button onclick="updateChartType('line')" class="btn-small" data-chart="line">
|
|
69
|
+
Line
|
|
70
|
+
</button>
|
|
71
|
+
<div id="indicatorSelector" class="indicator-selector" style="display: none;">
|
|
72
|
+
<label>Indicators:</label>
|
|
73
|
+
<div class="indicator-dropdown">
|
|
74
|
+
<button type="button" class="btn-small dropdown-toggle" onclick="toggleIndicatorDropdown()">
|
|
75
|
+
<span id="selectedIndicatorCount">None</span> <i class="fas fa-chevron-down"></i>
|
|
76
|
+
</button>
|
|
77
|
+
<div id="indicatorDropdownMenu" class="dropdown-menu dropdown-menu-scrollable">
|
|
78
|
+
<div class="dropdown-section">
|
|
79
|
+
<div class="dropdown-section-title">Simple MA</div>
|
|
80
|
+
<label class="dropdown-item">
|
|
81
|
+
<input type="checkbox" value="sma_12" onchange="updateSelectedIndicators()"> SMA 12
|
|
82
|
+
</label>
|
|
83
|
+
<label class="dropdown-item">
|
|
84
|
+
<input type="checkbox" value="sma_20" onchange="updateSelectedIndicators()"> SMA 20
|
|
85
|
+
</label>
|
|
86
|
+
<label class="dropdown-item">
|
|
87
|
+
<input type="checkbox" value="sma_50" onchange="updateSelectedIndicators()"> SMA 50
|
|
88
|
+
</label>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="dropdown-section">
|
|
91
|
+
<div class="dropdown-section-title">Exponential MA</div>
|
|
92
|
+
<label class="dropdown-item">
|
|
93
|
+
<input type="checkbox" value="ema_20" onchange="updateSelectedIndicators()"> EMA 20
|
|
94
|
+
</label>
|
|
95
|
+
<label class="dropdown-item">
|
|
96
|
+
<input type="checkbox" value="dema_20" onchange="updateSelectedIndicators()"> DEMA 20
|
|
97
|
+
</label>
|
|
98
|
+
<label class="dropdown-item">
|
|
99
|
+
<input type="checkbox" value="tema_20" onchange="updateSelectedIndicators()"> TEMA 20
|
|
100
|
+
</label>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="dropdown-section">
|
|
103
|
+
<div class="dropdown-section-title">Other MA</div>
|
|
104
|
+
<label class="dropdown-item">
|
|
105
|
+
<input type="checkbox" value="wma_20" onchange="updateSelectedIndicators()"> WMA 20
|
|
106
|
+
</label>
|
|
107
|
+
<label class="dropdown-item">
|
|
108
|
+
<input type="checkbox" value="kama_30" onchange="updateSelectedIndicators()"> KAMA 30
|
|
109
|
+
</label>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="dropdown-section">
|
|
112
|
+
<div class="dropdown-section-title">Bollinger Bands</div>
|
|
113
|
+
<label class="dropdown-item">
|
|
114
|
+
<input type="checkbox" value="bb_upper" onchange="updateSelectedIndicators()"> BB Upper
|
|
115
|
+
</label>
|
|
116
|
+
<label class="dropdown-item">
|
|
117
|
+
<input type="checkbox" value="bb_middle" onchange="updateSelectedIndicators()"> BB Middle
|
|
118
|
+
</label>
|
|
119
|
+
<label class="dropdown-item">
|
|
120
|
+
<input type="checkbox" value="bb_lower" onchange="updateSelectedIndicators()"> BB Lower
|
|
121
|
+
</label>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div id="priceChart" class="chart"></div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Volume Chart -->
|
|
132
|
+
<div class="chart-container">
|
|
133
|
+
<div class="chart-header">
|
|
134
|
+
<h2><i class="fas fa-chart-bar"></i> Volume</h2>
|
|
135
|
+
<div class="chart-controls">
|
|
136
|
+
<div class="chart-legend">
|
|
137
|
+
<span class="legend-item"><span class="legend-color" style="background-color: #00ff88;"></span> Price Up</span>
|
|
138
|
+
<span class="legend-item"><span class="legend-color" style="background-color: #ff3366;"></span> Price Down</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="indicator-selector">
|
|
141
|
+
<label>Indicators:</label>
|
|
142
|
+
<div class="indicator-dropdown">
|
|
143
|
+
<button type="button" class="btn-small dropdown-toggle" onclick="toggleVolumeIndicatorDropdown()">
|
|
144
|
+
<span id="selectedVolumeIndicatorCount">None</span> <i class="fas fa-chevron-down"></i>
|
|
145
|
+
</button>
|
|
146
|
+
<div id="volumeIndicatorDropdownMenu" class="dropdown-menu dropdown-menu-scrollable">
|
|
147
|
+
<div class="dropdown-section">
|
|
148
|
+
<div class="dropdown-section-title">Moving Averages</div>
|
|
149
|
+
<label class="dropdown-item">
|
|
150
|
+
<input type="checkbox" value="vol_sma_12" onchange="updateSelectedVolumeIndicators()"> SMA 12
|
|
151
|
+
</label>
|
|
152
|
+
<label class="dropdown-item">
|
|
153
|
+
<input type="checkbox" value="vol_sma_20" onchange="updateSelectedVolumeIndicators()"> SMA 20
|
|
154
|
+
</label>
|
|
155
|
+
<label class="dropdown-item">
|
|
156
|
+
<input type="checkbox" value="vol_sma_50" onchange="updateSelectedVolumeIndicators()"> SMA 50
|
|
157
|
+
</label>
|
|
158
|
+
<label class="dropdown-item">
|
|
159
|
+
<input type="checkbox" value="vol_ema_12" onchange="updateSelectedVolumeIndicators()"> EMA 12
|
|
160
|
+
</label>
|
|
161
|
+
<label class="dropdown-item">
|
|
162
|
+
<input type="checkbox" value="vol_ema_20" onchange="updateSelectedVolumeIndicators()"> EMA 20
|
|
163
|
+
</label>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="dropdown-section">
|
|
166
|
+
<div class="dropdown-section-title">Volume Analysis</div>
|
|
167
|
+
<label class="dropdown-item">
|
|
168
|
+
<input type="checkbox" value="obv" onchange="updateSelectedVolumeIndicators()"> OBV
|
|
169
|
+
</label>
|
|
170
|
+
<label class="dropdown-item">
|
|
171
|
+
<input type="checkbox" value="ad" onchange="updateSelectedVolumeIndicators()"> A/D Line
|
|
172
|
+
</label>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div id="volumeChart" class="chart"></div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Technical Indicators Grid -->
|
|
183
|
+
<div class="indicators-grid">
|
|
184
|
+
<!-- RSI Chart -->
|
|
185
|
+
<div class="chart-container">
|
|
186
|
+
<div class="chart-header">
|
|
187
|
+
<h3><i class="fas fa-wave-square"></i> RSI (14)</h3>
|
|
188
|
+
</div>
|
|
189
|
+
<div id="rsiChart" class="chart chart-small"></div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- MACD Chart -->
|
|
193
|
+
<div class="chart-container">
|
|
194
|
+
<div class="chart-header">
|
|
195
|
+
<h3><i class="fas fa-signal"></i> MACD</h3>
|
|
196
|
+
</div>
|
|
197
|
+
<div id="macdChart" class="chart chart-small"></div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<!-- Stochastic Oscillator Chart -->
|
|
201
|
+
<div class="chart-container">
|
|
202
|
+
<div class="chart-header">
|
|
203
|
+
<h3><i class="fas fa-chart-line"></i> Stochastic (5,3,3)</h3>
|
|
204
|
+
</div>
|
|
205
|
+
<div id="stochChart" class="chart chart-small"></div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- CCI Chart -->
|
|
209
|
+
<div class="chart-container">
|
|
210
|
+
<div class="chart-header">
|
|
211
|
+
<h3><i class="fas fa-compress-arrows-alt"></i> CCI (14)</h3>
|
|
212
|
+
</div>
|
|
213
|
+
<div id="cciChart" class="chart chart-small"></div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<!-- ADX Chart -->
|
|
217
|
+
<div class="chart-container">
|
|
218
|
+
<div class="chart-header">
|
|
219
|
+
<h3><i class="fas fa-arrows-alt-h"></i> ADX (14)</h3>
|
|
220
|
+
</div>
|
|
221
|
+
<div id="adxChart" class="chart chart-small"></div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<!-- Williams %R Chart -->
|
|
225
|
+
<div class="chart-container">
|
|
226
|
+
<div class="chart-header">
|
|
227
|
+
<h3><i class="fas fa-percentage"></i> Williams %R (14)</h3>
|
|
228
|
+
</div>
|
|
229
|
+
<div id="willrChart" class="chart chart-small"></div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- ATR Chart -->
|
|
233
|
+
<div class="chart-container">
|
|
234
|
+
<div class="chart-header">
|
|
235
|
+
<h3><i class="fas fa-expand-arrows-alt"></i> ATR (14)</h3>
|
|
236
|
+
</div>
|
|
237
|
+
<div id="atrChart" class="chart chart-small"></div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- ROC Chart -->
|
|
241
|
+
<div class="chart-container">
|
|
242
|
+
<div class="chart-header">
|
|
243
|
+
<h3><i class="fas fa-tachometer-alt"></i> ROC (10)</h3>
|
|
244
|
+
</div>
|
|
245
|
+
<div id="rocChart" class="chart chart-small"></div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<!-- Momentum Chart -->
|
|
249
|
+
<div class="chart-container">
|
|
250
|
+
<div class="chart-header">
|
|
251
|
+
<h3><i class="fas fa-rocket"></i> Momentum (10)</h3>
|
|
252
|
+
</div>
|
|
253
|
+
<div id="momChart" class="chart chart-small"></div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- Pattern Recognition & Signal Alerts -->
|
|
258
|
+
<div class="alerts-row">
|
|
259
|
+
<!-- Candlestick Patterns -->
|
|
260
|
+
<div class="chart-container">
|
|
261
|
+
<div class="chart-header">
|
|
262
|
+
<h2><i class="fas fa-shapes"></i> Candlestick Patterns</h2>
|
|
263
|
+
</div>
|
|
264
|
+
<div id="patternAlerts" class="alerts-container">
|
|
265
|
+
<p class="hint">Loading patterns...</p>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<!-- Signal Alerts -->
|
|
270
|
+
<div class="chart-container">
|
|
271
|
+
<div class="chart-header">
|
|
272
|
+
<h2><i class="fas fa-bell"></i> Signal Alerts</h2>
|
|
273
|
+
</div>
|
|
274
|
+
<div id="signalAlerts" class="alerts-container">
|
|
275
|
+
<p class="hint">Loading signals...</p>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<!-- Strategy Comparison -->
|
|
281
|
+
<div class="chart-container">
|
|
282
|
+
<div class="chart-header">
|
|
283
|
+
<h2><i class="fas fa-trophy"></i> Strategy Comparison</h2>
|
|
284
|
+
<button onclick="runStrategyComparison()" class="btn btn-primary">
|
|
285
|
+
<i class="fas fa-play"></i> Compare Strategies
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
<div id="strategyResults" class="strategy-results">
|
|
289
|
+
<p class="hint">Click "Compare Strategies" to see backtest results for different trading strategies.</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<script>
|
|
295
|
+
const ticker = '<%= @ticker %>';
|
|
296
|
+
let stockData = null;
|
|
297
|
+
let indicatorData = null;
|
|
298
|
+
let currentChartType = 'candlestick';
|
|
299
|
+
let currentPeriod = '90d'; // Default to 90 days to avoid performance issues
|
|
300
|
+
|
|
301
|
+
// ApexCharts instances
|
|
302
|
+
let priceChart = null;
|
|
303
|
+
let volumeChart = null;
|
|
304
|
+
let rsiChart = null;
|
|
305
|
+
let macdChart = null;
|
|
306
|
+
let stochChart = null;
|
|
307
|
+
let cciChart = null;
|
|
308
|
+
let adxChart = null;
|
|
309
|
+
let willrChart = null;
|
|
310
|
+
let atrChart = null;
|
|
311
|
+
let rocChart = null;
|
|
312
|
+
let momChart = null;
|
|
313
|
+
|
|
314
|
+
// Indicator configuration - Price Overlays
|
|
315
|
+
const INDICATOR_CONFIG = {
|
|
316
|
+
// Simple Moving Averages
|
|
317
|
+
sma_12: { name: 'SMA 12', color: '#4ecdc4' },
|
|
318
|
+
sma_20: { name: 'SMA 20', color: '#ffaa00' },
|
|
319
|
+
sma_50: { name: 'SMA 50', color: '#ff66ff' },
|
|
320
|
+
// Exponential Moving Average
|
|
321
|
+
ema_20: { name: 'EMA 20', color: '#00ff88' },
|
|
322
|
+
// Additional Moving Averages
|
|
323
|
+
wma_20: { name: 'WMA 20', color: '#ff9f43' },
|
|
324
|
+
dema_20: { name: 'DEMA 20', color: '#a55eea' },
|
|
325
|
+
tema_20: { name: 'TEMA 20', color: '#26de81' },
|
|
326
|
+
kama_30: { name: 'KAMA 30', color: '#fd9644' },
|
|
327
|
+
// Bollinger Bands
|
|
328
|
+
bb_upper: { name: 'BB Upper', color: '#ff6b6b' },
|
|
329
|
+
bb_middle: { name: 'BB Middle', color: '#ffd93d' },
|
|
330
|
+
bb_lower: { name: 'BB Lower', color: '#6bcb77' }
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Volume indicator configuration (moving averages for volume data)
|
|
334
|
+
const VOLUME_INDICATOR_CONFIG = {
|
|
335
|
+
vol_sma_12: { name: 'SMA 12', color: '#4ecdc4', period: 12, type: 'sma' },
|
|
336
|
+
vol_sma_20: { name: 'SMA 20', color: '#ffaa00', period: 20, type: 'sma' },
|
|
337
|
+
vol_sma_50: { name: 'SMA 50', color: '#ff66ff', period: 50, type: 'sma' },
|
|
338
|
+
vol_ema_12: { name: 'EMA 12', color: '#00d4ff', period: 12, type: 'ema' },
|
|
339
|
+
vol_ema_20: { name: 'EMA 20', color: '#00ff88', period: 20, type: 'ema' },
|
|
340
|
+
obv: { name: 'OBV', color: '#a55eea', type: 'line' },
|
|
341
|
+
ad: { name: 'A/D Line', color: '#26de81', type: 'line' }
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
let selectedIndicators = []; // User-selected indicators for line chart
|
|
345
|
+
let selectedVolumeIndicators = []; // User-selected indicators for volume chart
|
|
346
|
+
|
|
347
|
+
// Calculate SMA for an array of values
|
|
348
|
+
function calculateSMA(data, period) {
|
|
349
|
+
const result = [];
|
|
350
|
+
for (let i = 0; i < data.length; i++) {
|
|
351
|
+
if (i < period - 1) {
|
|
352
|
+
result.push(null);
|
|
353
|
+
} else {
|
|
354
|
+
let sum = 0;
|
|
355
|
+
for (let j = 0; j < period; j++) {
|
|
356
|
+
sum += data[i - j];
|
|
357
|
+
}
|
|
358
|
+
result.push(sum / period);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Calculate EMA for an array of values
|
|
365
|
+
function calculateEMA(data, period) {
|
|
366
|
+
const result = [];
|
|
367
|
+
const multiplier = 2 / (period + 1);
|
|
368
|
+
|
|
369
|
+
for (let i = 0; i < data.length; i++) {
|
|
370
|
+
if (i < period - 1) {
|
|
371
|
+
result.push(null);
|
|
372
|
+
} else if (i === period - 1) {
|
|
373
|
+
// First EMA is SMA
|
|
374
|
+
let sum = 0;
|
|
375
|
+
for (let j = 0; j < period; j++) {
|
|
376
|
+
sum += data[i - j];
|
|
377
|
+
}
|
|
378
|
+
result.push(sum / period);
|
|
379
|
+
} else {
|
|
380
|
+
// EMA = (Close - Previous EMA) * multiplier + Previous EMA
|
|
381
|
+
const ema = (data[i] - result[i - 1]) * multiplier + result[i - 1];
|
|
382
|
+
result.push(ema);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Load data when page loads
|
|
389
|
+
document.addEventListener('DOMContentLoaded', async function() {
|
|
390
|
+
await loadStockData();
|
|
391
|
+
await loadIndicators();
|
|
392
|
+
await loadAnalysis();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
async function loadStockData() {
|
|
396
|
+
try {
|
|
397
|
+
const response = await fetch(`/api/stock/${ticker}?period=${currentPeriod}`);
|
|
398
|
+
stockData = await response.json();
|
|
399
|
+
|
|
400
|
+
// Update price info
|
|
401
|
+
document.querySelector('.current-price').textContent = `$${stockData.current_price.toFixed(2)}`;
|
|
402
|
+
|
|
403
|
+
const changeClass = stockData.change >= 0 ? 'positive' : 'negative';
|
|
404
|
+
const changeSign = stockData.change >= 0 ? '+' : '';
|
|
405
|
+
document.querySelector('.price-change').className = `price-change ${changeClass}`;
|
|
406
|
+
document.querySelector('.price-change').textContent =
|
|
407
|
+
`${changeSign}${stockData.change.toFixed(2)} (${changeSign}${stockData.change_percent.toFixed(2)}%)`;
|
|
408
|
+
|
|
409
|
+
// Update metrics
|
|
410
|
+
document.getElementById('high52w').textContent = `$${stockData.high_52w.toFixed(2)}`;
|
|
411
|
+
document.getElementById('low52w').textContent = `$${stockData.low_52w.toFixed(2)}`;
|
|
412
|
+
|
|
413
|
+
// Render charts
|
|
414
|
+
renderPriceChart();
|
|
415
|
+
renderVolumeChart();
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error('Error loading stock data:', error);
|
|
418
|
+
alert('Failed to load stock data. Please try again.');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function loadIndicators() {
|
|
423
|
+
try {
|
|
424
|
+
const response = await fetch(`/api/indicators/${ticker}?period=${currentPeriod}`);
|
|
425
|
+
const data = await response.json();
|
|
426
|
+
|
|
427
|
+
// Check if API returned an error
|
|
428
|
+
if (data.error) {
|
|
429
|
+
console.warn('Indicators not available:', data.error);
|
|
430
|
+
document.getElementById('currentRSI').textContent = 'N/A';
|
|
431
|
+
document.getElementById('rsiSignal').textContent = 'TA-Lib not installed';
|
|
432
|
+
document.getElementById('rsiSignal').className = 'metric-signal';
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
indicatorData = data;
|
|
437
|
+
|
|
438
|
+
// Update RSI metric
|
|
439
|
+
const currentRSI = indicatorData.rsi[indicatorData.rsi.length - 1];
|
|
440
|
+
document.getElementById('currentRSI').textContent = currentRSI.toFixed(2);
|
|
441
|
+
|
|
442
|
+
let rsiSignal = '';
|
|
443
|
+
let rsiClass = '';
|
|
444
|
+
if (currentRSI < 30) {
|
|
445
|
+
rsiSignal = 'Oversold';
|
|
446
|
+
rsiClass = 'signal-buy';
|
|
447
|
+
} else if (currentRSI > 70) {
|
|
448
|
+
rsiSignal = 'Overbought';
|
|
449
|
+
rsiClass = 'signal-sell';
|
|
450
|
+
} else {
|
|
451
|
+
rsiSignal = 'Neutral';
|
|
452
|
+
rsiClass = 'signal-neutral';
|
|
453
|
+
}
|
|
454
|
+
const rsiSignalEl = document.getElementById('rsiSignal');
|
|
455
|
+
rsiSignalEl.textContent = rsiSignal;
|
|
456
|
+
rsiSignalEl.className = `metric-signal ${rsiClass}`;
|
|
457
|
+
|
|
458
|
+
// Render indicator charts
|
|
459
|
+
renderRSIChart();
|
|
460
|
+
renderMACDChart();
|
|
461
|
+
renderStochChart();
|
|
462
|
+
renderCCIChart();
|
|
463
|
+
renderADXChart();
|
|
464
|
+
renderWillrChart();
|
|
465
|
+
renderATRChart();
|
|
466
|
+
renderROCChart();
|
|
467
|
+
renderMomentumChart();
|
|
468
|
+
|
|
469
|
+
// Render alerts
|
|
470
|
+
renderPatternAlerts();
|
|
471
|
+
renderSignalAlerts();
|
|
472
|
+
|
|
473
|
+
// Re-render price chart if in line mode (to include updated indicator data)
|
|
474
|
+
if (currentChartType === 'line') {
|
|
475
|
+
renderPriceChart();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Re-render volume chart if volume indicators are selected
|
|
479
|
+
if (selectedVolumeIndicators.length > 0) {
|
|
480
|
+
renderVolumeChart();
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error('Error loading indicators:', error);
|
|
484
|
+
document.getElementById('currentRSI').textContent = 'Error';
|
|
485
|
+
document.getElementById('rsiSignal').textContent = 'Failed to load';
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function loadAnalysis() {
|
|
490
|
+
try {
|
|
491
|
+
const response = await fetch(`/api/analyze/${ticker}`);
|
|
492
|
+
const analysis = await response.json();
|
|
493
|
+
|
|
494
|
+
// Update market regime
|
|
495
|
+
const regime = analysis.regime.type.toUpperCase();
|
|
496
|
+
const regimeEl = document.getElementById('marketRegime');
|
|
497
|
+
regimeEl.textContent = regime;
|
|
498
|
+
|
|
499
|
+
let regimeClass = '';
|
|
500
|
+
if (regime === 'BULL') regimeClass = 'signal-buy';
|
|
501
|
+
else if (regime === 'BEAR') regimeClass = 'signal-sell';
|
|
502
|
+
else regimeClass = 'signal-neutral';
|
|
503
|
+
regimeEl.className = `metric-value ${regimeClass}`;
|
|
504
|
+
|
|
505
|
+
const regimeDetail = document.getElementById('regimeDetail');
|
|
506
|
+
const strengthText = typeof analysis.regime.strength === 'number'
|
|
507
|
+
? analysis.regime.strength.toFixed(2)
|
|
508
|
+
: analysis.regime.strength || 'N/A';
|
|
509
|
+
regimeDetail.textContent = `${analysis.regime.volatility || 'unknown'} volatility, ${strengthText} strength`;
|
|
510
|
+
regimeDetail.className = 'metric-signal';
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error('Error loading analysis:', error);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function renderPriceChart() {
|
|
517
|
+
// Destroy existing chart if it exists
|
|
518
|
+
if (priceChart) {
|
|
519
|
+
priceChart.destroy();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let options;
|
|
523
|
+
|
|
524
|
+
if (currentChartType === 'candlestick') {
|
|
525
|
+
// Prepare candlestick data
|
|
526
|
+
const candleData = stockData.dates.map((date, i) => ({
|
|
527
|
+
x: new Date(date),
|
|
528
|
+
y: [stockData.open[i], stockData.high[i], stockData.low[i], stockData.close[i]]
|
|
529
|
+
}));
|
|
530
|
+
|
|
531
|
+
options = {
|
|
532
|
+
series: [{
|
|
533
|
+
name: ticker,
|
|
534
|
+
data: candleData
|
|
535
|
+
}],
|
|
536
|
+
chart: {
|
|
537
|
+
type: 'candlestick',
|
|
538
|
+
height: 500,
|
|
539
|
+
group: 'stock-charts',
|
|
540
|
+
background: '#151b3d',
|
|
541
|
+
foreColor: '#e8eaf6',
|
|
542
|
+
toolbar: {
|
|
543
|
+
show: true,
|
|
544
|
+
tools: {
|
|
545
|
+
download: true,
|
|
546
|
+
zoom: true,
|
|
547
|
+
zoomin: true,
|
|
548
|
+
zoomout: true,
|
|
549
|
+
pan: true,
|
|
550
|
+
reset: true
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
zoom: {
|
|
554
|
+
enabled: true,
|
|
555
|
+
type: 'x'
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
dataLabels: {
|
|
559
|
+
enabled: false // Disable data labels for performance
|
|
560
|
+
},
|
|
561
|
+
plotOptions: {
|
|
562
|
+
candlestick: {
|
|
563
|
+
colors: {
|
|
564
|
+
upward: '#00ff88',
|
|
565
|
+
downward: '#ff3366'
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
xaxis: {
|
|
570
|
+
type: 'datetime',
|
|
571
|
+
labels: {
|
|
572
|
+
style: {
|
|
573
|
+
colors: '#9fa8da'
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
yaxis: {
|
|
578
|
+
tooltip: {
|
|
579
|
+
enabled: true
|
|
580
|
+
},
|
|
581
|
+
labels: {
|
|
582
|
+
style: {
|
|
583
|
+
colors: '#9fa8da'
|
|
584
|
+
},
|
|
585
|
+
formatter: (val) => '$' + val.toFixed(2)
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
grid: {
|
|
589
|
+
borderColor: '#2a3154',
|
|
590
|
+
strokeDashArray: 3
|
|
591
|
+
},
|
|
592
|
+
theme: {
|
|
593
|
+
mode: 'dark'
|
|
594
|
+
},
|
|
595
|
+
tooltip: {
|
|
596
|
+
theme: 'dark'
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
} else {
|
|
600
|
+
// Line chart with user-selected indicators using category axis (eliminates weekend gaps)
|
|
601
|
+
const series = [{
|
|
602
|
+
name: ticker,
|
|
603
|
+
data: stockData.close // Just values, aligned to categories
|
|
604
|
+
}];
|
|
605
|
+
|
|
606
|
+
// Build colors and stroke widths arrays starting with price line
|
|
607
|
+
const colors = ['#00d4ff'];
|
|
608
|
+
const strokeWidths = [3];
|
|
609
|
+
|
|
610
|
+
// Add selected indicators if available
|
|
611
|
+
if (indicatorData) {
|
|
612
|
+
selectedIndicators.forEach(indicatorKey => {
|
|
613
|
+
if (indicatorData[indicatorKey] && INDICATOR_CONFIG[indicatorKey]) {
|
|
614
|
+
series.push({
|
|
615
|
+
name: INDICATOR_CONFIG[indicatorKey].name,
|
|
616
|
+
data: indicatorData[indicatorKey] // Nulls are ok - ApexCharts handles them
|
|
617
|
+
});
|
|
618
|
+
colors.push(INDICATOR_CONFIG[indicatorKey].color);
|
|
619
|
+
strokeWidths.push(2);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
options = {
|
|
625
|
+
series: series,
|
|
626
|
+
chart: {
|
|
627
|
+
type: 'line',
|
|
628
|
+
height: 500,
|
|
629
|
+
group: 'stock-charts',
|
|
630
|
+
background: '#151b3d',
|
|
631
|
+
foreColor: '#e8eaf6',
|
|
632
|
+
toolbar: {
|
|
633
|
+
show: true,
|
|
634
|
+
tools: {
|
|
635
|
+
download: true,
|
|
636
|
+
zoom: true,
|
|
637
|
+
zoomin: true,
|
|
638
|
+
zoomout: true,
|
|
639
|
+
pan: true,
|
|
640
|
+
reset: true
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
zoom: {
|
|
644
|
+
enabled: true,
|
|
645
|
+
type: 'x'
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
colors: colors,
|
|
649
|
+
stroke: {
|
|
650
|
+
width: strokeWidths,
|
|
651
|
+
curve: 'smooth'
|
|
652
|
+
},
|
|
653
|
+
xaxis: {
|
|
654
|
+
type: 'category',
|
|
655
|
+
categories: stockData.dates,
|
|
656
|
+
tickAmount: 10, // Limit x-axis labels for readability
|
|
657
|
+
labels: {
|
|
658
|
+
rotate: -45,
|
|
659
|
+
rotateAlways: false,
|
|
660
|
+
style: {
|
|
661
|
+
colors: '#9fa8da'
|
|
662
|
+
},
|
|
663
|
+
formatter: (val) => {
|
|
664
|
+
// Format date for display (show month and day)
|
|
665
|
+
if (!val) return '';
|
|
666
|
+
const parts = val.split('-');
|
|
667
|
+
return parts.length >= 3 ? `${parts[1]}/${parts[2]}` : val;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
yaxis: {
|
|
672
|
+
labels: {
|
|
673
|
+
style: {
|
|
674
|
+
colors: '#9fa8da'
|
|
675
|
+
},
|
|
676
|
+
formatter: (val) => val ? '$' + val.toFixed(2) : ''
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
grid: {
|
|
680
|
+
borderColor: '#2a3154',
|
|
681
|
+
strokeDashArray: 3
|
|
682
|
+
},
|
|
683
|
+
legend: {
|
|
684
|
+
position: 'top',
|
|
685
|
+
horizontalAlign: 'left',
|
|
686
|
+
fontSize: '14px',
|
|
687
|
+
labels: {
|
|
688
|
+
colors: '#e8eaf6'
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
theme: {
|
|
692
|
+
mode: 'dark'
|
|
693
|
+
},
|
|
694
|
+
tooltip: {
|
|
695
|
+
theme: 'dark',
|
|
696
|
+
shared: true,
|
|
697
|
+
intersect: false,
|
|
698
|
+
x: {
|
|
699
|
+
formatter: (val, opts) => {
|
|
700
|
+
// Show full date in tooltip
|
|
701
|
+
const date = stockData.dates[opts.dataPointIndex];
|
|
702
|
+
return date || val;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
priceChart = new ApexCharts(document.querySelector("#priceChart"), options);
|
|
710
|
+
priceChart.render();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function renderVolumeChart() {
|
|
714
|
+
// Destroy existing chart if it exists
|
|
715
|
+
if (volumeChart) {
|
|
716
|
+
volumeChart.destroy();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Calculate bar colors based on price movement
|
|
720
|
+
const barColors = stockData.volume.map((vol, i) =>
|
|
721
|
+
i === 0 ? '#00d4ff' : (stockData.close[i] >= stockData.close[i - 1] ? '#00ff88' : '#ff3366')
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
// Build series array - volume bars first (plain array for mixed chart compatibility)
|
|
725
|
+
// Use 'column' type for vertical bars in mixed charts
|
|
726
|
+
const series = [{
|
|
727
|
+
name: 'Volume',
|
|
728
|
+
type: 'column',
|
|
729
|
+
data: stockData.volume
|
|
730
|
+
}];
|
|
731
|
+
|
|
732
|
+
// Build stroke widths array
|
|
733
|
+
const strokeWidths = [0]; // No stroke for bars
|
|
734
|
+
|
|
735
|
+
// Build colors array for series (first is for bars, rest are for line indicators)
|
|
736
|
+
// Note: Per-bar coloring is handled separately via fill options
|
|
737
|
+
const seriesColors = ['#00d4ff']; // Default bar color
|
|
738
|
+
|
|
739
|
+
// Add selected volume indicators as line series (using backend-calculated data)
|
|
740
|
+
if (indicatorData) {
|
|
741
|
+
selectedVolumeIndicators.forEach(indicatorKey => {
|
|
742
|
+
const config = VOLUME_INDICATOR_CONFIG[indicatorKey];
|
|
743
|
+
// Map frontend key to backend data key (vol_sma_12 -> indicatorData.vol_sma_12)
|
|
744
|
+
const backendData = indicatorData[indicatorKey];
|
|
745
|
+
|
|
746
|
+
if (config && backendData) {
|
|
747
|
+
series.push({
|
|
748
|
+
name: config.name,
|
|
749
|
+
type: 'line',
|
|
750
|
+
data: backendData
|
|
751
|
+
});
|
|
752
|
+
strokeWidths.push(2);
|
|
753
|
+
seriesColors.push(config.color);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const options = {
|
|
759
|
+
series: series,
|
|
760
|
+
chart: {
|
|
761
|
+
type: 'line', // Base type for mixed charts
|
|
762
|
+
height: 300,
|
|
763
|
+
group: 'stock-charts',
|
|
764
|
+
background: '#151b3d',
|
|
765
|
+
foreColor: '#e8eaf6',
|
|
766
|
+
toolbar: {
|
|
767
|
+
show: true,
|
|
768
|
+
tools: {
|
|
769
|
+
download: true,
|
|
770
|
+
zoom: true,
|
|
771
|
+
zoomin: true,
|
|
772
|
+
zoomout: true,
|
|
773
|
+
pan: true,
|
|
774
|
+
reset: true
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
zoom: {
|
|
778
|
+
enabled: true,
|
|
779
|
+
type: 'x'
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
colors: seriesColors,
|
|
783
|
+
stroke: {
|
|
784
|
+
width: strokeWidths,
|
|
785
|
+
curve: 'smooth'
|
|
786
|
+
},
|
|
787
|
+
dataLabels: {
|
|
788
|
+
enabled: false // Disable data labels for performance
|
|
789
|
+
},
|
|
790
|
+
plotOptions: {
|
|
791
|
+
bar: {
|
|
792
|
+
columnWidth: '95%',
|
|
793
|
+
distributed: false
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
fill: {
|
|
797
|
+
colors: [function({ value, seriesIndex, dataPointIndex, w }) {
|
|
798
|
+
if (seriesIndex === 0) {
|
|
799
|
+
// Bar series - use price movement colors
|
|
800
|
+
return barColors[dataPointIndex] || '#00d4ff';
|
|
801
|
+
}
|
|
802
|
+
// Line series - use their configured color
|
|
803
|
+
return seriesColors[seriesIndex] || '#00d4ff';
|
|
804
|
+
}]
|
|
805
|
+
},
|
|
806
|
+
xaxis: {
|
|
807
|
+
type: 'category',
|
|
808
|
+
categories: stockData.dates,
|
|
809
|
+
tickAmount: 10,
|
|
810
|
+
labels: {
|
|
811
|
+
rotate: -45,
|
|
812
|
+
rotateAlways: false,
|
|
813
|
+
style: {
|
|
814
|
+
colors: '#9fa8da'
|
|
815
|
+
},
|
|
816
|
+
formatter: (val) => {
|
|
817
|
+
if (!val) return '';
|
|
818
|
+
const parts = val.split('-');
|
|
819
|
+
return parts.length >= 3 ? `${parts[1]}/${parts[2]}` : val;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
yaxis: {
|
|
824
|
+
labels: {
|
|
825
|
+
style: {
|
|
826
|
+
colors: '#9fa8da'
|
|
827
|
+
},
|
|
828
|
+
formatter: (val) => {
|
|
829
|
+
if (val >= 1e9) return (val / 1e9).toFixed(1) + 'B';
|
|
830
|
+
if (val >= 1e6) return (val / 1e6).toFixed(1) + 'M';
|
|
831
|
+
if (val >= 1e3) return (val / 1e3).toFixed(1) + 'K';
|
|
832
|
+
return val.toFixed(0);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
grid: {
|
|
837
|
+
borderColor: '#2a3154',
|
|
838
|
+
strokeDashArray: 3
|
|
839
|
+
},
|
|
840
|
+
legend: {
|
|
841
|
+
show: selectedVolumeIndicators.length > 0,
|
|
842
|
+
position: 'top',
|
|
843
|
+
horizontalAlign: 'left',
|
|
844
|
+
fontSize: '14px',
|
|
845
|
+
labels: {
|
|
846
|
+
colors: '#e8eaf6'
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
theme: {
|
|
850
|
+
mode: 'dark'
|
|
851
|
+
},
|
|
852
|
+
tooltip: {
|
|
853
|
+
theme: 'dark',
|
|
854
|
+
shared: true,
|
|
855
|
+
intersect: false,
|
|
856
|
+
y: {
|
|
857
|
+
formatter: (val) => val ? val.toLocaleString() : ''
|
|
858
|
+
},
|
|
859
|
+
x: {
|
|
860
|
+
formatter: (val, opts) => stockData.dates[opts.dataPointIndex] || val
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
volumeChart = new ApexCharts(document.querySelector("#volumeChart"), options);
|
|
866
|
+
volumeChart.render();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function renderRSIChart() {
|
|
870
|
+
// Destroy existing chart if it exists
|
|
871
|
+
if (rsiChart) {
|
|
872
|
+
rsiChart.destroy();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Skip if indicators not available
|
|
876
|
+
if (!indicatorData || !indicatorData.rsi) {
|
|
877
|
+
document.querySelector("#rsiChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">Technical indicators not available (TA-Lib required)</p>';
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Filter out null/NaN/undefined values and build category-aligned data
|
|
882
|
+
const validDates = [];
|
|
883
|
+
const validRSI = [];
|
|
884
|
+
for (let i = 0; i < indicatorData.rsi.length; i++) {
|
|
885
|
+
if (indicatorData.dates[i] !== null &&
|
|
886
|
+
indicatorData.dates[i] !== undefined &&
|
|
887
|
+
indicatorData.rsi[i] !== null &&
|
|
888
|
+
indicatorData.rsi[i] !== undefined &&
|
|
889
|
+
!isNaN(indicatorData.rsi[i])) {
|
|
890
|
+
validDates.push(indicatorData.dates[i]);
|
|
891
|
+
validRSI.push(indicatorData.rsi[i]);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// If no valid data, show message
|
|
896
|
+
if (validDates.length === 0) {
|
|
897
|
+
document.querySelector("#rsiChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid RSI data available (warmup period or data issue)</p>';
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Create constant arrays for reference lines
|
|
902
|
+
const oversoldLine = validDates.map(() => 30);
|
|
903
|
+
const overboughtLine = validDates.map(() => 70);
|
|
904
|
+
|
|
905
|
+
const options = {
|
|
906
|
+
series: [
|
|
907
|
+
{ name: 'RSI', data: validRSI },
|
|
908
|
+
{ name: 'Oversold (30)', data: oversoldLine },
|
|
909
|
+
{ name: 'Overbought (70)', data: overboughtLine }
|
|
910
|
+
],
|
|
911
|
+
chart: {
|
|
912
|
+
type: 'line',
|
|
913
|
+
height: 350,
|
|
914
|
+
group: 'stock-charts',
|
|
915
|
+
background: '#151b3d',
|
|
916
|
+
foreColor: '#e8eaf6',
|
|
917
|
+
toolbar: {
|
|
918
|
+
show: true,
|
|
919
|
+
tools: {
|
|
920
|
+
download: true,
|
|
921
|
+
zoom: true,
|
|
922
|
+
zoomin: true,
|
|
923
|
+
zoomout: true,
|
|
924
|
+
pan: true,
|
|
925
|
+
reset: true
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
zoom: {
|
|
929
|
+
enabled: true,
|
|
930
|
+
type: 'x'
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
colors: ['#00d4ff', '#00ff88', '#ff3366'],
|
|
934
|
+
stroke: {
|
|
935
|
+
width: [3, 2, 2],
|
|
936
|
+
curve: 'smooth',
|
|
937
|
+
dashArray: [0, 5, 5]
|
|
938
|
+
},
|
|
939
|
+
xaxis: {
|
|
940
|
+
type: 'category',
|
|
941
|
+
categories: validDates,
|
|
942
|
+
tickAmount: 10,
|
|
943
|
+
labels: {
|
|
944
|
+
rotate: -45,
|
|
945
|
+
rotateAlways: false,
|
|
946
|
+
style: {
|
|
947
|
+
colors: '#9fa8da'
|
|
948
|
+
},
|
|
949
|
+
formatter: (val) => {
|
|
950
|
+
if (!val) return '';
|
|
951
|
+
const parts = val.split('-');
|
|
952
|
+
return parts.length >= 3 ? `${parts[1]}/${parts[2]}` : val;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
yaxis: {
|
|
957
|
+
min: 0,
|
|
958
|
+
max: 100,
|
|
959
|
+
labels: {
|
|
960
|
+
style: {
|
|
961
|
+
colors: '#9fa8da'
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
grid: {
|
|
966
|
+
borderColor: '#2a3154',
|
|
967
|
+
strokeDashArray: 3
|
|
968
|
+
},
|
|
969
|
+
legend: {
|
|
970
|
+
position: 'top',
|
|
971
|
+
horizontalAlign: 'left',
|
|
972
|
+
fontSize: '14px',
|
|
973
|
+
labels: {
|
|
974
|
+
colors: '#e8eaf6'
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
theme: {
|
|
978
|
+
mode: 'dark'
|
|
979
|
+
},
|
|
980
|
+
tooltip: {
|
|
981
|
+
theme: 'dark',
|
|
982
|
+
shared: true,
|
|
983
|
+
intersect: false,
|
|
984
|
+
x: {
|
|
985
|
+
formatter: (val, opts) => validDates[opts.dataPointIndex] || val
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
annotations: {
|
|
989
|
+
yaxis: [
|
|
990
|
+
{
|
|
991
|
+
y: 30,
|
|
992
|
+
borderColor: '#00ff88',
|
|
993
|
+
fillColor: '#00ff88',
|
|
994
|
+
opacity: 0.1
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
y: 70,
|
|
998
|
+
borderColor: '#ff3366',
|
|
999
|
+
fillColor: '#ff3366',
|
|
1000
|
+
opacity: 0.1
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
rsiChart = new ApexCharts(document.querySelector("#rsiChart"), options);
|
|
1007
|
+
rsiChart.render();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function renderMACDChart() {
|
|
1011
|
+
// Destroy existing chart if it exists
|
|
1012
|
+
if (macdChart) {
|
|
1013
|
+
macdChart.destroy();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Skip if indicators not available
|
|
1017
|
+
if (!indicatorData || !indicatorData.macd) {
|
|
1018
|
+
document.querySelector("#macdChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">Technical indicators not available (TA-Lib required)</p>';
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Filter out null/NaN/undefined values and build category-aligned data
|
|
1023
|
+
const validDates = [];
|
|
1024
|
+
const validMACD = [];
|
|
1025
|
+
const validSignal = [];
|
|
1026
|
+
const validHist = [];
|
|
1027
|
+
for (let i = 0; i < indicatorData.macd.length; i++) {
|
|
1028
|
+
if (indicatorData.dates[i] !== null &&
|
|
1029
|
+
indicatorData.dates[i] !== undefined &&
|
|
1030
|
+
indicatorData.macd[i] !== null &&
|
|
1031
|
+
indicatorData.macd[i] !== undefined &&
|
|
1032
|
+
indicatorData.macd_signal[i] !== null &&
|
|
1033
|
+
indicatorData.macd_signal[i] !== undefined &&
|
|
1034
|
+
indicatorData.macd_hist[i] !== null &&
|
|
1035
|
+
indicatorData.macd_hist[i] !== undefined &&
|
|
1036
|
+
!isNaN(indicatorData.macd[i]) &&
|
|
1037
|
+
!isNaN(indicatorData.macd_signal[i]) &&
|
|
1038
|
+
!isNaN(indicatorData.macd_hist[i])) {
|
|
1039
|
+
validDates.push(indicatorData.dates[i]);
|
|
1040
|
+
validMACD.push(indicatorData.macd[i]);
|
|
1041
|
+
validSignal.push(indicatorData.macd_signal[i]);
|
|
1042
|
+
validHist.push(indicatorData.macd_hist[i]);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// If no valid data, show message
|
|
1047
|
+
if (validDates.length === 0) {
|
|
1048
|
+
document.querySelector("#macdChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid MACD data available (warmup period or data issue)</p>';
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Prepare histogram data with colors for category axis
|
|
1053
|
+
const histData = validHist.map(h => ({
|
|
1054
|
+
y: h,
|
|
1055
|
+
fillColor: h >= 0 ? '#00ff88' : '#ff3366'
|
|
1056
|
+
}));
|
|
1057
|
+
|
|
1058
|
+
const options = {
|
|
1059
|
+
series: [
|
|
1060
|
+
{ name: 'Histogram', type: 'bar', data: histData },
|
|
1061
|
+
{ name: 'MACD', type: 'line', data: validMACD },
|
|
1062
|
+
{ name: 'Signal', type: 'line', data: validSignal }
|
|
1063
|
+
],
|
|
1064
|
+
chart: {
|
|
1065
|
+
height: 350,
|
|
1066
|
+
group: 'stock-charts',
|
|
1067
|
+
background: '#151b3d',
|
|
1068
|
+
foreColor: '#e8eaf6',
|
|
1069
|
+
toolbar: {
|
|
1070
|
+
show: true,
|
|
1071
|
+
tools: {
|
|
1072
|
+
download: true,
|
|
1073
|
+
zoom: true,
|
|
1074
|
+
zoomin: true,
|
|
1075
|
+
zoomout: true,
|
|
1076
|
+
pan: true,
|
|
1077
|
+
reset: true
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
zoom: {
|
|
1081
|
+
enabled: true,
|
|
1082
|
+
type: 'x'
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
plotOptions: {
|
|
1086
|
+
bar: {
|
|
1087
|
+
columnWidth: '80%'
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
colors: ['#00ff88', '#00d4ff', '#ffaa00'],
|
|
1091
|
+
stroke: {
|
|
1092
|
+
width: [0, 3, 3],
|
|
1093
|
+
curve: 'smooth'
|
|
1094
|
+
},
|
|
1095
|
+
xaxis: {
|
|
1096
|
+
type: 'category',
|
|
1097
|
+
categories: validDates,
|
|
1098
|
+
tickAmount: 10,
|
|
1099
|
+
labels: {
|
|
1100
|
+
rotate: -45,
|
|
1101
|
+
rotateAlways: false,
|
|
1102
|
+
style: {
|
|
1103
|
+
colors: '#9fa8da'
|
|
1104
|
+
},
|
|
1105
|
+
formatter: (val) => {
|
|
1106
|
+
if (!val) return '';
|
|
1107
|
+
const parts = val.split('-');
|
|
1108
|
+
return parts.length >= 3 ? `${parts[1]}/${parts[2]}` : val;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
yaxis: {
|
|
1113
|
+
labels: {
|
|
1114
|
+
style: {
|
|
1115
|
+
colors: '#9fa8da'
|
|
1116
|
+
},
|
|
1117
|
+
formatter: (val) => val ? val.toFixed(2) : ''
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
grid: {
|
|
1121
|
+
borderColor: '#2a3154',
|
|
1122
|
+
strokeDashArray: 3
|
|
1123
|
+
},
|
|
1124
|
+
legend: {
|
|
1125
|
+
position: 'top',
|
|
1126
|
+
horizontalAlign: 'left',
|
|
1127
|
+
fontSize: '14px',
|
|
1128
|
+
labels: {
|
|
1129
|
+
colors: '#e8eaf6'
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
theme: {
|
|
1133
|
+
mode: 'dark'
|
|
1134
|
+
},
|
|
1135
|
+
tooltip: {
|
|
1136
|
+
theme: 'dark',
|
|
1137
|
+
shared: true,
|
|
1138
|
+
intersect: false,
|
|
1139
|
+
x: {
|
|
1140
|
+
formatter: (val, opts) => validDates[opts.dataPointIndex] || val
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
macdChart = new ApexCharts(document.querySelector("#macdChart"), options);
|
|
1146
|
+
macdChart.render();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function renderStochChart() {
|
|
1150
|
+
if (stochChart) {
|
|
1151
|
+
stochChart.destroy();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (!indicatorData || !indicatorData.stoch_slowk || !indicatorData.stoch_slowd) {
|
|
1155
|
+
document.querySelector("#stochChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">Stochastic data not available</p>';
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Filter valid data points
|
|
1160
|
+
const validDates = [];
|
|
1161
|
+
const validSlowK = [];
|
|
1162
|
+
const validSlowD = [];
|
|
1163
|
+
for (let i = 0; i < indicatorData.stoch_slowk.length; i++) {
|
|
1164
|
+
if (indicatorData.dates[i] !== null &&
|
|
1165
|
+
indicatorData.stoch_slowk[i] !== null &&
|
|
1166
|
+
indicatorData.stoch_slowd[i] !== null &&
|
|
1167
|
+
!isNaN(indicatorData.stoch_slowk[i]) &&
|
|
1168
|
+
!isNaN(indicatorData.stoch_slowd[i])) {
|
|
1169
|
+
validDates.push(indicatorData.dates[i]);
|
|
1170
|
+
validSlowK.push(indicatorData.stoch_slowk[i]);
|
|
1171
|
+
validSlowD.push(indicatorData.stoch_slowd[i]);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (validDates.length === 0) {
|
|
1176
|
+
document.querySelector("#stochChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid Stochastic data available</p>';
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Create reference lines
|
|
1181
|
+
const oversoldLine = validDates.map(() => 20);
|
|
1182
|
+
const overboughtLine = validDates.map(() => 80);
|
|
1183
|
+
|
|
1184
|
+
const options = {
|
|
1185
|
+
series: [
|
|
1186
|
+
{ name: '%K', data: validSlowK },
|
|
1187
|
+
{ name: '%D', data: validSlowD },
|
|
1188
|
+
{ name: 'Oversold (20)', data: oversoldLine },
|
|
1189
|
+
{ name: 'Overbought (80)', data: overboughtLine }
|
|
1190
|
+
],
|
|
1191
|
+
chart: {
|
|
1192
|
+
type: 'line',
|
|
1193
|
+
height: 350,
|
|
1194
|
+
group: 'stock-charts',
|
|
1195
|
+
background: '#151b3d',
|
|
1196
|
+
foreColor: '#e8eaf6',
|
|
1197
|
+
toolbar: { show: true, tools: { download: true, zoom: true, zoomin: true, zoomout: true, pan: true, reset: true } },
|
|
1198
|
+
zoom: { enabled: true, type: 'x' }
|
|
1199
|
+
},
|
|
1200
|
+
colors: ['#00d4ff', '#ffaa00', '#00ff88', '#ff3366'],
|
|
1201
|
+
stroke: { width: [3, 3, 2, 2], curve: 'smooth', dashArray: [0, 0, 5, 5] },
|
|
1202
|
+
xaxis: {
|
|
1203
|
+
type: 'category',
|
|
1204
|
+
categories: validDates,
|
|
1205
|
+
tickAmount: 10,
|
|
1206
|
+
labels: {
|
|
1207
|
+
rotate: -45,
|
|
1208
|
+
style: { colors: '#9fa8da' },
|
|
1209
|
+
formatter: (val) => val ? `${val.split('-')[1]}/${val.split('-')[2]}` : ''
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
yaxis: { min: 0, max: 100, labels: { style: { colors: '#9fa8da' } } },
|
|
1213
|
+
grid: { borderColor: '#2a3154', strokeDashArray: 3 },
|
|
1214
|
+
legend: { position: 'top', horizontalAlign: 'left', fontSize: '14px', labels: { colors: '#e8eaf6' } },
|
|
1215
|
+
theme: { mode: 'dark' },
|
|
1216
|
+
tooltip: { theme: 'dark', shared: true, intersect: false, x: { formatter: (val, opts) => validDates[opts.dataPointIndex] || val } }
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
stochChart = new ApexCharts(document.querySelector("#stochChart"), options);
|
|
1220
|
+
stochChart.render();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function renderCCIChart() {
|
|
1224
|
+
if (cciChart) {
|
|
1225
|
+
cciChart.destroy();
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (!indicatorData || !indicatorData.cci_14) {
|
|
1229
|
+
document.querySelector("#cciChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">CCI data not available</p>';
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Filter valid data points
|
|
1234
|
+
const validDates = [];
|
|
1235
|
+
const validCCI = [];
|
|
1236
|
+
for (let i = 0; i < indicatorData.cci_14.length; i++) {
|
|
1237
|
+
if (indicatorData.dates[i] !== null &&
|
|
1238
|
+
indicatorData.cci_14[i] !== null &&
|
|
1239
|
+
!isNaN(indicatorData.cci_14[i])) {
|
|
1240
|
+
validDates.push(indicatorData.dates[i]);
|
|
1241
|
+
validCCI.push(indicatorData.cci_14[i]);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (validDates.length === 0) {
|
|
1246
|
+
document.querySelector("#cciChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid CCI data available</p>';
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Create reference lines
|
|
1251
|
+
const upperLine = validDates.map(() => 100);
|
|
1252
|
+
const lowerLine = validDates.map(() => -100);
|
|
1253
|
+
const zeroLine = validDates.map(() => 0);
|
|
1254
|
+
|
|
1255
|
+
const options = {
|
|
1256
|
+
series: [
|
|
1257
|
+
{ name: 'CCI', data: validCCI },
|
|
1258
|
+
{ name: 'Overbought (+100)', data: upperLine },
|
|
1259
|
+
{ name: 'Zero', data: zeroLine },
|
|
1260
|
+
{ name: 'Oversold (-100)', data: lowerLine }
|
|
1261
|
+
],
|
|
1262
|
+
chart: {
|
|
1263
|
+
type: 'line',
|
|
1264
|
+
height: 350,
|
|
1265
|
+
group: 'stock-charts',
|
|
1266
|
+
background: '#151b3d',
|
|
1267
|
+
foreColor: '#e8eaf6',
|
|
1268
|
+
toolbar: { show: true, tools: { download: true, zoom: true, zoomin: true, zoomout: true, pan: true, reset: true } },
|
|
1269
|
+
zoom: { enabled: true, type: 'x' }
|
|
1270
|
+
},
|
|
1271
|
+
colors: ['#a55eea', '#ff3366', '#9fa8da', '#00ff88'],
|
|
1272
|
+
stroke: { width: [3, 2, 1, 2], curve: 'smooth', dashArray: [0, 5, 5, 5] },
|
|
1273
|
+
xaxis: {
|
|
1274
|
+
type: 'category',
|
|
1275
|
+
categories: validDates,
|
|
1276
|
+
tickAmount: 10,
|
|
1277
|
+
labels: {
|
|
1278
|
+
rotate: -45,
|
|
1279
|
+
style: { colors: '#9fa8da' },
|
|
1280
|
+
formatter: (val) => val ? `${val.split('-')[1]}/${val.split('-')[2]}` : ''
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
yaxis: { labels: { style: { colors: '#9fa8da' }, formatter: (val) => val ? val.toFixed(0) : '' } },
|
|
1284
|
+
grid: { borderColor: '#2a3154', strokeDashArray: 3 },
|
|
1285
|
+
legend: { position: 'top', horizontalAlign: 'left', fontSize: '14px', labels: { colors: '#e8eaf6' } },
|
|
1286
|
+
theme: { mode: 'dark' },
|
|
1287
|
+
tooltip: { theme: 'dark', shared: true, intersect: false, x: { formatter: (val, opts) => validDates[opts.dataPointIndex] || val } }
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
cciChart = new ApexCharts(document.querySelector("#cciChart"), options);
|
|
1291
|
+
cciChart.render();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function renderADXChart() {
|
|
1295
|
+
if (adxChart) {
|
|
1296
|
+
adxChart.destroy();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (!indicatorData || !indicatorData.adx_14) {
|
|
1300
|
+
document.querySelector("#adxChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">ADX data not available</p>';
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Filter valid data points
|
|
1305
|
+
const validDates = [];
|
|
1306
|
+
const validADX = [];
|
|
1307
|
+
for (let i = 0; i < indicatorData.adx_14.length; i++) {
|
|
1308
|
+
if (indicatorData.dates[i] !== null &&
|
|
1309
|
+
indicatorData.adx_14[i] !== null &&
|
|
1310
|
+
!isNaN(indicatorData.adx_14[i])) {
|
|
1311
|
+
validDates.push(indicatorData.dates[i]);
|
|
1312
|
+
validADX.push(indicatorData.adx_14[i]);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (validDates.length === 0) {
|
|
1317
|
+
document.querySelector("#adxChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid ADX data available</p>';
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Create threshold line (25 = strong trend)
|
|
1322
|
+
const thresholdLine = validDates.map(() => 25);
|
|
1323
|
+
|
|
1324
|
+
const options = {
|
|
1325
|
+
series: [
|
|
1326
|
+
{ name: 'ADX', data: validADX },
|
|
1327
|
+
{ name: 'Strong Trend (25)', data: thresholdLine }
|
|
1328
|
+
],
|
|
1329
|
+
chart: {
|
|
1330
|
+
type: 'line',
|
|
1331
|
+
height: 350,
|
|
1332
|
+
group: 'stock-charts',
|
|
1333
|
+
background: '#151b3d',
|
|
1334
|
+
foreColor: '#e8eaf6',
|
|
1335
|
+
toolbar: { show: true, tools: { download: true, zoom: true, zoomin: true, zoomout: true, pan: true, reset: true } },
|
|
1336
|
+
zoom: { enabled: true, type: 'x' }
|
|
1337
|
+
},
|
|
1338
|
+
colors: ['#fd9644', '#9fa8da'],
|
|
1339
|
+
stroke: { width: [3, 2], curve: 'smooth', dashArray: [0, 5] },
|
|
1340
|
+
xaxis: {
|
|
1341
|
+
type: 'category',
|
|
1342
|
+
categories: validDates,
|
|
1343
|
+
tickAmount: 10,
|
|
1344
|
+
labels: {
|
|
1345
|
+
rotate: -45,
|
|
1346
|
+
style: { colors: '#9fa8da' },
|
|
1347
|
+
formatter: (val) => val ? `${val.split('-')[1]}/${val.split('-')[2]}` : ''
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1350
|
+
yaxis: { min: 0, max: 100, labels: { style: { colors: '#9fa8da' } } },
|
|
1351
|
+
grid: { borderColor: '#2a3154', strokeDashArray: 3 },
|
|
1352
|
+
legend: { position: 'top', horizontalAlign: 'left', fontSize: '14px', labels: { colors: '#e8eaf6' } },
|
|
1353
|
+
theme: { mode: 'dark' },
|
|
1354
|
+
tooltip: { theme: 'dark', shared: true, intersect: false, x: { formatter: (val, opts) => validDates[opts.dataPointIndex] || val } }
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
adxChart = new ApexCharts(document.querySelector("#adxChart"), options);
|
|
1358
|
+
adxChart.render();
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function renderWillrChart() {
|
|
1362
|
+
if (willrChart) {
|
|
1363
|
+
willrChart.destroy();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (!indicatorData || !indicatorData.willr_14) {
|
|
1367
|
+
document.querySelector("#willrChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">Williams %R data not available</p>';
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const validDates = [];
|
|
1372
|
+
const validWillr = [];
|
|
1373
|
+
for (let i = 0; i < indicatorData.willr_14.length; i++) {
|
|
1374
|
+
if (indicatorData.dates[i] !== null &&
|
|
1375
|
+
indicatorData.willr_14[i] !== null &&
|
|
1376
|
+
!isNaN(indicatorData.willr_14[i])) {
|
|
1377
|
+
validDates.push(indicatorData.dates[i]);
|
|
1378
|
+
validWillr.push(indicatorData.willr_14[i]);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (validDates.length === 0) {
|
|
1383
|
+
document.querySelector("#willrChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid Williams %R data available</p>';
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Williams %R reference lines (note: scale is -100 to 0)
|
|
1388
|
+
const overboughtLine = validDates.map(() => -20);
|
|
1389
|
+
const oversoldLine = validDates.map(() => -80);
|
|
1390
|
+
|
|
1391
|
+
const options = {
|
|
1392
|
+
series: [
|
|
1393
|
+
{ name: 'Williams %R', data: validWillr },
|
|
1394
|
+
{ name: 'Overbought (-20)', data: overboughtLine },
|
|
1395
|
+
{ name: 'Oversold (-80)', data: oversoldLine }
|
|
1396
|
+
],
|
|
1397
|
+
chart: {
|
|
1398
|
+
type: 'line',
|
|
1399
|
+
height: 350,
|
|
1400
|
+
group: 'stock-charts',
|
|
1401
|
+
background: '#151b3d',
|
|
1402
|
+
foreColor: '#e8eaf6',
|
|
1403
|
+
toolbar: { show: true, tools: { download: true, zoom: true, zoomin: true, zoomout: true, pan: true, reset: true } },
|
|
1404
|
+
zoom: { enabled: true, type: 'x' }
|
|
1405
|
+
},
|
|
1406
|
+
colors: ['#e056fd', '#ff3366', '#00ff88'],
|
|
1407
|
+
stroke: { width: [3, 2, 2], curve: 'smooth', dashArray: [0, 5, 5] },
|
|
1408
|
+
xaxis: {
|
|
1409
|
+
type: 'category',
|
|
1410
|
+
categories: validDates,
|
|
1411
|
+
tickAmount: 10,
|
|
1412
|
+
labels: {
|
|
1413
|
+
rotate: -45,
|
|
1414
|
+
style: { colors: '#9fa8da' },
|
|
1415
|
+
formatter: (val) => val ? `${val.split('-')[1]}/${val.split('-')[2]}` : ''
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
yaxis: { min: -100, max: 0, reversed: true, labels: { style: { colors: '#9fa8da' } } },
|
|
1419
|
+
grid: { borderColor: '#2a3154', strokeDashArray: 3 },
|
|
1420
|
+
legend: { position: 'top', horizontalAlign: 'left', fontSize: '14px', labels: { colors: '#e8eaf6' } },
|
|
1421
|
+
theme: { mode: 'dark' },
|
|
1422
|
+
tooltip: { theme: 'dark', shared: true, intersect: false, x: { formatter: (val, opts) => validDates[opts.dataPointIndex] || val } }
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
willrChart = new ApexCharts(document.querySelector("#willrChart"), options);
|
|
1426
|
+
willrChart.render();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function renderATRChart() {
|
|
1430
|
+
if (atrChart) {
|
|
1431
|
+
atrChart.destroy();
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (!indicatorData || !indicatorData.atr_14) {
|
|
1435
|
+
document.querySelector("#atrChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">ATR data not available</p>';
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const validDates = [];
|
|
1440
|
+
const validATR = [];
|
|
1441
|
+
for (let i = 0; i < indicatorData.atr_14.length; i++) {
|
|
1442
|
+
if (indicatorData.dates[i] !== null &&
|
|
1443
|
+
indicatorData.atr_14[i] !== null &&
|
|
1444
|
+
!isNaN(indicatorData.atr_14[i])) {
|
|
1445
|
+
validDates.push(indicatorData.dates[i]);
|
|
1446
|
+
validATR.push(indicatorData.atr_14[i]);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (validDates.length === 0) {
|
|
1451
|
+
document.querySelector("#atrChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid ATR data available</p>';
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const options = {
|
|
1456
|
+
series: [{ name: 'ATR', data: validATR }],
|
|
1457
|
+
chart: {
|
|
1458
|
+
type: 'area',
|
|
1459
|
+
height: 350,
|
|
1460
|
+
group: 'stock-charts',
|
|
1461
|
+
background: '#151b3d',
|
|
1462
|
+
foreColor: '#e8eaf6',
|
|
1463
|
+
toolbar: { show: true, tools: { download: true, zoom: true, zoomin: true, zoomout: true, pan: true, reset: true } },
|
|
1464
|
+
zoom: { enabled: true, type: 'x' }
|
|
1465
|
+
},
|
|
1466
|
+
colors: ['#ff6b6b'],
|
|
1467
|
+
fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.5, opacityTo: 0.1, stops: [0, 100] } },
|
|
1468
|
+
stroke: { width: 3, curve: 'smooth' },
|
|
1469
|
+
xaxis: {
|
|
1470
|
+
type: 'category',
|
|
1471
|
+
categories: validDates,
|
|
1472
|
+
tickAmount: 10,
|
|
1473
|
+
labels: {
|
|
1474
|
+
rotate: -45,
|
|
1475
|
+
style: { colors: '#9fa8da' },
|
|
1476
|
+
formatter: (val) => val ? `${val.split('-')[1]}/${val.split('-')[2]}` : ''
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
yaxis: { labels: { style: { colors: '#9fa8da' }, formatter: (val) => val ? val.toFixed(2) : '' } },
|
|
1480
|
+
grid: { borderColor: '#2a3154', strokeDashArray: 3 },
|
|
1481
|
+
legend: { position: 'top', horizontalAlign: 'left', fontSize: '14px', labels: { colors: '#e8eaf6' } },
|
|
1482
|
+
theme: { mode: 'dark' },
|
|
1483
|
+
tooltip: { theme: 'dark', shared: true, intersect: false, x: { formatter: (val, opts) => validDates[opts.dataPointIndex] || val } }
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
atrChart = new ApexCharts(document.querySelector("#atrChart"), options);
|
|
1487
|
+
atrChart.render();
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function renderROCChart() {
|
|
1491
|
+
if (rocChart) {
|
|
1492
|
+
rocChart.destroy();
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (!indicatorData || !indicatorData.roc_10) {
|
|
1496
|
+
document.querySelector("#rocChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">ROC data not available</p>';
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const validDates = [];
|
|
1501
|
+
const validROC = [];
|
|
1502
|
+
for (let i = 0; i < indicatorData.roc_10.length; i++) {
|
|
1503
|
+
if (indicatorData.dates[i] !== null &&
|
|
1504
|
+
indicatorData.roc_10[i] !== null &&
|
|
1505
|
+
!isNaN(indicatorData.roc_10[i])) {
|
|
1506
|
+
validDates.push(indicatorData.dates[i]);
|
|
1507
|
+
validROC.push(indicatorData.roc_10[i]);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (validDates.length === 0) {
|
|
1512
|
+
document.querySelector("#rocChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid ROC data available</p>';
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Zero line for reference
|
|
1517
|
+
const zeroLine = validDates.map(() => 0);
|
|
1518
|
+
|
|
1519
|
+
const options = {
|
|
1520
|
+
series: [
|
|
1521
|
+
{ name: 'ROC', data: validROC },
|
|
1522
|
+
{ name: 'Zero', data: zeroLine }
|
|
1523
|
+
],
|
|
1524
|
+
chart: {
|
|
1525
|
+
type: 'line',
|
|
1526
|
+
height: 350,
|
|
1527
|
+
group: 'stock-charts',
|
|
1528
|
+
background: '#151b3d',
|
|
1529
|
+
foreColor: '#e8eaf6',
|
|
1530
|
+
toolbar: { show: true, tools: { download: true, zoom: true, zoomin: true, zoomout: true, pan: true, reset: true } },
|
|
1531
|
+
zoom: { enabled: true, type: 'x' }
|
|
1532
|
+
},
|
|
1533
|
+
colors: ['#20bf6b', '#9fa8da'],
|
|
1534
|
+
stroke: { width: [3, 1], curve: 'smooth', dashArray: [0, 5] },
|
|
1535
|
+
xaxis: {
|
|
1536
|
+
type: 'category',
|
|
1537
|
+
categories: validDates,
|
|
1538
|
+
tickAmount: 10,
|
|
1539
|
+
labels: {
|
|
1540
|
+
rotate: -45,
|
|
1541
|
+
style: { colors: '#9fa8da' },
|
|
1542
|
+
formatter: (val) => val ? `${val.split('-')[1]}/${val.split('-')[2]}` : ''
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
yaxis: { labels: { style: { colors: '#9fa8da' }, formatter: (val) => val ? val.toFixed(2) + '%' : '' } },
|
|
1546
|
+
grid: { borderColor: '#2a3154', strokeDashArray: 3 },
|
|
1547
|
+
legend: { position: 'top', horizontalAlign: 'left', fontSize: '14px', labels: { colors: '#e8eaf6' } },
|
|
1548
|
+
theme: { mode: 'dark' },
|
|
1549
|
+
tooltip: { theme: 'dark', shared: true, intersect: false, x: { formatter: (val, opts) => validDates[opts.dataPointIndex] || val } }
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
rocChart = new ApexCharts(document.querySelector("#rocChart"), options);
|
|
1553
|
+
rocChart.render();
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function renderMomentumChart() {
|
|
1557
|
+
if (momChart) {
|
|
1558
|
+
momChart.destroy();
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (!indicatorData || !indicatorData.mom_10) {
|
|
1562
|
+
document.querySelector("#momChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">Momentum data not available</p>';
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const validDates = [];
|
|
1567
|
+
const validMom = [];
|
|
1568
|
+
for (let i = 0; i < indicatorData.mom_10.length; i++) {
|
|
1569
|
+
if (indicatorData.dates[i] !== null &&
|
|
1570
|
+
indicatorData.mom_10[i] !== null &&
|
|
1571
|
+
!isNaN(indicatorData.mom_10[i])) {
|
|
1572
|
+
validDates.push(indicatorData.dates[i]);
|
|
1573
|
+
validMom.push(indicatorData.mom_10[i]);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
if (validDates.length === 0) {
|
|
1578
|
+
document.querySelector("#momChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid Momentum data available</p>';
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Zero line for reference
|
|
1583
|
+
const zeroLine = validDates.map(() => 0);
|
|
1584
|
+
|
|
1585
|
+
const options = {
|
|
1586
|
+
series: [
|
|
1587
|
+
{ name: 'Momentum', data: validMom },
|
|
1588
|
+
{ name: 'Zero', data: zeroLine }
|
|
1589
|
+
],
|
|
1590
|
+
chart: {
|
|
1591
|
+
type: 'line',
|
|
1592
|
+
height: 350,
|
|
1593
|
+
group: 'stock-charts',
|
|
1594
|
+
background: '#151b3d',
|
|
1595
|
+
foreColor: '#e8eaf6',
|
|
1596
|
+
toolbar: { show: true, tools: { download: true, zoom: true, zoomin: true, zoomout: true, pan: true, reset: true } },
|
|
1597
|
+
zoom: { enabled: true, type: 'x' }
|
|
1598
|
+
},
|
|
1599
|
+
colors: ['#f7b731', '#9fa8da'],
|
|
1600
|
+
stroke: { width: [3, 1], curve: 'smooth', dashArray: [0, 5] },
|
|
1601
|
+
xaxis: {
|
|
1602
|
+
type: 'category',
|
|
1603
|
+
categories: validDates,
|
|
1604
|
+
tickAmount: 10,
|
|
1605
|
+
labels: {
|
|
1606
|
+
rotate: -45,
|
|
1607
|
+
style: { colors: '#9fa8da' },
|
|
1608
|
+
formatter: (val) => val ? `${val.split('-')[1]}/${val.split('-')[2]}` : ''
|
|
1609
|
+
}
|
|
1610
|
+
},
|
|
1611
|
+
yaxis: { labels: { style: { colors: '#9fa8da' }, formatter: (val) => val ? '$' + val.toFixed(2) : '' } },
|
|
1612
|
+
grid: { borderColor: '#2a3154', strokeDashArray: 3 },
|
|
1613
|
+
legend: { position: 'top', horizontalAlign: 'left', fontSize: '14px', labels: { colors: '#e8eaf6' } },
|
|
1614
|
+
theme: { mode: 'dark' },
|
|
1615
|
+
tooltip: { theme: 'dark', shared: true, intersect: false, x: { formatter: (val, opts) => validDates[opts.dataPointIndex] || val } }
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
momChart = new ApexCharts(document.querySelector("#momChart"), options);
|
|
1619
|
+
momChart.render();
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function renderPatternAlerts() {
|
|
1623
|
+
const container = document.getElementById('patternAlerts');
|
|
1624
|
+
|
|
1625
|
+
if (!indicatorData || !indicatorData.patterns || indicatorData.patterns.length === 0) {
|
|
1626
|
+
container.innerHTML = '<p class="no-alerts"><i class="fas fa-check-circle"></i> No patterns detected in the selected period</p>';
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
let html = '';
|
|
1631
|
+
indicatorData.patterns.forEach(pattern => {
|
|
1632
|
+
html += `
|
|
1633
|
+
<div class="alert-item ${pattern.signal}">
|
|
1634
|
+
<div class="alert-info">
|
|
1635
|
+
<span class="alert-pattern">${pattern.pattern}</span>
|
|
1636
|
+
<span class="alert-date">${pattern.date}</span>
|
|
1637
|
+
</div>
|
|
1638
|
+
<span class="alert-signal ${pattern.signal}">${pattern.signal}</span>
|
|
1639
|
+
</div>
|
|
1640
|
+
`;
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
container.innerHTML = html;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function renderSignalAlerts() {
|
|
1647
|
+
const container = document.getElementById('signalAlerts');
|
|
1648
|
+
|
|
1649
|
+
if (!indicatorData) {
|
|
1650
|
+
container.innerHTML = '<p class="no-alerts">Loading...</p>';
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const signals = [];
|
|
1655
|
+
|
|
1656
|
+
// Get latest valid RSI value
|
|
1657
|
+
const latestRsi = indicatorData.rsi?.filter(v => v !== null).slice(-1)[0];
|
|
1658
|
+
if (latestRsi !== undefined) {
|
|
1659
|
+
if (latestRsi < 30) {
|
|
1660
|
+
signals.push({ indicator: 'RSI', value: latestRsi.toFixed(1), signal: 'buy', message: 'Oversold territory' });
|
|
1661
|
+
} else if (latestRsi > 70) {
|
|
1662
|
+
signals.push({ indicator: 'RSI', value: latestRsi.toFixed(1), signal: 'sell', message: 'Overbought territory' });
|
|
1663
|
+
} else {
|
|
1664
|
+
signals.push({ indicator: 'RSI', value: latestRsi.toFixed(1), signal: 'hold', message: 'Neutral zone' });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Get latest Stochastic values
|
|
1669
|
+
const latestSlowK = indicatorData.stoch_slowk?.filter(v => v !== null).slice(-1)[0];
|
|
1670
|
+
const latestSlowD = indicatorData.stoch_slowd?.filter(v => v !== null).slice(-1)[0];
|
|
1671
|
+
if (latestSlowK !== undefined && latestSlowD !== undefined) {
|
|
1672
|
+
if (latestSlowK < 20 && latestSlowD < 20) {
|
|
1673
|
+
signals.push({ indicator: 'Stochastic', value: `${latestSlowK.toFixed(0)}/${latestSlowD.toFixed(0)}`, signal: 'buy', message: 'Oversold' });
|
|
1674
|
+
} else if (latestSlowK > 80 && latestSlowD > 80) {
|
|
1675
|
+
signals.push({ indicator: 'Stochastic', value: `${latestSlowK.toFixed(0)}/${latestSlowD.toFixed(0)}`, signal: 'sell', message: 'Overbought' });
|
|
1676
|
+
} else if (latestSlowK > latestSlowD && latestSlowK < 50) {
|
|
1677
|
+
signals.push({ indicator: 'Stochastic', value: `${latestSlowK.toFixed(0)}/${latestSlowD.toFixed(0)}`, signal: 'buy', message: '%K crossed above %D' });
|
|
1678
|
+
} else if (latestSlowK < latestSlowD && latestSlowK > 50) {
|
|
1679
|
+
signals.push({ indicator: 'Stochastic', value: `${latestSlowK.toFixed(0)}/${latestSlowD.toFixed(0)}`, signal: 'sell', message: '%K crossed below %D' });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Get latest CCI value
|
|
1684
|
+
const latestCci = indicatorData.cci_14?.filter(v => v !== null).slice(-1)[0];
|
|
1685
|
+
if (latestCci !== undefined) {
|
|
1686
|
+
if (latestCci < -100) {
|
|
1687
|
+
signals.push({ indicator: 'CCI', value: latestCci.toFixed(0), signal: 'buy', message: 'Oversold' });
|
|
1688
|
+
} else if (latestCci > 100) {
|
|
1689
|
+
signals.push({ indicator: 'CCI', value: latestCci.toFixed(0), signal: 'sell', message: 'Overbought' });
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Get latest Williams %R
|
|
1694
|
+
const latestWillr = indicatorData.willr_14?.filter(v => v !== null).slice(-1)[0];
|
|
1695
|
+
if (latestWillr !== undefined) {
|
|
1696
|
+
if (latestWillr < -80) {
|
|
1697
|
+
signals.push({ indicator: 'Williams %R', value: latestWillr.toFixed(0), signal: 'buy', message: 'Oversold' });
|
|
1698
|
+
} else if (latestWillr > -20) {
|
|
1699
|
+
signals.push({ indicator: 'Williams %R', value: latestWillr.toFixed(0), signal: 'sell', message: 'Overbought' });
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Get latest ADX (trend strength)
|
|
1704
|
+
const latestAdx = indicatorData.adx_14?.filter(v => v !== null).slice(-1)[0];
|
|
1705
|
+
if (latestAdx !== undefined) {
|
|
1706
|
+
if (latestAdx > 25) {
|
|
1707
|
+
signals.push({ indicator: 'ADX', value: latestAdx.toFixed(0), signal: 'hold', message: 'Strong trend' });
|
|
1708
|
+
} else {
|
|
1709
|
+
signals.push({ indicator: 'ADX', value: latestAdx.toFixed(0), signal: 'hold', message: 'Weak/No trend' });
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// MACD crossover
|
|
1714
|
+
const macdValues = indicatorData.macd?.filter(v => v !== null);
|
|
1715
|
+
const signalValues = indicatorData.macd_signal?.filter(v => v !== null);
|
|
1716
|
+
if (macdValues?.length >= 2 && signalValues?.length >= 2) {
|
|
1717
|
+
const prevMacd = macdValues[macdValues.length - 2];
|
|
1718
|
+
const currMacd = macdValues[macdValues.length - 1];
|
|
1719
|
+
const prevSignal = signalValues[signalValues.length - 2];
|
|
1720
|
+
const currSignal = signalValues[signalValues.length - 1];
|
|
1721
|
+
|
|
1722
|
+
if (prevMacd < prevSignal && currMacd > currSignal) {
|
|
1723
|
+
signals.push({ indicator: 'MACD', value: currMacd.toFixed(2), signal: 'buy', message: 'Bullish crossover' });
|
|
1724
|
+
} else if (prevMacd > prevSignal && currMacd < currSignal) {
|
|
1725
|
+
signals.push({ indicator: 'MACD', value: currMacd.toFixed(2), signal: 'sell', message: 'Bearish crossover' });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (signals.length === 0) {
|
|
1730
|
+
container.innerHTML = '<p class="no-alerts"><i class="fas fa-check-circle"></i> No significant signals detected</p>';
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
let html = '';
|
|
1735
|
+
signals.forEach(sig => {
|
|
1736
|
+
html += `
|
|
1737
|
+
<div class="alert-item ${sig.signal === 'buy' ? 'bullish' : sig.signal === 'sell' ? 'bearish' : 'neutral'}">
|
|
1738
|
+
<div class="alert-info">
|
|
1739
|
+
<span class="alert-pattern">${sig.indicator}: ${sig.value}</span>
|
|
1740
|
+
<span class="alert-date">${sig.message}</span>
|
|
1741
|
+
</div>
|
|
1742
|
+
<span class="alert-signal ${sig.signal}">${sig.signal}</span>
|
|
1743
|
+
</div>
|
|
1744
|
+
`;
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
container.innerHTML = html;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function updateChartType(type) {
|
|
1751
|
+
currentChartType = type;
|
|
1752
|
+
|
|
1753
|
+
// Update button states
|
|
1754
|
+
document.querySelectorAll('[data-chart]').forEach(btn => {
|
|
1755
|
+
btn.classList.remove('active');
|
|
1756
|
+
});
|
|
1757
|
+
document.querySelector(`[data-chart="${type}"]`).classList.add('active');
|
|
1758
|
+
|
|
1759
|
+
// Show/hide indicator selector based on chart type
|
|
1760
|
+
const indicatorSelector = document.getElementById('indicatorSelector');
|
|
1761
|
+
indicatorSelector.style.display = type === 'line' ? 'flex' : 'none';
|
|
1762
|
+
|
|
1763
|
+
// Re-render chart
|
|
1764
|
+
renderPriceChart();
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function toggleIndicatorDropdown() {
|
|
1768
|
+
const menu = document.getElementById('indicatorDropdownMenu');
|
|
1769
|
+
menu.classList.toggle('show');
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function updateSelectedIndicators() {
|
|
1773
|
+
const checkboxes = document.querySelectorAll('#indicatorDropdownMenu input[type="checkbox"]');
|
|
1774
|
+
selectedIndicators = [];
|
|
1775
|
+
|
|
1776
|
+
checkboxes.forEach(cb => {
|
|
1777
|
+
if (cb.checked) {
|
|
1778
|
+
selectedIndicators.push(cb.value);
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// Update the count display
|
|
1783
|
+
const countEl = document.getElementById('selectedIndicatorCount');
|
|
1784
|
+
if (selectedIndicators.length === 0) {
|
|
1785
|
+
countEl.textContent = 'None';
|
|
1786
|
+
} else if (selectedIndicators.length === 1) {
|
|
1787
|
+
countEl.textContent = INDICATOR_CONFIG[selectedIndicators[0]].name;
|
|
1788
|
+
} else {
|
|
1789
|
+
countEl.textContent = `${selectedIndicators.length} selected`;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Re-render the chart if in line mode
|
|
1793
|
+
if (currentChartType === 'line') {
|
|
1794
|
+
renderPriceChart();
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function toggleVolumeIndicatorDropdown() {
|
|
1799
|
+
const menu = document.getElementById('volumeIndicatorDropdownMenu');
|
|
1800
|
+
menu.classList.toggle('show');
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function updateSelectedVolumeIndicators() {
|
|
1804
|
+
const checkboxes = document.querySelectorAll('#volumeIndicatorDropdownMenu input[type="checkbox"]');
|
|
1805
|
+
selectedVolumeIndicators = [];
|
|
1806
|
+
|
|
1807
|
+
checkboxes.forEach(cb => {
|
|
1808
|
+
if (cb.checked) {
|
|
1809
|
+
selectedVolumeIndicators.push(cb.value);
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
// Update the count display
|
|
1814
|
+
const countEl = document.getElementById('selectedVolumeIndicatorCount');
|
|
1815
|
+
if (selectedVolumeIndicators.length === 0) {
|
|
1816
|
+
countEl.textContent = 'None';
|
|
1817
|
+
} else if (selectedVolumeIndicators.length === 1) {
|
|
1818
|
+
countEl.textContent = VOLUME_INDICATOR_CONFIG[selectedVolumeIndicators[0]].name;
|
|
1819
|
+
} else {
|
|
1820
|
+
countEl.textContent = `${selectedVolumeIndicators.length} selected`;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Re-render the volume chart
|
|
1824
|
+
renderVolumeChart();
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Close dropdowns when clicking outside
|
|
1828
|
+
document.addEventListener('click', function(event) {
|
|
1829
|
+
const priceDropdown = document.querySelector('#indicatorSelector .indicator-dropdown');
|
|
1830
|
+
if (priceDropdown && !priceDropdown.contains(event.target)) {
|
|
1831
|
+
document.getElementById('indicatorDropdownMenu').classList.remove('show');
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
const volumeDropdown = document.querySelector('#volumeIndicatorDropdownMenu')?.closest('.indicator-dropdown');
|
|
1835
|
+
if (volumeDropdown && !volumeDropdown.contains(event.target)) {
|
|
1836
|
+
document.getElementById('volumeIndicatorDropdownMenu').classList.remove('show');
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
async function runStrategyComparison() {
|
|
1841
|
+
const resultsDiv = document.getElementById('strategyResults');
|
|
1842
|
+
resultsDiv.innerHTML = '<p class="loading"><i class="fas fa-spinner fa-spin"></i> Running backtests...</p>';
|
|
1843
|
+
|
|
1844
|
+
try {
|
|
1845
|
+
const response = await fetch(`/api/compare/${ticker}`, { method: 'POST' });
|
|
1846
|
+
const results = await response.json();
|
|
1847
|
+
|
|
1848
|
+
let html = '<table class="results-table"><thead><tr>';
|
|
1849
|
+
html += '<th>Strategy</th><th>Return</th><th>Sharpe</th><th>Drawdown</th><th>Win Rate</th><th>Trades</th>';
|
|
1850
|
+
html += '</tr></thead><tbody>';
|
|
1851
|
+
|
|
1852
|
+
results.forEach((result, i) => {
|
|
1853
|
+
const returnClass = result.return >= 0 ? 'positive' : 'negative';
|
|
1854
|
+
const rank = i === 0 ? '<i class="fas fa-trophy" style="color: #FFD700;"></i> ' : '';
|
|
1855
|
+
html += `<tr>
|
|
1856
|
+
<td>${rank}${result.strategy}</td>
|
|
1857
|
+
<td class="${returnClass}">${result.return.toFixed(2)}%</td>
|
|
1858
|
+
<td>${result.sharpe.toFixed(2)}</td>
|
|
1859
|
+
<td>${result.drawdown.toFixed(2)}%</td>
|
|
1860
|
+
<td>${result.win_rate.toFixed(2)}%</td>
|
|
1861
|
+
<td>${result.trades}</td>
|
|
1862
|
+
</tr>`;
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
html += '</tbody></table>';
|
|
1866
|
+
resultsDiv.innerHTML = html;
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
console.error('Error comparing strategies:', error);
|
|
1869
|
+
resultsDiv.innerHTML = '<p class="error">Failed to compare strategies. Please try again.</p>';
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
async function updatePeriod(period) {
|
|
1874
|
+
currentPeriod = period;
|
|
1875
|
+
|
|
1876
|
+
// Update button states
|
|
1877
|
+
document.querySelectorAll('[data-period]').forEach(btn => {
|
|
1878
|
+
btn.classList.remove('active');
|
|
1879
|
+
});
|
|
1880
|
+
document.querySelector(`[data-period="${period}"]`).classList.add('active');
|
|
1881
|
+
|
|
1882
|
+
// Reload data with new period
|
|
1883
|
+
await loadStockData();
|
|
1884
|
+
await loadIndicators();
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function refreshData() {
|
|
1888
|
+
location.reload();
|
|
1889
|
+
}
|
|
1890
|
+
</script>
|