# Paper book: equal-weight + wire unlock to warehouse — Implementation Plan (Phase 2F-A3b)

> **For agentic workers:** REQUIRED SUB-SKILL: subagent-driven-development (recommended) or
> executing-plans. TDD. Steps use `- [ ]`.

**Goal:** Make the paper book actually hold positions: size the replayable sleeves by equal-weight (1/N
active sleeves) in BOTH the backfill and the nightly snapshot, and build the unlock (and snapshot ts-trend)
sleeve from the in-cluster warehouse instead of the absent `crypto_pit_dir`.

**Architecture:** A shared `equal_weights` helper used by `backfill_paper_book` (per date) and the
`PaperSnapshotService`/asset path; unlock's vol panel built from the warehouse `crypto_close_panel`
(close + fallback qvol/funding) + the existing `unlock_calendar.json` events. Read-only/additive.

**Tech Stack:** Python 3.12, pytest, Dagster. **Repo:** `~/Work/fxhnt`, branch
`feat/backfill-stablecoin-unlock`. **Spec:** `docs/superpowers/specs/2026-06-21-paper-equal-weight-and-unlock-design.md`.

**Verified context:** warehouse `crypto_close_panel(suffix="USDT")` → 197 symbols × 365 closes; ts-trend
`current_weights` returns 70 names on the latest date; `_paper_backfill_sleeve_weights()` returns `{}` (the
allocator covers a different edge set) → 0 positions; `_unlock()` + `_live_sleeves` use the absent
`/root/.fxhnt/crypto_pit`. `UnlockShortRunner(panel={sym:{day:(close,qvol,funding)}}, events=[{sym,cliff_day,n,cat}])`.

---

## Task 1: shared `equal_weights` helper + `backfill_paper_book` uses per-date 1/N

**Files:** Modify `src/fxhnt/application/paper_book.py` (add helper) + `src/fxhnt/application/paper_backfill.py`;
Test `tests/unit/test_equal_weights.py` + extend `tests/integration/test_paper_backfill.py`

- [ ] **Step 1: Failing unit test**
```python
# tests/unit/test_equal_weights.py
from fxhnt.application.paper_book import equal_weights
def test_equal_weights_over_active_sleeves():
    swb = {"a": {"X": 1.0}, "b": {"Y": -1.0}, "c": {}}   # c is empty/inactive
    assert equal_weights(swb) == {"a": 0.5, "b": 0.5}    # 1/N over the 2 active; empties dropped
def test_equal_weights_empty():
    assert equal_weights({"a": {}}) == {}
```
- [ ] **Step 2:** run → FAIL.
- [ ] **Step 3:** Add to `paper_book.py`:
```python
def equal_weights(symbol_weights_by_sleeve: dict[str, dict[str, float]]) -> dict[str, float]:
    """Equal-weight the sleeves that produced a non-empty book. {sleeve: 1/N} over active sleeves."""
    active = [s for s, w in symbol_weights_by_sleeve.items() if w]
    if not active:
        return {}
    w = 1.0 / len(active)
    return {s: w for s in active}
```
- [ ] **Step 4:** PASS.
- [ ] **Step 5: Failing integration test** — extend `test_paper_backfill.py`: with 2 active sleeves on a
  date, assert each gets notional `0.5*capital` (positions sized, not zero). Then make `backfill_paper_book`
  compute `sleeve_weights` per date from the day's active sleeves via `equal_weights(...)` (build
  `symbol_weights_by_sleeve` first, then `equal_weights`, then `persist_paper_book`). Drop the incoming
  `sleeve_weights` param dependency (keep it for back-compat but override with equal_weights, OR remove it
  and update callers — your call; keep the test green).
- [ ] **Step 6:** PASS. **Step 7:** `git commit -m "feat(paper): equal-weight active replayable sleeves (1/N) in backfill"`

---

## Task 2: nightly snapshot path uses equal_weights (so live /paper isn't flat)

**Files:** Modify `src/fxhnt/application/paper_snapshot.py`; Test extend `tests/integration/test_paper_snapshot.py`

- [ ] **Step 1: Failing test** — `PaperSnapshotService.snapshot(...)` with 2 active sleeves persists
  positions sized at `0.5*capital` each (currently it relies on injected `sleeve_weights` which is `{}` in
  prod → 0 positions). Assert non-empty positions.
- [ ] **Step 2:** run → FAIL.
- [ ] **Step 3:** In `PaperSnapshotService.snapshot`, after building `symbol_weights_by_sleeve` from each
  sleeve's `current_weights`, compute `sleeve_weights = equal_weights(symbol_weights_by_sleeve)` (instead of
  using the injected/allocator weights) before `persist_paper_book`. Keep the per-sleeve skip-on-fail.
- [ ] **Step 4:** PASS. **Step 5:** `git commit -m "feat(paper): nightly snapshot equal-weights active sleeves"`

---

## Task 3: build unlock (+ snapshot ts-trend) from the warehouse, not crypto_pit_dir

**Files:** Modify `src/fxhnt/cli.py` (`_paper_backfill_sleeves._unlock`) + `src/fxhnt/adapters/orchestration/assets.py`
(`_live_sleeves` `_tstrend` + `_unlock`); Test `tests/integration/test_unlock_warehouse_panel.py`

- [ ] **Step 1: Failing test** — build a fake warehouse `crypto_close_panel` ({sym:{day:close}}) for a name
  with a cliff in a historical-cliff calendar; construct the unlock sleeve from a warehouse-derived
  `(close, qvol, funding)` panel; assert `current_weights(as_of in the pre-window)` returns that name with a
  negative (short) weight.
- [ ] **Step 2:** run → FAIL.
- [ ] **Step 3:** Add a helper `warehouse_unlock_panel(close_panel) -> {sym: {day: (close, qvol_fallback,
  funding_fallback)}}` (qvol_fallback large enough to clear `min_qvol`, funding 0.0 — documented
  approximation since the warehouse `features` may lack qvol/funding). In `cli.py _unlock()`: replace
  `CryptoPitPanelSource(s.crypto_pit_dir).panel()` with `warehouse_unlock_panel(<the warehouse close
  panel already built for ts-trend>)`; keep `events = json.loads(unlock_calendar.json)`;
  `UnlockShortRunner(panel, events)`. In `assets.py _live_sleeves`: change BOTH `_tstrend` (use
  `crypto_close_panel` instead of `CryptoPitPanelSource`) and `_unlock` (warehouse panel) the same way, so
  the nightly snapshot matches the backfill.
- [ ] **Step 4:** PASS. **Step 5:** `git commit -m "feat(paper): unlock + snapshot ts-trend read warehouse panel (drop crypto_pit_dir)"`

---

## Task 4: drop the dead allocator-weights helper from the CLI

**Files:** Modify `src/fxhnt/cli.py`

- [ ] **Step 1:** Remove the now-unused `_paper_backfill_sleeve_weights` call + helper from the
  `paper-backfill` command (equal-weight in `backfill_paper_book` replaces it). Pass `sleeves` +
  `dates` + `closes_by_date` to `backfill_paper_book` (which now equal-weights internally). Keep the
  existing CLI test green (update it for the new signature if needed).
- [ ] **Step 2:** `uv run pytest tests/integration/test_paper_backfill_cli.py -q` → PASS.
- [ ] **Step 3:** `git commit -m "refactor(paper): drop dead allocator sleeve-weights from paper-backfill"`

---

## Task 5: Full suite + finish + deploy + re-backfill + verify NON-flat curve

- [ ] **Step 1:** `uv run pytest -q` → all green.
- [ ] **Step 2:** **superpowers:finishing-a-development-branch** — merge `feat/backfill-stablecoin-unlock`
  → master, push (auto-deploys). Confirm the cockpit workflow Succeeded + dashboard pod healthy.
- [ ] **Step 3:** Re-run backfill in-cluster:
  `kubectl exec deploy/dagster -c daemon -n foxhunt -- fxhnt paper-backfill --days 365`. Confirm it reports
  positions on many dates (not "0 positions").
- [ ] **Step 4: VERIFY** — query `paper_positions` per sleeve (non-empty for ts-trend + unlock; stablecoin
  on depeg days) via:
  `kubectl exec deploy/dagster -c daemon -n foxhunt -- python -c "from sqlalchemy import create_engine,text; from fxhnt.config import get_settings; e=create_engine(get_settings().operational_dsn); [print(r.sleeve, r.n) for r in e.connect().execute(text('select sleeve,count(*) n from paper_positions group by sleeve'))]"`.
  Confirm `https://dashboard.fxhnt.ai/paper/replay` equity curve is NON-flat (varies from $100k) and
  `/paper` shows live positions. Report what renders.

---

## Self-review notes
- equal_weights is the single sizing source for BOTH backfill + nightly snapshot (consistency).
- unlock vol panel from warehouse close + fallback qvol/funding (documented approximation); never crash.
- Read-only/additive; no sleeve `run()`/`advance()` or `combine_book` changed.
- The full `_live_sleeves`↔backfill composition dedup remains a noted follow-up (not this plan).
- Don't `git add -A` (`*.dbn` gitignored — verify).

## Acceptance criteria (from spec)
- shared `equal_weights` (1/N active); unit-tested. ✅ T1
- backfill + nightly snapshot both use it; positions persist. ✅ T1, T2
- unlock builds from warehouse + calendar (no crypto_pit_dir); produces pre-cliff shorts; tested. ✅ T3
- re-backfill → paper_positions non-empty across dates; /paper/replay curve NON-flat. ✅ T5
- live /paper book populated by the nightly snapshot. ✅ T2, T5
