# B3 — Survivorship-free price-only backtest of the equity-factor sleeves

**Status:** design approved 2026-06-17.
**Why:** pre-screen the equity-factor constructions against history (DSR/OOS verdict) *before* committing weeks of blind forward. Fundamentals are DOW-30-only on our Tiingo plan (see [[project_fxhnt_b1_b2_paper_tracks_live]]), so this is **price-only** (momentum + low-vol) — which is exactly what the live sleeves now run, and prices are broad + survivorship-free.

## Scope
Backtest the 3 price-only constructions (`long`/`ls`/`tilt`, momentum+low-vol) over the full available price history, **survivorship-free**, → a gauntlet verdict per construction. The walk-forward engine also generalizes to the single-market price "kinds" (which already fit `domain/backtest.py run_backtest`), but the equity-factor sleeves are the target.

## Reuse (do not rebuild)
- `domain/strategies/equity_factor.py` — `momentum_score`, `lowvol_score`, `composite_price`, `construction_weights`, `book_return`.
- `domain/backtest.py compute_stats` — Sharpe/CAGR/maxDD on any return series.
- The gauntlet (`GauntletSettings`: dsr_min 0.95, oos_min_sharpe, max_is_oos_decay 0.50, oos_fraction 0.40) + its evaluation path (as used in `application/research.py`).
- The DuckDB warehouse (`adapters/warehouse/duckdb_feature_store.py`, `WarehouseIngest`) — the SSOT cache.
- `adapters/data/tiingo_daily.py` + `tiingo_universe.py` (supported-tickers with start/end dates).

## Architecture
### B3a — Point-in-time data ingest → warehouse (the heavy piece; build FIRST)
- A `HistoricalEodIngest` service: for each ticker in the survivorship-free US universe (`supported_tickers`: assetType=Stock, USD, NYSE/NASDAQ/AMEX, **including delisted** via end-date), fetch full Tiingo EOD (date, adjClose, close, volume) and bulk-write to the warehouse (long-format features per `DuckDbFeatureStore`). Also persist each ticker's `start_date`/`end_date` (a `ticker_membership` table/feature) for PIT membership.
- **Resumable + incremental:** track which tickers/date-ranges are already ingested (skip/append only new). Safe to re-run.
- **Cost discipline (HARD):** the full universe is ~22k tickers / ~20GB — about half our 40GB/mo Tiingo bandwidth. So: (1) build + validate the ingest on a **small batch (~100 tickers)** first; (2) the **full pull is a separate, deliberate, checkpointed run** (log running ticker count + bytes; never silent). Do NOT run the full pull as part of the build task.

### B3b — PIT reconstruction + walk-forward + verdict (after B3a data exists)
- `universe_asof(D, n)` (pure over warehouse reads): tickers with `start ≤ D < end` (active at D), ranked by trailing dollar-volume (close×volume from warehouse over a window ending at D) → top-N.
- `prices_asof` / returns from the warehouse.
- Walk-forward (monthly): PIT universe → `compute_factor_scores` price-only (momentum+lowvol) **reading the warehouse** (a warehouse-backed `DailyBarClient`, not live Tiingo) → `construction_weights` → hold → `book_return` with cost → daily return series per construction.
- Verdict: `compute_stats` + IS/OOS split (40% holdout) + DSR + decay → GO/NO_GO per construction → JSON report under `/data/surfer/backtest/` (+ optional cockpit `backtest_summary` field).

## Defaults
window = full available · rebalance monthly · cost **15 bps/turnover** (flat; conservative since it understates 1990s–2000s spreads) · OOS holdout 40% · n=150 · DSR trial-count = the 3 constructions (+ any factor variants) tested.

## Testing
- B3a: ingest service with a fake Tiingo client → warehouse round-trip (rows written, membership stored, resumable skip on re-run, delisted ticker retained). No live network in tests.
- B3b: PIT reconstruction on a synthetic warehouse (delisted ticker excluded after end-date; dollar-volume rank as-of D; insufficient-history name dropped). Walk-forward determinism. Verdict on a known return series. No live network.
- strict mypy, two-stage subagent review.

## Out of scope
- Fundamentals factors (need the Tiingo add-on — separate; value/quality kept in the domain for then).
- Nightly automation of the backtest (on-demand/occasional; the data ingest can be nightly-incremental, the backtest is run deliberately).
- Live execution.

## Honest caveats
- One-time data pull is real (~22k calls / ~20GB / ~1 day; bandwidth-tight) — hence the small-batch-first + checkpoint discipline.
- Cost model is flat-approximate, not era-exact.
- Tiingo adjClose handles splits+dividends (total return); delisted final returns rely on Tiingo's end-date data (verify a sample).

## Build order
1. **B3a** — warehouse schema + `HistoricalEodIngest` (resumable) + tests, validated on ~100 tickers. **[checkpoint before the full pull]**
2. Full PIT ingest run (deliberate, monitored).
3. **B3b** — `universe_asof` + warehouse-backed bars + walk-forward + verdict + report + tests.
4. Wire a verdict surface (report / optional cockpit field).
