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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user