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-78 — failed_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.


Triage annotation — 2026-09-28 (FIRST annotation on this row; the body above is untouched)

This row has carried no triage annotation since it was raised 2026-09-11. That was an oversight, not a judgement — it is corrected here.

Both defects present verbatim at HEAD 864da4e. src/thales/execution/failed_streaks.py:73-78 (failed_symbols_on) has no side condition, and its _index at :56-70 buckets a failed order into failed[date][sym] with no side test at :67. :111-116 (fresh_derisking_failures) still masks on o.get("symbol") not in failed_prev_session. The grep-pinned test is still the only pin: tests/test_execution/test_failed_streaks.py:83-93 reads Path("src/thales/cli.py").read_text() and asserts on substrings — :83-93, one line longer than the body's :83-92; the last assert is at :93. The cli run wiring is unchanged (cli.py:1129, :1142, :1151, :1157, :1176, with the unreadable-history fail-safe at :1152-1156), and there is still no CliRunner harness for cli run anywhere in tests/.

THE PREMISE IS NO LONGER AN ANALOGY — the masking set is measurably non-empty in live data. The body argues by construction that a failed BUY on day N-1 can mask a first failed SELL on day N. Measured this run from data/state/meanrev/order_log.jsonl: failed BUY orders exist on seven sessions — SATS on 2026-07-20/21/22/23 and BRK-B on 2026-08-17/18/21 — while AVB's failed SELL streak runs 21 consecutive sessions, 2026-08-17 → 2026-09-18. BRK-B's failed buys on 08-17 and 08-18 populated prev_failed for 08-18 and 08-21, so a first-ever BRK-B sell failure on either of those days would have exited 0 — a silent de-risking failure on a live book. The row is strictly stronger than it claims; this measurement should be written into the build's fixture set rather than re-derived.

One stale comment found next door, worth fixing in the same PR: cli.py:1143 still describes the AVB situation as "nine-plus" sessions. It is 21.

Compounding, and the fix sites do NOT overlap. OSR-1 shows the same exit-4 alarm is blind to broker-side terminal rejections (digest.py:114, state.py); this row shows it is blind to a second path (the side-blind mask, failed_streaks.py:73). Two independent silences on one alarm, two disjoint fixes. A builder taking either should read both rows.

Blocked on: nothing. No owner ruling, no credentials, no forward clock — execution/simulated.py exists. Build-ready. Effort: the body says ~0.75 pd; the side-aware mask is ~1 h and the whole cost is the missing CliRunner + SimulatedBroker harness for cli run, for which there is no precedent fixture. 0.75 pd is honest and possibly light.


Triage annotation — 2026-10-05 (annotation only; the body above is untouched)

Both defects verbatim at HEAD 502690b, line cites unchanged. Side-blind: src/thales/execution/failed_streaks.py:73-78 — failed_symbols_on(orders, session) returns set(failed.get(session, {})) - ok.get(session, set()) with no side condition, consumed by fresh_derisking_failures at :111-116 (o.get("symbol") not in failed_prev_session). Wiring intact in src/thales/cli.py (:1145 import, :1149, :1151, :1157, the 🔴 banner :1169, raise typer.Exit(4) :1176). Grep-pinned: the only test is still tests/test_execution/test_failed_streaks.py:83-92, whose four assertions are all substring matches against cli.py read as text — every neutering mode the body lists (try-wrap, an exception before the raise, reordering) still passes green. The buy-masking case remains untested: the behavioural test above it uses prev_failed = {"AVB"} (sell-side) and its only buy case is a same-day fresh buy, not a prior-session one.

THE MASKING SCENARIO HAS A DOCUMENTED NEAR-MISS IN LIVE DATA — it failed to fire by one session. From data/state/meanrev/order_log.jsonl:

2026-08-26  AVB sell  failed (emergency_exit)
2026-08-27  AVB BUY   failed (selection)      <-- a failed BUY
2026-08-28  (no failures)
2026-08-31  AVB sell  failed (emergency_exit)

AVB is the only symbol in any sleeve's log carrying both buy- and sell-side failures. On 08-31 previous_session resolved to 08-28, which was clean, so the sell escalated as NEW — correct and loud. Had that sell failed on 08-28 instead, the 08-27 failed BUY would have suppressed it, exactly as the body describes. The row is no longer arguing from a constructed example.

Fleet-wide base rates, re-measured from the three committed order logs (run_summary and market_replace_of rows excluded): 20 sessions carry a failed SELL or close, 8 carry a failed BUY, 3 carry both — meanrev 4,229 orders / 28 failed, momentum 595 / 1 failed, vrp 62 / 0 failed. So failed de-risking orders are common and failed BUYs are frequent enough to be a live masking source.

EXPIRY — the horizon line is now stale and should be read as history. It says "live daily — the AVB streak proves failed de-risking orders are not hypothetical". The AVB streak ended 2026-09-18, and the three sessions since (10-01, 10-02, 10-05) are clean fleet-wide; the momentum sleeve has exactly one failed order in its entire 2026-04-07 → 2026-10-05 log (SATS buy, 2026-07-01). The defect is latent today rather than bleeding. The pre-registered kill criterion ("two behavioural tests, both red today") is unaffected and still accurate.

Rank this week: 3 of 12 (P0 — latent, but it is the one alarm for exposure left on after a failed exit, and it has two independent ways to stay silent: a masked escalation and a test that cannot see behaviour). BLOCKED on: owner approval only. 0.75 pd; src/thales/execution/simulated.py is still present for the behavioural harness.


Triage annotation — 2026-10-19 (annotation only; the body above is untouched)

This row's 2026-10-05 expiry note — "latent today rather than bleeding, because the AVB streak ended 2026-09-18 and the sessions since are clean fleet-wide" — is false. The streak resumed, and it failed again today.

Re-measured this run across all three sleeves' order logs (run_summary and market_replace_of rows excluded):

2026-10-05 notethis run
sessions carrying a failed SELL/close2026
sessions carrying a failed BUY810
sessions carrying both34

Failed de-risking sells (cause: emergency_exit) on 2026-10-09, 10-12, 10-14, 10-15, 10-16 and 2026-10-19 — six of the last eight sessions that carry any failure, today included. Per-sleeve: meanrev 35 failed of 4,383 orders; momentum 1 of 476 (SATS, a buy, 2026-07-01); vrp 0 of 17.

A cleaner arming instance than the one in the body. The row argues the mask from the 2026-08-26 → 08-31 near-miss. On 2026-10-09 AVB's de-risking SELL failed and WBD's BUY failed (cause: selection) in the same session, so on 2026-10-12 failed_symbols_on(prev_session) = {AVB, WBD} — a buy-only symbol sitting in the set that fresh_derisking_failures (failed_streaks.py:111-116) masks against. Nothing was in fact masked (WBD had no sell failure to suppress), but that is the arming condition the row describes, six sessions ago, on the daily-selecting sleeve, and it is newer and tidier than the fixture the body derives. Worth re-deriving the test fixture from it.

Both defects verbatim at HEAD. failed_streaks.py:73-78 (failed_symbols_on = failed − ok, no side condition; _index at :56-70 buckets with no side test at :67); the only pin on the escalation is still tests/test_execution/test_failed_streaks.py:83-92, which reads src/thales/cli.py as text and asserts four substrings — grep -rn 'invoke(app, \["run"' tests/ returns nothing, so there is still no CliRunner harness for cli run anywhere. cli.py:1140 still says "nine-plus sessions".

One new consequence the cutover added. The escalation did fire Exit(4) twice this month (10-09 and 10-14, AVB fresh after a clean prior session). On the Windows box exit 4 becomes Box-Result: failed (trade), which hands the sleeve to GitHub's backup leg — and from 2026-10-19T00:15Z that leg is refused for billing, while the _alert mail is withheld under config/fleet.yaml:61's weekly_digest_only. So on a day like today, a correctly-fired exit-4 has no backup runner and no mail; it survives only as a Box-Result trailer that one reader checks by hand.

Live state: blocked on approval only · effort ~0.75 pd · urgency UP — the defect is armed and de-risking orders are failing today.