// ── Backtester — full port of TBS backtester with Graphite Ledger skin ── const TBS_BASE = 'https://tbs.dagevos.cloud'; const CRYPTO_ASSETS = [ { v:'BTCUSDT', l:'BTC — Bitcoin' }, { v:'ETHUSDT', l:'ETH — Ethereum' }, { v:'XRPUSDT', l:'XRP — XRP' }, { v:'BNBUSDT', l:'BNB — BNB' }, { v:'SOLUSDT', l:'SOL — Solana' }, { v:'DOGEUSDT', l:'DOGE — Dogecoin' }, { v:'ADAUSDT', l:'ADA — Cardano' }, { v:'AVAXUSDT', l:'AVAX — Avalanche' }, { v:'LINKUSDT', l:'LINK — Chainlink' }, { v:'NEARUSDT', l:'NEAR — NEAR Protocol' }, { v:'UNIUSDT', l:'UNI — Uniswap' }, { v:'ARBUSDT', l:'ARB — Arbitrum' }, { v:'OPUSDT', l:'OP — Optimism' }, { v:'SUIUSDT', l:'SUI — Sui' }, { v:'INJUSDT', l:'INJ — Injective' }, { v:'ATOMUSDT', l:'ATOM — Cosmos' }, { v:'PEPEUSDT', l:'PEPE — Pepe' }, { v:'TAOUSDT', l:'TAO — Bittensor' }, { v:'HYPEUSDT', l:'HYPE — Hyperliquid' },{ v:'DOTUSDT', l:'DOT — Polkadot' }, { v:'TRXUSDT', l:'TRX — TRON' }, { v:'SHIBUSDT', l:'SHIB — Shiba Inu' }, { v:'BCHUSDT', l:'BCH — Bitcoin Cash' },{ v:'LTCUSDT', l:'LTC — Litecoin' }, { v:'APTUSDT', l:'APT — Aptos' }, { v:'HBARUSDT', l:'HBAR — Hedera' }, { v:'ICPUSDT', l:'ICP — Internet Computer' },{ v:'SEIUSDT', l:'SEI — Sei' }, { v:'WIFUSDT', l:'WIF — Dogwifhat' }, { v:'FILUSDT', l:'FIL — Filecoin' }, { v:'STXUSDT', l:'STX — Stacks' }, { v:'PENGUUSDT',l:'PENGU — Pudgy Penguins' }, { v:'TONUSDT', l:'TON — Toncoin' }, ]; const TRADFI_ASSETS = [ { v:'SPX', l:'SPX — S&P 500' }, { v:'NASDAQ', l:'NASDAQ — Nasdaq' }, { v:'GOLD', l:'GOLD — Gold' }, { v:'SILVER', l:'SILVER — Silver' }, ]; const ALL_ASSETS = [...CRYPTO_ASSETS, ...TRADFI_ASSETS]; const aLabel = v => { const a = ALL_ASSETS.find(x => x.v === v); return a ? a.l.split(' — ')[0] : v; }; const fmtPct = v => (v >= 0 ? '+' : '') + v.toFixed(2) + '%'; const retColor = v => v >= 50 ? TH.green : v >= 0 ? TH.inkSoft : TH.red; const retColorStr = v => v >= 500 ? TH.green : v >= 100 ? TH.green : v >= 0 ? TH.inkSoft : TH.red; const METRIC_LABELS = { calmar:'Calmar', totalReturn:'Total Return', cagr:'CAGR', sharpe:'Sharpe', lowDD:'Max DD ↓' }; function scoreDisplayStr(r, metric) { if (!isFinite(r.score) || r.liquidated) return '—'; switch (metric) { case 'totalReturn': return fmtPct(r.totalReturn); case 'cagr': return fmtPct(r.cagr); case 'sharpe': return r.sharpe?.toFixed(3) ?? '—'; case 'lowDD': return r.maxDrawdown?.toFixed(1) + '%'; default: return r.score?.toFixed(2) ?? '—'; } } // ── Micro-components ───────────────────────────────────────────── function SLabel({ children }) { return
{children}
; } function SField({ label, style = {}, children }) { return
{label && {label}}{children}
; } function SSelect({ value, onChange, children, style = {} }) { return ( ); } function SInput({ value, onChange, ...props }) { return ( onChange(e.target.value)} style={{ width: '100%', background: TH.bg, border: `1px solid ${TH.rule}`, color: TH.ink, fontFamily: TH.mono, fontSize: 12, padding: '7px 10px', outline: 'none', boxSizing: 'border-box' }} onFocus={e => e.target.style.borderColor = TH.accent} onBlur={e => e.target.style.borderColor = TH.rule} {...props} /> ); } function AssetSelect({ value, onChange }) { return ( {CRYPTO_ASSETS.map(a => )} {TRADFI_ASSETS.map(a => )} ); } function MaTypeSel({ value, onChange }) { return ; } function DirSel({ value, onChange }) { return ( ); } function MetricSel({ value, onChange }) { return ( ); } function LevPair({ longVal, onLong, shortVal, onShort }) { return (
); } function AccentLabel({ children }) { return
{children}
; } function RunBtn({ onClick, loading, disabled, children, style = {} }) { return ( ); } function SecBtn({ onClick, loading, disabled, children, style = {} }) { return ( ); } function MetricRow({ label, value, color }) { return (
{label}
{value}
); } // ── Optimize best-params grid ───────────────────────────────────── function OptBestGrid({ cells }) { return (
{cells.map((c, i) => (
{c.label}
{c.value}
))}
); } // ── Reusable result table ───────────────────────────────────────── function ResultTable({ head, rows, activeIdx, onRowClick }) { return (
{head.map((h, i) => ( ))} {rows.map((r, i) => ( onRowClick && onRowClick(i, r)} style={{ borderBottom: `1px solid ${TH.rule}`, cursor: onRowClick ? 'pointer' : 'default', background: i === activeIdx ? TH.panelHi : i === 0 ? 'rgba(212,168,75,0.04)' : 'transparent', transition: 'background .1s' }} onMouseEnter={e => { if (i !== activeIdx) e.currentTarget.style.background = TH.panelHi; }} onMouseLeave={e => { e.currentTarget.style.background = i === activeIdx ? TH.panelHi : i === 0 ? 'rgba(212,168,75,0.04)' : 'transparent'; }}> {r.cells.map((cell, j) => ( ))} ))}
{h}
{cell.text} {i === 0 && j === 0 && BEST}
); } // ── Trade log ───────────────────────────────────────────────────── function TradeLog({ trades }) { if (!trades || trades.length === 0) return null; const wins = trades.filter(t => !t.open && t.returnPct > 0).length; const closed = trades.filter(t => !t.open).length; const wr = closed > 0 ? ((wins / closed) * 100).toFixed(0) + '%' : '—'; const hardHits = trades.filter(t => t.stoppedOut && t.hardStop).length; const closeHits = trades.filter(t => t.stoppedOut && !t.hardStop).length; const slParts = []; if (hardHits > 0) slParts.push(`${hardHits} hard`); if (closeHits > 0) slParts.push(`${closeHits} close`); const slStr = slParts.length > 0 ? ` · ${slParts.join(', ')} SL` : ''; const fmtPrice = v => { if (v == null) return '—'; const n = Number(v); if (n === 0) return '$0'; const dec = n < 0.0001 ? 8 : n < 0.01 ? 6 : n < 1 ? 4 : 2; return '$' + n.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec }); }; return (
◉ TRADE LOG
{trades.length} trades · {wr} WR{slStr}
{['#','Entry','Exit','Dir','Entry Px','Exit Px','Return','Days','Status'].map((h,i) => ( ))} {[...trades].reverse().map((t, idx) => { const n = trades.length - idx; const ret = t.returnPct; const retStr = (ret >= 0 ? '+' : '') + ret.toFixed(2) + '%'; const holdDays = t.entryDate && t.exitDate ? Math.round((new Date(t.exitDate) - new Date(t.entryDate)) / 86400000) : '—'; let statusEl, statusColor; if (t.liquidated) { statusEl = 'LIQUIDATED'; statusColor = TH.red; } else if (t.stoppedOut && t.hardStop) { statusEl = 'HARD STOP'; statusColor = TH.red; } else if (t.stoppedOut) { statusEl = 'STOPPED OUT'; statusColor = '#f97316'; } else if (t.open) { statusEl = 'OPEN'; statusColor = '#818cf8'; } else { statusEl = 'Closed'; statusColor = TH.muted; } return ( ); })}
{h}
{n} {t.entryDate || '—'} {t.exitDate || '—'} {(t.direction || '').toUpperCase()} {fmtPrice(t.entryPrice)} {fmtPrice(t.exitPrice)} 0 ? TH.green : ret < 0 ? TH.red : TH.ink, fontWeight: 600 }}>{retStr} {holdDays}d {statusEl}
); } // ── Main component ──────────────────────────────────────────────── function Backtester({ go }) { const wide = useWide(); // Auth const [token, setToken] = React.useState(null); const [authErr, setAuthErr] = React.useState(false); // Mode const [mode, setMode] = React.useState('sweep'); const [overallSub, setOverallSub] = React.useState('sweep'); // Shared period const [period, setPeriod] = React.useState('5Y'); const [dateFrom, setDateFrom] = React.useState(''); const [dateTo, setDateTo] = React.useState(''); // Sweep const [asset, setAsset] = React.useState('BTCUSDT'); const [maType, setMaType] = React.useState('SMA'); const [direction, setDirection] = React.useState('long'); const [levLong, setLevLong] = React.useState(1.0); const [levShort, setLevShort] = React.useState(1.0); const [specificMA, setSpecificMA] = React.useState(''); // Single const [sAsset, setSAsset] = React.useState('BTCUSDT'); const [sMaType, setSMaType] = React.useState('SMA'); const [sMaPeriod, setSMaPeriod] = React.useState(44); const [sTradeAsset, setSTradeAsset] = React.useState('ETHUSDT'); const [sDir, setSDir] = React.useState('long'); const [sLevLong, setSLevLong] = React.useState(1.0); const [sLevShort, setSLevShort] = React.useState(1.0); // Dual const [d1Asset, setD1Asset] = React.useState('ETHUSDT'); const [d1MaType, setD1MaType] = React.useState('SMA'); const [d1Period, setD1Period] = React.useState(34); const [d2Asset, setD2Asset] = React.useState('BTCUSDT'); const [d2MaType, setD2MaType] = React.useState('SMA'); const [d2Period, setD2Period] = React.useState(44); const [dTrade, setDTrade] = React.useState('BTCUSDT'); const [dDir, setDDir] = React.useState('long'); const [dLevLong, setDLevLong] = React.useState(1.0); const [dLevShort, setDLevShort] = React.useState(1.0); // Crossover const [xAsset, setXAsset] = React.useState('BTCUSDT'); const [xMaType, setXMaType] = React.useState('SMA'); const [xFast, setXFast] = React.useState(20); const [xSlow, setXSlow] = React.useState(50); const [xDir, setXDir] = React.useState('long'); const [xLevLong, setXLevLong] = React.useState(1.0); const [xLevShort, setXLevShort] = React.useState(1.0); // Overall sweep params const [oAssets, setOAssets] = React.useState(() => { const on = new Set(['BTCUSDT','ETHUSDT','SOLUSDT','SPX','NASDAQ','GOLD','SILVER']); return Object.fromEntries(ALL_ASSETS.map(a => [a.v, on.has(a.v)])); }); const [oPeriod, setOPeriod] = React.useState('5Y'); const [oMaType, setOMaType] = React.useState('SMA'); const [oDir, setODir] = React.useState('long'); const [oLevLong, setOLevLong] = React.useState(1.0); const [oLevShort, setOLevShort] = React.useState(1.0); const [oMetric, setOMetric] = React.useState('calmar'); // Overall dual params const [odMaType1, setOdMaType1] = React.useState('SMA'); const [odPeriod1, setOdPeriod1] = React.useState(34); const [odMaType2, setOdMaType2] = React.useState('SMA'); const [odPeriod2, setOdPeriod2] = React.useState(44); const [odDir, setOdDir] = React.useState('long'); const [odMetric, setOdMetric] = React.useState('calmar'); // SL / win-skip const [slType, setSlType] = React.useState('none'); const [slPct, setSlPct] = React.useState(5); const [hardSlType, setHardSlType] = React.useState('none'); const [hardSlPct, setHardSlPct] = React.useState(3); const [slOpen, setSlOpen] = React.useState(false); const [winSkipCount, setWinSkipCount] = React.useState(0); const [winSkipMinPct,setWinSkipMinPct]= React.useState(10); // Optimize metric const [optMetric, setOptMetric] = React.useState('calmar'); // Loading const [loading, setLoading] = React.useState(false); const [optLoading, setOptLoading] = React.useState(false); const [slLoading, setSlLoading] = React.useState(false); const [skipLoading, setSkipLoading] = React.useState(false); const [overallLoading, setOverallLoading] = React.useState(false); // Status / error const [statusMsg, setStatusMsg] = React.useState(''); const [error, setError] = React.useState(''); // Results const [sweepData, setSweepData] = React.useState(null); const [metrics, setMetrics] = React.useState(null); const [chartTitle, setChartTitle] = React.useState(''); const [chartSub, setChartSub] = React.useState(''); const [sortCol, setSortCol] = React.useState('totalReturn'); const [sortAsc, setSortAsc] = React.useState(false); const [activeRow, setActiveRow] = React.useState(null); const [trades, setTrades] = React.useState(null); const [optimizeResult,setOptimizeResult]= React.useState(null); // {data, mode, metric, activeIdx} const [slResult, setSlResult] = React.useState(null); // {data, metric, activeIdx} const [skipResult, setSkipResult] = React.useState(null); // {data, metric, activeIdx} // Refs for async-safe param snapshots const currentParamsRef = React.useRef(null); const currentOptParamsRef = React.useRef(null); // Chart const canvasRef = React.useRef(null); const chartRef = React.useRef(null); React.useEffect(() => () => { chartRef.current?.destroy(); }, []); // Auth React.useEffect(() => { fetch(`${TBS_BASE}/auth/guest`, { method: 'POST' }) .then(r => r.json()) .then(d => d.token ? setToken(d.token) : setAuthErr(true)) .catch(() => setAuthErr(true)); }, []); const authFetch = React.useCallback((path, opts = {}) => fetch(`${TBS_BASE}${path}`, { ...opts, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...(opts.headers || {}) }, }), [token]); // Draw chart const drawChart = React.useCallback((equityCurve, bah, label) => { if (!canvasRef.current || !window.Chart) return; chartRef.current?.destroy(); chartRef.current = null; const ctx = canvasRef.current.getContext('2d'); chartRef.current = new window.Chart(ctx, { type: 'line', data: { labels: equityCurve.map(p => p.date), datasets: [ { label, data: equityCurve.map(p => p.equity), borderColor: TH.accent, backgroundColor: 'rgba(212,168,75,0.06)', borderWidth: 2, pointRadius: 0, fill: true, tension: 0.1 }, { label: 'Buy & Hold', data: (bah || []).map(p => p.equity), borderColor: '#5f5a4e', borderDash: [5,5], borderWidth: 1.5, pointRadius: 0, fill: false, tension: 0.1 }, ], }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: TH.inkSoft, font: { family: 'JetBrains Mono, monospace', size: 11 }, boxWidth: 12, padding: 16 } }, tooltip: { backgroundColor: TH.panel, borderColor: TH.rule, borderWidth: 1, titleColor: TH.muted, bodyColor: TH.ink, titleFont: { family: 'JetBrains Mono, monospace', size: 10 }, bodyFont: { family: 'JetBrains Mono, monospace', size: 12 }, callbacks: { label: ctx => ` ${ctx.dataset.label}: ${ctx.parsed.y >= 0 ? '+' : ''}${ctx.parsed.y.toFixed(1)}%` }, }, }, scales: { x: { ticks: { color: TH.muted, font: { family: 'JetBrains Mono, monospace', size: 10 }, maxTicksLimit: 8 }, grid: { color: TH.rule } }, y: { ticks: { color: TH.muted, font: { family: 'JetBrains Mono, monospace', size: 10 }, callback: v => (v >= 0 ? '+' : '') + v.toFixed(0) + '%' }, grid: { color: TH.rule } }, }, }, }); }, []); const getSLConfig = () => ({ slType, slPct: slPct / 100, hardSlType, hardSlPct: hardSlPct / 100, winSkipCount, winSkipMinPct, }); const buildMetrics = data => ({ totalReturn: data.totalReturn, cagr: data.cagr, maxDrawdown: data.maxDrawdown, sharpe: data.sharpe, tradeCount: data.tradeCount, liquidated: data.liquidated, vsBah: data.totalReturn - (data.buyAndHold?.totalReturn ?? 0), }); const metricsFromCurve = (curve, bah, trds) => { if (!curve?.length) return null; const start = curve[0].equity, end = curve[curve.length - 1].equity; const totalReturn = end - 100; let peak = start, maxDD = 0; for (const p of curve) { if (p.equity > peak) peak = p.equity; const dd = (peak - p.equity) / peak * 100; if (dd > maxDD) maxDD = dd; } const ms = new Date(curve[curve.length-1].date) - new Date(curve[0].date); const years = ms / (365.25 * 86400000); const cagr = years > 0 ? (Math.pow(end / start, 1 / years) - 1) * 100 : totalReturn; const bahReturn = (Array.isArray(bah) ? bah : [])?.slice(-1)[0]?.equity - 100 || 0; return { totalReturn, cagr, maxDrawdown: maxDD, sharpe: null, tradeCount: trds?.length ?? 0, liquidated: false, vsBah: totalReturn - bahReturn }; }; const clearResults = () => { setSweepData(null); setMetrics(null); setTrades(null); setOptimizeResult(null); setSlResult(null); setSkipResult(null); setChartTitle(''); setChartSub(''); setError(''); setStatusMsg(''); setActiveRow(null); currentParamsRef.current = null; currentOptParamsRef.current = null; }; const changeMode = m => { setMode(m); clearResults(); }; // ── Run backtest ─────────────────────────────────────────────── const run = async () => { if (!token) return; setLoading(true); setError(''); setStatusMsg('Running…'); setOptimizeResult(null); setSlResult(null); setSkipResult(null); const t0 = Date.now(); try { const sl = getSLConfig(); if (mode === 'sweep') { const sp = parseInt(specificMA); if (sp >= 4 && sp <= 240) { const qp = new URLSearchParams({ asset, period, ma: sp, type: maType, leverageLong: levLong, leverageShort: levShort, direction, ...sl }); if (period === 'CUSTOM') { qp.set('dateFrom', dateFrom); qp.set('dateTo', dateTo); } const res = await authFetch(`/api/backtest/equity?${qp}`); const d = await res.json(); if (!res.ok) throw new Error(d.error); const m = metricsFromCurve(d.equityCurve, d.buyAndHold, d.trades); currentParamsRef.current = { asset, period, dateFrom, dateTo, maType, leverageLong: +levLong, leverageShort: +levShort, direction, bestPeriod: sp, bahReturn: m?.vsBah != null ? m.totalReturn - m.vsBah : 0 }; setChartTitle(`${maType} ${sp} — Equity Curve`); setChartSub(`${aLabel(asset)} · ${period === 'CUSTOM' ? `${dateFrom}→${dateTo}` : period} · L${levLong}x/S${levShort}x · ${direction}`); drawChart(d.equityCurve, d.buyAndHold, `${maType} ${sp}`); setMetrics(m); setTrades(d.trades); setSweepData(null); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · ${d.trades?.length ?? 0} trades`); } else { const body = { asset, period, maType, leverageLong: +levLong, leverageShort: +levShort, direction, ...sl }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } const res = await authFetch('/api/backtest', { method: 'POST', body: JSON.stringify(body) }); const d = await res.json(); if (!res.ok) throw new Error(d.error); const best = d.results.find(r => r.period === d.bestPeriod); currentParamsRef.current = { asset, period, dateFrom, dateTo, maType, leverageLong: +levLong, leverageShort: +levShort, direction, bestPeriod: d.bestPeriod, bahReturn: d.buyAndHold.totalReturn }; setSweepData(d); setChartTitle(`${maType} ${d.bestPeriod} — Best Equity Curve`); setChartSub(`${aLabel(asset)} · ${period === 'CUSTOM' ? `${dateFrom}→${dateTo}` : period} · L${levLong}x/S${levShort}x · ${direction} · best of ${d.results.length} periods`); drawChart(d.equityCurveBest, d.buyAndHold.equityCurve, `${maType} ${d.bestPeriod} (Best)`); setActiveRow(d.bestPeriod); setMetrics(buildMetrics({ ...best, buyAndHold: d.buyAndHold })); setTrades(d.tradesBest); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · Best: ${maType} ${d.bestPeriod} · ${d.results.length} periods`); } } else if (mode === 'single') { const body = { signalAsset: sAsset, maType: sMaType, maPeriod: +sMaPeriod, tradeAsset: sTradeAsset, timePeriod: period, direction: sDir, leverageLong: +sLevLong, leverageShort: +sLevShort, ...sl }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } const res = await authFetch('/api/backtest/single', { method: 'POST', body: JSON.stringify(body) }); const d = await res.json(); if (!res.ok) throw new Error(d.error); const label = `${aLabel(sTradeAsset)} via ${aLabel(sAsset)} ${sMaType}${sMaPeriod}`; setChartTitle(label); setChartSub(`${period === 'CUSTOM' ? `${dateFrom}→${dateTo}` : period} · ${sDir} · L${sLevLong}x/S${sLevShort}x`); drawChart(d.equityCurve, d.buyAndHold.equityCurve, label); setMetrics(buildMetrics(d)); setTrades(d.trades); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · ${d.tradeCount} trades`); } else if (mode === 'dual') { const body = { asset1: d1Asset, maType1: d1MaType, maPeriod1: +d1Period, asset2: d2Asset, maType2: d2MaType, maPeriod2: +d2Period, tradeAsset: dTrade, timePeriod: period, direction: dDir, leverageLong: +dLevLong, leverageShort: +dLevShort, ...sl }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } const res = await authFetch('/api/backtest/dual', { method: 'POST', body: JSON.stringify(body) }); const d = await res.json(); if (!res.ok) throw new Error(d.error); currentOptParamsRef.current = { asset1: d1Asset, maType1: d1MaType, maPeriod1: +d1Period, asset2: d2Asset, maType2: d2MaType, maPeriod2: +d2Period, tradeAsset: dTrade, timePeriod: period, dateFrom, dateTo, direction: dDir }; const label = `${aLabel(dTrade)}: ${aLabel(d1Asset)} ${d1MaType}${d1Period} + ${aLabel(d2Asset)} ${d2MaType}${d2Period}`; setChartTitle(label); setChartSub(`${period === 'CUSTOM' ? `${dateFrom}→${dateTo}` : period} · ${dDir} · L${dLevLong}x/S${dLevShort}x`); drawChart(d.equityCurve, d.buyAndHold.equityCurve, label); setMetrics(buildMetrics(d)); setTrades(d.trades); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · ${d.tradeCount} trades`); } else if (mode === 'crossover') { const body = { asset: xAsset, timePeriod: period, maType: xMaType, fastPeriod: +xFast, slowPeriod: +xSlow, direction: xDir, leverageLong: +xLevLong, leverageShort: +xLevShort, ...sl }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } const res = await authFetch('/api/backtest/crossover', { method: 'POST', body: JSON.stringify(body) }); const d = await res.json(); if (!res.ok) throw new Error(d.error); currentOptParamsRef.current = { asset: xAsset, maType: xMaType, timePeriod: period, dateFrom, dateTo, direction: xDir }; const label = `${aLabel(xAsset)} ${xMaType} ${xFast}/${xSlow} Crossover`; setChartTitle(label); setChartSub(`${period === 'CUSTOM' ? `${dateFrom}→${dateTo}` : period} · ${xDir} · L${xLevLong}x/S${xLevShort}x`); drawChart(d.equityCurve, d.buyAndHold.equityCurve, label); setMetrics(buildMetrics(d)); setTrades(d.trades); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · ${d.tradeCount} trades`); } } catch (e) { setError(e.message || 'Backtest failed'); } finally { setLoading(false); } }; // ── Sweep row click ──────────────────────────────────────────── const loadRowCurve = async rowPeriod => { const p = currentParamsRef.current; if (!p || !token) return; const sl = getSLConfig(); const qp = new URLSearchParams({ asset: p.asset, period: p.period, ma: rowPeriod, type: p.maType, leverageLong: p.leverageLong, leverageShort: p.leverageShort, direction: p.direction, ...sl }); if (p.period === 'CUSTOM') { qp.set('dateFrom', p.dateFrom); qp.set('dateTo', p.dateTo); } try { const res = await authFetch(`/api/backtest/equity?${qp}`); const d = await res.json(); if (!res.ok) throw new Error(d.error); setChartTitle(`${p.maType} ${rowPeriod} — Equity Curve`); setChartSub(`${aLabel(p.asset)} · ${p.period === 'CUSTOM' ? `${p.dateFrom}→${p.dateTo}` : p.period} · L${p.leverageLong}x/S${p.leverageShort}x · ${p.direction}`); drawChart(d.equityCurve, d.buyAndHold, `${p.maType} ${rowPeriod}`); setActiveRow(rowPeriod); setMetrics(metricsFromCurve(d.equityCurve, d.buyAndHold, d.trades)); setTrades(d.trades); } catch (e) { console.error('curve load failed', e); } }; // ── Find Best (optimize) ─────────────────────────────────────── const runOptimize = async () => { if (!token) return; setOptLoading(true); setError(''); setStatusMsg('Scanning…'); const t0 = Date.now(); try { let res, data, endpoint, body, optMode; if (mode === 'sweep') { endpoint = '/api/backtest/optimize'; body = { asset, period, maType, direction, metric: optMetric }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } optMode = 'sweep'; currentOptParamsRef.current = { asset, period, dateFrom, dateTo, maType, direction }; } else if (mode === 'dual') { endpoint = '/api/backtest/dual/optimize'; body = { asset1: d1Asset, maType1: d1MaType, asset2: d2Asset, maType2: d2MaType, tradeAsset: dTrade, timePeriod: period, direction: dDir, metric: optMetric }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } optMode = 'dual'; currentOptParamsRef.current = { asset1: d1Asset, maType1: d1MaType, asset2: d2Asset, maType2: d2MaType, tradeAsset: dTrade, timePeriod: period, dateFrom, dateTo, direction: dDir }; } else if (mode === 'crossover') { endpoint = '/api/backtest/crossover/optimize'; body = { asset: xAsset, timePeriod: period, maType: xMaType, direction: xDir, metric: optMetric }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } optMode = 'crossover'; currentOptParamsRef.current = { asset: xAsset, maType: xMaType, timePeriod: period, dateFrom, dateTo, direction: xDir }; } else return; res = await authFetch(endpoint, { method: 'POST', body: JSON.stringify(body) }); data = await res.json(); if (!res.ok) throw new Error(data.error); const b = data.best; const elChartLabel = optMode === 'crossover' ? `Crossover F${b.fastPeriod}/S${b.slowPeriod} (Opt)` : optMode === 'dual' ? 'Dual Opt' : `${b.maType} ${b.period} (Opt)`; drawChart(b.equityCurve, data.buyAndHold.equityCurve, elChartLabel); setChartTitle(optMode === 'crossover' ? `${xMaType} Crossover — Fast ${b.fastPeriod} / Slow ${b.slowPeriod} (Opt)` : optMode === 'dual' ? `Trade ${aLabel(dTrade)} — Dual Optimized` : `${b.maType} ${b.period} — Optimized Best`); setChartSub(`${METRIC_LABELS[optMetric]} optimized · ${data.results.length} combinations`); setOptimizeResult({ data, mode: optMode, metric: optMetric, activeIdx: 0 }); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · Best: ${optMode === 'crossover' ? `F${b.fastPeriod}/S${b.slowPeriod}` : optMode === 'dual' ? `MA1=${b.maPeriod1} MA2=${b.maPeriod2}` : `${b.maType} ${b.period}`}`); } catch (e) { setError(e.message || 'Optimize failed'); } finally { setOptLoading(false); } }; // ── Find Best SL ─────────────────────────────────────────────── const runFindBestSL = async () => { if (!token) return; setSlLoading(true); setError(''); setStatusMsg('Testing SL strategies…'); const t0 = Date.now(); try { let res, data, endpoint, body; if (mode === 'sweep') { const p = currentParamsRef.current; if (!p?.bestPeriod) throw new Error('Run a sweep first to identify the best MA period'); endpoint = '/api/backtest/optimize-sl/sweep'; body = { asset: p.asset, period: p.period, maType: p.maType, maPeriod: p.bestPeriod, leverageLong: p.leverageLong, leverageShort: p.leverageShort, direction: p.direction, metric: optMetric }; if (p.period === 'CUSTOM') { body.dateFrom = p.dateFrom; body.dateTo = p.dateTo; } } else if (mode === 'dual') { endpoint = '/api/backtest/optimize-sl/dual'; body = { asset1: d1Asset, maType1: d1MaType, maPeriod1: +d1Period, asset2: d2Asset, maType2: d2MaType, maPeriod2: +d2Period, tradeAsset: dTrade, timePeriod: period, direction: dDir, leverageLong: +dLevLong, leverageShort: +dLevShort, metric: optMetric }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } } else if (mode === 'crossover') { endpoint = '/api/backtest/optimize-sl/crossover'; body = { asset: xAsset, timePeriod: period, maType: xMaType, fastPeriod: +xFast, slowPeriod: +xSlow, direction: xDir, leverageLong: +xLevLong, leverageShort: +xLevShort, metric: optMetric }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } } else return; res = await authFetch(endpoint, { method: 'POST', body: JSON.stringify(body) }); data = await res.json(); if (!res.ok) throw new Error(data.error); const b = data.best; drawChart(b.equityCurve, data.buyAndHold.equityCurve, `SL: ${b.slLabel}`); setChartTitle(`Best Stop Loss: ${b.slLabel}`); setChartSub(`${METRIC_LABELS[optMetric]} optimized · ${data.results.length} SL types tested`); setSlResult({ data, metric: optMetric, activeIdx: 0 }); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · Best SL: ${b.slLabel}`); } catch (e) { setError(e.message || 'SL optimize failed'); } finally { setSlLoading(false); } }; // ── Find Best Skip ───────────────────────────────────────────── const runFindBestSkip = async () => { if (!token) return; const p = currentParamsRef.current; if (!p?.bestPeriod) { setError('Run a sweep first to get a best MA period'); return; } setSkipLoading(true); setError(''); setStatusMsg('Scanning win-skip…'); const t0 = Date.now(); try { const sl = getSLConfig(); const body = { asset: p.asset, period: p.period, maType: p.maType, maPeriod: p.bestPeriod, leverageLong: p.leverageLong, leverageShort: p.leverageShort, direction: p.direction, metric: optMetric, ...sl }; if (p.period === 'CUSTOM') { body.dateFrom = p.dateFrom; body.dateTo = p.dateTo; } const res = await authFetch('/api/backtest/optimize-skip/sweep', { method: 'POST', body: JSON.stringify(body) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); const b = data.best; drawChart(b.equityCurve, data.buyAndHold.equityCurve, `Skip: ${b.label}`); setChartTitle(`Best Win-Skip: ${b.label}`); setChartSub(`${METRIC_LABELS[optMetric]} optimized · ${data.results.length} combinations`); setSkipResult({ data, metric: optMetric, activeIdx: 0 }); setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · Best: ${b.label}`); } catch (e) { setError(e.message || 'Skip optimize failed'); } finally { setSkipLoading(false); } }; // ── Find Overall Best ────────────────────────────────────────── const runOverall = async () => { if (!token) return; setOverallLoading(true); setError(''); setStatusMsg('Scanning all assets…'); const t0 = Date.now(); const excludeAssets = ALL_ASSETS.map(a => a.v).filter(v => !oAssets[v]); try { let res, data, optMode; if (overallSub === 'sweep') { res = await authFetch('/api/backtest/overall-optimize', { method: 'POST', body: JSON.stringify({ period: oPeriod, maType: oMaType, direction: oDir, metric: oMetric, excludeAssets }), }); data = await res.json(); if (!res.ok) throw new Error(data.error); currentOptParamsRef.current = { period: oPeriod, maType: oMaType, direction: oDir }; optMode = 'overall-sweep'; const b = data.best; drawChart(b.equityCurve, data.buyAndHold.equityCurve, `${aLabel(b.asset)} ${b.maType} ${b.period}`); setChartTitle(`${aLabel(b.asset)} ${b.maType} ${b.period} — Global Best`); setChartSub(`${oPeriod} · L${b.leverageLong}x/S${b.leverageShort}x · ${oDir}`); } else { res = await authFetch('/api/backtest/dual/overall-optimize', { method: 'POST', body: JSON.stringify({ maType1: odMaType1, maPeriod1: +odPeriod1, maType2: odMaType2, maPeriod2: +odPeriod2, timePeriod: oPeriod, direction: odDir, metric: odMetric, excludeAssets }), }); data = await res.json(); if (!res.ok) throw new Error(data.error); currentOptParamsRef.current = { maType1: odMaType1, maPeriod1: +odPeriod1, maType2: odMaType2, maPeriod2: +odPeriod2, timePeriod: oPeriod, direction: odDir }; optMode = 'overall-dual'; const b = data.best; drawChart(b.equityCurve, data.buyAndHold.equityCurve, 'Global Dual Opt'); setChartTitle(`Trade ${aLabel(b.tradeAsset)} — Global Best Dual`); setChartSub(`${aLabel(b.asset1)}+${aLabel(b.asset2)} · L${b.leverageLong}x/S${b.leverageShort}x`); } setOptimizeResult({ data, mode: optMode, metric: overallSub === 'sweep' ? oMetric : odMetric, activeIdx: 0 }); const b = data.best; setStatusMsg(`Done ${((Date.now()-t0)/1000).toFixed(1)}s · Best: ${optMode === 'overall-sweep' ? `${aLabel(b.asset)} ${b.maType} ${b.period}` : `${aLabel(b.asset1)}+${aLabel(b.asset2)}→${aLabel(b.tradeAsset)}`}`); } catch (e) { setError(e.message || 'Overall optimize failed'); } finally { setOverallLoading(false); } }; // ── Optimize result row click ────────────────────────────────── const handleOptRowClick = async (idx, r) => { setOptimizeResult(prev => prev ? { ...prev, activeIdx: idx } : prev); const p = currentOptParamsRef.current; if (!p || !token) return; try { let res, d; const optMode = optimizeResult?.mode; if (optMode === 'sweep') { const qs = new URLSearchParams({ asset: p.asset, period: p.period, ma: r.period, type: p.maType, leverageLong: r.leverageLong, leverageShort: r.leverageShort, direction: p.direction }); res = await authFetch(`/api/backtest/equity?${qs}`); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold, `${p.maType} ${r.period}`); } else if (optMode === 'dual') { const body = { asset1: p.asset1, maType1: p.maType1, maPeriod1: r.maPeriod1, asset2: p.asset2, maType2: p.maType2, maPeriod2: r.maPeriod2, tradeAsset: p.tradeAsset, timePeriod: p.timePeriod, direction: p.direction, leverageLong: r.leverageLong, leverageShort: r.leverageShort }; if (p.timePeriod === 'CUSTOM') { body.dateFrom = p.dateFrom; body.dateTo = p.dateTo; } res = await authFetch('/api/backtest/dual', { method: 'POST', body: JSON.stringify(body) }); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold.equityCurve, `MA1=${r.maPeriod1} MA2=${r.maPeriod2}`); } else if (optMode === 'crossover') { const body = { asset: p.asset, timePeriod: p.timePeriod, maType: p.maType, fastPeriod: r.fastPeriod, slowPeriod: r.slowPeriod, direction: p.direction, leverageLong: r.leverageLong, leverageShort: r.leverageShort }; if (p.timePeriod === 'CUSTOM') { body.dateFrom = p.dateFrom; body.dateTo = p.dateTo; } res = await authFetch('/api/backtest/crossover', { method: 'POST', body: JSON.stringify(body) }); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold.equityCurve, `Crossover F${r.fastPeriod}/S${r.slowPeriod}`); } else if (optMode === 'overall-sweep') { const qs = new URLSearchParams({ asset: r.asset, period: p.period, ma: r.period, type: r.maType, leverageLong: r.leverageLong, leverageShort: r.leverageShort, direction: p.direction }); res = await authFetch(`/api/backtest/equity?${qs}`); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold, `${aLabel(r.asset)} ${r.maType} ${r.period}`); } else if (optMode === 'overall-dual') { const body = { asset1: r.asset1, maType1: p.maType1, maPeriod1: r.maPeriod1, asset2: r.asset2, maType2: p.maType2, maPeriod2: r.maPeriod2, tradeAsset: r.tradeAsset, timePeriod: p.timePeriod, direction: p.direction, leverageLong: r.leverageLong, leverageShort: r.leverageShort }; res = await authFetch('/api/backtest/dual', { method: 'POST', body: JSON.stringify(body) }); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold.equityCurve, `${aLabel(r.asset1)}+${aLabel(r.asset2)}→${aLabel(r.tradeAsset)}`); } } catch (e) { console.error('opt row click failed', e); } }; // ── SL result row click ──────────────────────────────────────── const handleSLRowClick = async (idx, r) => { setSlResult(prev => prev ? { ...prev, activeIdx: idx } : prev); const p = currentParamsRef.current; const op = currentOptParamsRef.current; if (!token) return; try { let res, d; const slCfg = { slType: r.slType, slPct: r.slPct, hardSlType: r.hardSlType ?? 'none', hardSlPct: r.hardSlPct }; if (mode === 'sweep' && p) { const qs = new URLSearchParams({ asset: p.asset, period: p.period, ma: p.bestPeriod, type: p.maType, leverageLong: p.leverageLong, leverageShort: p.leverageShort, direction: p.direction, ...slCfg }); if (p.period === 'CUSTOM') { qs.set('dateFrom', p.dateFrom); qs.set('dateTo', p.dateTo); } res = await authFetch(`/api/backtest/equity?${qs}`); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold, `SL: ${r.slLabel}`); setTrades(d.trades); } else if (mode === 'dual') { const body = { asset1: d1Asset, maType1: d1MaType, maPeriod1: +d1Period, asset2: d2Asset, maType2: d2MaType, maPeriod2: +d2Period, tradeAsset: dTrade, timePeriod: period, direction: dDir, leverageLong: +dLevLong, leverageShort: +dLevShort, ...slCfg }; if (period === 'CUSTOM') { body.dateFrom = dateFrom; body.dateTo = dateTo; } res = await authFetch('/api/backtest/dual', { method: 'POST', body: JSON.stringify(body) }); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold.equityCurve, `Dual SL: ${r.slLabel}`); setTrades(d.trades); } else if (mode === 'crossover' && op) { const body = { asset: op.asset, timePeriod: op.timePeriod, maType: op.maType, fastPeriod: +xFast, slowPeriod: +xSlow, direction: op.direction, leverageLong: +xLevLong, leverageShort: +xLevShort, ...slCfg }; if (op.timePeriod === 'CUSTOM') { body.dateFrom = op.dateFrom; body.dateTo = op.dateTo; } res = await authFetch('/api/backtest/crossover', { method: 'POST', body: JSON.stringify(body) }); d = await res.json(); drawChart(d.equityCurve, d.buyAndHold.equityCurve, `Crossover SL: ${r.slLabel}`); setTrades(d.trades); } } catch (e) { console.error('SL row click failed', e); } }; // ── Skip result row click ────────────────────────────────────── const handleSkipRowClick = async (idx, r) => { setSkipResult(prev => prev ? { ...prev, activeIdx: idx } : prev); const p = currentParamsRef.current; if (!p || !token) return; const sl = getSLConfig(); const qs = new URLSearchParams({ asset: p.asset, period: p.period, ma: p.bestPeriod, type: p.maType, leverageLong: p.leverageLong, leverageShort: p.leverageShort, direction: p.direction, ...sl, winSkipCount: r.skipCount, winSkipMinPct: r.minWinPct }); if (p.period === 'CUSTOM') { qs.set('dateFrom', p.dateFrom); qs.set('dateTo', p.dateTo); } try { const res = await authFetch(`/api/backtest/equity?${qs}`); const d = await res.json(); drawChart(d.equityCurve, d.buyAndHold, `Skip: ${r.label}`); setTrades(d.trades); } catch (e) { console.error('skip row click failed', e); } }; // ── Save preset ──────────────────────────────────────────────── const savePreset = () => { const sl = getSLConfig(); let preset, filename; if (mode === 'sweep') { const p = currentParamsRef.current; if (!p?.bestPeriod) { alert('Run a backtest first so the best MA period is known.'); return; } preset = { version: 1, source: 'walkforward-backtester', mode: 'sweep', asset: p.asset, maType: p.maType, maPeriod: p.bestPeriod, direction: p.direction, leverageLong: p.leverageLong, leverageShort: p.leverageShort, slType: sl.slType, slPct: sl.slPct }; filename = `${p.asset.replace('USDT','').toLowerCase()}-${p.maType.toLowerCase()}${p.bestPeriod}-preset.json`; } else if (mode === 'dual') { preset = { version: 1, source: 'walkforward-backtester', mode: 'dual', asset1: d1Asset, maType1: d1MaType, maPeriod1: +d1Period, asset2: d2Asset, maType2: d2MaType, maPeriod2: +d2Period, tradeAsset: dTrade, direction: dDir, leverageLong: +dLevLong, leverageShort: +dLevShort, slType: sl.slType, slPct: sl.slPct }; filename = `dual-${d1Asset.replace('USDT','').toLowerCase()}-${d2Asset.replace('USDT','').toLowerCase()}-preset.json`; } else if (mode === 'crossover') { preset = { version: 1, source: 'walkforward-backtester', mode: 'crossover', asset: xAsset, maType: xMaType, fastPeriod: +xFast, slowPeriod: +xSlow, direction: xDir, leverageLong: +xLevLong, leverageShort: +xLevShort, slType: sl.slType, slPct: sl.slPct }; filename = `${xAsset.replace('USDT','').toLowerCase()}-crossover-${xFast}-${xSlow}-preset.json`; } else return; preset.exportedAt = new Date().toISOString(); const blob = new Blob([JSON.stringify(preset, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }; // ── Sorted sweep table ───────────────────────────────────────── const sortedResults = React.useMemo(() => { if (!sweepData?.results) return []; return [...sweepData.results.map(r => ({ ...r, vsBah: r.totalReturn - sweepData.buyAndHold.totalReturn }))] .sort((a, b) => { const va = a[sortCol] ?? 0, vb = b[sortCol] ?? 0; return sortAsc ? va - vb : vb - va; }); }, [sweepData, sortCol, sortAsc]); const onSortCol = col => { if (sortCol === col) setSortAsc(a => !a); else { setSortCol(col); setSortAsc(false); } }; // ── Build optimize result rows ───────────────────────────────── const buildOptRows = (results, optMode, metric) => { return results.map(r => { let firstText; if (optMode === 'crossover') firstText = `Fast ${r.fastPeriod} / Slow ${r.slowPeriod}`; else if (optMode === 'sweep') firstText = `${r.maType} ${r.period}`; else if (optMode === 'overall-sweep') firstText = `${aLabel(r.asset)} ${r.maType} ${r.period}`; else if (optMode === 'dual') firstText = `${r.maPeriod1} / ${r.maPeriod2}`; else firstText = `${aLabel(r.asset1)}+${aLabel(r.asset2)}→${aLabel(r.tradeAsset)}`; return { _raw: r, cells: [ { text: firstText }, { text: `${r.leverageLong}x / ${r.leverageShort}x`, color: TH.inkSoft }, { text: fmtPct(r.totalReturn), color: retColorStr(r.totalReturn) }, { text: fmtPct(r.cagr), color: retColorStr(r.cagr) }, { text: r.maxDrawdown?.toFixed(1) + '%', color: TH.red }, { text: scoreDisplayStr(r, metric), color: r.score >= 0.5 ? TH.green : r.score >= 0 ? TH.inkSoft : TH.red }, { text: r.tradeCount + (r.liquidated ? ' ⚠' : ''), color: r.liquidated ? TH.red : TH.inkSoft }, ], }; }); }; // ── Build SL result rows ─────────────────────────────────────── const buildSLRows = (results, metric) => results.map(r => ({ _raw: r, cells: [ { text: r.slLabel }, { text: fmtPct(r.totalReturn), color: retColorStr(r.totalReturn) }, { text: fmtPct(r.cagr), color: retColorStr(r.cagr) }, { text: r.maxDrawdown?.toFixed(1) + '%', color: TH.red }, { text: scoreDisplayStr(r, metric), color: r.score >= 0.5 ? TH.green : r.score >= 0 ? TH.inkSoft : TH.red }, { text: r.tradeCount + (r.liquidated ? ' ⚠' : ''), color: r.liquidated ? TH.red : TH.inkSoft }, ], })); // ── Build skip result rows ───────────────────────────────────── const buildSkipRows = (results, metric) => results.map(r => ({ _raw: r, cells: [ { text: r.label }, { text: fmtPct(r.totalReturn), color: retColorStr(r.totalReturn) }, { text: fmtPct(r.cagr), color: retColorStr(r.cagr) }, { text: r.maxDrawdown?.toFixed(1) + '%', color: TH.red }, { text: scoreDisplayStr(r, metric), color: r.score >= 0.5 ? TH.green : r.score >= 0 ? TH.inkSoft : TH.red }, { text: r.tradeCount + (r.liquidated ? ' ⚠' : ''), color: r.liquidated ? TH.red : TH.inkSoft }, ], })); // ── Controls ─────────────────────────────────────────────────── const PeriodSection = ( <> {period === 'CUSTOM' && (
)} ); const SLSection = (
setSlOpen(o => !o)} style={{ fontFamily: TH.mono, fontSize: 10, color: TH.muted, letterSpacing: 1.5, cursor: 'pointer', display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderTop: `1px solid ${TH.rule}` }}> STOP-LOSS & WIN-SKIP{slOpen ? '▲' : '▼'}
{slOpen && ( <> {slType === 'fixed' && } {hardSlType === 'fixed' && }
Skip after win
trades after % win
)}
); const showOptButtons = (mode === 'sweep' || mode === 'dual' || mode === 'crossover'); const showSkipBtn = mode === 'sweep'; const showRunBtn = mode !== 'overall'; const controls = { sweep: ( <> {PeriodSection} setLevLong(+v||1)} shortVal={levShort} onShort={v => setLevShort(+v||1)}/> {SLSection} ), single: ( <> {PeriodSection} SIGNAL ASSET
TRADE ASSET setSLevLong(+v||1)} shortVal={sLevShort} onShort={v => setSLevShort(+v||1)}/> {SLSection} ), dual: ( <> {PeriodSection} CONDITION 1
— AND —
CONDITION 2
TRADE ASSET setDLevLong(+v||1)} shortVal={dLevShort} onShort={v => setDLevShort(+v||1)}/> {SLSection} ), crossover: ( <> {PeriodSection}
setXLevLong(+v||1)} shortVal={xLevShort} onShort={v => setXLevShort(+v||1)}/> {SLSection} ), overall: ( <>
{[['sweep','MA Sweep'],['dual','Dual Cond']].map(([k,l]) => (
setOverallSub(k)} style={{ flex: 1, padding: '9px 0', textAlign: 'center', fontFamily: TH.mono, fontSize: 10, letterSpacing: 1.2, textTransform: 'uppercase', cursor: 'pointer', background: overallSub === k ? TH.accent : 'transparent', color: overallSub === k ? TH.bg : TH.inkSoft }}>{l}
))}
{ALL_ASSETS.map(a => ( ))}
{overallSub === 'sweep' ? ( <> ) : ( <> CONDITION 1
CONDITION 2
)} {overallLoading ? 'Scanning…' : 'Find Overall Best →'} ), }; // ── Metrics panel ────────────────────────────────────────────── const metricsPanel = metrics ? ( <> = 1 ? TH.green : TH.inkSoft}/> ) : (
Run a backtest
to see metrics
); // ── Sweep table ──────────────────────────────────────────────── const SWEEP_COLS = [ { k:'period', l:'MA' }, { k:'totalReturn', l:'Return' }, { k:'vsBah', l:'vs B&H' }, { k:'cagr', l:'CAGR' }, { k:'maxDrawdown', l:'Max DD' }, { k:'sharpe', l:'Sharpe' }, { k:'tradeCount', l:'Trades' }, ]; const sweepTable = mode === 'sweep' && sweepData ? (
◉ {sortedResults.length} PERIODS — click row to view curve
{SWEEP_COLS.map(c => ( ))} {sortedResults.map(r => { const isBest = r.period === sweepData.bestPeriod; const isActive = r.period === activeRow; return ( loadRowCurve(r.period)} style={{ borderBottom: `1px solid ${TH.rule}`, cursor: 'pointer', background: isActive ? TH.panelHi : isBest ? 'rgba(212,168,75,0.05)' : 'transparent' }} onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = TH.panelHi; }} onMouseLeave={e => { e.currentTarget.style.background = isActive ? TH.panelHi : isBest ? 'rgba(212,168,75,0.05)' : 'transparent'; }}> ); })}
onSortCol(c.k)} style={{ padding: '7px 10px', textAlign: c.k === 'period' ? 'left' : 'right', color: sortCol === c.k ? TH.accent : TH.muted, fontSize: 10, letterSpacing: 1.2, cursor: 'pointer', whiteSpace: 'nowrap', fontWeight: 400, userSelect: 'none' }}> {c.l}{sortCol === c.k ? (sortAsc ? ' ↑' : ' ↓') : ''}
{r.maType || maType} {r.period} {isBest && BEST} {fmtPct(r.totalReturn)} = 0 ? TH.inkSoft : TH.red }}>{fmtPct(r.vsBah)} {fmtPct(r.cagr)} {r.maxDrawdown?.toFixed(2)}% = 1 ? TH.green : r.sharpe >= 0 ? TH.inkSoft : TH.red }}>{r.sharpe?.toFixed(3)} {r.tradeCount}
) : null; // ── Result cards ─────────────────────────────────────────────── const ResultCard = ({ title, subtitle, badge, bestGrid, tableHead, tableRows, activeIdx, onRowClick }) => (
{title}
{subtitle &&
{subtitle}
}
{badge &&
{badge}
}
{bestGrid}
); const MODES = [['sweep','MA Sweep'],['single','Single MA'],['dual','Dual Condition'],['crossover','Crossover'],['overall','Find Overall Best']]; return (
{/* Breadcrumb / status */}
TOOLS / BACKTESTER
{statusMsg && {statusMsg}} {error && ⚠ {error}} {authErr && ⚠ Cannot reach API}
{/* Mode tabs */}
{MODES.map(([k, l]) => (
changeMode(k)} style={{ padding: '11px 18px', fontFamily: TH.mono, fontSize: 10, letterSpacing: 1.5, textTransform: 'uppercase', cursor: 'pointer', whiteSpace: 'nowrap', color: mode === k ? TH.accent : TH.inkSoft, borderBottom: mode === k ? `2px solid ${TH.accent}` : '2px solid transparent', marginBottom: -1, flexShrink: 0 }}> {l}
))}
{/* Layout */}
{/* LEFT: controls */}
◉ INPUTS
{controls[mode]} {showRunBtn && (
{!token ? 'Connecting…' : 'Run Backtest →'}
)} {showOptButtons && (
Find Best Leverage Find Best SL {showSkipBtn && Find Best Skip} ↓ Save Preset
)} {!showOptButtons && mode !== 'overall' && ( ↓ Save Preset )}
{/* CENTER: chart + results */}
{chartTitle || 'EQUITY CURVE'}
{chartSub &&
{chartSub}
}
{/* Chart */}
{!chartTitle && !loading && !optLoading && !slLoading && !skipLoading && !overallLoading && (
Configure and run a backtest
)} {(loading || optLoading || slLoading || skipLoading || overallLoading) && (
Computing…
)}
{/* Optimize result */} {optimizeResult && (() => { const { data, mode: optMode, metric, activeIdx } = optimizeResult; const b = data.best; const mLabel = METRIC_LABELS[metric] || 'Score'; let bestCells; if (optMode === 'crossover') bestCells = [{ label:'Fast MA', value: b.fastPeriod }, { label:'Slow MA', value: b.slowPeriod }, { label:'Long Lev', value: b.leverageLong+'x' }, { label:'Short Lev', value: b.leverageShort+'x' }, { label:'Return', value: fmtPct(b.totalReturn), color: retColorStr(b.totalReturn) }, { label:'CAGR', value: fmtPct(b.cagr), color: retColorStr(b.cagr) }, { label:'Max DD', value: b.maxDrawdown?.toFixed(1)+'%', color: TH.red }, { label: mLabel, value: scoreDisplayStr(b, metric) }]; else if (optMode === 'sweep') bestCells = [{ label:'MA Period', value: `${b.maType} ${b.period}` }, { label:'Long Lev', value: b.leverageLong+'x' }, { label:'Short Lev', value: b.leverageShort+'x' }, { label:'Return', value: fmtPct(b.totalReturn), color: retColorStr(b.totalReturn) }, { label:'CAGR', value: fmtPct(b.cagr), color: retColorStr(b.cagr) }, { label:'Max DD', value: b.maxDrawdown?.toFixed(1)+'%', color: TH.red }, { label: mLabel, value: scoreDisplayStr(b, metric) }]; else if (optMode === 'overall-sweep') bestCells = [{ label:'Asset', value: aLabel(b.asset) }, { label:'MA Period', value: `${b.maType} ${b.period}` }, { label:'Long Lev', value: b.leverageLong+'x' }, { label:'Short Lev', value: b.leverageShort+'x' }, { label:'Return', value: fmtPct(b.totalReturn), color: retColorStr(b.totalReturn) }, { label:'CAGR', value: fmtPct(b.cagr), color: retColorStr(b.cagr) }, { label:'Max DD', value: b.maxDrawdown?.toFixed(1)+'%', color: TH.red }, { label: mLabel, value: scoreDisplayStr(b, metric) }]; else if (optMode === 'dual') bestCells = [{ label:'MA 1', value: b.maPeriod1 }, { label:'MA 2', value: b.maPeriod2 }, { label:'Long Lev', value: b.leverageLong+'x' }, { label:'Short Lev', value: b.leverageShort+'x' }, { label:'Return', value: fmtPct(b.totalReturn), color: retColorStr(b.totalReturn) }, { label:'CAGR', value: fmtPct(b.cagr), color: retColorStr(b.cagr) }, { label:'Max DD', value: b.maxDrawdown?.toFixed(1)+'%', color: TH.red }, { label: mLabel, value: scoreDisplayStr(b, metric) }]; else bestCells = [{ label:'Cond 1', value: `${aLabel(b.asset1)} ${b.maPeriod1}` }, { label:'Cond 2', value: `${aLabel(b.asset2)} ${b.maPeriod2}` }, { label:'Trade', value: aLabel(b.tradeAsset) }, { label:'Long Lev', value: b.leverageLong+'x' }, { label:'Return', value: fmtPct(b.totalReturn), color: retColorStr(b.totalReturn) }, { label:'CAGR', value: fmtPct(b.cagr), color: retColorStr(b.cagr) }, { label:'Max DD', value: b.maxDrawdown?.toFixed(1)+'%', color: TH.red }, { label: mLabel, value: scoreDisplayStr(b, metric) }]; const tableHead = optMode === 'crossover' ? ['Fast / Slow','Leverage','Return','CAGR','Max DD',mLabel,'Trades'] : optMode === 'overall-sweep' ? ['Asset · MA','Leverage','Return','CAGR','Max DD',mLabel,'Trades'] : optMode === 'overall-dual' ? ['Cond1+Cond2→Trade','Leverage','Return','CAGR','Max DD',mLabel,'Trades'] : optMode === 'dual' ? ['MA1 / MA2','Leverage','Return','CAGR','Max DD',mLabel,'Trades'] : ['MA Period','Leverage','Return','CAGR','Max DD',mLabel,'Trades']; return ( } tableHead={tableHead} tableRows={buildOptRows(data.results, optMode, metric)} activeIdx={activeIdx} onRowClick={(i, r) => handleOptRowClick(i, r._raw)} /> ); })()} {/* SL result */} {slResult && (() => { const { data, metric, activeIdx } = slResult; const b = data.best; const mLabel = METRIC_LABELS[metric] || 'Score'; const bestCells = [ { label:'SL Type', value: b.slLabel }, { label:'Return', value: fmtPct(b.totalReturn), color: retColorStr(b.totalReturn) }, { label:'CAGR', value: fmtPct(b.cagr), color: retColorStr(b.cagr) }, { label:'Max DD', value: b.maxDrawdown?.toFixed(1)+'%', color: TH.red }, { label: mLabel, value: scoreDisplayStr(b, metric) }, { label:'Trades', value: b.tradeCount }, ]; return ( } tableHead={['Stop Loss','Return','CAGR','Max DD',mLabel,'Trades']} tableRows={buildSLRows(data.results, metric)} activeIdx={activeIdx} onRowClick={(i, r) => handleSLRowClick(i, r._raw)} /> ); })()} {/* Skip result */} {skipResult && (() => { const { data, metric, activeIdx } = skipResult; const b = data.best; const mLabel = METRIC_LABELS[metric] || 'Score'; const bestCells = [ { label:'Skip Setting', value: b.label }, { label:'Return', value: fmtPct(b.totalReturn), color: retColorStr(b.totalReturn) }, { label:'CAGR', value: fmtPct(b.cagr), color: retColorStr(b.cagr) }, { label:'Max DD', value: b.maxDrawdown?.toFixed(1)+'%', color: TH.red }, { label: mLabel, value: scoreDisplayStr(b, metric) }, { label:'Trades', value: b.tradeCount }, ]; return ( } tableHead={['Skip Setting','Return','CAGR','Max DD',mLabel,'Trades']} tableRows={buildSkipRows(data.results, metric)} activeIdx={activeIdx} onRowClick={(i, r) => handleSkipRowClick(i, r._raw)} /> ); })()} {sweepTable}
{/* RIGHT: metrics */}
◉ RESULTS
{metricsPanel}
); } Object.assign(window, { Backtester });