Files
momentum devandClaude Opus 4.8 f170305a41 No-chase entry shadow-validation: VWAP-pullback halves the ORB loss but FAILS the locked bar
The mechanics study found the no-chase entry (buy the VWAP reclaim, not the breakout high)
was the dominant lever. This codifies it as a permanent, pre-registered shadow-validation —
the disciplined close-out of the 4-lever investigation arc.

- strategy/evaluation/vwap_pullback_entry.py: pure find_vwap_pullback_entry — first forward
  bar that dips to the CUMULATIVE session VWAP (calculate_vwap, NOT the per-bar minute_bars.vwap
  midpoint, which would make 'reclaim' trivially true) and closes back above it; entry = its
  close, stop = its low (the tight no-chase structure stop). 4 tests.
- research/labeler.py validate_no_chase_entry + `validate-entry`: simulates it vs the ORB-high
  breakout via the live simulate_exit on real forward bars, judged in PERCENT net of a REALISTIC
  per-name cost proxy (4% sub-$3 / 2% else — no NBBO in bars). PRE-REGISTERED bar locked: mean
  net% must be STRICTLY POSITIVE, 'less negative than ORB' is NOT a pass.

IN-SAMPLE RESULT (v1 gappers, 19 autocorrelated days): the no-chase entry HALVES the loss
(ORB -7.94% -> no-chase -3.61% net, median -4.23%, offered on 75%), robustly (day-jackknife
[-3.83%,-3.48%]) -- confirming not-chasing the spike-top is the strongest mechanic -- but it
still does NOT cross zero under realistic cost: PRE-REGISTERED BAR = FAIL. Consistent with the
whole arc: no mechanizable entry/exit/selection lever beats cost on the full gapper book; the
path to positive is FEWER/cleaner setups (cut the ~70% fader population) COMBINED with this
no-chase entry. validate-entry re-runs forward; promote only if it clears the bar out-of-sample.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:08:56 -04:00

56 lines
2.6 KiB
Python

"""No-chase entry primitive — buy the VWAP reclaim, not the breakout high.
The mechanics study (2026-06-29, wf_bdff509d) found that the ORB-high breakout entry
CHASES the spike-top: from it only ~8% residual upside remains and it whipsaws out 65%
of the time. Not chasing — entering on the first pullback that RECLAIMS the cumulative
session VWAP — was the single most powerful mechanic (beat the breakout entry by ~4.4pt
net%, 2.5-5x more than any stop/exit tweak) because it SHRINKS per-trade risk (the stop
sits at the pullback low, ~1.6% away, vs the ORB stop ~7% away) instead of fighting the
runner/fader tradeoff.
This is a PURE primitive: given the forward bars after the ORB decision point (each
carrying the CUMULATIVE SESSION VWAP, not the per-bar vwap), return the first bar that
dips to/through VWAP and closes back above it -- entry = that close, stop = that bar's
low. SHADOW/validation only (research.labeler validate-entry); not wired into live entry.
IMPORTANT: the `vwap` column MUST be the cumulative session VWAP (calculate_vwap over the
session from the open), NOT minute_bars.vwap (a per-bar typical price ~midpoint, for which
low<=vwap<=close is trivially almost always true). The caller is responsible for that.
"""
from __future__ import annotations
from dataclasses import dataclass
import pandas as pd
@dataclass
class PullbackEntry:
found: bool
entry_price: float | None = None # the reclaim bar's close
stop_price: float | None = None # the reclaim bar's low (the no-chase tight stop)
entry_idx: int | None = None # index into the forward bars
reason: str = ""
def find_vwap_pullback_entry(bars: pd.DataFrame) -> PullbackEntry:
"""First forward bar that pulls back to the cumulative VWAP and reclaims it
(low <= vwap <= close): enter at its close, stop at its low. Pure. `bars` must
carry the CUMULATIVE SESSION VWAP in column `vwap` (see module note)."""
if bars is None or len(bars) == 0:
return PullbackEntry(False, reason="no bars")
b = bars.reset_index(drop=True)
low = b["low"].astype(float)
close = b["close"].astype(float)
vwap = b["vwap"].astype(float)
for i in range(len(b)):
v = float(vwap.iloc[i])
if v <= 0:
continue
lo, cl = float(low.iloc[i]), float(close.iloc[i])
if lo <= v <= cl and cl > lo: # touched VWAP from above, closed back over it
return PullbackEntry(True, entry_price=cl, stop_price=lo, entry_idx=i,
reason="vwap_reclaim")
return PullbackEntry(False, reason="no vwap reclaim")