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 = `

Current Signal Context

--
-

Historical Score vs BTC Price

+

Historical Score vs BTC Price

Score Bracket Performance

Score RangeLabelDaysAvg 30dAvg 90dAvg 180dAvg 1yrWin Rate (1yr)Max GainMax LossAvg 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;