Skip to content

Earnings Decision Lab v4.1.0 — Resilient Forward Settlement

Latest

Choose a tag to compare

@YANGRUID YANGRUID released this 05 Sep 04:17

v4.1.0 — Resilient Forward Settlement

Released 2026-09-05. Tag v4.1.0. Previous release v4.0.2.

A minor release, not a patch: it changes settlement semantics, the database schema, the
scheduler, provider normalization, and how the Track Record reports realized outcomes.

Why

On 2026-09-04 the 15:30 ET forward window settled 8 of 35 configurations and failed 27, every
one under required exit side missing on leg(s) [...]. The five contracts blocking them had
answered within milliseconds — they had answered that nobody was bidding.

Captured in-process on the shared production connection, each delivered:

tickPrice(66 DELAYED_BID)     = -1.0
tickSize (69 DELAYED_BID_SIZE)= 0
tickPrice(67 DELAYED_ASK)     = 0.01   <- ask side quotes normally

Control contracts on the same delayed feed returned real bids of 0.65 (size 416) and 0.01
(size 4), so -1 with size 0 is not a feed limitation. It is IBKR stating that the bid book
is empty.

What changed

Provider normalization

_write_tick still refuses to record -1 as a price — it is not a price — but no longer
discards the statement with it. The sentinel is recorded as {side}_no_data_sentinel, bid and
ask sizes gained canonical fields, and OptionQuote carries bid_size, ask_size,
bid_book_empty and ask_book_empty, each set only from what the provider actually said.

The bounded warm-up gained a terminal predicate. An explicitly empty book stops it after one
attempt; genuine silence still spends all five. Measured: 7.54 s per affected leg before,
1.51 s after.

Failure taxonomy

NO_BID / NO_ASK (the book is empty) are distinguished from REQUIRED_SIDE_TIMEOUT (nothing
arrived). The persisted failure detail now names the required side, the contract id, and the
observed bid/ask/last with their sizes — instead of only the leg indices.

End-of-day settlement fallback

Per leg, in strict order:

Order Pricing source Meaning
1 EXECUTABLE_BID / EXECUTABLE_ASK A real required-side quote captured at or before the close. Unchanged, and still first.
2 MARKET_CLOSE_FALLBACK That contract's own final same-session closing mark. A closing mark, explicitly not a fill.
3 EXPIRATION_INTRINSIC_AT_CLOSE Only for a contract expiring that day with no usable mark, against the official underlying close.

A contract that is not expiring never reaches step 3, so a living option is never written
down to zero merely because its book was empty. No Black-Scholes or other model price, no
prior-day close, no midpoint, and no last-trade substitution exists anywhere in the path.

v4_eod_settlement_fallback runs weekdays at 16:30 ET, applying this to whatever that
session's window could not price. It is idempotent — a configuration that already settled is
skipped — so it is a no-op on a normal day.

Two real IBKR constraints

Both confirmed live, not assumed:

  • Error 162 — daily (EODChart) bars are unavailable for OPTION contracts on this
    entitlement, so option closing marks come from the last regular-hours intraday trade bar of
    the session.
  • Error 2188 — "up-to-the-second historical data requires additional subscription" sits in
    IBKR's 2100–2200 warning band and arrives before the real bars. Classifying it as a system
    error failed every historical request whose window reached near the present, which is exactly
    the request an end-of-day close must make. Handling it also exposed a latent bug in that notice
    path: it coerced a pending result to a dict, which would have made the next historicalData
    callback raise on .append.

The CLOSE market-data tick (9/75) is deliberately not used: IBKR defines it as the
previous session's close. LULU's 127-call reported close = 3.79 against an ask of 0.01
that afternoon.

Schema — append-only settlement history

Migration d1f3a5c7e9b2:

  • drops uq_v4_shadow_config_settlement_one_per_config;
  • adds a partial unique index on shadow_config_result_id WHERE status = 'SETTLED';
  • adds pricing_method, recovery_provenance, supersedes_settlement_id.

A failed attempt stays exactly as written and is never rewritten (the UPDATE-rejecting trigger
is unchanged). A recovery is appended as a new row pointing back at what it supersedes. "Never
settled twice" became a database guarantee rather than an application convention. Read models
resolve a configuration to its settlement of record so a recovered position is counted once,
not once as failed and again as settled.

The downgrade refuses to run while any configuration carries superseded history, rather than
destroying evidence to restore the old constraint.

Track Record — settlement quality is first-class

Every settled position carries an evidence grade, worst-wins across its legs:

Grade Meaning
EXECUTABLE_BID_ASK Every leg priced on its own required side
MARKET_CLOSE_FALLBACK At least one leg priced at that session's closing mark
EXPIRATION_INTRINSIC_AT_CLOSE Intrinsic at the official underlying close
UNRESOLVED No permitted price could be established

The Track Record reports counts and rates for all four, and offers two views:

  • All forward outcomes — every settlement of record.
  • Executable-only outcomes — realized metrics restricted to settlements transactable on
    every leg.

The second is an analytics filter and nothing more. No observation is deleted, both views read
the same immutable rows, and the quality counts always describe the full set so the two
reconcile. A closing mark is never presented as an executable fill.

Recovery of 2026-09-04

All 27 blocked configurations were settled at 16:54:13 ET — the real observation instant, not
restamped to 15:30. 16 unique contracts were quoted once each and mapped back to every
configuration sharing them. Expiration intrinsic was never needed: every blocked contract had a
real traded closing mark.

The original 27 failed rows and all 24 original observations are byte-identical to their
pre-recovery digests (65f1c8fd… settlements, 73091416… observations). The 15:30 window's own
telemetry still reads "1 of 6 captured", deliberately unaltered.

Verification

1,695 backend tests (ruff and mypy clean over 231 source files); frontend tsc, eslint and
build clean. Migration head d1f3a5c7e9b2.

Not in this release

Nothing here changes candidate generation, strike geometry, expected-move methodology, the
V4.4A valuation, V4.4B ranking v1, strategy semantics, or the six cohort definitions.