sqa 0.0.31 → 0.0.32

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/CLAUDE.md +21 -0
  4. data/README.md +56 -32
  5. data/docs/api/dataframe.md +0 -1
  6. data/docs/assets/images/sqa.jpg +0 -0
  7. data/docs/concepts/index.md +2 -10
  8. data/docs/data_frame.md +0 -1
  9. data/docs/getting-started/index.md +0 -16
  10. data/docs/getting-started/installation.md +2 -2
  11. data/docs/getting-started/quick-start.md +4 -4
  12. data/docs/index.md +0 -2
  13. data/docs/strategies/bollinger-bands.md +1 -1
  14. data/docs/strategies/rsi.md +1 -1
  15. data/examples/sinatra_app/Gemfile +20 -0
  16. data/examples/sinatra_app/Gemfile.lock +268 -0
  17. data/examples/sinatra_app/QUICKSTART.md +13 -3
  18. data/examples/sinatra_app/README.md +12 -2
  19. data/examples/sinatra_app/RUNNING_WITHOUT_TALIB.md +90 -0
  20. data/examples/sinatra_app/TROUBLESHOOTING.md +95 -0
  21. data/examples/sinatra_app/app.rb +85 -25
  22. data/examples/sinatra_app/public/css/style.css +101 -37
  23. data/examples/sinatra_app/public/debug_macd.html +82 -0
  24. data/examples/sinatra_app/start.sh +53 -0
  25. data/examples/sinatra_app/views/dashboard.erb +558 -146
  26. data/examples/sinatra_app/views/layout.erb +2 -2
  27. data/lib/sqa/data_frame/alpha_vantage.rb +13 -3
  28. data/lib/sqa/data_frame.rb +21 -15
  29. data/lib/sqa/indicator.rb +17 -4
  30. data/lib/sqa/stock.rb +73 -11
  31. data/lib/sqa/ticker.rb +9 -2
  32. data/lib/sqa/version.rb +1 -1
  33. data/lib/sqa.rb +12 -4
  34. data/mkdocs.yml +4 -40
  35. metadata +7 -21
  36. data/docs/alpha_vantage_technical_indicators.md +0 -62
  37. data/docs/average_true_range.md +0 -9
  38. data/docs/bollinger_bands.md +0 -15
  39. data/docs/candlestick_pattern_recognizer.md +0 -4
  40. data/docs/donchian_channel.md +0 -5
  41. data/docs/double_top_bottom_pattern.md +0 -3
  42. data/docs/exponential_moving_average.md +0 -19
  43. data/docs/fibonacci_retracement.md +0 -30
  44. data/docs/head_and_shoulders_pattern.md +0 -3
  45. data/docs/market_profile.md +0 -4
  46. data/docs/momentum.md +0 -19
  47. data/docs/moving_average_convergence_divergence.md +0 -23
  48. data/docs/peaks_and_valleys.md +0 -11
  49. data/docs/relative_strength_index.md +0 -6
  50. data/docs/simple_moving_average.md +0 -8
  51. data/docs/stochastic_oscillator.md +0 -4
  52. data/docs/ta_lib.md +0 -160
  53. data/docs/true_range.md +0 -12
  54. data/docs/true_strength_index.md +0 -46
  55. data/docs/weighted_moving_average.md +0 -48
@@ -42,6 +42,21 @@
42
42
  </div>
43
43
  </div>
44
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
+
45
60
  <!-- Main Price Chart -->
46
61
  <div class="chart-container">
47
62
  <div class="chart-header">
@@ -104,6 +119,13 @@ const ticker = '<%= @ticker %>';
104
119
  let stockData = null;
105
120
  let indicatorData = null;
106
121
  let currentChartType = 'candlestick';
122
+ let currentPeriod = '90d'; // Default to 90 days to avoid performance issues
123
+
124
+ // ApexCharts instances
125
+ let priceChart = null;
126
+ let volumeChart = null;
127
+ let rsiChart = null;
128
+ let macdChart = null;
107
129
 
108
130
  // Load data when page loads
109
131
  document.addEventListener('DOMContentLoaded', async function() {
@@ -114,7 +136,7 @@ document.addEventListener('DOMContentLoaded', async function() {
114
136
 
115
137
  async function loadStockData() {
116
138
  try {
117
- const response = await fetch(`/api/stock/${ticker}`);
139
+ const response = await fetch(`/api/stock/${ticker}?period=${currentPeriod}`);
118
140
  stockData = await response.json();
119
141
 
120
142
  // Update price info
@@ -141,8 +163,19 @@ async function loadStockData() {
141
163
 
142
164
  async function loadIndicators() {
143
165
  try {
144
- const response = await fetch(`/api/indicators/${ticker}`);
145
- indicatorData = await response.json();
166
+ const response = await fetch(`/api/indicators/${ticker}?period=${currentPeriod}`);
167
+ const data = await response.json();
168
+
169
+ // Check if API returned an error
170
+ if (data.error) {
171
+ console.warn('Indicators not available:', data.error);
172
+ document.getElementById('currentRSI').textContent = 'N/A';
173
+ document.getElementById('rsiSignal').textContent = 'TA-Lib not installed';
174
+ document.getElementById('rsiSignal').className = 'metric-signal';
175
+ return;
176
+ }
177
+
178
+ indicatorData = data;
146
179
 
147
180
  // Update RSI metric
148
181
  const currentRSI = indicatorData.rsi[indicatorData.rsi.length - 1];
@@ -169,6 +202,8 @@ async function loadIndicators() {
169
202
  renderMACDChart();
170
203
  } catch (error) {
171
204
  console.error('Error loading indicators:', error);
205
+ document.getElementById('currentRSI').textContent = 'Error';
206
+ document.getElementById('rsiSignal').textContent = 'Failed to load';
172
207
  }
173
208
  }
174
209
 
@@ -189,7 +224,10 @@ async function loadAnalysis() {
189
224
  regimeEl.className = `metric-value ${regimeClass}`;
190
225
 
191
226
  const regimeDetail = document.getElementById('regimeDetail');
192
- regimeDetail.textContent = `${analysis.regime.volatility} volatility, ${analysis.regime.strength.toFixed(2)} strength`;
227
+ const strengthText = typeof analysis.regime.strength === 'number'
228
+ ? analysis.regime.strength.toFixed(2)
229
+ : analysis.regime.strength || 'N/A';
230
+ regimeDetail.textContent = `${analysis.regime.volatility || 'unknown'} volatility, ${strengthText} strength`;
193
231
  regimeDetail.className = 'metric-signal';
194
232
  } catch (error) {
195
233
  console.error('Error loading analysis:', error);
@@ -197,174 +235,534 @@ async function loadAnalysis() {
197
235
  }
198
236
 
199
237
  function renderPriceChart() {
200
- const trace = currentChartType === 'candlestick' ? {
201
- type: 'candlestick',
202
- x: stockData.dates,
203
- open: stockData.open,
204
- high: stockData.high,
205
- low: stockData.low,
206
- close: stockData.close,
207
- name: ticker,
208
- increasing: { line: { color: '#26a69a' } },
209
- decreasing: { line: { color: '#ef5350' } }
210
- } : {
211
- type: 'scatter',
212
- mode: 'lines',
213
- x: stockData.dates,
214
- y: stockData.close,
215
- name: ticker,
216
- line: { color: '#2196F3', width: 2 }
217
- };
218
-
219
- // Add moving averages if we have indicator data
220
- const traces = [trace];
221
- if (indicatorData) {
222
- traces.push({
223
- type: 'scatter',
224
- mode: 'lines',
225
- x: indicatorData.dates,
226
- y: indicatorData.sma_20,
227
- name: 'SMA 20',
228
- line: { color: '#FF9800', width: 1, dash: 'dash' }
229
- });
230
- traces.push({
231
- type: 'scatter',
232
- mode: 'lines',
233
- x: indicatorData.dates,
234
- y: indicatorData.sma_50,
235
- name: 'SMA 50',
236
- line: { color: '#9C27B0', width: 1, dash: 'dash' }
237
- });
238
+ // Destroy existing chart if it exists
239
+ if (priceChart) {
240
+ priceChart.destroy();
238
241
  }
239
242
 
240
- const layout = {
241
- title: '',
242
- xaxis: { title: 'Date', rangeslider: { visible: false } },
243
- yaxis: { title: 'Price ($)' },
244
- plot_bgcolor: '#ffffff',
245
- paper_bgcolor: '#ffffff',
246
- margin: { l: 50, r: 50, t: 20, b: 50 },
247
- height: 400,
248
- showlegend: true,
249
- legend: { x: 0, y: 1 }
250
- };
243
+ let options;
244
+
245
+ if (currentChartType === 'candlestick') {
246
+ // Prepare candlestick data
247
+ const candleData = stockData.dates.map((date, i) => ({
248
+ x: new Date(date),
249
+ y: [stockData.open[i], stockData.high[i], stockData.low[i], stockData.close[i]]
250
+ }));
251
+
252
+ options = {
253
+ series: [{
254
+ name: ticker,
255
+ data: candleData
256
+ }],
257
+ chart: {
258
+ type: 'candlestick',
259
+ height: 500,
260
+ group: 'stock-charts',
261
+ background: '#151b3d',
262
+ foreColor: '#e8eaf6',
263
+ toolbar: {
264
+ show: true,
265
+ tools: {
266
+ download: true,
267
+ zoom: true,
268
+ zoomin: true,
269
+ zoomout: true,
270
+ pan: true,
271
+ reset: true
272
+ }
273
+ },
274
+ zoom: {
275
+ enabled: true,
276
+ type: 'x'
277
+ }
278
+ },
279
+ dataLabels: {
280
+ enabled: false // Disable data labels for performance
281
+ },
282
+ plotOptions: {
283
+ candlestick: {
284
+ colors: {
285
+ upward: '#00ff88',
286
+ downward: '#ff3366'
287
+ }
288
+ }
289
+ },
290
+ xaxis: {
291
+ type: 'datetime',
292
+ labels: {
293
+ style: {
294
+ colors: '#9fa8da'
295
+ }
296
+ }
297
+ },
298
+ yaxis: {
299
+ tooltip: {
300
+ enabled: true
301
+ },
302
+ labels: {
303
+ style: {
304
+ colors: '#9fa8da'
305
+ },
306
+ formatter: (val) => '$' + val.toFixed(2)
307
+ }
308
+ },
309
+ grid: {
310
+ borderColor: '#2a3154',
311
+ strokeDashArray: 3
312
+ },
313
+ theme: {
314
+ mode: 'dark'
315
+ },
316
+ tooltip: {
317
+ theme: 'dark'
318
+ }
319
+ };
320
+ } else {
321
+ // Line chart with moving averages
322
+ const series = [{
323
+ name: ticker,
324
+ data: stockData.dates.map((date, i) => [new Date(date).getTime(), stockData.close[i]])
325
+ }];
326
+
327
+ // Add moving averages if available
328
+ if (indicatorData) {
329
+ series.push({
330
+ name: 'SMA 20',
331
+ data: indicatorData.dates.map((date, i) => [new Date(date).getTime(), indicatorData.sma_20[i]])
332
+ });
333
+ series.push({
334
+ name: 'SMA 50',
335
+ data: indicatorData.dates.map((date, i) => [new Date(date).getTime(), indicatorData.sma_50[i]])
336
+ });
337
+ }
251
338
 
252
- Plotly.newPlot('priceChart', traces, layout, { responsive: true });
339
+ options = {
340
+ series: series,
341
+ chart: {
342
+ type: 'line',
343
+ height: 500,
344
+ group: 'stock-charts',
345
+ background: '#151b3d',
346
+ foreColor: '#e8eaf6',
347
+ toolbar: {
348
+ show: true,
349
+ tools: {
350
+ download: true,
351
+ zoom: true,
352
+ zoomin: true,
353
+ zoomout: true,
354
+ pan: true,
355
+ reset: true
356
+ }
357
+ },
358
+ zoom: {
359
+ enabled: true,
360
+ type: 'x'
361
+ }
362
+ },
363
+ colors: ['#00d4ff', '#ffaa00', '#ff66ff'],
364
+ stroke: {
365
+ width: [3, 2, 2],
366
+ curve: 'smooth'
367
+ },
368
+ xaxis: {
369
+ type: 'datetime',
370
+ labels: {
371
+ style: {
372
+ colors: '#9fa8da'
373
+ }
374
+ }
375
+ },
376
+ yaxis: {
377
+ labels: {
378
+ style: {
379
+ colors: '#9fa8da'
380
+ },
381
+ formatter: (val) => '$' + val.toFixed(2)
382
+ }
383
+ },
384
+ grid: {
385
+ borderColor: '#2a3154',
386
+ strokeDashArray: 3
387
+ },
388
+ legend: {
389
+ position: 'top',
390
+ horizontalAlign: 'left',
391
+ fontSize: '14px',
392
+ labels: {
393
+ colors: '#e8eaf6'
394
+ }
395
+ },
396
+ theme: {
397
+ mode: 'dark'
398
+ },
399
+ tooltip: {
400
+ theme: 'dark',
401
+ shared: true,
402
+ intersect: false
403
+ }
404
+ };
405
+ }
406
+
407
+ priceChart = new ApexCharts(document.querySelector("#priceChart"), options);
408
+ priceChart.render();
253
409
  }
254
410
 
255
411
  function renderVolumeChart() {
256
- const colors = stockData.close.map((close, i) => {
257
- if (i === 0) return '#2196F3';
258
- return close >= stockData.close[i - 1] ? '#26a69a' : '#ef5350';
259
- });
260
-
261
- const trace = {
262
- type: 'bar',
263
- x: stockData.dates,
264
- y: stockData.volume,
265
- name: 'Volume',
266
- marker: { color: colors }
267
- };
412
+ // Destroy existing chart if it exists
413
+ if (volumeChart) {
414
+ volumeChart.destroy();
415
+ }
268
416
 
269
- const layout = {
270
- title: '',
271
- xaxis: { title: 'Date' },
272
- yaxis: { title: 'Volume' },
273
- plot_bgcolor: '#ffffff',
274
- paper_bgcolor: '#ffffff',
275
- margin: { l: 50, r: 50, t: 20, b: 50 },
276
- height: 200,
277
- showlegend: false
417
+ // Prepare volume data with timestamps and colors
418
+ const volumeData = stockData.dates.map((date, i) => ({
419
+ x: new Date(date).getTime(),
420
+ y: stockData.volume[i],
421
+ fillColor: i === 0 ? '#00d4ff' : (stockData.close[i] >= stockData.close[i - 1] ? '#00ff88' : '#ff3366')
422
+ }));
423
+
424
+ const options = {
425
+ series: [{
426
+ name: 'Volume',
427
+ data: volumeData
428
+ }],
429
+ chart: {
430
+ type: 'bar',
431
+ height: 300,
432
+ group: 'stock-charts',
433
+ background: '#151b3d',
434
+ foreColor: '#e8eaf6',
435
+ toolbar: {
436
+ show: true,
437
+ tools: {
438
+ download: true,
439
+ zoom: true,
440
+ zoomin: true,
441
+ zoomout: true,
442
+ pan: true,
443
+ reset: true
444
+ }
445
+ },
446
+ zoom: {
447
+ enabled: true,
448
+ type: 'x'
449
+ }
450
+ },
451
+ dataLabels: {
452
+ enabled: false // Disable data labels for performance
453
+ },
454
+ plotOptions: {
455
+ bar: {
456
+ columnWidth: '95%',
457
+ colors: {
458
+ ranges: []
459
+ }
460
+ }
461
+ },
462
+ xaxis: {
463
+ type: 'datetime',
464
+ labels: {
465
+ style: {
466
+ colors: '#9fa8da'
467
+ }
468
+ }
469
+ },
470
+ yaxis: {
471
+ labels: {
472
+ style: {
473
+ colors: '#9fa8da'
474
+ },
475
+ formatter: (val) => {
476
+ if (val >= 1e9) return (val / 1e9).toFixed(1) + 'B';
477
+ if (val >= 1e6) return (val / 1e6).toFixed(1) + 'M';
478
+ if (val >= 1e3) return (val / 1e3).toFixed(1) + 'K';
479
+ return val.toFixed(0);
480
+ }
481
+ }
482
+ },
483
+ grid: {
484
+ borderColor: '#2a3154',
485
+ strokeDashArray: 3
486
+ },
487
+ theme: {
488
+ mode: 'dark'
489
+ },
490
+ tooltip: {
491
+ theme: 'dark',
492
+ y: {
493
+ formatter: (val) => val.toLocaleString()
494
+ }
495
+ }
278
496
  };
279
497
 
280
- Plotly.newPlot('volumeChart', [trace], layout, { responsive: true });
498
+ volumeChart = new ApexCharts(document.querySelector("#volumeChart"), options);
499
+ volumeChart.render();
281
500
  }
282
501
 
283
502
  function renderRSIChart() {
284
- const trace = {
285
- type: 'scatter',
286
- mode: 'lines',
287
- x: indicatorData.dates,
288
- y: indicatorData.rsi,
289
- name: 'RSI',
290
- line: { color: '#2196F3', width: 2 }
291
- };
503
+ // Destroy existing chart if it exists
504
+ if (rsiChart) {
505
+ rsiChart.destroy();
506
+ }
292
507
 
293
- const oversold = {
294
- type: 'scatter',
295
- mode: 'lines',
296
- x: indicatorData.dates,
297
- y: Array(indicatorData.dates.length).fill(30),
298
- name: 'Oversold',
299
- line: { color: '#4CAF50', width: 1, dash: 'dash' }
300
- };
508
+ // Skip if indicators not available
509
+ if (!indicatorData || !indicatorData.rsi) {
510
+ document.querySelector("#rsiChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">Technical indicators not available (TA-Lib required)</p>';
511
+ return;
512
+ }
301
513
 
302
- const overbought = {
303
- type: 'scatter',
304
- mode: 'lines',
305
- x: indicatorData.dates,
306
- y: Array(indicatorData.dates.length).fill(70),
307
- name: 'Overbought',
308
- line: { color: '#F44336', width: 1, dash: 'dash' }
309
- };
514
+ // Filter out null/NaN/undefined values (RSI has warmup period)
515
+ const validRSIIndices = [];
516
+ for (let i = 0; i < indicatorData.rsi.length; i++) {
517
+ if (indicatorData.dates[i] !== null &&
518
+ indicatorData.dates[i] !== undefined &&
519
+ indicatorData.rsi[i] !== null &&
520
+ indicatorData.rsi[i] !== undefined &&
521
+ !isNaN(indicatorData.rsi[i])) {
522
+ validRSIIndices.push(i);
523
+ }
524
+ }
525
+
526
+ // If no valid data, show message
527
+ if (validRSIIndices.length === 0) {
528
+ document.querySelector("#rsiChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid RSI data available (warmup period or data issue)</p>';
529
+ return;
530
+ }
310
531
 
311
- const layout = {
312
- title: '',
313
- xaxis: { title: 'Date' },
314
- yaxis: { title: 'RSI', range: [0, 100] },
315
- plot_bgcolor: '#ffffff',
316
- paper_bgcolor: '#ffffff',
317
- margin: { l: 50, r: 50, t: 20, b: 50 },
318
- height: 250,
319
- showlegend: true,
320
- legend: { x: 0, y: 1 }
532
+ const options = {
533
+ series: [
534
+ {
535
+ name: 'RSI',
536
+ data: validRSIIndices.map(i => [new Date(indicatorData.dates[i]).getTime(), indicatorData.rsi[i]])
537
+ },
538
+ {
539
+ name: 'Oversold (30)',
540
+ data: validRSIIndices.map(i => [new Date(indicatorData.dates[i]).getTime(), 30])
541
+ },
542
+ {
543
+ name: 'Overbought (70)',
544
+ data: validRSIIndices.map(i => [new Date(indicatorData.dates[i]).getTime(), 70])
545
+ }
546
+ ],
547
+ chart: {
548
+ type: 'line',
549
+ height: 350,
550
+ group: 'stock-charts',
551
+ background: '#151b3d',
552
+ foreColor: '#e8eaf6',
553
+ toolbar: {
554
+ show: true,
555
+ tools: {
556
+ download: true,
557
+ zoom: true,
558
+ zoomin: true,
559
+ zoomout: true,
560
+ pan: true,
561
+ reset: true
562
+ }
563
+ },
564
+ zoom: {
565
+ enabled: true,
566
+ type: 'x'
567
+ }
568
+ },
569
+ colors: ['#00d4ff', '#00ff88', '#ff3366'],
570
+ stroke: {
571
+ width: [3, 2, 2],
572
+ curve: 'smooth',
573
+ dashArray: [0, 5, 5]
574
+ },
575
+ xaxis: {
576
+ type: 'datetime',
577
+ labels: {
578
+ style: {
579
+ colors: '#9fa8da'
580
+ }
581
+ }
582
+ },
583
+ yaxis: {
584
+ min: 0,
585
+ max: 100,
586
+ labels: {
587
+ style: {
588
+ colors: '#9fa8da'
589
+ }
590
+ }
591
+ },
592
+ grid: {
593
+ borderColor: '#2a3154',
594
+ strokeDashArray: 3
595
+ },
596
+ legend: {
597
+ position: 'top',
598
+ horizontalAlign: 'left',
599
+ fontSize: '14px',
600
+ labels: {
601
+ colors: '#e8eaf6'
602
+ }
603
+ },
604
+ theme: {
605
+ mode: 'dark'
606
+ },
607
+ tooltip: {
608
+ theme: 'dark',
609
+ shared: true,
610
+ intersect: false
611
+ },
612
+ annotations: {
613
+ yaxis: [
614
+ {
615
+ y: 30,
616
+ borderColor: '#00ff88',
617
+ fillColor: '#00ff88',
618
+ opacity: 0.1
619
+ },
620
+ {
621
+ y: 70,
622
+ borderColor: '#ff3366',
623
+ fillColor: '#ff3366',
624
+ opacity: 0.1
625
+ }
626
+ ]
627
+ }
321
628
  };
322
629
 
323
- Plotly.newPlot('rsiChart', [trace, oversold, overbought], layout, { responsive: true });
630
+ rsiChart = new ApexCharts(document.querySelector("#rsiChart"), options);
631
+ rsiChart.render();
324
632
  }
325
633
 
326
634
  function renderMACDChart() {
327
- const macd = {
328
- type: 'scatter',
329
- mode: 'lines',
330
- x: indicatorData.dates,
331
- y: indicatorData.macd,
332
- name: 'MACD',
333
- line: { color: '#2196F3', width: 2 }
334
- };
635
+ // Destroy existing chart if it exists
636
+ if (macdChart) {
637
+ macdChart.destroy();
638
+ }
335
639
 
336
- const signal = {
337
- type: 'scatter',
338
- mode: 'lines',
339
- x: indicatorData.dates,
340
- y: indicatorData.macd_signal,
341
- name: 'Signal',
342
- line: { color: '#FF9800', width: 2 }
343
- };
640
+ // Skip if indicators not available
641
+ if (!indicatorData || !indicatorData.macd) {
642
+ document.querySelector("#macdChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">Technical indicators not available (TA-Lib required)</p>';
643
+ return;
644
+ }
344
645
 
345
- const hist = {
346
- type: 'bar',
347
- x: indicatorData.dates,
348
- y: indicatorData.macd_hist,
349
- name: 'Histogram',
350
- marker: {
351
- color: indicatorData.macd_hist.map(v => v >= 0 ? '#26a69a' : '#ef5350')
646
+ // Filter out null/NaN/undefined values (MACD has warmup period)
647
+ const validIndices = [];
648
+ for (let i = 0; i < indicatorData.macd.length; i++) {
649
+ if (indicatorData.dates[i] !== null &&
650
+ indicatorData.dates[i] !== undefined &&
651
+ indicatorData.macd[i] !== null &&
652
+ indicatorData.macd[i] !== undefined &&
653
+ indicatorData.macd_signal[i] !== null &&
654
+ indicatorData.macd_signal[i] !== undefined &&
655
+ indicatorData.macd_hist[i] !== null &&
656
+ indicatorData.macd_hist[i] !== undefined &&
657
+ !isNaN(indicatorData.macd[i]) &&
658
+ !isNaN(indicatorData.macd_signal[i]) &&
659
+ !isNaN(indicatorData.macd_hist[i])) {
660
+ validIndices.push(i);
352
661
  }
353
- };
662
+ }
354
663
 
355
- const layout = {
356
- title: '',
357
- xaxis: { title: 'Date' },
358
- yaxis: { title: 'MACD' },
359
- plot_bgcolor: '#ffffff',
360
- paper_bgcolor: '#ffffff',
361
- margin: { l: 50, r: 50, t: 20, b: 50 },
362
- height: 250,
363
- showlegend: true,
364
- legend: { x: 0, y: 1 }
664
+ // If no valid data, show message
665
+ if (validIndices.length === 0) {
666
+ document.querySelector("#macdChart").innerHTML = '<p style="text-align: center; padding: 40px; color: #9fa8da;">No valid MACD data available (warmup period or data issue)</p>';
667
+ return;
668
+ }
669
+
670
+ // Prepare histogram data with colors (only valid values)
671
+ const histData = validIndices.map(i => ({
672
+ x: new Date(indicatorData.dates[i]).getTime(),
673
+ y: indicatorData.macd_hist[i],
674
+ fillColor: indicatorData.macd_hist[i] >= 0 ? '#00ff88' : '#ff3366'
675
+ }));
676
+
677
+ const options = {
678
+ series: [
679
+ {
680
+ name: 'Histogram',
681
+ type: 'bar',
682
+ data: histData
683
+ },
684
+ {
685
+ name: 'MACD',
686
+ type: 'line',
687
+ data: validIndices.map(i => [new Date(indicatorData.dates[i]).getTime(), indicatorData.macd[i]])
688
+ },
689
+ {
690
+ name: 'Signal',
691
+ type: 'line',
692
+ data: validIndices.map(i => [new Date(indicatorData.dates[i]).getTime(), indicatorData.macd_signal[i]])
693
+ }
694
+ ],
695
+ chart: {
696
+ height: 350,
697
+ group: 'stock-charts',
698
+ background: '#151b3d',
699
+ foreColor: '#e8eaf6',
700
+ toolbar: {
701
+ show: true,
702
+ tools: {
703
+ download: true,
704
+ zoom: true,
705
+ zoomin: true,
706
+ zoomout: true,
707
+ pan: true,
708
+ reset: true
709
+ }
710
+ },
711
+ zoom: {
712
+ enabled: true,
713
+ type: 'x'
714
+ }
715
+ },
716
+ plotOptions: {
717
+ bar: {
718
+ columnWidth: '80%'
719
+ }
720
+ },
721
+ colors: ['#00ff88', '#00d4ff', '#ffaa00'],
722
+ stroke: {
723
+ width: [0, 3, 3],
724
+ curve: 'smooth'
725
+ },
726
+ xaxis: {
727
+ type: 'datetime',
728
+ labels: {
729
+ style: {
730
+ colors: '#9fa8da'
731
+ }
732
+ }
733
+ },
734
+ yaxis: {
735
+ labels: {
736
+ style: {
737
+ colors: '#9fa8da'
738
+ },
739
+ formatter: (val) => val.toFixed(2)
740
+ }
741
+ },
742
+ grid: {
743
+ borderColor: '#2a3154',
744
+ strokeDashArray: 3
745
+ },
746
+ legend: {
747
+ position: 'top',
748
+ horizontalAlign: 'left',
749
+ fontSize: '14px',
750
+ labels: {
751
+ colors: '#e8eaf6'
752
+ }
753
+ },
754
+ theme: {
755
+ mode: 'dark'
756
+ },
757
+ tooltip: {
758
+ theme: 'dark',
759
+ shared: true,
760
+ intersect: false
761
+ }
365
762
  };
366
763
 
367
- Plotly.newPlot('macdChart', [hist, macd, signal], layout, { responsive: true });
764
+ macdChart = new ApexCharts(document.querySelector("#macdChart"), options);
765
+ macdChart.render();
368
766
  }
369
767
 
370
768
  function updateChartType(type) {
@@ -413,6 +811,20 @@ async function runStrategyComparison() {
413
811
  }
414
812
  }
415
813
 
814
+ async function updatePeriod(period) {
815
+ currentPeriod = period;
816
+
817
+ // Update button states
818
+ document.querySelectorAll('[data-period]').forEach(btn => {
819
+ btn.classList.remove('active');
820
+ });
821
+ document.querySelector(`[data-period="${period}"]`).classList.add('active');
822
+
823
+ // Reload data with new period
824
+ await loadStockData();
825
+ await loadIndicators();
826
+ }
827
+
416
828
  function refreshData() {
417
829
  location.reload();
418
830
  }