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.
@@ -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>