fix: backtest chart auto-switches linear/log based on price range

- Added time range buttons (30D/90D/6M/1Y/2Y/4Y/ALL) to backtest chart
- Auto-detects: if price range spans >20x → log scale, else linear
- Short ranges (30D-2Y) now show meaningful price movement instead of flat line
- Zone backgrounds updated to match new thresholds (35/50/65)
- Monospace font, better tooltips with zone labels
- Chart properly destroys and recreates on range change
This commit is contained in:
BizzleBot 2026-03-21 23:00:46 +00:00
parent fb590105ce
commit f1d38f9abb

View File

@ -1435,7 +1435,7 @@ function renderAll() {
// Rebuild the main content // Rebuild the main content
document.getElementById('mainContent').innerHTML = ` document.getElementById('mainContent').innerHTML = `
<div class="section"><div class="context-box" id="contextBox"><h2>Current Signal Context</h2><div class="context-score" id="ctxScore">--</div><div class="context-percentile" id="ctxPercentile"></div><div class="context-return" id="ctxReturn"></div><div class="comparable-list" id="ctxComparables"></div></div></div> <div class="section"><div class="context-box" id="contextBox"><h2>Current Signal Context</h2><div class="context-score" id="ctxScore">--</div><div class="context-percentile" id="ctxPercentile"></div><div class="context-return" id="ctxReturn"></div><div class="comparable-list" id="ctxComparables"></div></div></div>
<div class="section"><div class="card"><h2>Historical Score vs BTC Price</h2><div class="chart-dual"><canvas id="dualChart"></canvas></div></div></div> <div class="section"><div class="card"><div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:12px"><h2 style="margin:0">Historical Score vs BTC Price</h2><div id="btRangeBar" style="display:flex;gap:4px"><button class="range-btn" data-days="30">30D</button><button class="range-btn" data-days="90">90D</button><button class="range-btn" data-days="180">6M</button><button class="range-btn" data-days="365">1Y</button><button class="range-btn" data-days="730">2Y</button><button class="range-btn" data-days="1460">4Y</button><button class="range-btn active" data-days="0">ALL</button></div></div><div class="chart-dual"><canvas id="dualChart"></canvas></div></div></div>
<div class="section"><div class="card"><h2>Score Bracket Performance</h2><div style="overflow-x:auto"><table><thead><tr><th>Score Range</th><th>Label</th><th>Days</th><th>Avg 30d</th><th>Avg 90d</th><th>Avg 180d</th><th>Avg 1yr</th><th>Win Rate (1yr)</th><th>Max Gain</th><th>Max Loss</th><th>Avg Max DD</th></tr></thead><tbody id="bracketBody"></tbody></table></div></div></div> <div class="section"><div class="card"><h2>Score Bracket Performance</h2><div style="overflow-x:auto"><table><thead><tr><th>Score Range</th><th>Label</th><th>Days</th><th>Avg 30d</th><th>Avg 90d</th><th>Avg 180d</th><th>Avg 1yr</th><th>Win Rate (1yr)</th><th>Max Gain</th><th>Max Loss</th><th>Avg Max DD</th></tr></thead><tbody id="bracketBody"></tbody></table></div></div></div>
<div class="section"><div class="card"><h2>Major Signal Events (Score Crossed 70/80/90+)</h2><div id="signalEvents"></div></div></div> <div class="section"><div class="card"><h2>Major Signal Events (Score Crossed 70/80/90+)</h2><div id="signalEvents"></div></div></div>
`; `;
@ -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; const chart = backtestData.chart_data;
if (!chart || !chart.length) return; 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 = { const zonePlugin = {
id: 'zoneBackground', id: 'zoneBackground',
beforeDraw(chart) { 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; if (!y) return;
const zones = [ const zones = [
{ min: 0, max: 40, color: 'rgba(239,68,68,0.06)' }, { min: 0, max: 35, color: 'rgba(239,68,68,0.06)' },
{ min: 40, max: 70, color: 'rgba(234,179,8,0.06)' }, { min: 35, max: 50, color: 'rgba(234,179,8,0.04)' },
{ min: 70, max: 100, color: 'rgba(34,197,94,0.08)' }, { 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) { for (const z of zones) {
const yTop = y.getPixelForValue(Math.min(z.max, 100)); const yTop = y.getPixelForValue(Math.min(z.max, 100));
@ -1505,10 +1519,24 @@ function renderDualChart() {
c.fillStyle = z.color; c.fillStyle = z.color;
c.fillRect(left, yTop, right - left, yBot - yTop); 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', type: 'line',
plugins: [zonePlugin], plugins: [zonePlugin],
data: { data: {
@ -1522,7 +1550,7 @@ function renderDualChart() {
borderWidth: 1.5, borderWidth: 1.5,
fill: false, fill: false,
tension: 0.2, tension: 0.2,
pointRadius: 0, pointRadius: data.length < 100 ? 2 : 0,
yAxisID: 'y', yAxisID: 'y',
}, },
{ {
@ -1542,27 +1570,53 @@ function renderDualChart() {
maintainAspectRatio: false, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false }, interaction: { mode: 'index', intersect: false },
plugins: { plugins: {
legend: { labels: { color: '#94a3b8', font: { size: 11 } } }, legend: { labels: { color: '#94a3b8', font: { size: 11, family: 'monospace' } } },
tooltip: { 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: { callbacks: {
label: function(ctx) { label: function(ctx) {
if (ctx.datasetIndex === 1) return 'BTC: $' + Math.round(ctx.parsed.y).toLocaleString(); 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: { scales: {
x: { ticks: { color: '#94a3b8', maxTicksLimit: 20, maxRotation: 45 }, grid: { color: '#1e293b' } }, 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' }, grid: { color: '#1e293b' }, 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' } }, title: { display: true, text: 'Score (0-100)', color: '#f7931a', font: { family: 'monospace', size: 11 } } },
y1: { position: 'right', type: 'logarithmic', ticks: { color: '#22d3ee', callback: v => '$' + v.toLocaleString() }, y1: {
grid: { drawOnChartArea: false }, title: { display: true, text: 'BTC Price (log)', color: '#22d3ee' } } 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() { function renderBracketTable() {
const brackets = backtestData.bracket_stats; const brackets = backtestData.bracket_stats;
if (!brackets) return; if (!brackets) return;