Appearance
Pullback Mean Reversion (H1)
The second hypothesis through the loop, and the first of the Phase 2c campaign. Long-only breakout has no out-of-sample edge on MES/MNQ (VIX-regime breakout); the classic opposing structure for equity indices is short-horizon mean reversion inside a long-term uptrend. Code: pullback_reversion.py.
Pre-registration. Hypothesis, exact rule, fitted-vs-fixed parameters, and the pass bar below were written before the real-data run. Only the Results section was filled afterward.
Hypothesis
Equity indices trend up over the long run but overshoot to the downside on short horizons: a sharp multi-day sell-off tends to be followed by a bounce, not continuation. Buying oversold dips while the primary trend is still up (price above its 200-day average) should capture that bounce; requiring the trend filter avoids catching a falling knife in a genuine bear leg. This is the mirror image of the breakout that failed — fade the new low instead of chasing the new high.
Exact rule
Long/flat, one contract, single entry per cycle:
Enter long when the close falls below the prior N-day low and the close is above its 200-day SMA (trend filter). Exit on the first close above the prior M-day high (reversion target) or after 10 bars held (time stop), whichever comes first.
Exits are never blocked — the trend filter may only veto new entries (binding protocol). The trend filter rides the same precomputed date-map gate transport as the VIX-regime gate: the feature layer emits close_above_sma_200 and entry_gate_from_features turns it into an ISO-date → allowed map, so the strategy itself stays dumb and the filter can't overfit alongside the fitted lookbacks. During the SMA's 200-bar warmup the gate is silent (entries allowed).
Fitted vs fixed
| Parameter | Kind | Values |
|---|---|---|
| N — prior-low entry lookback | fitted (walk-forward grid) | |
| M — prior-high exit lookback | fitted (walk-forward grid) | |
| 200-day SMA trend filter | fixed prior | on, window 200 |
| Time stop T | fixed prior | 10 bars (swing-hold ceiling) |
Six grid points (N × M), re-fit per rolling train window by train Sharpe; the trend filter and time stop are held fixed throughout, never fitted.
Data lineage
| Input | Storage key | Alignment rule |
|---|---|---|
| MES/MNQ daily bars | mes_1d.parquet / mnq_1d.parquet | Panama back-adjusted at load |
| 200-day SMA | derived from bars' own close | Same-day (SMA at D uses closes ≤ D) |
No auxiliary series — the trend filter is computed from the bars themselves, so there is no publication lag to respect.
Pass bar
Per the campaign protocol, this candidate passes only if its stitched walk-forward out-of-sample result beats both buy-and-hold (same span) and the re-fit swing_breakout baseline on Sharpe, on both MES and MNQ, with the moving-block bootstrap 90% CI on OOS Sharpe excluding 0 and no materially worse max drawdown than buy-and-hold. Anything short of that is a documented negative result.
Evaluation protocol
sh
cd strategies
uv run python -m vibe_trader.backtest.evaluate --symbol MES,MNQ \
--candidate pullback_reversion --source databento --walk-forwardResults
First real-data run: 2026-07-14, Databento MES/MNQ daily bars 2019-05-06 → 2026-07-12 (2,239 bars each, Panama-adjusted), US$100k account, fixed 1 contract. Compare rows, not magnitudes (1 micro ≈ $30–38k notional).
Full period (in-sample, default N=5 / M=3):
| Metric | buy_and_hold | swing_breakout | pullback_reversion | buy_and_hold | swing_breakout | pullback_reversion | |
|---|---|---|---|---|---|---|---|
| MES | MNQ | ||||||
| Total return | +21.8% | +10.2% | +12.2% | +35.9% | +18.5% | +21.6% | |
| Sharpe | 0.67 | 0.58 | 0.80 | 0.63 | 0.58 | 0.77 | |
| Sortino | 0.97 | 0.80 | 1.20 | 0.90 | 0.79 | 1.12 | |
| Max drawdown | −5.9% | −2.5% | −2.6% | −11.6% | −7.0% | −3.2% | |
| Win rate | — | 53.9% | 73.1% | — | 51.4% | 72.2% | |
| Exposure | 100% | 56.7% | 21.8% | 100% | 53.2% | 22.0% |
Walk-forward, out-of-sample — the number to trust (13 splits, test span 2020-12-15 → 2026-03-20; N/M re-fit per split by train Sharpe, trend filter and 10-bar time stop fixed):
| Metric | swing_breakout | pullback_reversion | buy_and_hold (same span) | swing_breakout | pullback_reversion | buy_and_hold | |
|---|---|---|---|---|---|---|---|
| MES | MNQ | ||||||
| Total return | −0.9% | +10.7% | +12.8% | −0.6% | +20.6% | +14.8% | |
| CAGR | −0.2% | +2.0% | +2.3% | −0.1% | +3.6% | +2.7% | |
| Sharpe | −0.07 | 0.85 | 0.55 | −0.01 | 0.86 | 0.37 | |
| Sortino | −0.10 | 1.26 | 0.80 | −0.01 | 1.24 | 0.52 | |
| Max drawdown | −4.9% | −2.8% | −6.1% | −9.5% | −4.8% | −12.7% | |
| Win rate | 43.2% | 71.8% | — | 46.9% | 74.2% | — | |
| Profit factor | 0.90 | 2.18 | — | 0.99 | 2.18 | — | |
| Trades | 37 | 71 | 1 | 32 | 66 | 1 | |
| Exposure | 43.8% | 29.8% | 99.9% | 41.7% | 27.1% | 99.9% | |
| Rolls crossed | 9 | 5 | 21 | 10 | 5 | 21 |
Bootstrap 90% CI on OOS Sharpe (moving-block, 1,000 resamples):
| Symbol | swing_breakout | pullback_reversion | buy_and_hold |
|---|---|---|---|
| MES | [−0.70, 0.63] (incl. 0) | [0.35, 1.50] | [0.05, 1.17] |
| MNQ | [−0.60, 0.55] (incl. 0) | [0.31, 1.42] | [−0.12, 0.93] (incl. 0) |
Margin headroom: at 1 contract on a US$100k account, peak initial-margin utilization is ~2.8% (MES) / ~4.3% (MNQ) — see the overnight margin constants. The strategy is capital-light; a portfolio view (roadmap item 4) would size up.
Reading
The hypothesis is supported — pullback mean reversion clears the pass bar on both symbols. Out of sample the candidate beats both the re-fit swing_breakout baseline (Sharpe −0.07 / −0.01) and buy-and-hold (0.55 / 0.37) on Sharpe, on MES and MNQ; its bootstrap 90% CI excludes 0 on both ([0.35, 1.50] and [0.31, 1.42]); and its max drawdown is less than half buy-and-hold's (−2.8% vs −6.1% on MES, −4.8% vs −12.7% on MNQ) — the trend filter keeps it out of sustained bear legs. It does this at ~28% market exposure, so the edge is risk-adjusted, not a leverage artifact. The walk-forward grid stably picks the fastest fade (N=3, exit on the 5-day high) in most splits, and OOS Sharpe ≈ in-sample Sharpe (0.85 vs 0.80 on MES), so the selection transfers rather than overfitting. This is the campaign's first pass, and the mirror image of the failed breakout: fading new lows inside an uptrend works where chasing new highs did not.
Robustness and caveats below temper how far to push this.
Robustness
Because a pass warrants more scrutiny than a fail, the OOS result was stress- tested (all runs still exclude 0 on the bootstrap CI):
- Walk-forward window (the 504/126 split is arbitrary): re-running at 378/63, 756/189, and 252/126 gives OOS Sharpe 0.84–1.08 (MES) and 0.86–1.09 (MNQ) — the pass is not an artifact of one window choice.
- Bootstrap config: across seeds {0,1,2,7} and block lengths {10,20,40,63} the 90% CI lower bound stays 0.26–0.35 (MES) / 0.27–0.41 (MNQ), never touching 0.
- Trend filter contribution: removing the 200-day filter (ungated pullback) keeps a positive edge but worsens drawdown — MES −4.8% vs −2.8% gated; MNQ is larger, Sharpe 0.65 vs 0.86 and drawdown −8.6% vs −4.8%. The filter earns its place as drawdown control, exactly as hypothesized, and the mean-reversion signal itself (not the filter) is the return driver.
- Fill model (the dominant realism concern — see caveats): re-pricing the same OOS trades at the next bar's open instead of the signal-bar close is now a scored code path (
fill.py,evaluate --fill next_open), not prose — and it corrects the earlier hand-computed "no change", which was optimistic on MES. It costs a small, reproducible amount: OOS Sharpe MES 0.85 → 0.81, MNQ 0.86 → 0.83 (no slippage; 0.84 → 0.80 / 0.86 → 0.82 with the 1-tick graduation prior), both still well above buy-and-hold. The multi-day bounce is not an artifact of trading the exact oversold close — it persists to the next session's open at a ~0.04-Sharpe cost, not zero.
Caveats
- A research pass is not a live edge. This is one of several campaign candidates (ledger); the bar is a screen, not a deployment green-light. The ~28% exposure raises a capital-efficiency question a portfolio view (roadmap item 4) should answer.
- Fill realism — tested, and the pass holds. Like every strategy in the harness, the engine fills entries/exits at the same bar's close that generated the signal. That is more favorable to a mean-reverter (it buys the very down-close that just moved and sells the up-close that just reverted) than to the breakout baseline, so it was the prime suspect for inflating the pass. Re-pricing the OOS trades at the next bar's open — now a scored path (
--fill next_open), not a claim — costs ~0.04 Sharpe on both symbols (see Robustness) and leaves the edge well above buy-and-hold: not a same-bar-close artifact. What is not yet modeled is explicit slippage/commission beyond the flat US$1.37/side and the bid-ask crossed on entry; the graduation adds a 1-tick prior for exactly that. - Feature transport is backtest-only: the trend gate is a precomputed date map passed via strategy config, as with the VIX candidate; a live deployment would publish it as custom
Dataon the message bus. - Panama-adjusted series: the prior-N-day low is computed on back-adjusted levels; roll gaps are small on MES/MNQ but shift old levels.
- The 200-day SMA and 10-bar time stop are priors, not fitted optima; the numbers to watch are the out-of-sample table and its bootstrap CI.
Graduation (Phase 2d) — sized + slippage
Phase 2c passed H1 as a fixed 1-contract, flat-fee, no-slippage result at ~28% exposure and ~3–4% peak margin — capital-light to the point of leaving most of the account idle, and not yet charged the bid-ask it crosses. Phase 2d asks the graduation question: does the edge survive realistic execution at realistic size? This section appends to the Phase 2c record above; it does not rewrite it. The Phase 2c walk-forward table remains the 1-contract baseline this extends.
Pre-registration. The sizer (all four numbers), the slippage prior, and the graduation bar below were fixed before the run — they are the Phase 2d plan's priors, not values tuned to the result. Only the Results were filled afterward. The signals are unchanged from Phase 2c (protocol 6, verified by an acceptance test), so this re-prices the same trades at size and cost; it does not re-select the edge.
Sizer (fixed priors, never fitted)
Vol-targeted and margin-capped, per sizing.py:
contracts = clamp( floor( (risk_frac × equity) / (ATR₂₀ × multiplier) ), 1, floor( margin_frac × equity / overnight_initial_margin ) )
| Prior | Value | Meaning |
|---|---|---|
risk_frac | 0.5% of equity | risked to one ATR₂₀ daily move per contract |
margin_frac | 25% of equity | hard ceiling on overnight initial margin committed |
| ATR window | 20 daily bars | true range through the signal bar (no extra lag) |
| equity | account equity at signal | long/flat, so cash = equity when a new entry fires |
Quantity is set at entry and sold in full at exit, so each cycle stays single-entry/single-exit. Sizing never touches signals: the sized run produces the identical entry/exit timestamps as the 1-contract run (only quantity varies) — an acceptance test proves it, because a sizer that shifted one signal would silently invalidate the Phase 2c pass this extends.
Slippage (fixed prior)
1 tick per side on entries, exits, and each roll leg, debited into the scored equity/CI (not a post-hoc column) via slippage.py. A 0/1/2-tick sensitivity table is reported so a marginal pass can't hide in the cost assumption.
Graduation bar (pre-register)
With sizing and 1-tick slippage on, the stitched OOS result must, on both symbols: beat buy-and-hold (same span) on Sharpe, keep its bootstrap 90% CI excluding 0, and hold max drawdown not materially worse than buy-and-hold's — the campaign bar's shape, judged on the sized curve.
Results
Real-data walk-forward, 2019-05-06 → 2026-07-12 (stitched OOS 2020-12-15 → 2026-03-20, 13 splits; N/M re-fit per split by train Sharpe; 200-day filter, 10-bar time stop, and the sizer all fixed), US$100k account, 1 tick/side slippage on. pullback_reversion is the Phase 2c 1-contract run re-priced with slippage; pullback_sized adds the vol/margin sizer.
| Metric (OOS) | MES 1-contract | MES sized | MES buy&hold | MNQ 1-contract | MNQ sized | MNQ buy&hold | |
|---|---|---|---|---|---|---|---|
| Total return | +10.5% | +19.8% | +12.7% | +20.5% | +20.5% | +14.7% | |
| Sharpe | 0.84 | 1.05 | 0.55 | 0.86 | 0.86 | 0.36 | |
| Sortino | 1.24 | 1.61 | 0.79 | 1.24 | 1.24 | 0.52 | |
| Max drawdown | −2.8% | −2.8% | −6.1% | −4.9% | −4.9% | −12.7% | |
| Exposure | 29.8% | 30.1% | 99.9% | 27.1% | 27.1% | 99.9% | |
| Peak margin util | 2.8% | 8.0% | 2.8% | 4.3% | 4.3% | 4.5% | |
| Trades | 71 | 71 | 1 | 66 | 66 | 1 | |
| Fee drag | 0.19% | 0.30% | 0.00% | 0.18% | 0.18% | 0.00% |
Bootstrap 90% CI on OOS Sharpe (moving-block, on the slippage-adjusted returns): MES sized [0.54, 1.66], MNQ sized [0.31, 1.42] — both exclude 0.
Slippage sensitivity (OOS Sharpe, re-scored on the selected segments):
| tick/side | MES sized | MNQ sized |
|---|---|---|
| 0 | 1.07 | 0.86 |
| 1 (prior) | 1.05 | 0.86 |
| 2 | 1.04 | 0.85 |
Verdict — H1 graduates on both symbols
With realistic sizing and 1-tick slippage on, the sized OOS result clears the graduation bar on both MES and MNQ: Sharpe 1.05 / 0.86 beats buy-and-hold (0.55 / 0.36), both bootstrap CIs exclude 0, and drawdown (−2.8% / −4.9%) stays less than half buy-and-hold's. Two things the sizing surfaced:
- On MES the vol/margin sizer improves risk-adjusted return (Sharpe 0.85 → 1.05): it holds ~2–3 contracts (peak margin 2.8% → 8.0%) at the same drawdown fraction, so more of the idle account works without buying more risk per dollar. The mean-reversion edge scales cleanly with size.
- On MNQ the sizer floors to 1 contract and the sized run is identical to the 1-contract run (peak margin 4.3%, unchanged). MNQ's larger per-contract vol (ATR × US$2 multiplier) saturates the 0.5% risk budget at one contract on a US$100k account, so the vol target — correctly — declines to add size. Sizing up MNQ would require either a larger account or a looser risk prior, and loosening the prior is fitting.
Slippage is a non-event at this trade count (Sharpe moves ≤0.03 across 0/1/2 ticks): H1 holds 1–10 days and crosses few rolls, so the bid-ask barely dents a risk-adjusted edge earned over multi-day bounces. The graduation confirms the Phase 2c pass was not an artifact of ignoring size or cost. Recorded as a pass in the ledger: "H1 graduation — sized + slippage".
Graduation caveats
- The 25% margin cap binds at entry equity. An intra-hold equity dip can push realized peak utilization modestly above 25% before the position closes; the cap is a sizing rule at entry, not a live margin call. At the priors here MES tops out ~8% OOS (~14% full-period as equity compounds), far from the ceiling, so this never bit — but a looser risk prior would make it matter.
- MNQ sizes to 1 on a US$100k account, so its "graduation" is the 1-contract pass re-priced with slippage. That is a real result (the vol target declines to add size), not a sizing bug.
- Everything the Phase 2c caveats list still applies — a graduation pass is a stronger screen, not a live green-light; the feature transport is still backtest-only (Phase 2d workstream 3, deferred).
Deployability & regime validity (Phase 2e)
Phase 2e steps back from the graduation to ask what the deployable number really is, before spending live-adapter engineering on H1.
The honest deployable headline is the 1-contract 0.84 / 0.86, not the 1.05. The sized 1.05 is time-varying leverage on one symbol — the vol/ margin sizer holds 2-3 MES contracts when ATR is low, on the identical ~71 trades — and on MNQ it floors to 1 contract (sized ≡ 1-contract, 0.86). Sizing cannot change selection or timing, so the graduated-signal number to carry forward is the un-leveraged 1-contract, 1-tick-slippage OOS Sharpe: 0.84 (MES) / 0.86 (MNQ).
Correlation to buy-and-hold, finally reported (metrics.benchmark_relative, now a column in evaluate): over the OOS span H1's returns correlate ~0.50 to buy-and-hold with beta ~0.27 on both symbols. H1 is low-beta conditional long equity — it captures roughly a quarter of the index move — so a meaningful slice of the Sharpe edge is a lower-volatility / idle-cash effect (no risk-free yield is credited to the idle ~70%), not a large independent alpha. This is the "measured alpha vs diluted long" number the Phase 2c/2d summaries never carried.
Regime stress test — full note. The graduation rests on the 2019+ micro window, one secular bull regime. Re-running H1's exact walk-forward on the deep e-mini parent history (ES/NQ, Databento to 2010-06-06, ~3x the span, leverage-matched) is the cheapest experiment that could break it. It does not break — OOS Sharpe stays positive and CI-significant on both (0.74 ES / 0.58 NQ), above buy-and-hold, drawdown modest at matched leverage, next-open fill robust — so the edge is real, not a bull artifact. But it right-sizes the confidence: the edge is thinner over a full cycle (0.84 → 0.74, 0.86 → 0.58) and its margin over buy-and-hold collapses (NQ 0.58 vs 0.51, not the micro's 0.86 vs 0.36 — buy-and-hold's own Sharpe is higher across 2012-2026). The trend filter's protection actually holds on deep history (H1 beats buy-and-hold in every scored OOS stress window); the whipsaw failure mode — H1 losing more than the market — surfaces only in the in-sample 2011 slow-bleed, mild. Qualified pass: take H1 toward paper trading with calibrated expectations — a modest, low-exposure long-biased reversion sleeve with a thinner edge than the micro window advertised.
Paper trading & feature transport (Phase 2g)
H1 is the sole live core of the paper stack, on MES+MNQ at 1 contract, the honest 0.84/0.86 config (entry_lookback=5, exit_lookback=3, not the sized variant). It runs through feature transport v2: the 200-day trend gate is published as a custom Nautilus Data event on the message bus (feature_transport.py) so the same strategy code runs backtest and live — the deferred Phase 2d workstream 3. The binding acceptance test is green: a gated backtest through the transport reproduces the date-map results to the bar (tests/test_feature_transport.py); the date-map path stays the tested fallback.
Expectations are calibrated to the deep-history profile (~0.6–0.75 Sharpe, a real ~5% drawdown in a sustained downtrend), not the graduated micro numbers. The paper burn-in is a plumbing validation — fills, transport, session handling, slippage vs the 1-tick prior — not an edge test: at ~13 trades/yr/symbol a year of paper is statistically non-diagnostic for the edge. Full pre-registered expectations and kill/triage criteria: the paper-stack note.