stocks / static /backtest.html
Arrechenash's picture
Initial Commit
e04e112
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LT Backtest</title>
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/backtest.css">
<script src="https://unpkg.com/lightweight-charts@4.2.1/dist/lightweight-charts.standalone.production.js"></script>
</head>
<body>
<nav class="main-nav" id="main-nav"></nav>
<div class="page-wrapper">
<div class="control-panel">
<div class="input-row">
<div class="input-group">
<label class="input-label">Symbols (comma-separated)</label>
<input type="text" id="symbols-input" class="input-field" placeholder="AAPL, NVDA, TSLA...">
</div>
<div class="input-group">
<label class="input-label">Entry Condition</label>
<input type="text" id="entry-input" class="input-field" value="time == '09:30'" placeholder="time == '09:30'">
</div>
<div class="input-group">
<label class="input-label">Exit Condition</label>
<input type="text" id="exit-input" class="input-field" value="time == '16:00'" placeholder="time == '16:00'">
</div>
<div class="input-group">
<label class="input-label">Offset (days)</label>
<input type="number" id="offset-input" class="input-field" value="0" min="0" max="30">
</div>
</div>
<div class="action-row">
<button class="btn btn-secondary" id="export-btn" onclick="exportToCsv()" style="display: none;">Export to CSV</button>
<button class="btn btn-primary" id="run-btn" onclick="runBacktest()">Run Backtest</button>
</div>
</div>
<div class="summary-bar" id="summary-bar" style="display: none;">
<div class="summary-card">
<div class="summary-label">Win Rate</div>
<div class="summary-value" id="win-rate">0%</div>
</div>
<div class="summary-card" id="card-profit">
<div class="summary-label">Total Profit</div>
<div class="summary-value" id="total-profit">$0</div>
</div>
<div class="summary-card">
<div class="summary-label">Profit Factor</div>
<div class="summary-value" id="profit-factor">0</div>
</div>
<div class="summary-card">
<div class="summary-label">Expectancy</div>
<div class="summary-value" id="expectancy">$0</div>
</div>
<div class="summary-card">
<div class="summary-label">Avg Win / Loss</div>
<div class="summary-value" id="avg-win-loss">$0 / $0</div>
</div>
<div class="summary-card">
<div class="summary-label">Max Drawdown</div>
<div class="summary-value" id="max-drawdown" style="color: var(--negative);">0%</div>
</div>
<div class="summary-card">
<div class="summary-label">Final Equity</div>
<div class="summary-value" id="final-equity">$0</div>
</div>
</div>
<div id="equity-chart-container" class="equity-chart-container" style="display: none;">
<div class="summary-label" style="margin-bottom: 10px;">Equity Curve</div>
<div id="equity-chart" style="height: 300px; width: 100%;"></div>
</div>
<div class="view-controls" id="view-controls" style="display: none;">
<div class="summary-label">Execution Results</div>
<div class="view-toggle">
<div class="toggle-btn active" onclick="switchView('grid')" id="view-grid-btn">Grid View</div>
<div class="toggle-btn" onclick="switchView('list')" id="view-list-btn">Trade Log</div>
</div>
</div>
<div id="results-list" class="trade-log-container" style="display: none;">
<table class="trade-table" id="trade-table">
<thead>
<tr>
<th data-column="symbol" onclick="sortTable('symbol')">Symbol</th>
<th data-column="date" onclick="sortTable('date')" class="sort-col">Date</th>
<th data-column="status" onclick="sortTable('status')">Status</th>
<th data-column="ret" onclick="sortTable('ret')">Return</th>
<th data-column="profit" onclick="sortTable('profit')">Profit</th>
<th data-column="entry" onclick="sortTable('entry')">Entry</th>
<th data-column="exit" onclick="sortTable('exit')">Exit</th>
<th data-column="sector" onclick="sortTable('sector')">Sector</th>
<th data-column="gap_pct" onclick="sortTable('gap_pct')">Gap %</th>
</tr>
</thead>
<tbody id="trade-table-body"></tbody>
</table>
</div>
<div id="results-grid" class="results-grid"></div>
<div id="status" class="status">Enter symbols and click Run Backtest</div>
</div>
<script src="/static/js/financial-api.js"></script>
<script>
let backtestResults = [];
let backtestColumnMap = {};
let loadedCandidates = null;
// Sort state
let currentSortColumn = 'date';
let sortDirection = 'desc';
// Convert Unix timestamp to NY timezone
function toNyTimestamp(unixTs) {
if (!unixTs) return unixTs;
// Get the date in NY timezone as string components
const nyStr = new Date(unixTs * 1000).toLocaleString('en-US', {
timeZone: 'America/New_York',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
});
// Parse the NY time string as UTC (not local time)
const [datePart, timePart] = nyStr.split(', ');
const [month, day, year] = datePart.split('/');
const [hour, minute, second] = timePart.split(':');
const nyDateAsUtc = Date.UTC(
parseInt(year), parseInt(month) - 1, parseInt(day),
parseInt(hour), parseInt(minute), parseInt(second)
);
return Math.floor(nyDateAsUtc / 1000);
}
// Format Unix timestamp used in charts (already shifted via toNyTimestamp)
function formatNyTime(unixTs) {
if (!unixTs) return '--:--';
const date = new Date(unixTs * 1000);
const h = String(date.getUTCHours()).padStart(2, '0');
const m = String(date.getUTCMinutes()).padStart(2, '0');
const s = String(date.getUTCSeconds()).padStart(2, '0');
return `${h}:${m}:${s}`;
}
function initFromScanner() {
const cached = sessionStorage.getItem('scanner_last_results');
if (cached) {
try {
const results = JSON.parse(cached);
if (results && results.length > 0) {
loadedCandidates = results.map(r => ({
symbol: r.symbol,
date: r.date
})).slice(0, 50);
const symbolsInput = document.getElementById('symbols-input');
symbolsInput.value = loadedCandidates.map(c => c.symbol).join(', ');
document.getElementById('status').textContent =
`Loaded ${loadedCandidates.length} symbols from Scanner`;
}
} catch (e) {
console.error('Failed to load scan results:', e);
}
}
}
initFromScanner();
loadFormValues();
// Save form values to localStorage
function saveFormValues() {
const values = {
symbols: document.getElementById('symbols-input').value,
entry: document.getElementById('entry-input').value,
exit: document.getElementById('exit-input').value,
offset: document.getElementById('offset-input').value
};
localStorage.setItem('backtest_form_values', JSON.stringify(values));
}
// Load form values from localStorage
function loadFormValues() {
const saved = localStorage.getItem('backtest_form_values');
if (saved) {
try {
const values = JSON.parse(saved);
if (values.symbols) document.getElementById('symbols-input').value = values.symbols;
if (values.entry) document.getElementById('entry-input').value = values.entry;
if (values.exit) document.getElementById('exit-input').value = values.exit;
if (values.offset) document.getElementById('offset-input').value = values.offset;
} catch (e) {
console.error('Failed to load form values:', e);
}
}
}
async function runBacktest() {
saveFormValues();
const entry = document.getElementById('entry-input').value;
const exit = document.getElementById('exit-input').value;
const offset = document.getElementById('offset-input').value;
const status = document.getElementById('status');
const grid = document.getElementById('results-grid');
const summaryBar = document.getElementById('summary-bar');
const btn = document.getElementById('run-btn');
let urlParams;
if (loadedCandidates && loadedCandidates.length > 0) {
urlParams = new URLSearchParams({
candidates: JSON.stringify(loadedCandidates),
entry: entry,
exit: exit,
offset: offset
});
} else {
const symbols = document.getElementById('symbols-input').value.trim();
if (!symbols) {
status.textContent = 'Please enter symbols or run a scan first';
return;
}
urlParams = new URLSearchParams({
symbols: symbols,
entry: entry,
exit: exit,
offset: offset
});
}
btn.disabled = true;
status.textContent = 'Running backtest...';
grid.innerHTML = '';
summaryBar.style.display = 'none';
// Clear previous results
const resultsList = document.getElementById('results-list');
const equityChartContainer = document.getElementById('equity-chart-container');
const viewControls = document.getElementById('view-controls');
const exportBtn = document.getElementById('export-btn');
resultsList.style.display = 'none';
document.getElementById('trade-table-body').innerHTML = '';
equityChartContainer.style.display = 'none';
document.getElementById('equity-chart').innerHTML = '';
viewControls.style.display = 'none';
exportBtn.style.display = 'none';
try {
const response = await fetch(`/api/backtest?${urlParams}`);
const result = await response.json();
if (result.error) {
status.textContent = 'Error: ' + result.error;
return;
}
backtestResults = result.trades || [];
backtestColumnMap = result.column_map || {};
// Also save candidates for stats
const candidates = (result.trades || [])
.filter(t => t.symbol && t.date)
.map(t => ({symbol: t.symbol, date: t.date}));
sessionStorage.setItem('last_candidates', JSON.stringify(candidates));
if (backtestResults.length === 0) {
status.textContent = 'No trades found';
return;
}
// Update summary
const summary = result.summary;
document.getElementById('win-rate').textContent = summary.win_rate + '%';
document.getElementById('profit-factor').textContent = summary.profit_factor;
document.getElementById('expectancy').textContent = (summary.expectancy >= 0 ? '+' : '') + '$' + summary.expectancy.toFixed(2);
document.getElementById('avg-win-loss').textContent = `$${summary.avg_win.toFixed(0)} / $${Math.abs(summary.avg_loss).toFixed(0)}`;
document.getElementById('max-drawdown').textContent = summary.max_drawdown + '%';
const profitEl = document.getElementById('total-profit');
profitEl.textContent = (summary.total_profit >= 0 ? '+' : '') + '$' + summary.total_profit.toFixed(2);
document.getElementById('card-profit').className = 'summary-card ' + (summary.total_profit >= 0 ? 'positive' : 'negative');
const equityEl = document.getElementById('final-equity');
equityEl.textContent = '$' + summary.final_equity.toLocaleString();
equityEl.parentElement.className = 'summary-card ' + (summary.final_equity >= summary.initial_capital ? 'positive' : 'negative');
summaryBar.style.display = 'grid';
document.getElementById('view-controls').style.display = 'flex';
status.textContent = '';
// Render Equity Curve
if (result.equity_curve && result.equity_curve.length > 0) {
renderEquityChart(result.equity_curve);
}
// Show export button
document.getElementById('export-btn').style.display = 'block';
// Cache results
sessionStorage.setItem('backtest_last_result', JSON.stringify(result));
// Initial render of view
renderResults();
} catch (e) {
status.textContent = 'Error: ' + e.message;
} finally {
btn.disabled = false;
}
}
let currentView = 'list';
function switchView(view) {
currentView = view;
document.getElementById('view-grid-btn').classList.toggle('active', view === 'grid');
document.getElementById('view-list-btn').classList.toggle('active', view === 'list');
renderResults();
}
function renderResults() {
const grid = document.getElementById('results-grid');
const list = document.getElementById('results-list');
if (currentView === 'grid') {
grid.style.display = 'grid';
list.style.display = 'none';
grid.innerHTML = '';
backtestResults.forEach(trade => {
const card = createTradeCard(trade);
grid.appendChild(card);
renderTradeChart(trade, `chart-${trade.symbol}-${trade.date}`);
});
} else {
grid.style.display = 'none';
list.style.display = 'block';
renderListView();
updateSortIndicators();
}
}
function getSortIndicator(col) {
if (currentSortColumn !== col) return '';
return sortDirection === 'asc' ? ' ▲' : ' ▼';
}
function sortTable(column) {
if (currentSortColumn === column) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortColumn = column;
sortDirection = column === 'date' ? 'desc' : 'asc';
}
updateSortIndicators();
renderListView();
}
function updateSortIndicators() {
document.querySelectorAll('.trade-table th[data-column]').forEach(th => {
const col = th.getAttribute('data-column');
th.textContent = th.getAttribute('data-column') === 'symbol' ? 'Symbol' :
th.getAttribute('data-column') === 'date' ? 'Date' :
th.getAttribute('data-column') === 'status' ? 'Status' :
th.getAttribute('data-column') === 'ret' ? 'Return' :
th.getAttribute('data-column') === 'profit' ? 'Profit' :
th.getAttribute('data-column') === 'entry' ? 'Entry' :
th.getAttribute('data-column') === 'exit' ? 'Exit' :
th.getAttribute('data-column') === 'sector' ? 'Sector' :
th.getAttribute('data-column') === 'gap_pct' ? 'Gap %' : col;
if (currentSortColumn === col) {
th.textContent += sortDirection === 'asc' ? ' ▲' : ' ▼';
}
});
}
function renderListView() {
const tbody = document.getElementById('trade-table-body');
tbody.innerHTML = '';
// Sort results
const sortedResults = [...backtestResults].sort((a, b) => {
let valA = a[currentSortColumn];
let valB = b[currentSortColumn];
// Handle date sorting
if (currentSortColumn === 'date') {
valA = new Date(valA);
valB = new Date(valB);
} else if (currentSortColumn === 'status') {
// WIN comes before LOSS for consistency
const statusA = a.profit >= 0 ? 0 : 1;
const statusB = b.profit >= 0 ? 0 : 1;
return sortDirection === 'asc' ? statusA - statusB : statusB - statusA;
} else if (typeof valA === 'number' && typeof valB === 'number') {
// Handle null/undefined for numeric columns
valA = valA ?? (sortDirection === 'asc' ? Infinity : -Infinity);
valB = valB ?? (sortDirection === 'asc' ? Infinity : -Infinity);
} else {
// String comparison
valA = valA ?? '';
valB = valB ?? '';
}
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
sortedResults.forEach((trade, idx) => {
const tr = document.createElement('tr');
const isPositive = trade.profit >= 0;
const retStr = (trade.ret >= 0 ? '+' : '') + trade.ret.toFixed(2) + '%';
tr.innerHTML = `
<td><strong>${trade.symbol}</strong></td>
<td>${trade.date}</td>
<td><span class="badge" style="background:${isPositive ? 'rgba(74, 222, 128, 0.1)' : 'rgba(248, 113, 113, 0.1)'}; color:${isPositive ? 'var(--positive)' : 'var(--negative)'}">${isPositive ? 'WIN' : 'LOSS'}</span></td>
<td class="${isPositive ? 'positive' : 'negative'}">${retStr}</td>
<td class="${isPositive ? 'positive' : 'negative'}">$${trade.profit.toFixed(0)}</td>
<td>${trade.entry.toFixed(2)} (${trade.in})</td>
<td>${trade.exit.toFixed(2)} (${trade.out})</td>
<td><span class="badge">${trade.sector || '--'}</span></td>
<td>${(trade.gap_pct || 0).toFixed(1)}%</td>
`;
tr.onclick = () => toggleRowChart(idx, trade);
tbody.appendChild(tr);
});
}
function toggleRowChart(idx, trade) {
const tbody = document.getElementById('trade-table-body');
const existing = document.getElementById(`chart-row-${idx}`);
if (existing) {
existing.remove();
return;
}
// Close other charts first? Maybe not, user might want to compare.
const chartTr = document.createElement('tr');
chartTr.id = `chart-row-${idx}`;
chartTr.className = 'chart-row';
chartTr.innerHTML = `
<td colspan="9">
<div class="expanded-chart-container" id="expanded-chart-${idx}"></div>
</td>
`;
// Insert after the clicked tr
const rows = tbody.querySelectorAll('tr:not(.chart-row)');
rows[idx].after(chartTr);
renderTradeChart(trade, `expanded-chart-${idx}`);
}
async function renderTradeChart(trade, containerId) {
const container = document.getElementById(containerId);
if (!container) return;
try {
const response = await fetch(`/api/scan?symbols=${trade.symbol}&timeframe=1m&start=${trade.date}&end=${trade.date}`);
const result = await response.json();
const item = result.results?.find(r => r.symbol === trade.symbol);
if (!item || !item.daily_bars || item.daily_bars.length === 0) {
container.innerHTML = '<div class="no-chart-data" style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:0.75rem;">No chart data</div>';
return;
}
const bars = item.daily_bars;
const candleData = bars.map(b => {
const unixTs = typeof b.time === 'number' ? b.time : new Date(b.time).getTime() / 1000;
return {
time: toNyTimestamp(unixTs),
open: b.open,
high: b.high,
low: b.low,
close: b.close
};
});
const chart = LightweightCharts.createChart(container, {
width: container.clientWidth,
height: container.clientHeight || 240,
layout: { background: { color: '#000000' }, textColor: '#666666', fontSize: 10 },
grid: { vertLines: { color: '#222222' }, horzLines: { color: '#222222' } },
rightPriceScale: { borderColor: '#222222', autoScale: true },
timeScale: { borderColor: '#222222', timeVisible: true, secondsVisible: false, timeZone: 'America/New_York', barSpacing: 6 }
});
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#4ade80',
downColor: '#f87171',
borderUpColor: '#4ade80',
borderDownColor: '#f87171',
wickUpColor: '#4ade80',
wickDownColor: '#f87171'
});
candlestickSeries.setData(candleData);
const toNYTimeString = (unixSec) => {
const date = new Date(unixSec * 1000);
const h = String(date.getUTCHours()).padStart(2, '0');
const m = String(date.getUTCMinutes()).padStart(2, '0');
return `${h}:${m}`;
};
const entryTime = candleData.find(b => toNYTimeString(b.time) === trade.in);
const exitTime = candleData.find(b => toNYTimeString(b.time) === trade.out);
if (entryTime && exitTime) {
candlestickSeries.setMarkers([
{ time: entryTime.time, position: 'belowBar', color: '#4ade80', shape: 'arrowUp', text: 'Entry' },
{ time: exitTime.time, position: 'aboveBar', color: '#f87171', shape: 'arrowDown', text: 'Exit' }
]);
const PADDING = 30;
const entryIdx = candleData.indexOf(entryTime);
const exitIdx = exitTime ? candleData.indexOf(exitTime) : entryIdx;
const fromIdx = Math.max(0, entryIdx - PADDING);
const toIdx = Math.min(candleData.length - 1, exitIdx + PADDING);
chart.timeScale().setVisibleLogicalRange({ from: fromIdx, to: toIdx });
} else {
chart.timeScale().fitContent();
}
const resizeObserver = new ResizeObserver(entries => {
if (entries[0]) {
chart.applyOptions({ width: entries[0].contentRect.width });
}
});
resizeObserver.observe(container);
} catch (e) {
console.error('Chart error:', e);
container.innerHTML = '<div class="no-chart-data" style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:0.75rem;">Error</div>';
}
}
function exportToCsv() {
if (!backtestResults || backtestResults.length === 0) return;
// Sort by date ascending (oldest first) for CSV export
const sortedResults = [...backtestResults].sort((a, b) => {
return new Date(a.date) - new Date(b.date);
});
// Get original headers from data
const originalHeaders = Object.keys(sortedResults[0]);
// Build header mapping: original -> new name (or keep original if no mapping)
const headerMap = {};
originalHeaders.forEach(h => {
headerMap[h] = backtestColumnMap[h] || h;
});
// Use new headers in order (symbol first, then mapped columns)
const headers = originalHeaders.map(h => headerMap[h]);
const rows = sortedResults.map(trade => {
return originalHeaders.map(header => {
let val = trade[header];
if (val === undefined || val === null) val = "";
// Format numbers
if (typeof val === 'number' && !Number.isInteger(val)) {
val = val.toFixed(4);
}
// Escape commas if string
if (typeof val === 'string' && val.includes(',')) {
val = `"${val}"`;
}
return val;
}).join(',');
});
const csvContent = [headers.join(','), ...rows].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `backtest_${new Date().toISOString().split('T')[0]}_trades.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function renderEquityChart(data) {
const container = document.getElementById('equity-chart');
const wrapper = document.getElementById('equity-chart-container');
if (!container || !data || data.length === 0) return;
wrapper.style.display = 'block';
container.innerHTML = '';
const chart = LightweightCharts.createChart(container, {
width: container.clientWidth,
height: 180,
layout: { background: { color: '#000000' }, textColor: '#666666', fontSize: 10 },
grid: { vertLines: { color: '#111111' }, horzLines: { color: '#111111' } },
rightPriceScale: { borderColor: '#222222', autoScale: true },
timeScale: { borderColor: '#222222' }
});
const lineSeries = chart.addLineSeries({
color: '#3b82f6',
lineWidth: 2,
title: 'Equity'
});
const chartData = data.map(d => ({
time: d.date,
value: d.equity
}));
lineSeries.setData(chartData);
chart.timeScale().fitContent();
const resizeObserver = new ResizeObserver(entries => {
if (entries[0]) {
chart.applyOptions({ width: entries[0].contentRect.width });
}
});
resizeObserver.observe(container);
}
function createTradeCard(trade) {
const card = document.createElement('div');
card.className = 'trade-card';
const isPositive = trade.profit >= 0;
const retStr = (trade.ret >= 0 ? '+' : '') + trade.ret.toFixed(2) + '%';
card.innerHTML = `
<div class="trade-header">
<span class="trade-symbol">${trade.symbol}</span>
<span class="trade-pnl ${isPositive ? 'positive' : 'negative'}">${retStr}</span>
</div>
<div class="trade-chart" id="chart-${trade.symbol}-${trade.date}"></div>
<div class="trade-details">
<div class="trade-detail">
<div class="trade-detail-label">Entry</div>
<div class="trade-detail-value">${trade.entry.toFixed(2)}</div>
</div>
<div class="trade-detail">
<div class="trade-detail-label">Exit</div>
<div class="trade-detail-value">${trade.exit.toFixed(2)}</div>
</div>
<div class="trade-detail">
<div class="trade-detail-label">In Time</div>
<div class="trade-detail-value">${trade.in}</div>
</div>
<div class="trade-detail">
<div class="trade-detail-label">Out Time</div>
<div class="trade-detail-value">${trade.out}</div>
</div>
</div>
`;
return card;
}
function restoreCachedBacktest() {
const cached = sessionStorage.getItem('backtest_last_result');
if (cached) {
try {
const result = JSON.parse(cached);
if (result && result.trades) {
backtestResults = result.trades;
backtestColumnMap = result.column_map || {};
// Also save candidates for stats
const candidates = (result.trades || [])
.filter(t => t.symbol && t.date)
.map(t => ({symbol: t.symbol, date: t.date}));
sessionStorage.setItem('last_candidates', JSON.stringify(candidates));
// Update summary
const summary = result.summary;
document.getElementById('win-rate').textContent = summary.win_rate + '%';
document.getElementById('profit-factor').textContent = summary.profit_factor;
document.getElementById('expectancy').textContent = (summary.expectancy >= 0 ? '+' : '') + '$' + summary.expectancy.toFixed(2);
document.getElementById('avg-win-loss').textContent = `$${summary.avg_win.toFixed(0)} / $${Math.abs(summary.avg_loss).toFixed(0)}`;
document.getElementById('max-drawdown').textContent = summary.max_drawdown + '%';
const profitEl = document.getElementById('total-profit');
profitEl.textContent = (summary.total_profit >= 0 ? '+' : '') + '$' + summary.total_profit.toFixed(2);
document.getElementById('card-profit').className = 'summary-card ' + (summary.total_profit >= 0 ? 'positive' : 'negative');
const equityEl = document.getElementById('final-equity');
equityEl.textContent = '$' + summary.final_equity.toLocaleString();
equityEl.parentElement.className = 'summary-card ' + (summary.final_equity >= summary.initial_capital ? 'positive' : 'negative');
document.getElementById('summary-bar').style.display = 'grid';
document.getElementById('view-controls').style.display = 'flex';
document.getElementById('status').textContent = 'Backtest results restored from cache';
if (result.equity_curve && result.equity_curve.length > 0) {
renderEquityChart(result.equity_curve);
}
document.getElementById('export-btn').style.display = 'block';
renderResults();
}
} catch (e) {
console.error('Failed to restore cached backtest:', e);
}
}
}
restoreCachedBacktest();
document.getElementById('symbols-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') runBacktest();
});
</script>
<script src="/static/js/nav.js"></script>
</body>
</html>