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.

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-295load_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.