Fix O(n^2) blowup and look-ahead leak in SMC/divergence backtest scoring

market_structure() (SMC) and detect_divergence() were each precomputed
once over the ENTIRE multi-year backtest range and reused unchanged for
every candle, so every candle's score could see results derived from
years of future price data — a look-ahead bug that inflated both
single-run backtest and walk-forward results, undermining the very
overfitting check walk-forward exists to provide. A prior fix bounded
this to a per-candle trailing window, which closed most of the leak but
still rescanned pivots from scratch on every candle (O(window) per
candle), too slow to enable 15m/30m walk-forward runs.

The real fix: pivot detection is itself a bounded rolling-window scan
(each position only depends on a few bars on either side), so it can be
precomputed once for the whole dataset just like BB/RSI/MACD. Per candle,
_compute_scores_series now just advances a monotonic pointer over
already-known pivots to whatever is causally confirmable as of that
candle — O(1) amortized across the whole run instead of O(window) or
O(n) per candle. Added an optional precomputed_pivots param to
detect_divergence() (backward compatible) to reuse this for RSI/MACD
divergence too.

Net effect: 16,000 candles went from 16.1s to 1.7s (confirmed empirically,
on top of an earlier ~10x from fixing the raw O(n^2)), and scaling stays
linear at 32,000 candles (3.2s). Walk-forward's timeframe options are now
15m/30m/1h/4h/1d (up from 1h/4h/1d) since 15m at the 3-year default
lookback now costs roughly 30s instead of 5+ minutes. Also wired
walk_forward.py's grid search to actually reuse one computed score series
across all 27 parameter combinations per fold (it was recomputing full
classification for every combination despite the scoring/threshold split
added earlier). 156 backend tests passing (3 new: causal-score regression,
pivot-detection-runs-once, order-block-window-bounded).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Le
2026-07-04 10:42:32 +07:00
parent 56325113b3
commit 1c022264f5
8 changed files with 476 additions and 131 deletions
@@ -120,10 +120,13 @@ const SIGNAL_ICONS: Record<string, string> = {
const EXCHANGES = ['binance', 'bybit', 'mexc', 'gate', 'bingx'];
const TIMEFRAMES = ALL_TIMEFRAMES;
// Walk-forward is capped at 1h/4h/1d — over the ~3yr default lookback, 1w
// gives only ~150 candles and 1M only ~36, too few to split into folds
// with enough history left for indicator warmup (BB/RSI need 20+ candles).
const WF_TIMEFRAMES = ['1h', '4h', '1d'];
// 1w/1M excluded — over the ~3yr default lookback, 1w gives only ~150
// candles and 1M only ~36, too few to split into folds with enough
// history left for indicator warmup (BB/RSI need 20+ candles). 15m/30m/1h/
// 4h/1d are all fast now that SMC/divergence scoring is O(1)-amortized
// per candle instead of rescanning pivots on every one (see
// backtest_engine.py's _compute_scores_series).
const WF_TIMEFRAMES = ['15m', '30m', '1h', '4h', '1d'];
const selectClass = 'rounded-md border border-border-default bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary';
const thClass = 'px-3 py-2 text-left font-medium text-text-secondary';