Move the only qualifying R:R scan to 15:30 ET with chained Telegram alerts, put outcome eval after a final-bar OHLCV fetch, enforce NY trading-day requalify semantics, stamp paper trades fill_mode=near_close, and migrate stored schedule_* keys to America/New_York.
8.1 KiB
Execution recovery (2026-07-18) — when you fill is the edge you leave on the table
Report: reports/execution_recovery_matrix.json / .md
Follows Phase A A4 (phase-a-matrix.md).
Arms: close_control · next_open · stale_close · next_open_gap2.
Mechanics verified before sign-off: stale_close re-anchors stop to
fill − 1.5×ATR(signal day); no same-day stop after a close fill; each t−1
candidate keeps its own gate/rank cross-section (no lookahead). Numbers trusted.
Results
| Arm | Full Sharpe | Full CAGR | Full DD | Train Sharpe | Val Sharpe |
|---|---|---|---|---|---|
| close_control | 1.77 | 48.3% | 21.6% | 1.75 | 1.68 |
| stale_close | 1.57 | 40.6% | 21.8% | 1.36 | 1.74 |
| next_open | 1.20 | 30.0% | 28.2% | 0.94 | 1.44 |
| next_open_gap2 | 1.06 | 25.2% | 27.8% | 0.72 | 1.41 |
Monotonicity (the strongest evidence)
In every window, Sharpe recovers as the fill moves toward the signal:
| Window | next_open → stale_close → close |
|---|---|
| Train | 0.94 → 1.36 → 1.75 |
| Validation | 1.44 → 1.74 → 1.68* |
| Full | 1.20 → 1.57 → 1.77 |
*Val close 1.68 is within SE of stale 1.74 — not a break of the story.
A dead edge does not produce a monotone gradient in fill timing. A live edge that is progressively surrendered to execution delay does. Combined with full-period DD recovery (28.2% → 21.8% ≈ close’s 21.6%), this confirms the diagnosis: when you fill, not decaying alpha.
Auto-flag recover: False is not a null
Pre-registered recovery applied the “≥ close − 0.5 SE” bar to stale_close,
the lower-bound arm (one full session of lag). Flags:
| Flag | Result |
|---|---|
| near_close_control (val) | True (Δ +0.06) |
| beats_next_open | True (val Δ +0.30) |
| train_ok | False (1.36 vs need ≥ ~1.39 — miss by ~0.03) |
The floor missed “≥ close − 0.5 SE” by 0.03 while the live design is expected to sit above the floor. The flag worked correctly on the wrong object.
Log sentence: Partial recovery proven; full recovery needs same-day fill.
Live outcome bracket
| Bound | Arm | Meaning |
|---|---|---|
| Floor | stale_close ~1.57 full | One-session-stale close fill (conservative) |
| Ceiling | close_control ~1.77 full | Same-day close; optimistic only by final ~15 min of signal info |
Real near-close execution (scan ~15:30–15:40 ET on a ~99% complete bar, MOC by 15:50/15:55) is signal-at-partial-bar filled at the same close the control uses. Live truth is bracketed [1.57, 1.77] with residual uncertainty of ~15 minutes of staleness, not 24 hours — expect near the top of the bracket.
stale_close alone already justifies the schedule change. No further fill-timing
simulation on this snapshot — the bracket is the result. You cannot simulate
15:45 partial bars from daily data, and you do not need to.
Gap-cap — dead (third tail-trim instance)
next_open_gap2 worse than plain next_open on every window (full Sharpe 1.06 vs
1.20). 262 full-period gap-ups skipped — they were continuations.
This is the third independent instance of the same lesson:
- Take-profit exits (gate target as TP)
- FIP as an in-book filter
- Gap-up entry caps
Any rule that trims the right tail improves cosmetic quality metrics and destroys P&L. Standing method rule: momentum filters must be presumed guilty of tail-trimming until shown otherwise.
Decisions locked
- Biggest lever is execution scheduling, not a strategy rewrite.
- Until near-close execution is live: grade strategy promotion under
fill_mode=next_open; keep close-fill as historical control. - After near-close ships: grade under a close-like / near-close fill mode (actual live path).
- No more sim arms on fill timing for this snapshot.
- Gap-cap: do not ship.
- Strategy work (nasdaq_all, fip_id, sector) waits until the execution path is decided — those experiments must be graded under the fill mode you will trade.
Ops design — implementation plan (code-checked)
Assumptions verified against current code before ship:
- Intraday pipeline already fetches/upserts the in-progress day-t bar all
session (
fetch_ohlcvend_date defaults to today). Near-close job = OHLCV fetch → R:R scan (no new snapshot synthesizer). - One global
schedule_timezone(defaultEurope/Berlin); storedSystemSettingvalues shadow code defaults — defaults alone do not migrate prod. observe_reentry_gate_transitionsstamps timestamps with no same-day guard today — dual scans would accelerate fail→requalify unless fixed intrade_policy.
Semantic guard (ship step 1 — precondition)
In trade_policy (not the scheduler):
reentry_gate_requalified_atmay only be set whenreentry_gate_failed_atfalls on an earlier America/New_York trading date than the current observation.
Manual mid-day scans stay allowed; same-day fail+qualify cannot unlock. Unit test: fail 10:00 / qualify 15:35 same day → still locked; qualify next day → unlocked.
Schedule split
| Slot (America/New_York) | Jobs |
|---|---|
| Morning (~02:00) | OHLCV backfill, benchmark, sentiment, fundamentals — no qualifying R:R scan |
| Near-close (~15:30 Mon–Fri) | OHLCV fetch (refresh day-t bar) → R:R scan (only daily qualifying observation) |
| After close (~16:30–17:00) | Outcome eval on its own slot (not chained to the partial-bar scan) |
| Intraday hourly | Unchanged in NY terms; last ~16:00 still mid-session under 15m feed |
- Near-close scan 1–5 only; US-holiday no-ops are fine (stale identical data can’t flip gates) — comment only, no exchange calendar.
- Do not run morning + near-close qualifying scans; move the scan, don’t add a second.
Behavior change to document (not an accident)
With scan at ~15:35 ET, stops closed by earlier same-day intraday outcome
evals can get a same-day fail observation — closer to the promoted
gate_reset arm (stop-day close may establish failure) than today’s
strict_gate_reset analogue (scan always before any eval). Stops after the
bell still wait a day. Rewrite README “Live timing matters” / post-stop sections
and a line here when shipping.
Feed / paper honesty
- Document 15-minute delayed SIP: 15:35 scan may see ~15:20 prices; OK for 12-1.
- Paper entry price ≈ scan entry (near close) is nearly automatic; add
fill_mode=near_closeera tag so Track Record can separate morning-scan / near-close / future broker-routed eras. - Morning sentiment staleness is display-only; gate is price-only (GTL parity / neutral-sentiment backtest). One doc line closes that.
Stored settings migration (ship step 4)
Flip global default TZ to America/New_York and re-express crons in NY time.
Also migration (or documented Admin rewrite) of stored schedule_* keys so
prod does not keep 07:00 Berlin silently.
Ship order
trade_policydistinct-day requalify guard + unit test- Near-close job = existing fetch → scan; outcome eval own after-close slot
- Paper
fill_mode=near_closeera tag; verify entry marking - Defaults + stored settings migration + README/research timing rewrite
Out of scope
Broker MOC routing, more fill-timing sim, nasdaq_all / fip / sector (grade later under the fill mode you trade).
Implementation status
| Item | Status |
|---|---|
| Research evidence | Closed — this doc + matrix report |
| Distinct-day gate-reset guard | Shipped — trade_policy + unit test |
| Schedule split + near-close scan→alert | Shipped — morning / near-close / after-close (fetch→outcome) |
| Paper era tag | Shipped — fill_mode=near_close on new paper trades |
| Settings migration + docs | Shipped — alembic 023 rewrites schedule_*; README updated |
Shipped behavior change (not accidental)
Near-close scan at ~15:30 ET lets same-day fail observations after earlier
intraday stop closes — closer to promoted gate_reset than the old
morning-scan strict_gate_reset analogue. Documented in README.