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
|
// 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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user