# EXQ-1 — one invalid symbol dumps the ENTIRE order batch to market orders (live momentum exposed; contaminates the December TCA gate)

status: open · raised panel run 13 (2026-08-24) · class: execution-quality (live trading path) · judgement: **YES** · effort ~0.5 pd · horizon: live now, 3rd recurrence (2026-08-17 / 08-18 / 08-21)

---

**Plain-language summary for an owner reading one paragraph.** Your live
momentum execution is supposed to place *marketable-limit* orders (it exists
precisely because plain market orders slip ~+101 bps vs the 10 bps cost
model). But whenever a single dash-class ticker such as `BRK-B` is in the
batch, the one pre-trade quote request for the *whole book* throws, the code
catches it and returns **no** quotes for **anyone**, and every order that day
falls back to a **market** order. On 2026-08-21 that was all 133 meanrev
orders. It also silently poisons the December go-live TCA criterion (median
slippage ≤ 50 bps), which then measures market-order slippage while believing
it is measuring marketable-limit. Two coupled root causes; a small, local fix.

## Mechanism — verified line-by-line at HEAD `05895ab`

**(a) Whole-batch quote fail-open.**
`src/thales/execution/alpaca_broker.py:~278-289` issues ONE batched
`get_stock_latest_quote(symbol_or_symbols=list(symbols))` inside a single
try/except. Any `APIError` — e.g. `invalid symbol: BRK-B` — is caught and the
function returns `{}` for the **entire** universe (log line: "quote fetch
failed — falling back to market orders", `:~288`). Then
`src/thales/execution/pipeline.py:~318-319` sets `limit_price=None` for every
order whose symbol is absent from the (now empty) map → the whole batch
submits **MARKET** via the retry path. The function docstring (`:~273-275`)
promises *per-symbol* omission; the batch-level except breaks that promise.

**(b) Missing reverse symbology map — the standing trigger.**
`src/thales/data/universe.py:~34` normalizes `BRK.B → BRK-B` one-directionally
for the data side. There is **no** `to_alpaca_symbol` and **no** reverse map
anywhere in `src/thales` (grep-confirmed). Alpaca lists Berkshire class B as
`BRK.B`, so the dash-form order both fails on its own
(`42210000 asset "BRK-B" not found`) **and** is the recurring trigger for (a).
Every dash-class name in the Russell 1000 (`BRK-B`, `BF-B`, `LEN-B`,
`UHAL-B`, …) is a candidate trigger.

**Blast radius — production is exposed.** `config/settings.yaml:~267`
`order_type: "marketable_limit"` (live momentum) and `config/meanrev.yaml:~153`
both route through the shared `pipeline.execute_orders` / `get_latest_quotes`
path. So the LIVE momentum book carries the same whole-batch downgrade, not
just the negative-control sleeve.

**Severity beyond one day's slippage (added by the adversarial review).**
Marketable-limit exists because market orders slipped +101 bps vs the 10 bps
model (`pipeline.py:~297-302`). One bad symbol silently reverts the entire
book to market for the day, so every fill books market slippage while the
pre-registered TCA gate (`settings.yaml` `tca_median_slippage_bps_max: 50`)
believes it is measuring marketable-limit. This **contaminates the
December-gate TCA criterion**, not merely a single day's execution quality.

## Data plan
None — static code + the 08-21 job log already in the repo
(`research/2026-08-21_meanrev_clearing_day_churn.md:46-62` log-proves the three
recurrences). Owned.

## Test design + kill criterion (pre-registered, negative-controlled)
Unit test with a data-client stub that raises `APIError("invalid symbol: X")`
on ONE symbol in a multi-symbol request: `get_latest_quotes` must still return
quotes for **every other** symbol (deny-asserted / red today, green post-fix).
Second test: `to_alpaca_symbol("BRK-B") == "BRK.B"` and the response is
reverse-mapped back to `BRK-B`, both directions. **Kill criterion:** both
tests green post-fix; revert either the symbology map or the per-symbol
quote-fetch and the corresponding test reds. If, after the fix, a live batch
still degrades to market en masse, the root cause was mis-identified — reopen.

## Fix shape (~0.5 pd, PROPOSE-only — this row does not implement it)
1. `to_alpaca_symbol()` dash→dot translation at the Alpaca boundary (order
   submit **and** the quote request), reverse-mapping response keys back — this
   alone removes the known `BRK-B` trigger.
2. Defense in depth in `get_latest_quotes`: on an `APIError` naming an invalid
   symbol, drop it and retry once (or chunk the request), so one bad name can
   never zero the batch — this closes the class, not just the known trigger.

## Ops note (not a code leg, no kill criterion) — AVB stuck position
Alpaca `40010001 "asset AVB is not active"`: the broker HOLDS the position but
marks the asset untradeable, so the strategy cannot exit it. Broker-side action
only (manual close, or waiting out the corporate action). Recorded so it is not
mistaken for a code defect; nothing in this row fixes it.

---

## BUILT — 2026-08-26

**Outcome: shipped.** Both legs of the proposed fix shape, plus one the row
did not specify (see below). Full suite green (1238 passed, 1 skipped).

**Route — recorded honestly:** built on the owner's direct instruction in an
interactive session, NOT via `queue/approved/` and the Wednesday implementer.
The approval gate exists to stop the MACHINE self-approving; the owner moving
his own item is not a gate breach, but the row should not read as though it
passed through triage when it did not.

**Timing.** Momentum's next monthly full rebuild is **Tue 2026-09-01** — the
concentrated exposure, since the flagship is quiet between selections. The
universe carries exactly two live triggers (`BRK-B`, `BF-B`).

**What shipped** (`src/thales/execution/alpaca_broker.py`):

1. `to_alpaca_symbol` / `from_alpaca_symbol` — class-share translation scoped
   to the `AAA-B` ↔ `AAA.B` shape, so OCC option symbols and ordinary tickers
   pass through untouched.
2. Per-symbol quote resilience — `_fetch_quotes_resilient` drops the symbol
   the error NAMES (word-bounded match; a bare substring test would drop good
   names) and retries; if the error names nothing actionable it BISECTS.
   Bounded at 24 calls, degrading to market orders only for the unpriced
   remainder — never for the whole book.
3. **Beyond the row's fix shape:** the reverse map is applied to every
   read-back path too (`get_positions`, `get_position`, `get_open_orders`,
   `get_order_status`, `get_order_by_client_id`). Without this the fix would
   have introduced a NEW defect — orders would finally fill, and every
   class-share position would reconcile as an unknown symbol.

**Kill criterion — met, with isolation.** Reverting the symbology map reds
only the three symbology tests (batch-resilience tests stay green); reverting
the per-symbol fetch reds only the batch tests (pure symbology tests stay
green). Both verified by running each revert.

**Not fixed here (unchanged from the row's ops note):** the AVB stuck
position. Alpaca `40010001 asset not active` — the broker holds it but marks
it untradeable, so no code change can exit it. As of 2026-08-26 it has failed
7 consecutive sessions (since 08-17), which is long enough that the digest's
`!! N FAILED order(s)` line has become permanent furniture — a real new
failure would now hide behind it. Broker-side action, owner.

---

*Provenance recorded by triage 2026-09-07 (annotation only; the record above is untouched): shipped in **PR #117** (`2072f4b`, merged 2026-08-26).*
