Files
signal-platform/docs/research/execution-recovery.md
T
dennisthiessen 736451e26f feat: near-close scan schedule and distinct-day gate reset
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.
2026-07-18 17:55:39 +02:00

193 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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 t1
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% ≈ closes 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:3015: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:
1. Take-profit exits (gate target as TP)
2. FIP as an in-book filter
3. **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
1. **Biggest lever is execution scheduling**, not a strategy rewrite.
2. **Until near-close execution is live:** grade strategy promotion under
`fill_mode=next_open`; keep close-fill as historical control.
3. **After near-close ships:** grade under a close-like / near-close fill mode
(actual live path).
4. **No more sim arms** on fill timing for this snapshot.
5. **Gap-cap:** do not ship.
6. **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_ohlcv` end_date defaults to today). Near-close job =
**OHLCV fetch → R:R scan** (no new snapshot synthesizer).
- One global `schedule_timezone` (default `Europe/Berlin`); stored
`SystemSetting` values shadow code defaults — **defaults alone do not
migrate prod**.
- `observe_reentry_gate_transitions` stamps timestamps with **no same-day
guard** today — dual scans would accelerate fail→requalify unless fixed in
`trade_policy`.
### Semantic guard (ship step 1 — precondition)
In `trade_policy` (not the scheduler):
> `reentry_gate_requalified_at` may only be set when `reentry_gate_failed_at`
> falls 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 MonFri) | OHLCV fetch (refresh day-t bar) → **R:R scan** (only daily qualifying observation) |
| After close (~16:3017: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 **15 only**; US-holiday no-ops are fine (stale identical data
cant flip gates) — comment only, no exchange calendar.
- **Do not** run morning + near-close qualifying scans; move the scan, dont 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 todays
`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_close` era 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
1. `trade_policy` distinct-day requalify guard + unit test
2. Near-close job = existing fetch → scan; outcome eval own after-close slot
3. Paper `fill_mode=near_close` era tag; verify entry marking
4. 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.