diff --git a/dashboard/server.py b/dashboard/server.py
index e026c1a..bd63654 100644
--- a/dashboard/server.py
+++ b/dashboard/server.py
@@ -1435,7 +1435,7 @@ function renderAll() {
// Rebuild the main content
document.getElementById('mainContent').innerHTML = `
- Historical Score vs BTC Price
+ Historical Score vs BTC Price
Score Bracket Performance
| Score Range | Label | Days | Avg 30d | Avg 90d | Avg 180d | Avg 1yr | Win Rate (1yr) | Max Gain | Max Loss | Avg Max DD |
|---|
Major Signal Events (Score Crossed 70/80/90+)
`;
@@ -1480,24 +1480,38 @@ function renderContext() {
}
}
-function renderDualChart() {
+let btDualChart = null;
+let btCurrentRange = 0;
+
+function renderDualChart(days) {
+ if (days === undefined) days = btCurrentRange;
+ btCurrentRange = days;
const chart = backtestData.chart_data;
if (!chart || !chart.length) return;
- const ctx = document.getElementById('dualChart').getContext('2d');
- const labels = chart.map(d => d.date);
- const scores = chart.map(d => d.score);
- const prices = chart.map(d => d.price);
- // Zone backgrounds via plugin
+ let data = chart;
+ if (days > 0) data = chart.slice(-days);
+
+ const ctx = document.getElementById('dualChart').getContext('2d');
+ const labels = data.map(d => d.date);
+ const scores = data.map(d => d.score);
+ const prices = data.map(d => d.price);
+
+ // Use log scale only for ALL or 4Y+ (when price spans multiple orders of magnitude)
+ const priceMin = Math.min(...prices.filter(p => p > 0));
+ const priceMax = Math.max(...prices);
+ const useLog = (priceMax / priceMin) > 20; // >20x range = log makes sense
+
const zonePlugin = {
id: 'zoneBackground',
beforeDraw(chart) {
- const { ctx: c, chartArea: {left, right, top, bottom}, scales: {y} } = chart;
+ const { ctx: c, chartArea: {left, right}, scales: {y} } = chart;
if (!y) return;
const zones = [
- { min: 0, max: 40, color: 'rgba(239,68,68,0.06)' },
- { min: 40, max: 70, color: 'rgba(234,179,8,0.06)' },
- { min: 70, max: 100, color: 'rgba(34,197,94,0.08)' },
+ { min: 0, max: 35, color: 'rgba(239,68,68,0.06)' },
+ { min: 35, max: 50, color: 'rgba(234,179,8,0.04)' },
+ { min: 50, max: 65, color: 'rgba(234,179,8,0.06)' },
+ { min: 65, max: 100, color: 'rgba(34,197,94,0.08)' },
];
for (const z of zones) {
const yTop = y.getPixelForValue(Math.min(z.max, 100));
@@ -1505,10 +1519,24 @@ function renderDualChart() {
c.fillStyle = z.color;
c.fillRect(left, yTop, right - left, yBot - yTop);
}
+ // Threshold lines
+ [35, 50, 65].forEach(val => {
+ const yPos = y.getPixelForValue(val);
+ c.beginPath();
+ c.setLineDash([4, 4]);
+ c.strokeStyle = 'rgba(255,255,255,0.06)';
+ c.lineWidth = 1;
+ c.moveTo(left, yPos);
+ c.lineTo(right, yPos);
+ c.stroke();
+ c.setLineDash([]);
+ });
}
};
- new Chart(ctx, {
+ if (btDualChart) btDualChart.destroy();
+
+ btDualChart = new Chart(ctx, {
type: 'line',
plugins: [zonePlugin],
data: {
@@ -1522,7 +1550,7 @@ function renderDualChart() {
borderWidth: 1.5,
fill: false,
tension: 0.2,
- pointRadius: 0,
+ pointRadius: data.length < 100 ? 2 : 0,
yAxisID: 'y',
},
{
@@ -1542,27 +1570,53 @@ function renderDualChart() {
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
- legend: { labels: { color: '#94a3b8', font: { size: 11 } } },
+ legend: { labels: { color: '#94a3b8', font: { size: 11, family: 'monospace' } } },
tooltip: {
+ backgroundColor: 'rgba(10,10,15,0.95)',
+ borderColor: 'rgba(255,255,255,0.08)',
+ borderWidth: 1,
+ titleFont: { family: 'monospace', size: 11 },
+ bodyFont: { family: 'monospace', size: 11 },
callbacks: {
label: function(ctx) {
if (ctx.datasetIndex === 1) return 'BTC: $' + Math.round(ctx.parsed.y).toLocaleString();
- return 'Score: ' + ctx.parsed.y;
+ const s = ctx.parsed.y;
+ let zone = s >= 80 ? 'Extreme Accum' : s >= 65 ? 'Strong Accum' : s >= 50 ? 'Moderate' : s >= 35 ? 'Neutral' : 'Caution';
+ return 'Score: ' + s.toFixed(1) + ' (' + zone + ')';
}
}
}
},
scales: {
- x: { ticks: { color: '#94a3b8', maxTicksLimit: 20, maxRotation: 45 }, grid: { color: '#1e293b' } },
- y: { position: 'left', min: 0, max: 100, ticks: { color: '#f7931a' }, grid: { color: '#1e293b' },
- title: { display: true, text: 'Score (0-100)', color: '#f7931a' } },
- y1: { position: 'right', type: 'logarithmic', ticks: { color: '#22d3ee', callback: v => '$' + v.toLocaleString() },
- grid: { drawOnChartArea: false }, title: { display: true, text: 'BTC Price (log)', color: '#22d3ee' } }
+ x: { ticks: { color: '#94a3b8', maxTicksLimit: 15, maxRotation: 45, font: { family: 'monospace', size: 10 } }, grid: { color: 'rgba(255,255,255,0.03)' } },
+ y: { position: 'left', min: 0, max: 100, ticks: { color: '#f7931a', font: { family: 'monospace', size: 10 } }, grid: { color: 'rgba(255,255,255,0.03)' },
+ title: { display: true, text: 'Score (0-100)', color: '#f7931a', font: { family: 'monospace', size: 11 } } },
+ y1: {
+ position: 'right',
+ type: useLog ? 'logarithmic' : 'linear',
+ ticks: {
+ color: '#22d3ee',
+ font: { family: 'monospace', size: 10 },
+ callback: v => '$' + (v >= 1000 ? (v/1000).toFixed(v >= 10000 ? 0 : 1) + 'k' : v.toLocaleString())
+ },
+ grid: { drawOnChartArea: false },
+ title: { display: true, text: useLog ? 'BTC Price (log)' : 'BTC Price', color: '#22d3ee', font: { family: 'monospace', size: 11 } }
+ }
}
}
});
}
+// Backtest range button handlers
+document.addEventListener('click', function(e) {
+ if (e.target.closest('#btRangeBar .range-btn')) {
+ const btn = e.target.closest('.range-btn');
+ document.querySelectorAll('#btRangeBar .range-btn').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ renderDualChart(parseInt(btn.dataset.days));
+ }
+});
+
function renderBracketTable() {
const brackets = backtestData.bracket_stats;
if (!brackets) return;