// Terminal variant — dual-mode bootstrapper.
// Reads config / runs / currentRun via window.BacktestDataSource, then renders
// the dense mono-terminal UI from the handoff design.
const THEMES = [
{ id: 'terminal-dark', label: 'TERMINAL DARK' },
{ id: 'bright', label: 'BRIGHT' },
{ id: 'amber-crt', label: 'AMBER CRT' },
{ id: 'solarized-dark', label: 'SOLARIZED DARK' },
{ id: 'solarized-light', label: 'SOLARIZED LIGHT' },
{ id: 'nord', label: 'NORD' },
{ id: 'synthwave', label: 'SYNTHWAVE' },
];
// Slider + manual number input. Defined at module scope (not inside TerminalVariant)
// so React doesn't rebuild the component type on every parent render — keeps the
// internal `draft` state alive while the user is typing.
const ParamSlider = ({ pdef, value, isStatic, updateParam, dim }) => {
const clamped = Math.max(pdef.min, Math.min(pdef.max, Number(value)));
const pct = ((clamped - pdef.min) / (pdef.max - pdef.min || 1)) * 100;
const isInt = Number.isInteger(pdef.step);
const display = isInt ? String(clamped) : Number(clamped).toFixed(3);
const [draft, setDraft] = React.useState(display);
const [focused, setFocused] = React.useState(false);
React.useEffect(() => {
if (!focused) setDraft(display);
}, [display, focused]);
const commit = () => {
const n = parseFloat(draft);
if (!Number.isFinite(n)) {
setDraft(display);
return;
}
const c = Math.max(pdef.min, Math.min(pdef.max, n));
updateParam(pdef.name, c);
setDraft(isInt ? String(c) : Number(c).toFixed(3));
};
return (
{/* HEADER */}
◆ QUANT.TERM v{data?.version?.semver || '0.4.1'}
{data?.version?.build ? ` #${data.version.build}` : ''}
SESSION{' '}
{run?.run?.id
? '#' + run.run.id.toString().slice(-4).toUpperCase()
: previewBars?.length
? 'PREVIEW'
: 'IDLE'}
ENGINE {isStatic ? 'STATIC' : 'ONLINE'}
BARS {run?.run?.bars ?? (previewBars?.length || '—')}
TRADES {run?.run?.trades ?? tradesRows.length}
MODE {isStatic ? 'STATIC' : 'SERVER'}
THEME
{/* CONFIG (left column) */}
[ config ] 01
{symbols.map((a) => (
))}
{timeframes.map((t) => (
))}
{selectedStrategy && selectedStrategy.params.length > 0 && (
<>
[ params ]
{selectedStrategy.params.map((p) => {
if (p.name === 'direction_mode') {
const mode = params[p.name] ?? p.default;
const opts = [
{ val: 0, label: 'BOTH' },
{ val: 1, label: 'LONG' },
{ val: 2, label: 'SHORT' },
];
return (
{opts.map((o) => (
))}
);
}
return (
);
})}
>
)}
[ capital ]
setCapital({ ...capital, initial: e.target.value })}
/>
setCapital({ ...capital, leverage: e.target.value })}
/>
setCapital({ ...capital, size_pct: e.target.value })}
/>
[ history ]
{(!data.runs || data.runs.length === 0) && (
No past runs.
)}
{(data.runs || []).map((r) => {
const net = parseFloat(r.net_profit_pct || '0');
const active = run?.run?.id === r.id;
return (
loadRun(r.id)}
title={r.id}
>
= 0 ? green : red }}>
{net >= 0 ? '+' : ''}
{r.net_profit_pct}%
{' '}
{(r.strategy || '').slice(0, 16)}{' '}
{r.symbol}/{r.timeframe}
);
})}
{/* CHART (centre column) */}
{previewError && (
preview error: {previewError}
)}
{/* TRADES (full-width row below chart — grid-area: trades) */}
{['Overview', 'Performance', 'Trades', 'Properties'].map((t) => (
))}
showing {tradesRows.length} closed
{(() => {
const cols = [
{ id: 'idx', label: '#' },
{ id: 'side', label: 'Side' },
{ id: 'entry_ts', label: 'Entry time' },
{ id: 'exit_ts', label: 'Exit time' },
{ id: 'entry', label: 'Entry $' },
{ id: 'exit', label: 'Exit $' },
{ id: 'qty', label: 'Qty' },
{ id: 'pnl', label: 'P&L $' },
{ id: 'pnl_pct', label: 'P&L %' },
{ id: 'bars', label: 'Bars' },
{ id: 'signal', label: 'Signal' },
];
const totalWidth = cols.reduce((s, c) => s + colWidths[c.id], 0);
const ResizeHandle = ({ colId }) => (
);
return (
{cols.map((c) => )}
{cols.map((c) => (
|
{c.label}
|
))}
{tradesRows.map((tr) => {
const pnl = parseFloat(tr.pnl ?? 0);
const pnlPct = parseFloat(tr.pnl_pct ?? 0);
const isLong = window.ChartUtils.isLongSide(tr.side);
return (
| {tr.id} |
{isLong ? '▲ LONG' : '▼ SHORT'}
|
{tr.entry_ts} |
{tr.exit_ts} |
{fmt(tr.entry, 2)} |
{fmt(tr.exit, 2)} |
{fmt(tr.qty, 4)} |
= 0 ? green : red }}>{fmtSigned(pnl)} |
= 0 ? green : red }}>{fmtSigned(pnlPct, 2, '%')} |
{tr.bars} |
{tr.signal} |
);
})}
);
})()}
{/* SUMMARY (right column) */}
[ summary ] {run ? 'LIVE' : 'IDLE'}
Net Profit
{metrics.net_profit_pct ? fmtSigned(metrics.net_profit_pct, 2, '%') : '—'}
{metrics.net_profit_closed ?? metrics.net_profit ?? '—'}
Max DD
{metrics.max_drawdown_pct ? fmtSigned(-Math.abs(parseFloat(metrics.max_drawdown_pct)), 2, '%') : '—'}
{metrics.max_drawdown ?? '—'}
Sharpe
{metrics.sharpe != null ? fmt(metrics.sharpe, 2) : '—'}
annualized
Sortino
{metrics.sortino != null ? fmt(metrics.sortino, 2) : '—'}
annualized
[ stats ]
Profit factor
{metrics.profit_factor ?? '—'}
Total trades
{metrics.total_closed_trades ?? tradesRows.length}
Win rate
{metrics.percent_profitable ?? '—'}
{(() => {
const isLong = window.ChartUtils.isLongSide;
const counts = { all: { w: 0, l: 0 }, long: { w: 0, l: 0 }, short: { w: 0, l: 0 } };
for (const t of tradesRows) {
const pnl = parseFloat(t.pnl ?? 0);
if (pnl === 0) continue; // breakeven excluded — matches backend convention
const bucket = isLong(t.side) ? counts.long : counts.short;
const k = pnl > 0 ? 'w' : 'l';
bucket[k] += 1;
counts.all[k] += 1;
}
const ratio = (w, l) => (l > 0 ? (w / l).toFixed(2) : (w > 0 ? '∞' : '—'));
const Row = ({ label, w, l }) => (
{label}
{w}
/
{l}
{' • '}{ratio(w, l)}
);
return (
<>
|
>
);
})()}
Largest win
{metrics.largest_win ?? '—'}
Largest loss
{metrics.largest_loss ?? '—'}
Commission paid
{metrics.commissions_paid ?? '—'}
);
};
window.TerminalVariant = TerminalVariant;