1 · The bug, and filling in the missing cost step
finalize_trade()'s lookahead loop breaks — rather than falling through to the for…else time-stop branch — when a trade opens near the live edge of the dataset and runs out of forward bars before the 60‑minute window elapses. That leaves exit_px = None, which crashes on the very next line. Because the exception unwinds the whole symbol's trade list, it silently discarded every trade for that symbol, not just the one broken trade near the edge. First run on freshly-fetched 2026 data: only 31/100 and 101/166 symbols survived to produce a result. Patched to flatten at the last available bar (matching the code's own stated intent) — 60/100 and 135/166 symbols now complete; the rest are excluded by the engine's real 0.5%-median-ATR liquidity gate, not by crashes.
Separately: strategy_engine.py's trade backtest returns gross R-multiples only — no fee/slippage deduction anywhere in that file. vwap_cost_validate.py's event-level scan does apply costs, but that's a different, coarser signal-quality check, not the trade backtest. Since the front page's net figures clearly come from some cost step, just not one present in the shared code, the tables below apply the same cost basis vwap_cost_validate.py already uses elsewhere in the repo — 0.055% taker + 0.02% slippage per side, round trip — directly to every simulated trade.
2 · Does the edge generalize, or is it specific to the screened list?
The Overview page's universe (78 symbols passing an ATR filter) is a subset of the hand-curated 166-symbol screened list. Re-running the same unmodified E8 code on an entirely different, unscreened universe — Bybit's top 100 perps by 24h turnover, no pre-selection — answers whether the edge is a property of the signal or an artifact of which symbols made the list.
| Metric | Top 100 (unscreened) | Screened (166) |
|---|---|---|
| Symbols used | 60 / 100 | 135 / 166 |
| Trades | 221,804 | 624,676 |
| Win rate | 55.0% | 53.6% |
| Event-scan gross 1h fade | +0.363% | +0.534% |
| Event-scan net (taker std. cost) | +0.223% | +0.394% |
| Trade-backtest gross avg R / PF | +0.360 / 1.93 | +0.358 / 1.89 |
| Trade-backtest net avg R / PF | +0.123 / 1.25 | +0.150 / 1.30 |
It generalizes. Win rate within 1.4 points, net PF within 0.05, net avg R within 0.03R. The screened list shows a larger gross pre-cost drift (median entry deviation 3.47 ATR vs 3.35, more volatile/less liquid names), but that gap mostly closes once realistic costs are applied. This is not a screened-universe artifact — but §5 below found that the edge being compared here was itself inflated by a VWAP lookahead bug, so read this as "the bug's effect generalizes," not "the strategy generalizes."
3 · Does the actual VWAP Fade Model indicator hold up?
E8 is a research proxy: fade the instant |close − VWAP| > 2×ATR, no confirmation. The indicator this research line is meant to validate is stricter — it only enters after a full confirmed-reversal sequence (trigger → armed → a reversal candle that opens beyond the band, closes back inside it, is the opposite color, and clears a volume-confirmation filter), and its stated target is price returning to the live VWAP, not a fixed R-multiple. Backtesting that indicator exactly as specified, across 18 combinations of volume filter and stop type, on the top-100 universe:
Every configuration lost money. Stop-loss exits average about −1.3R; take-profit (VWAP-touch) exits average only about +0.3R — the stop sits a full ATR beyond the reversal candle's own extreme, and reversal candles are by definition wide, while the VWAP target is often much closer. Tighter volume confirmation roughly halves the loss but never flips the sign.
4 · Isolating why: entry timing, not exit design
Given E8 (loose trigger, fixed-R exit) is profitable and the indicator (confirmed trigger, VWAP-chase exit) is not, the natural question is which piece is doing the work. Swapping just the exit scheme — running the indicator's real confirmed-reversal entries through E8's own fixed-R exit (1×ATR stop from entry, 2R target, 60‑minute time stop) instead of the indicator's VWAP-chase target — isolates it. All three rows below use the same top-100 universe and the same cost basis.
| Entry | Exit scheme | Trades | Gross avg R | Gross PF | Net avg R | Net PF |
|---|---|---|---|---|---|---|
| Indicator's confirmed reversal | Indicator's own (VWAP-chase target, candle-extreme stop) | 151,555 | −0.012 | 0.98 | −0.373 | 0.58 |
| Indicator's confirmed reversal | E8's fixed-R (2R target, 1×ATR stop, 60m time stop) | 195,075 | +0.013 | 1.03 | −0.758 | 0.56 |
| E8's raw deviation trigger (no confirmation) | E8's fixed-R | 221,804 | +0.360 | 1.93 | +0.123 | 1.25 |
Net profit factor, all three on the same top-100 universe. Grey tick = breakeven (PF 1.0).
Reading the result: swapping the exit scheme changes almost nothing — the confirmed-entry gross edge is ≈0 either way (PF 0.98 vs 1.03, a coin flip before costs). E8's real edge comes from firing the instant price first closes beyond 2×ATR. The indicator's confirmation sequence — waiting for an opposite-colored candle to open beyond the band, close back inside it, and clear a volume filter — means it enters only after most of the immediate reversion has already happened. What's left to trade at that point is close to noise, and at roughly 2,000 trades/symbol over 8 months (a trade every ~3 hours per symbol, all day), transaction costs alone turn that coin flip into a clear net loss.
5 · The real root cause: a VWAP lookahead bug
Testing whether E8 fires on every qualifying bar or something more selective (see the questions this section answers) meant reading session_vwap() closely — and it isn't a running, cumulative VWAP at all. It computes a single value per day from bars[lo:hi], the entire day's bars, and then every bar that day — including one at 00:05 UTC — is compared against a VWAP that already reflects volume from 23:55 UTC that hasn't happened yet. vwap_cost_validate.py's event scan has the identical pattern. This is separate from, and larger than, the missing-cost issue in §1: it means the reference level E8 fades toward is not something a live trader could ever observe.
Re-running E8's exact entry rule and fixed-R exit scheme with a proper running VWAP (the same calculation the actual Pine indicator uses, and what this reproduction's engine already computes correctly) — same top-100 universe, same costs:
| Variant | Trades | Gross avg R | Gross PF | Net avg R | Net PF |
|---|---|---|---|---|---|
| E8 as coded (lookahead VWAP) | 221,804 | +0.360 | 1.93 | +0.123 | 1.25 |
| E8, corrected VWAP, any bar after flat | 680,967 | +0.039 | 1.04 | −0.793 | 0.57 |
| E8, corrected VWAP, fresh-cross only (first bar of each new excursion, never re-chases) | 198,508 | +0.035 | 1.05 | −0.766 | 0.56 |
With VWAP computed correctly, E8's gross edge collapses to essentially zero — the same coin-flip territory as the confirmed-reversal indicator in §3-4. Requiring a fresh excursion instead of allowing E8 to re-chase a sustained deviation doesn't help. Neither does restricting to a specific session:
| Session (UTC) | Trades | Net avg R | Net PF |
|---|---|---|---|
| Asia 00–08 | 179,728 | −0.842 | 0.56 |
| Europe 08–14 | 190,244 | −0.843 | 0.53 |
| US 14–21 | 211,363 | −0.692 | 0.61 |
| Late 21–24 | 99,632 | −0.822 | 0.55 |
Every session is a net loser; US hours are the least bad (PF 0.61) but still lose money. This means §2's generalization result and §4's isolation result are both still procedurally correct — the edge really does generalize across universes, and swapping the exit scheme really doesn't matter — but the edge they're comparing was itself an artifact of comparing prices to a VWAP computed with hindsight. Once that's fixed, there isn't a real edge left for either signal to generalize or for an exit scheme to preserve.
6 · What this means going forward
The apparent mean-reversion drift E8 was built to capture doesn't survive computing its own reference level honestly. Neither loosening the entry (trading every qualifying bar vs. only fresh excursions) nor restricting to a specific trading session recovers a net edge once VWAP is a true running value. Before iterating further on stop/target levels or confirmation logic, the two threads worth pulling are: whether a different anchor entirely (e.g. a shorter rolling VWAP, or a structural level like a value-area edge) reacts fast enough to be tradeable in real time, and whether any of E8's sibling strategies (E3–E5) share the same VWAP-computation pattern and need the same fix.
7 · Checked at 1-minute execution precision too
Since E8's lookahead bug is a full-day VWAP information leak rather than an entry-timing issue, there was no strong reason to expect bar size to matter here — and it doesn't. Re-running the as-coded (bug-intact) reproduction with entries and the SL/TP walk-forward at 1-minute precision instead of 5-minute: gross PF 1.93→1.84, net PF 1.25→1.27, essentially unchanged. The corrected, lookahead-free version (fade toward a true running VWAP) is similarly stable: net PF 0.575→0.577. Full detail, including two more real bugs this rebuild caught (ATR and the trend/range efficiency ratio both need to stay on the 5-minute signal definition even when execution runs at 1-minute precision — see why), is on the ATR-VWAP variant study page, §6.