# Paper book: ISV-adaptive sleeve weighting — Implementation Plan (Phase 2F-A3c)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or
> superpowers:executing-plans to implement this plan task-by-task. TDD. Steps use checkbox (`- [ ]`) syntax.

**Goal:** Replace the paper book's equal-weight sleeve sizing with **ISV-adaptive** inverse-vol weights —
state-derived lookback (`L_t`), concentration cap (`cap_t`), and a max-with-floor cold-start — driven by
each sleeve's OWN causally-accumulated daily returns, applied identically in the backfill and the nightly
snapshot.

**Architecture:** Two new pure functions in `application/paper_book.py` — `sleeve_daily_return` (one shared
return formula) and `isv_adaptive_weights` (the A/B/C ISV logic, reusing the inverse-vol math from
`book_allocator`). A new `paper_sleeve_ret(run_date, sleeve, ret)` table + `PaperRepo` read/write so the
nightly snapshot rebuilds the same trailing window the backfill accumulates. `backfill_paper_book` and
`PaperSnapshotService` swap `equal_weights` → `isv_adaptive_weights` and persist each date's per-sleeve
return. Read-only/additive; equal-weight remains the cold-start fallback.

**Tech Stack:** Python 3.12, pytest, SQLAlchemy, Dagster. **Repo:** `~/Work/fxhnt`, branch
`feat/paper-dynamic-weighting`. **Spec:** `docs/superpowers/specs/2026-06-21-paper-dynamic-weighting-design.md`.

**Verified context:**
- `book_allocator._inverse_vol_weights(window: {edge: np.ndarray}, max_sleeve_weight) -> {edge: float}` does
  raw `∝1/std` normalize + an **iterative cap-and-renormalize loop** (mirror it for the two-sided water-fill).
- `domain.paper.Position(sleeve, symbol, qty, entry_price, entry_date)`. `target_positions(...)` re-derives
  positions each run_date at that date's close → `positions_before(d).entry_price` = prior date's close.
- `PaperRepo.positions_before(run_date) -> list[Position]` (prior persisted book) already exists; `upsert_nav`
  / `migrate` (create_all) / StaticPool-SQLite test pattern exist.
- `paper_book.equal_weights(symbol_weights_by_sleeve)` exists (cold-start fallback).
- Call sites: `backfill_paper_book` (`application/paper_backfill.py`), `PaperSnapshotService.snapshot`
  (`application/paper_snapshot.py`), assets driver `build_paper_book_snapshot` (passes `repo`).
- Structural constants: `L_min=10`, `L_max=60`, `φ=0.5`, `κ=2.0`. `epoch_day = date.toordinal()`.

---

## Task 1: shared `sleeve_daily_return` pure function

**Files:** Modify `src/fxhnt/application/paper_book.py`; Test `tests/unit/test_sleeve_daily_return.py`

- [ ] **Step 1: Write the failing test**

```python
# tests/unit/test_sleeve_daily_return.py
from fxhnt.application.paper_book import sleeve_daily_return
from fxhnt.domain.paper import Position

def _pos(sym, qty, entry):  # sleeve fixed; entry_price carries the PRIOR date's close
    return Position("s", sym, qty, entry, "2026-01-01")

def test_long_return_is_pnl_over_notional():
    prev = [_pos("A", 2.0, 100.0)]          # notional 200
    r = sleeve_daily_return(prev, {"A": 110.0})   # pnl = 2*(110-100)=20
    assert r == 20.0 / 200.0

def test_short_position_return():
    prev = [_pos("A", -2.0, 100.0)]         # notional |−2*100|=200
    r = sleeve_daily_return(prev, {"A": 90.0})    # pnl = -2*(90-100)=20
    assert r == 20.0 / 200.0

def test_unknown_symbol_contributes_zero():
    prev = [_pos("A", 2.0, 100.0), _pos("B", 1.0, 50.0)]   # notional 200+50=250
    r = sleeve_daily_return(prev, {"A": 110.0})            # B unknown -> 0 pnl; pnl=20
    assert r == 20.0 / 250.0

def test_no_prior_positions_returns_none():
    assert sleeve_daily_return([], {"A": 110.0}) is None

def test_zero_notional_returns_none():
    assert sleeve_daily_return([_pos("A", 0.0, 100.0)], {"A": 110.0}) is None
```

- [ ] **Step 2: Run → FAIL.** `uv run pytest tests/unit/test_sleeve_daily_return.py -q` (ImportError).
- [ ] **Step 3: Implement** in `paper_book.py` (near `equal_weights`):

```python
from fxhnt.domain.paper import (  # add Position to the existing import block
    Position, Trade, diff_trades, realized_delta, target_positions,
)

def sleeve_daily_return(prev_positions: list[Position],
                        close_by_symbol: dict[str, float]) -> float | None:
    """One date's return for a sleeve from its prior-date positions, marked to today's closes.

    The prior close is carried by `Position.entry_price` (positions are re-derived each date at that
    date's close). Return = Σ qty·(close − entry_price) / Σ|qty·entry_price|. Unknown symbol → entry
    (0 pnl). None when there is no prior position or the notional is 0. Pure; no I/O.
    """
    notional = sum(abs(p.qty * p.entry_price) for p in prev_positions)
    if notional <= 0.0:
        return None
    pnl = sum(p.qty * (close_by_symbol.get(p.symbol, p.entry_price) - p.entry_price)
              for p in prev_positions)
    return pnl / notional
```

- [ ] **Step 4: Run → PASS.**
- [ ] **Step 5: Commit** `git add -p` (verify no `*.dbn`) then
  `git commit -m "feat(paper): shared sleeve_daily_return pure fn"`

---

## Task 2: `isv_adaptive_weights` helper (A adaptive lookback, B floor, C cap)

**Files:** Modify `src/fxhnt/application/paper_book.py`; Test `tests/unit/test_isv_adaptive_weights.py`

- [ ] **Step 1: Write the failing tests**

```python
# tests/unit/test_isv_adaptive_weights.py
import math
from fxhnt.application.paper_book import isv_adaptive_weights

def _series(vals, start=1):   # {epoch_day: ret}
    return {start + i: v for i, v in enumerate(vals)}

def test_empty_active_returns_empty():
    assert isv_adaptive_weights({}, []) == {}

def test_no_history_is_equal_weight():
    w = isv_adaptive_weights({"a": {}, "b": {}}, ["a", "b"])
    assert w == {"a": 0.5, "b": 0.5}

def test_low_vol_sleeve_gets_higher_weight():
    lo = _series([0.001, -0.001] * 30)    # ~60 obs low vol
    hi = _series([0.05, -0.05] * 30)      # ~60 obs high vol
    w = isv_adaptive_weights({"lo": lo, "hi": hi}, ["lo", "hi"])
    assert math.isclose(sum(w.values()), 1.0, abs_tol=1e-9)
    assert w["lo"] > w["hi"]

def test_rising_vol_regime_shrinks_lookback():
    # disjoint regime ratio rho>1 (recent block more volatile than the prior block) -> L_t < L_max.
    # calm must extend through the PRIOR block (indices 40-49); only the last 10 (indices 50-59) are hot,
    # so std(recent L_min=10) > std(prior L_min=10). Expose L_t via the debug return for determinism.
    calm = [0.01, -0.01] * 25            # 50 obs low vol (covers the prior block)
    hot = [0.06, -0.06] * 5              # last 10 obs high vol (the recent block)
    s = _series(calm + hot)
    _, dbg = isv_adaptive_weights({"x": s, "y": _series([0.01, -0.01] * 30)},
                                  ["x", "y"], _debug=True)
    assert dbg["L_t"] < 60
    flat = _series([0.01, -0.01] * 40)
    _, dbg2 = isv_adaptive_weights({"x": flat, "y": flat}, ["x", "y"], _debug=True)
    assert dbg2["L_t"] == 60

def test_cap_never_below_one_over_n():
    s = _series([0.02, -0.02] * 30)
    _, dbg = isv_adaptive_weights({"a": s, "b": s, "c": s}, ["a", "b", "c"], _debug=True)
    assert dbg["cap_t"] >= 1.0 / 3 - 1e-9

def test_floor_holds_after_waterfill():
    # one very-low-vol sleeve would dominate; floor keeps the other above phi/N.
    dom = _series([0.0001, -0.0001] * 40)
    weak = _series([0.2, -0.2] * 40)
    w = isv_adaptive_weights({"dom": dom, "weak": weak}, ["dom", "weak"])
    assert math.isclose(sum(w.values()), 1.0, abs_tol=1e-9)
    assert w["weak"] >= 0.5 / 2 - 1e-9          # w_floor = phi/N = 0.25
    assert w["dom"] <= 1.0 + 1e-9

def test_mixed_history_cold_sleeve_gets_floor():
    has = _series([0.02, -0.02] * 30)
    w = isv_adaptive_weights({"has": has, "cold": {1: 0.01}}, ["has", "cold"])   # cold has <2 obs
    assert math.isclose(w["cold"], 0.5 / 2, abs_tol=1e-9)                        # exactly w_floor
    assert math.isclose(sum(w.values()), 1.0, abs_tol=1e-9)
```

- [ ] **Step 2: Run → FAIL.**
- [ ] **Step 3: Implement** in `paper_book.py`. Add `import numpy as np` and
  `from fxhnt.application.book_allocator import _inverse_vol_weights` is NOT used (we own the clamp); instead:

```python
import numpy as np

_L_MIN = 10
_L_MAX = 60
_PHI = 0.5
_KAPPA = 2.0

def _regime_lookback(series_by_sleeve: dict[str, dict[int, float]], active: list[str]) -> int:
    """Adaptive L_t from a DISJOINT recent-vs-prior vol ratio on the active sleeves' mean return series.
    rho = std(recent L_min) / std(prior L_min); rho>1 (vol rising) -> shorten. Hold L_max until 2*L_min
    obs exist. Floor L_min, anchor L_max."""
    # union date axis, 0-fill, mean across active = one representative series
    days = sorted({d for s in active for d in series_by_sleeve.get(s, {})})
    if len(days) < 2 * _L_MIN:
        return _L_MAX
    idx = {d: i for i, d in enumerate(days)}
    mat = np.zeros((len(active), len(days)))
    for r, s in enumerate(active):
        for d, v in series_by_sleeve.get(s, {}).items():
            mat[r, idx[d]] = v
    rep = mat.mean(axis=0)
    recent, prior = rep[-_L_MIN:], rep[-2 * _L_MIN:-_L_MIN]
    sp = float(np.std(prior))
    if sp <= 1e-12:
        return _L_MAX
    rho = float(np.std(recent)) / sp
    return int(round(min(_L_MAX, max(_L_MIN, _L_MAX / max(1.0, rho)))))

def _waterfill(init: dict[str, float], w_floor: float, cap: float) -> dict[str, float]:
    """Two-sided iterative clamp-to-[w_floor, cap] + renormalize the free mass, until stable (<=N passes).
    Feasible because w_floor*N <= 1 <= cap*N. Mirrors the cap loop in book_allocator._inverse_vol_weights."""
    w = dict(init)
    for _ in range(len(w) + 1):
        clamped = {k: v for k, v in w.items() if v <= w_floor + 1e-15 or v >= cap - 1e-15}
        if clamped:
            fixed = {k: min(cap, max(w_floor, w[k])) for k in clamped}
            free = {k: w[k] for k in w if k not in clamped}
            remaining = 1.0 - sum(fixed.values())
            ft = sum(free.values())
            nxt = dict(fixed)
            for k in free:
                nxt[k] = (free[k] / ft * remaining) if ft > 1e-12 else (remaining / len(free) if free else 0.0)
        else:
            nxt = w
        if all(abs(nxt[k] - w[k]) < 1e-12 for k in w):
            w = nxt
            break
        w = nxt
    return {k: min(cap, max(w_floor, v)) for k, v in w.items()}

def isv_adaptive_weights(returns_by_sleeve: dict[str, dict[int, float]],
                         active_sleeves: list[str], *, _debug: bool = False):
    """ISV-adaptive sleeve weights: adaptive lookback L_t (A), N_eff-derived cap_t (C), and a
    max-with-floor cold-start via two-sided water-fill (B). Cold-start / no-history -> equal-weight.
    Pure. Returns {sleeve: weight} (sum 1), or ({...}, {"L_t":..,"cap_t":..}) when _debug."""
    active = [s for s in active_sleeves if s]
    n = len(active)
    if n == 0:
        return ({}, {"L_t": _L_MAX, "cap_t": 0.0}) if _debug else {}
    has = [s for s in active if len(returns_by_sleeve.get(s, {})) >= 2]
    if not has:
        eq = {s: 1.0 / n for s in active}
        return (eq, {"L_t": _L_MAX, "cap_t": 1.0}) if _debug else eq
    L_t = _regime_lookback(returns_by_sleeve, has)
    inv = {}
    for s in has:
        vals = [returns_by_sleeve[s][d] for d in sorted(returns_by_sleeve[s])][-L_t:]
        v = float(np.std(vals)) if len(vals) > 1 else 0.0
        inv[s] = (1.0 / v) if v > 1e-12 else 0.0
    tot = sum(inv.values())
    raw = {s: inv[s] / tot for s in has} if tot > 0 else {s: 1.0 / len(has) for s in has}
    n_eff = (sum(raw.values()) ** 2) / sum(v * v for v in raw.values()) if raw else 1.0
    cap_t = min(1.0, max(1.0 / n, _KAPPA / n_eff))
    w_floor = _PHI / n
    cold = [s for s in active if s not in has]
    init = {s: w_floor for s in cold}
    has_mass = max(0.0, 1.0 - w_floor * len(cold))
    for s in has:
        init[s] = raw[s] * has_mass
    w = _waterfill(init, w_floor, cap_t)
    return (w, {"L_t": L_t, "cap_t": cap_t}) if _debug else w
```

- [ ] **Step 4: Run → PASS.** Adjust tolerances only if a test's expected math is off (not the asserts' intent).
- [ ] **Step 5: Commit** `git commit -m "feat(paper): isv_adaptive_weights (adaptive lookback/cap + max-with-floor)"`

---

## Task 3: `paper_sleeve_ret` table + PaperRepo read/write

**Files:** Modify `src/fxhnt/adapters/persistence/cockpit_models.py`,
`src/fxhnt/adapters/persistence/paper_repo.py`; Test `tests/integration/test_paper_sleeve_ret.py`

- [ ] **Step 1: Write the failing test**

```python
# tests/integration/test_paper_sleeve_ret.py
import datetime as dt
from fxhnt.adapters.persistence.paper_repo import PaperRepo

def _repo():
    r = PaperRepo("sqlite://"); r.migrate(); return r

def test_upsert_and_read_trailing_returns():
    r = _repo(); at = dt.datetime(2026, 1, 2, tzinfo=dt.UTC)
    r.upsert_sleeve_ret("2026-01-01", "ts", 0.01, at=at)
    r.upsert_sleeve_ret("2026-01-02", "ts", 0.02, at=at)
    r.upsert_sleeve_ret("2026-01-02", "sc", -0.01, at=at)
    out = r.sleeve_returns(before="2026-01-03")
    ed1 = dt.date(2026, 1, 1).toordinal(); ed2 = dt.date(2026, 1, 2).toordinal()
    assert out["ts"] == {ed1: 0.01, ed2: 0.02}
    assert out["sc"] == {ed2: -0.01}

def test_before_is_strict_and_idempotent():
    r = _repo(); at = dt.datetime(2026, 1, 2, tzinfo=dt.UTC)
    r.upsert_sleeve_ret("2026-01-02", "ts", 0.02, at=at)
    r.upsert_sleeve_ret("2026-01-02", "ts", 0.05, at=at)   # overwrite same (date,sleeve)
    assert r.sleeve_returns(before="2026-01-02") == {}      # strict < before
    out = r.sleeve_returns(before="2026-01-03")
    assert out["ts"][dt.date(2026, 1, 2).toordinal()] == 0.05
```

- [ ] **Step 2: Run → FAIL.**
- [ ] **Step 3a: Add the model** to `cockpit_models.py` (after `PaperNavRow`):

```python
class PaperSleeveRetRow(CockpitBase):
    """One sleeve's simulated daily return for one run_date (backfill + snapshot both write). Feeds the
    ISV-adaptive trailing-vol window. (run_date, sleeve) PK — idempotent per date+sleeve."""
    __tablename__ = "paper_sleeve_ret"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    sleeve: Mapped[str] = mapped_column(String(32), primary_key=True)
    ret: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)
```

- [ ] **Step 3b: Add repo methods** to `paper_repo.py` (import `PaperSleeveRetRow`; add after the nav block):

```python
    def upsert_sleeve_ret(self, run_date: str, sleeve: str, ret: float, *, at: dt.datetime) -> None:
        """Idempotent per (run_date, sleeve): write/overwrite that date's per-sleeve simulated return."""
        rd = dt.date.fromisoformat(run_date)
        with Session(self._engine) as s:
            s.merge(PaperSleeveRetRow(run_date=rd, sleeve=sleeve, ret=float(ret), src_updated_at=at))
            s.commit()

    def sleeve_returns(self, *, before: str) -> dict[str, dict[int, float]]:
        """Trailing per-sleeve return series STRICTLY before `before`, as {sleeve: {epoch_day: ret}}
        (epoch_day = date.toordinal()) — the shape isv_adaptive_weights expects."""
        cutoff = dt.date.fromisoformat(before)
        out: dict[str, dict[int, float]] = {}
        with Session(self._engine) as s:
            rows = s.scalars(select(PaperSleeveRetRow)
                             .where(PaperSleeveRetRow.run_date < cutoff)
                             .order_by(PaperSleeveRetRow.run_date.asc()))
            for r in rows:
                out.setdefault(r.sleeve, {})[r.run_date.toordinal()] = r.ret
        return out
```

- [ ] **Step 4: Run → PASS.**
- [ ] **Step 5: Commit** `git commit -m "feat(paper): paper_sleeve_ret table + PaperRepo upsert/sleeve_returns"`

---

## Task 4: `backfill_paper_book` — causal returns + isv_adaptive_weights

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

- [ ] **Step 1: Write the failing test** (append to `test_paper_backfill.py`). Use a fake `PositionSource`
  with two sleeves of differing realized vol over many dates; assert (a) `paper_sleeve_ret` populated, and
  (b) on a late date the low-vol sleeve's persisted `weight` exceeds the high-vol sleeve's (moved off 1/N).

```python
def test_backfill_uses_isv_adaptive_weights(tmp_path):
    import datetime as dt
    from fxhnt.adapters.persistence.paper_repo import PaperRepo
    from fxhnt.application.paper_backfill import backfill_paper_book

    class Sleeve:
        def __init__(self, sym): self.sym = sym
        def current_weights(self, d): return {self.sym: 1.0}   # always active, one name

    repo = PaperRepo("sqlite://"); repo.migrate()
    dates = [f"2026-01-{i:02d}" for i in range(1, 29)]          # 28 days
    # low-vol symbol LO drifts smoothly; high-vol HI oscillates hard
    closes_by_date = {}
    for i, d in enumerate(dates):
        lo = 100.0 + 0.05 * i
        hi = 100.0 + (8.0 if i % 2 else -8.0)
        closes_by_date[d] = {"LO": lo, "HI": hi}
    n = backfill_paper_book(repo, capital=100_000.0,
                            sleeves={"lo": Sleeve("LO"), "hi": Sleeve("HI")},
                            dates=dates, closes_by_date=closes_by_date,
                            at=dt.datetime(2026, 2, 1, tzinfo=dt.UTC))
    assert n == len(dates)
    rets = repo.sleeve_returns(before="2026-02-01")
    assert "lo" in rets and "hi" in rets and len(rets["lo"]) > 10
    assert repo.weight_for("lo") > repo.weight_for("hi")        # low-vol sleeve weighted up
```

- [ ] **Step 2: Run → FAIL.**
- [ ] **Step 3: Implement.** Replace the `equal_weights` import + sizing with the causal-returns loop:

```python
from fxhnt.application.paper_book import isv_adaptive_weights, persist_paper_book, sleeve_daily_return
# (drop the equal_weights import)
```

Rewrite the loop body of `backfill_paper_book` so that, per date ascending:

```python
    persisted = 0
    for date in sorted(dates):
        returns_by_sleeve = repo.sleeve_returns(before=date)          # history strictly before `date`
        symbol_weights_by_sleeve: dict[str, dict[str, float]] = {}
        for name, sleeve in sleeves.items():
            try:
                w = sleeve.current_weights(date)
            except Exception as exc:  # noqa: BLE001 — one bad sleeve must not drop the date
                _log.warning("paper-backfill: sleeve %s current_weights failed on %s, skipping: %s",
                             name, date, exc)
                continue
            if not w:
                _log.info("paper-backfill: sleeve %s has no weights as of %s, skipping", name, date)
                continue
            symbol_weights_by_sleeve[name] = w
        active = list(symbol_weights_by_sleeve)
        persist_paper_book(
            repo, capital=capital, run_date=date,
            sleeve_weights=isv_adaptive_weights(returns_by_sleeve, active),
            symbol_weights_by_sleeve=symbol_weights_by_sleeve,
            prices=closes_by_date.get(date, {}), at=at)
        # AFTER sizing+persisting this date, book its realized return (uses THIS date's close) for the
        # NEXT date's window — causal. prior = the book persisted for the immediately preceding date.
        prior = repo.positions_before(date)
        if prior:
            by_sleeve: dict[str, list] = {}
            for p in prior:
                by_sleeve.setdefault(p.sleeve, []).append(p)
            for sname, plist in by_sleeve.items():
                r = sleeve_daily_return(plist, closes_by_date.get(date, {}))
                if r is not None:
                    repo.upsert_sleeve_ret(date, sname, r, at=at)
        persisted += 1
    return persisted
```

  Also update the module docstring (equal-weight → ISV-adaptive; mention the causal return window).

- [ ] **Step 4: Run → PASS** (`uv run pytest tests/integration/test_paper_backfill.py -q`). Keep the existing
  backfill tests green — if one asserted equal-weight sizing, update it to the ISV expectation.
- [ ] **Step 5: Commit** `git commit -m "feat(paper): backfill builds causal per-sleeve returns + ISV-adaptive weights"`

---

## Task 5: `PaperSnapshotService` — same ISV path off `paper_sleeve_ret`

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

- [ ] **Step 1: Write the failing test** — seed `paper_sleeve_ret` history (two sleeves, differing vol) +
  a prior-date book via one `persist_paper_book`, then `PaperSnapshotService.snapshot(as_of=...)` and assert
  (a) it persists positions, (b) `repo.weight_for(low_vol) > repo.weight_for(high_vol)`, (c) a new
  `paper_sleeve_ret` row for `as_of` was written.

```python
def test_snapshot_uses_isv_adaptive_weights():
    import datetime as dt
    from fxhnt.adapters.persistence.paper_repo import PaperRepo
    from fxhnt.application.paper_snapshot import PaperSnapshotService

    repo = PaperRepo("sqlite://"); repo.migrate()
    at = dt.datetime(2026, 2, 1, tzinfo=dt.UTC)
    base = dt.date(2026, 1, 1).toordinal()
    for k in range(20):                          # seed trailing returns: lo low-vol, hi high-vol
        d = dt.date.fromordinal(base + k).isoformat()
        repo.upsert_sleeve_ret(d, "lo", 0.001 * (1 if k % 2 else -1), at=at)
        repo.upsert_sleeve_ret(d, "hi", 0.05 * (1 if k % 2 else -1), at=at)
    # prior book so snapshot has a positions_before to compute as_of's own return
    from fxhnt.application.paper_book import persist_paper_book
    persist_paper_book(repo, capital=100_000.0, run_date="2026-01-24",
                       sleeve_weights={"lo": 0.5, "hi": 0.5},
                       symbol_weights_by_sleeve={"lo": {"LO": 1.0}, "hi": {"HI": 1.0}},
                       prices={"LO": 100.0, "HI": 100.0}, at=at)

    class Sleeve:
        def __init__(self, sym): self.sym = sym
        def current_weights(self, d): return {self.sym: 1.0}

    svc = PaperSnapshotService(repo, capital=100_000.0,
                               sleeves={"lo": Sleeve("LO"), "hi": Sleeve("HI")},
                               sleeve_weights={}, prices={"LO": 101.0, "HI": 108.0})
    n = svc.snapshot(as_of="2026-01-25", at=at)
    assert n > 0
    assert repo.weight_for("lo") > repo.weight_for("hi")
    assert repo.sleeve_returns(before="2026-01-26").get("lo", {})
```

- [ ] **Step 2: Run → FAIL.**
- [ ] **Step 3: Implement.** In `paper_snapshot.py` swap the import (`equal_weights` →
  `isv_adaptive_weights, sleeve_daily_return`) and rewrite `snapshot`'s sizing + return-persist:

```python
        returns_by_sleeve = self._repo.sleeve_returns(before=as_of)
        # ... existing per-sleeve current_weights try/except loop builds symbol_weights_by_sleeve ...
        active = list(symbol_weights_by_sleeve)
        n = persist_paper_book(
            self._repo, capital=self._capital, run_date=as_of,
            sleeve_weights=isv_adaptive_weights(returns_by_sleeve, active),
            symbol_weights_by_sleeve=symbol_weights_by_sleeve,
            prices=self._prices, at=at, enabled=self._enabled)
        if self._enabled:
            prior = self._repo.positions_before(as_of)
            by_sleeve: dict[str, list] = {}
            for p in prior:
                by_sleeve.setdefault(p.sleeve, []).append(p)
            for sname, plist in by_sleeve.items():
                r = sleeve_daily_return(plist, self._prices)
                if r is not None:
                    self._repo.upsert_sleeve_ret(as_of, sname, r, at=at)
        return n
```

  Update the class/method docstrings (equal-weight → ISV-adaptive; note the shared return fn + table).

- [ ] **Step 4: Run → PASS** (`uv run pytest tests/integration/test_paper_snapshot.py
  tests/unit/test_paper_snapshot_asset.py -q`). Update any existing test asserting equal-weight.
- [ ] **Step 5: Commit** `git commit -m "feat(paper): nightly snapshot uses ISV-adaptive weights off paper_sleeve_ret"`

---

## Task 6: Full suite + finish + deploy + re-backfill + verify

- [ ] **Step 1:** `cd ~/Work/fxhnt && uv run pytest -q` → all green. Fix any equal-weight-era assertions.
- [ ] **Step 2:** Confirm nothing stray staged: `git status --short | grep -i '\.dbn'` must print nothing.
- [ ] **Step 3:** **superpowers:finishing-a-development-branch** — merge `feat/paper-dynamic-weighting`
  → master, push (auto-deploys via Gitea→Argo). Confirm the cockpit workflow Succeeded + dashboard pod
  healthy (`kubectl get pods -n foxhunt | grep -E 'dashboard|dagster'`).
- [ ] **Step 4:** Re-run the backfill in-cluster (rebuilds `paper_positions`, `paper_nav`, `paper_sleeve_ret`):
  `kubectl exec deploy/dagster -c daemon -n foxhunt -- fxhnt paper-backfill --days 365`. Confirm it reports
  positions on many dates.
- [ ] **Step 5: VERIFY.** Query that the ISV path engaged and the curve is non-flat + risk-weighted:
```bash
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)
with e.connect() as c:
    print('sleeve_ret rows:', list(c.execute(text('select sleeve,count(*) n,round(avg(ret)::numeric,5) from paper_sleeve_ret group by sleeve'))))
    print('distinct sleeve weights (off 1/N?):', list(c.execute(text('select sleeve,count(distinct round(weight::numeric,4)) from paper_positions group by sleeve'))))
    print('nav span:', list(c.execute(text('select min(equity),max(equity),count(*) from paper_nav'))))
"
```
  Confirm: `paper_sleeve_ret` populated; `paper_positions.weight` takes >1 distinct value per sleeve (proves
  weights move, not stuck at 1/N); `paper_nav` equity varies (non-flat). Then load
  `https://dashboard.fxhnt.ai/paper/replay` and `/paper` and report what renders.

---

## Self-review notes
- `sleeve_daily_return` + `isv_adaptive_weights` are the single shared sources used by BOTH backfill +
  snapshot (consistency #5). No duplicated return/weight formula.
- Causality: each date's return is booked AFTER sizing that date, used only for the next date's window.
- Two-sided water-fill (Task 2 `_waterfill`) guarantees the floor holds post-renormalize (#4); cap+floor one
  joint range (`[w_floor, cap_t]`).
- Disjoint regime windows (#2): `_regime_lookback` compares recent vs prior block, holds `L_max` < 2·L_min.
- Mixed-history (#6): cold (<2 obs) active sleeves pinned at `w_floor`; all-cold → equal-weight.
- Documented inert-at-N≤3 caveats stand (cap rarely binds, lookback often in bootstrap) — degrades to
  equal-weight, never a flat/degenerate book.
- Read-only/additive: no sleeve `run()`/`advance()`, `combine_book`, or gate-record change.
- NEVER `git add -A` — `*.dbn` is gitignored; verify nothing stray staged before each commit.

## Acceptance criteria (from spec)
- `isv_adaptive_weights` (active-filter, equal-weight bootstrap, adaptive `L_t`/`cap_t`, mixed-history,
  two-sided water-fill); unit-tested incl. (i)-(vii). ✅ T2
- `sleeve_daily_return` pure fn; both callers import it. ✅ T1, T4, T5
- `paper_sleeve_ret` table + `upsert_sleeve_ret`/`sleeve_returns`; idempotent; tested. ✅ T3
- backfill builds causal window + ISV weights + persists returns; integration-tested. ✅ T4
- snapshot reads trailing returns + ISV weights; tested. ✅ T5
- re-backfill → `paper_sleeve_ret` populated, `/paper/replay` non-flat + risk-weighted, `/paper` matches. ✅ T6
- full suite green; read-only/additive; no `*.dbn` staged. ✅ T6
