Thales
← research journal

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

FOS-3 — the new exit-4 escalation can be masked by yesterday's failed BUY, and its only test reads the source as text: the one exposure-left-on alarm has two ways to stay silent

status: open · raised panel run 21 (2026-09-11, change-review catch on PR #154 — does NOT count toward the frontier cadence) · class: loudness & safety (G2, but the exit-4 path is trading-contract, not observability) · effort ~0.75 pd · horizon: live daily — the AVB streak proves failed de-risking orders are not hypothetical


Plain-language summary for an owner reading one paragraph. Last week's IGD-1 leg-3 ruling built the escalation this shop had been missing: thales run now exits 4 — redding CI — when a de-risking sell fails for a symbol that was NOT already failing, so a NEW stuck exit pages instead of joining the furniture. Two defects in the build, found by this run's change review. First, "already failing" is side-blind: if symbol X had a failed BUY yesterday (a symbol halt or one broker-wide buying-power rejection day poisons every symbol at once) and X's SELL fails today for the first time, today's exposure-left-on event exits 0 and lands only in the digest — the ruling's letter is met, its rationale (a failed buy leaves no exposure; the sell does) is not. Second, the only test pinning the escalation reads cli.py AS TEXT and asserts substrings — it fails if the code is deleted, but passes if the code is present and neutered (an exception before the raise, a later try-wrap, reordering that makes it unreachable). The single alarm for "the book cannot get out of a name" is guarded by a detector that cannot fail in its primary failure mode.

Mechanism — ground truth verified this run at HEAD fa55f0d

  • src/thales/execution/failed_streaks.py:73-78failed_symbols_on returns every failed symbol for the prior session with no side condition; fresh_derisking_failures (:111-116) filters today's de-risking failures on symbol not in failed_prev_session. A prior-day failed buy therefore suppresses a first-ever failed sell. The existing test (:71 region) only exercises a sell-history prev_failed — the buy-masking case was never considered. Contrast: the unreadable-history fallback (cli.py:1152-1156) correctly fails safe (unknown → all NEW), so the side-blindness is an oversight in otherwise fail-safe-aware code.
  • tests/test_execution/test_failed_streaks.py:83-92 — reads src/thales/cli.py as a string and asserts substrings between "Orders FAILED:" and "Exit 4: state is persisted". Its own docstring admits why ("the run command builds a live broker"). A blanket try/except around cli.py:1143-1176, an exception between computing fresh (:1157) and raise typer.Exit(4) (:1176), or reordering all pass green. The behavioral pieces of failed_streaks.py ARE tested; the escalation wiring is not.
  • The converse defect direction errs loud, not silent — a HALT day (no orders attempted) resets prev_failed, so a standing streak re-escalates as NEW after any halt — and is accepted as-is.

Fix shape (propose-only)

Make the masking set side-aware: only prior de-risking failures mask (at failed_symbols_on or its call site). Build a thin injectable-broker harness for cli.run (SimulatedBroker + tmp state dir + Typer CliRunner — src/thales/execution/simulated.py already exists, so this is real work, not a wish) and delete the text-grep test.

Pre-registered kill criterion / done-when

Two behavioral tests, both red today:

  1. Symbol X fails a BUY on session N−1 and a first-ever SELL on session N → run exits 4 (currently 0).
  2. CliRunner-invoked thales run with an injected broker exits 4 on a fresh de-risking failure, 0 on a standing one, and treats unreadable history as all-NEW — verified by mutation: wrapping the escalation block in try/except or removing the raise must red the suite.

Done when the text-grep test no longer exists and the behavioral tests run in the pre-trade gate (exit-4 is the run command's contract, not observability). Keeping the grep test "alongside" does not satisfy this row.