Files
dennisthiessen d9c4cd35eb
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m21s
Deploy / deploy (push) Successful in 40s
docs: mark near-close decision baseline as shipped
2026-07-18 19:22:10 +02:00

8.5 KiB
Raw Permalink Blame History

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 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. Superseded 2026-07-18 — near-close shipped to prod (commits 736451e, a71dd4a; migration 023). Decision baseline is now fill_mode=close (≈ the live near-close path). Live fill quality is monitored via fill_mode=near_close paper trades: entry price vs the day's final close (available after the after-close fetch overwrites the partial bar).
  3. After near-close ships: grade under a close-like / near-close fill mode (actual live path). ← Active.
  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 Shippedtrade_policy + unit test
Schedule split + near-close scan→alert Shipped — morning / near-close / after-close (fetch→outcome)
Paper era tag Shippedfill_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.