# Equity Leg Deployable — Design

**Date:** 2026-07-16
**Status:** Design. Makes the IBKR US-ETF `multistrat` book an honest-sized, capacity-bounded deploy edge — the growth vehicle above the crypto book's ~$350k break.
**Author:** jgrusewski (+ Claude)
**Builds on:** the capacity-aware allocator + honest-reference pipeline (crypto) + the load-once/reprice provider (merged 2026-07-16).

## 1. Problem

The fund's crypto book caps at ~$350k (measured; xsfunding-driven). To grow past that toward $1M the **IBKR
US-ETF `multistrat` book** must carry the additional capital. But `multistrat` is NOT deployable today:
1. **It can't be honest-sized.** The crypto provider (`honest_allocation_inputs`) gives equity sids no series,
   so `record_allocation` falls back to `multistrat`'s ~7-day forward NAV record, which is < `min_history_days`
   (60) → it is dropped from the allocation entirely (gets $0).
2. **Its capacity is a $10M stub** (`equity_default_ceiling`) — not a real, data-derived ceiling.

User decision (2026-07-16): build it the rigorous way — **ingest real ETF volume** for a data-driven ADV
capacity model, and charge **real trading costs** (not the inflated 15bp research stub).

## 2. Goal

An **equity honest-reference** analog (mirroring the crypto one) that produces, for `multistrat`:
- a **cost-netted honest backtest sizing series** (~441 days on Yahoo PIT → passes `min_history_days`), and
- a **real ADV-derived capacity ceiling** (from ingested ETF volume),
merged into `record_allocation` so the equity book is honest-sized + capacity-bounded exactly like crypto.
Scope stays PAPER; the forward gate (`multistrat` promote_on_pass, currently 7/20) remains the live-deploy
switch. As equity grows into it, the capacity-aware allocator already shifts capital correctly.

## 3. What exists (verified 2026-07-16) — reuse

- `multistrat.book_series(closes, ...)` → the GROSS unlevered daily book return (~441d). `multistrat.target_weights(closes, ...)` → the per-instrument position vector per day (for turnover). `FUND_INSTRUMENTS = [SPY, IEF, GLD, PDBC, DBMF]`.
- `equity_backtest_runner._turnover(prev, cur)` (two-way round-trip turnover) + its `cost_bps_per_turnover` idiom — the cost-charging pattern to mirror (but with REAL, per-instrument costs, not a flat 15bp).
- Yahoo ingestion: `YahooDailyClient.adj_closes(sym)` extracts `ind["quote"][0].close`/`adjclose`; the SAME Yahoo response already carries `ind["quote"][0].volume` — volume is fetchable by extending the extraction, no new data source. The nightly asset (`assets.py:408-421`) snapshots `adj_closes` into `YahooPitCloseRow` via `snapshot_yahoo_closes`; `YahooPitClient.adj_closes` reads it back PIT.
- `honest_reference.honest_sleeve_returns` (turnover-basis cost, sqrt-impact on ADV) + `capacity_curve` + `allocation_engine.capacity_ceiling_from_curve` — the crypto templates to mirror for equity.
- `record_allocation` merges `honest_returns` (provider) with a nav fallback; the load-once provider pattern.

## 4. Design

### 4a. Ingest ETF volume (the real ADV input)
- `YahooDailyClient.volumes(symbol) -> dict[str, float]`: extract `ind["quote"][0]["volume"]` (raw daily share volume), same fetch/retry path as `adj_closes`.
- `YahooPitVolumeRow` (cockpit_models, `yahoo_pit_volume`, cols: symbol, date, volume, first_seen_at) + `ForwardNavRepo.snapshot_yahoo_volumes` / `yahoo_pit_volumes(symbol)` — PIT store parallel to closes (append-only, first-seen frozen — same PIT discipline).
- Nightly asset (`assets.py`, next to the close snapshot): also `snapshot_yahoo_volumes(sym, live.volumes(sym), at)`.
- **Backfill**: a `fxhnt backfill-etf-volume` CLI — one Yahoo fetch per FUND_INSTRUMENT (25y range), snapshot the volume history, so ADV is available immediately (not only forward). PIT: first-seen frozen.
- **Dollar-ADV**: `adv_usd[sym][day] = adj_close[sym][day] * volume[sym][day]`, then a trailing **median** over a window (e.g. 63d) for a stable, outlier-robust ADV (median, not mean — a single volume spike must not inflate capacity). Missing volume → that day excluded (no fabricated ADV).

### 4b. Real equity cost model (`equity_honest.py` — the equity analog of honest_reference)
Per instrument, per rebalance day, charge on TRADED weight (turnover-basis, mirroring the crypto model):
- **Market impact** = `IMPACT_COEFF_BPS * impact_k * sqrt(traded_notional / adv_usd)` bps — REAL, from ingested ADV. This is what makes capacity bite: as AUM grows, `traded_notional` rises → impact rises → Sharpe degrades.
- **Spread** = a realistic PER-ETF half-spread constant (bps), documented from published typical ETF spreads (SPY ~0.3, IEF ~1.0, GLD ~1.0, PDBC ~4.0, DBMF ~8.0 bps). This is the ONE estimated input (no ETF bid/ask feed); it is a documented constant, NOT the inflated flat 15bp. `slip_bps = max(impact, spread_bps[sym])` (same max-with-spread-floor as crypto).
- **Commission** = IBKR real per-share (tiered ~ $0.0035/share, min/max) converted to bps at the day's price: `commission_bps = 100 * per_share / price`. Charged per traded share.
- `equity_honest_returns(book_ret_by_day, weights_by_day, adv_usd, spread_bps, *, aum, participation_cap, impact_k, commission_per_share) -> dict[str,float]`: for each day, net = `book_ret[day] - Σ_sym traded_w * (slip_bps + commission_bps)/1e4`, where `traded_w = |w[sym,day] - w[sym,day-1]|`. Also apply the **participation cap** to positions (a position that would exceed `participation_cap * adv_usd` is scaled down — capacity, not just cost), consistent with `honest_sleeve_returns`.

### 4c. Equity capacity curve + ceiling
- `equity_capacity_curve(book_ret, weights, adv_usd, spread_bps, *, aums, ...)` → `{aum: equity_honest_returns(...at aum)}` (load-once: book_ret/weights/adv computed once, repriced per AUM — mirrors the crypto load-once).
- Ceiling = `capacity_ceiling_from_curve({aum: sharpe_of(series)}, min_sharpe)` — the largest AUM whose net Sharpe stays viable. For these liquid ETFs the ceiling is high but REAL (DBMF/PDBC ADV-limited); report it next to the curve.

### 4d. Equity provider + wiring
- `equity_allocation_inputs(repo, deploy_sids, *, target_aum, aums, participation_cap, impact_k) -> (returns_by_strategy, ceilings)`: for each equity deploy sid (venue `ibkr-equity`, i.e. `multistrat`), build closes+volume from the PIT store, compute `book_series`/`target_weights`, then the cost-netted series at `target_aum` (ISO-keyed) + the ADV capacity ceiling. Non-equity sids ignored (crypto provider handles them).
- `record_allocation`: when `store is not None`, ALSO call `equity_allocation_inputs` for the equity deploy sids and MERGE: `honest_returns |= eq_returns`, `ceilings |= eq_ceilings`. Then the existing `returns = {sid: honest_returns.get(sid) or nav_fallback}` picks up `multistrat`'s 441-day cost-netted series → eligible (>60) → sized; `target_capital` caps it at the real ADV ceiling. `store=None`/no-equity → byte-identical to today.

## 5. Falsification / validation gates (in-cluster, after build)
- **G1 (sizes):** `multistrat` now has ≥ `min_history_days` in the allocation returns (not dropped). Report its cost-netted Sharpe/vol vs the gross book_series (costs must REDUCE Sharpe, not increase it).
- **G2 (real capacity):** the ADV ceiling is finite and data-derived (report it + the tightest-ETF binding). Sanity: it must be well above the fund's current $100k and reflect DBMF/PDBC being the tightest.
- **G3 (cost realism):** the equity cost must be materially BELOW the old 15bp stub for the liquid ETFs (SPY/IEF/GLD) — report the effective bps/turnover; if it lands ~15bp we mis-modeled.
- **G4 (combined marginal gate):** with `multistrat` sized, re-run the marginal gate for {crypto book, multistrat}. Report whether the uncorrelated equity book SURVIVES alongside the crypto book (it should add marginal Sharpe vs the crypto-book-alone, unlike vs volatile positioning-alone) and the combined deployment. Accept the honest verdict.
- **PIT integrity:** the volume snapshot is first-seen-frozen (a re-fetch must not rewrite history).

## 6. Scope
**In:** volume extraction + PIT store + snapshot + backfill CLI; dollar-ADV (median); `equity_honest.py` (real cost model + capacity curve); `equity_allocation_inputs` + `record_allocation` merge; tests; in-cluster gates.
**Out (follow-on):** live intraday spread/quote feed for ETFs (spread stays a documented per-ETF constant); options/VRP equity; raising the live `capital_ceiling` / going live (human gate); execute-side IBKR sizing beyond what already exists.

## 7. Critical review — where this could go wrong
- **Spread is the one estimated input.** No ETF bid/ask feed → per-ETF published constants. Document them; they dominate cost for liquid ETFs at fund scale (impact ~0 there), so a wrong spread mis-sizes. Keep them conservative-realistic and visible, not buried.
- **book_series is gross + unlevered.** Confirm the cost netting uses the SAME weights `book_series` applies (via `target_weights` = `L·tw·scale`), else cost/return bases mismatch. The turnover must be the ACTUAL traded weight, not the raw signal.
- **ADV median window + PIT.** A too-short ADV window is noisy; too-long lags real liquidity. 63d median is a choice — report it. Volume PIT-frozen so a Yahoo revision can't retroactively inflate past capacity.
- **Capacity for liquid ETFs is high → the ceiling may not bind until >$1M.** That is correct (deep ETFs), but it means the equity ceiling won't shape sizing at $100k–$350k — it is the GROWTH guard, not a near-term constraint. State it.
- **Combined Sharpe dilution (§5 G4).** Equity is lower-Sharpe; the blended book Sharpe drops as equity's weight grows. The marginal gate + risk-parity handle WHETHER to include it; do not force it if it fails the gate against the crypto book.
- **Equity turnover cost realism.** multistrat is adaptive (daily reweight) → non-trivial turnover; the cost model must charge it honestly (two-way turnover) or it flatters the equity Sharpe.

## 8. Decisions locked
- **Ingest real ETF volume** (extend the Yahoo extraction + PIT snapshot + backfill) for a data-driven dollar-ADV (63d median). **Real costs**: sqrt-impact on real ADV + per-ETF documented spread + real IBKR commission (NOT the flat 15bp). Sizing series = `book_series` NET of these. Provider merged into `record_allocation`; forward gate unchanged; PAPER scope. Live spread feed = follow-on.
