# PLB-1 — the placebo's C5 beta companion reads the FROZEN research store, which by design ends before the live window begins, so it has never computed and never can; a working live-SPY loader already exists three modules away and documents this exact trap

status: open · raised 2026-09-10 (owner-side interactive session, correcting its own record from #157 after the first decision-grade placebo run) · class: truth divergence between layers · judgement: **YES** · effort ~0.1 pd · horizon: fired at the 2026-09-08 maturity; fires again at every future placebo read, including December's

---

**Plain-language summary for an owner reading one paragraph.** The placebo
test asks: did our stock-picking beat 200 random 50-name portfolios? A
registered companion (C5, 2026-08-01) was supposed to repeat that comparison
with the market's own move subtracted, so a rising tide could not flatter
either side. It has never once produced a number. The reason is not a stale
file anybody forgot to refresh: it reads `data/raw/SPY.parquet`, which is
part of the **deliberately frozen** owned research store, and that store ends
before the live trading window starts. The two can never overlap — not now,
not in December. Meanwhile the forward OOS monitor computes a perfectly good
SPY comparison over the same window, because it calls a different loader that
refreshes SPY when the on-disk copy is behind.

## Mechanism — ground truth at HEAD `82e802e`

- `src/thales/backtest/placebo.py:288-295` — `load_spy_returns(parquet_dir)`
  reads `data/raw/SPY.parquet` and returns `{}` if the file is absent or
  unreadable. There is no refresh path.
- The owned store is frozen by design (CLAUDE.md, "acquire-once → freeze →
  own"): sampled at HEAD, **299 of the first 300 symbols end 2026-06-09**,
  and `SPY.parquet` ends at a **2026-06-05** return. The live clean clock
  starts **2026-06-11**. The overlap is empty by construction, so the
  companion's own guard fires and prints "unavailable" — correctly, and
  forever.
- `src/thales/execution/digest.py:351-354` — `_load_spy_aligned` does the
  same job and **names this exact hazard in its docstring**: *"Fetches SPY
  alone if the on-disk parquet is stale (SPY is not in the universe, so
  nothing else refreshes it). Degrades to {} on any failure."* SPY is not a
  universe member, so the daily fetch never touches it — the trap was
  understood, written down, and handled in one caller only.
- `src/thales/cli.py:2979` routes the oos-monitor through that working
  loader; `cli.py:792` routes the placebo through the frozen-store one. On
  2026-09-09 the first produced excess-over-SPY Sharpe −2.720 (SPY +1.443,
  beta 0.41 / corr 0.49 over 60d) while the second reported 0 usable days —
  same session, same window, same benchmark, two different answers because
  of two different loaders.

## Why it matters

The 2026-09-09 placebo is the first decision-grade read and it stands at the
**2nd percentile** of random stock-picking. The registration built C5
precisely so that number could be read net of the market's move — the check
that separates "the strategy is bad" from "the tape was good and we lagged
it." That check has been unavailable at the one reading that mattered, and
December's gate reads the same evaluator.

## Fix shape (propose-only)

Point the placebo at the loader that works — pass the digest's
`_load_spy_aligned` behaviour (fetch-if-stale, degrade to `{}` on failure)
rather than a bare parquet read, or lift it into one shared helper both
callers use. The registration's rule that SPY may never be silently proxied
by another symbol is preserved either way: the fix changes where SPY comes
from, never what it is.

## Test design + negative control

Fixture: a live window that starts after the on-disk SPY series ends → the
companion must still compute (proving the refresh path is reached), and a
fixture where the fetch fails → it must still degrade to the explicit
"unavailable" note rather than a proxy or a crash. **Negative control, shown
in the PR:** restore the bare parquet read and assert the first test fails.

## Kill criterion (pre-registered)

If the fetch path cannot reach SPY from the environment the evaluator
actually runs in (the owner's Mac, per the AUDIT.md register), the item dies
and is replaced by a one-line change to the run instruction — fetch SPY
before running the placebo — because a companion that needs an undocumented
manual step is worse than one that says "unavailable" out loud.

## Prior art / relitigation

None: `grep -riE "beta.compan|c5|spy" research/queue/ research/RESEARCH_QUEUE.md`
returns only a commit hash (`6c5da39`). This row CORRECTS the record in
#157, which attributed the failure to a stale file — the framing was wrong
and the corrected finding is sharper. Sibling in shape, not in substance, to
`research/queue/open/fos-2-fleet-email-missing-streak-split.md`: two callers,
two sources, the divergent one silent.

**Cap note:** `open/` was at its pre-registered 12-file cap before this row,
so filing it puts the directory at 13. That is deliberate and disclosed, not
an oversight: Monday's triage owns the mechanical overflow rule and should
adjudicate this row against the incumbents rather than have the filer
pre-empt it.
