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:
parent
fb590105ce
commit
f1d38f9abb
@ -1435,7 +1435,7 @@ function renderAll() {
|
||||
// Rebuild the main content
|
||||
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="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>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;
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user