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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +21 -0
- data/README.md +56 -32
- data/docs/api/dataframe.md +0 -1
- data/docs/assets/images/sqa.jpg +0 -0
- data/docs/concepts/index.md +2 -10
- data/docs/data_frame.md +0 -1
- data/docs/getting-started/index.md +0 -16
- data/docs/getting-started/installation.md +2 -2
- data/docs/getting-started/quick-start.md +4 -4
- data/docs/index.md +0 -2
- data/docs/strategies/bollinger-bands.md +1 -1
- data/docs/strategies/rsi.md +1 -1
- data/examples/sinatra_app/Gemfile +20 -0
- data/examples/sinatra_app/Gemfile.lock +268 -0
- data/examples/sinatra_app/QUICKSTART.md +13 -3
- data/examples/sinatra_app/README.md +12 -2
- data/examples/sinatra_app/RUNNING_WITHOUT_TALIB.md +90 -0
- data/examples/sinatra_app/TROUBLESHOOTING.md +95 -0
- data/examples/sinatra_app/app.rb +85 -25
- data/examples/sinatra_app/public/css/style.css +101 -37
- data/examples/sinatra_app/public/debug_macd.html +82 -0
- data/examples/sinatra_app/start.sh +53 -0
- data/examples/sinatra_app/views/dashboard.erb +558 -146
- data/examples/sinatra_app/views/layout.erb +2 -2
- data/lib/sqa/data_frame/alpha_vantage.rb +13 -3
- data/lib/sqa/data_frame.rb +21 -15
- data/lib/sqa/indicator.rb +17 -4
- data/lib/sqa/stock.rb +73 -11
- data/lib/sqa/ticker.rb +9 -2
- data/lib/sqa/version.rb +1 -1
- data/lib/sqa.rb +12 -4
- data/mkdocs.yml +4 -40
- metadata +7 -21
- data/docs/alpha_vantage_technical_indicators.md +0 -62
- data/docs/average_true_range.md +0 -9
- data/docs/bollinger_bands.md +0 -15
- data/docs/candlestick_pattern_recognizer.md +0 -4
- data/docs/donchian_channel.md +0 -5
- data/docs/double_top_bottom_pattern.md +0 -3
- data/docs/exponential_moving_average.md +0 -19
- data/docs/fibonacci_retracement.md +0 -30
- data/docs/head_and_shoulders_pattern.md +0 -3
- data/docs/market_profile.md +0 -4
- data/docs/momentum.md +0 -19
- data/docs/moving_average_convergence_divergence.md +0 -23
- data/docs/peaks_and_valleys.md +0 -11
- data/docs/relative_strength_index.md +0 -6
- data/docs/simple_moving_average.md +0 -8
- data/docs/stochastic_oscillator.md +0 -4
- data/docs/ta_lib.md +0 -160
- data/docs/true_range.md +0 -12
- data/docs/true_strength_index.md +0 -46
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
498
|
+
volumeChart = new ApexCharts(document.querySelector("#volumeChart"), options);
|
|
499
|
+
volumeChart.render();
|
|
281
500
|
}
|
|
282
501
|
|
|
283
502
|
function renderRSIChart() {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
630
|
+
rsiChart = new ApexCharts(document.querySelector("#rsiChart"), options);
|
|
631
|
+
rsiChart.render();
|
|
324
632
|
}
|
|
325
633
|
|
|
326
634
|
function renderMACDChart() {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
}
|