# SKW-2 — the skew capture sends dash-class symbols Alpaca rejects; BRK-B and BF-B have never captured

status: open · raised 2026-09-01 (owner session; observed daily in skew logs since at least 08-28, first flagged 2026-08-29) · class: capture-stream completeness — **deliberately a queue row, not a stealth fix, because the remedy changes what a pre-registered stream collects** · judgement: owner call · effort ~0.25 pd · horizon: every capture day adds two more permanent per-name notches

---

**Plain-language summary.** Every skew snapshot logs
`skew: BRK-B chain fetch failed: invalid underlying symbol: BRK-B` (and the
same for `BF-B`). Same root cause as EXQ-1 (2026-08-26): the internal canon
is the dash form, Alpaca wants `BRK.B`. The trading path got a symbology map
at the broker boundary; the capture path (`options_skew.py`) still sends the
dash form, so the two class-share names in the 903-name universe fail their
chain fetch daily and have NEVER contributed rows to `options_skew.jsonl`,
`options_chains/`, or `options_iv_term/`.

## Why this is not just "apply the fix"

The fix is one line (`to_alpaca_symbol` at the fetch, reverse-map on write —
the helpers already exist in `execution/alpaca_broker.py`). But applying it
CHANGES A PRE-REGISTERED CAPTURE STREAM mid-accrual: two names would begin
appearing on the fix date, with a permanent pre-fix hole behind them. The
moat doctrine treats stream semantics as registered, and the B4 skew
activation pin computes cross-name medians and row floors over the stream —
so the change must be:

1. dated in `CAPTURES.md` (per-name coverage note: BRK-B/BF-B absent
   `<stream start>`→`<fix date>`, present after — a vendor-reject hole, not a
   collection change);
2. checked against the B4 activation criteria for sensitivity (adding 2 of
   ~900 names to medians is almost certainly immaterial — say so with the
   arithmetic, don't assume it);
3. applied to the capture path only (the trading boundary is already fixed).

## Test design + kill criterion

Unit: a fake chain client asserting the wire symbol is `BRK.B` while the
persisted row's `underlying` is `BRK-B`; revert the map → red. Live check:
first post-fix capture day shows both names in the day's jsonl and chains
parquet, and the daily `chain fetch failed` warnings disappear.

## Explicitly out of scope

The per-name backfill of the pre-fix hole (no free historical source — the
same reason the whole stream is forward-only).

---

## Triage note — 2026-09-07 (annotation only; the body above is untouched)

**Premise re-verified from the data, and the row GAINED scope this week.**
At HEAD `e25f80b`, `src/thales/data/options_skew.py:280` still sends the
internal dash canon straight to the wire
(`OptionChainRequest(underlying_symbol=symbol)`), with no `to_alpaca_symbol`
— the helper that exists and is applied at every trading boundary
(`alpaca_broker.py:31`, used at `:153, :199, :240, :410`). Independently
confirmed: **zero** rows for `BRK-B`/`BF-B` (or their dot forms) anywhere in
`data/options_skew.jsonl` across the whole stream, while `BRK-B` is in the
universe (`data/universe.csv:684`). Neither prerequisite has landed —
`CAPTURES.md` still carries no per-name coverage note for either name.

**New scope, not triage's doing:** SKW-4's built record explicitly re-routes
its own leg 3 — the per-name coverage census — into this row
(`research/queue/built/skw-4-non-session-rows-served-and-per-name-coverage.md:126-129`),
naming GHC and NVR at 0 of 62 capture days. So this row now owns four
never-captured names, not two, and the CAPTURES.md dated note it asks for
covers all of them. Effort is still ~0.25 pd; the census leg adds a little.
