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.

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-BAAA.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).