# Stablecoin Peg-Reversion (Treasury-Rotation) Sleeve — Implementation Plan

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

**Goal:** Add a `stablecoin_rotation` live paper-track sleeve — a daily-rebalanced SHORT-rich stablecoin peg-reversion book — booking paper NAV nightly via Dagster, gated WAIT until forward-proven.

**Architecture:** Recomputable-series `ForwardStrategy` (mirrors `crypto_tstrend`): each night recompute the daily short-rich book from fresh Binance spot daily closes (`BinanceSpotHistory`), apply a `PageHinkleyDecay` kill, book only new days. Short-rich only (long-cheap = death-spiral trap, excluded).

**Tech Stack:** Python 3, pytest, numpy, Dagster 1.12, fxhnt (`ForwardTracker`, `BinanceSpotHistory`, `PageHinkleyDecay`, `STRATEGY_REGISTRY`, `_run_paper_tracker`).

**Spec:** `docs/superpowers/specs/2026-06-21-stablecoin-rotation-sleeve-design.md`

---

## File Structure
- **Create** `src/fxhnt/domain/stablecoin_reversion.py` — `short_rich_weights` (pure).
- **Create** `src/fxhnt/application/stablecoin_runner.py` — `StableReversionRunner` + `StableRunResult`.
- **Create** `src/fxhnt/application/stablecoin_strategy.py` — `StableRotationForward`.
- **Modify** `src/fxhnt/registry.py` — add `stablecoin_rotation`.
- **Modify** `src/fxhnt/adapters/orchestration/assets.py` — add `stablecoin_rotation_nav`; add to `cockpit_forward` deps.
- **Modify** `src/fxhnt/adapters/orchestration/definitions.py` — import + job selection + `defs.assets`.
- **Modify** `tests/integration/test_orchestration_definitions.py` — asset count 8→9 + assertions.
- **Create** `tests/unit/test_stablecoin_reversion.py`, `tests/integration/test_stablecoin_strategy.py`, `tests/integration/test_stablecoin_wiring.py`.

---

## Task 0: Branch
- [ ] **Step 1**
```bash
cd /home/jgrusewski/Work/fxhnt && git checkout -b feat/stablecoin-rotation-sleeve
git add docs/superpowers/specs/2026-06-21-stablecoin-rotation-sleeve-design.md docs/superpowers/plans/2026-06-21-stablecoin-rotation-sleeve.md
git commit -m "docs: stablecoin-rotation sleeve spec + implementation plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 1: Domain weights + runner

**Files:** Create `src/fxhnt/domain/stablecoin_reversion.py`, `src/fxhnt/application/stablecoin_runner.py`; Tests `tests/unit/test_stablecoin_reversion.py`.

- [ ] **Step 1: Write failing tests** — `tests/unit/test_stablecoin_reversion.py`:
```python
import numpy as np

from fxhnt.application.stablecoin_runner import StableReversionRunner
from fxhnt.domain.stablecoin_reversion import short_rich_weights


def test_shorts_only_rich_equal_weight():
    w = short_rich_weights({"A": 0.006, "B": 0.001, "C": 0.008}, thresh=0.005)
    assert set(w) == {"A", "C"}
    assert all(v == -0.5 for v in w.values())          # equal-weight short, gross 1


def test_never_longs_cheap_and_empty_when_none_rich():
    assert short_rich_weights({"A": -0.02, "B": -0.006}, thresh=0.005) == {}   # death-spiral guard
    assert short_rich_weights({"A": 0.001}, thresh=0.005) == {}


def test_runner_books_short_reversion_profit():
    # S0 rich (1.01) on day 0, reverts to 1.0 on day 1 -> short profits
    panel = {"S0": {0: 1.01, 1: 1.00}, "S1": {0: 1.00, 1: 1.00}}
    res = StableReversionRunner(panel, thresh_bp=50.0, cost_bps=4.0).run()
    assert res.dates == [1]
    # realized = (-1)*(1.0/1.01 - 1) - turnover(=1)*cost/2 ; cost=4bp
    expected = -1.0 * (1.00 / 1.01 - 1.0) - 1.0 * (4.0 / 1e4) / 2.0
    assert abs(res.returns[0] - expected) < 1e-12
```

- [ ] **Step 2: Run, verify FAIL** — `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python -m pytest tests/unit/test_stablecoin_reversion.py -v` (ModuleNotFoundError).

- [ ] **Step 3a: Implement** `src/fxhnt/domain/stablecoin_reversion.py`:
```python
"""Stablecoin peg-reversion weights. Pure, no I/O.

SHORT-rich only: short every stablecoin trading above peg beyond a threshold (mint/redeem arb pulls it
back). Long-cheap is NEVER taken — buying a depegging stable is the death-spiral trap (UST -> 0)."""
from __future__ import annotations


def short_rich_weights(dev: dict[str, float], thresh: float) -> dict[str, float]:
    """dev = {sym: close-1.0} as-of a day. Equal-weight SHORT (-1/k each) on names with dev > thresh
    (rich), gross 1. Returns {} if no name is rich. Cheap names (dev < 0) are never taken."""
    rich = [s for s, d in dev.items() if d > thresh]
    if not rich:
        return {}
    w = -1.0 / len(rich)
    return {s: w for s in rich}
```

- [ ] **Step 3b: Implement** `src/fxhnt/application/stablecoin_runner.py`:
```python
"""Daily-rebalanced SHORT-rich stablecoin peg-reversion backtest over a spot-close panel.

panel = {symbol: {epoch_day: close}}. Each day d->nxt: short the rich names (short_rich_weights),
book realized daily return minus turnover cost. Flat on days with no rich name."""
from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from fxhnt.domain.stablecoin_reversion import short_rich_weights


@dataclass(frozen=True)
class StableRunResult:
    returns: np.ndarray
    dates: list[int]          # epoch-day each booked return is realized on (nxt)
    n_active_avg: float


class StableReversionRunner:
    def __init__(self, panel: dict[str, dict[int, float]], *, thresh_bp: float = 50.0,
                 cost_bps: float = 4.0, periods_per_year: int = 365) -> None:
        self._p = panel
        self._th = thresh_bp / 1e4
        self._cost = cost_bps / 1e4
        self._ppy = periods_per_year

    def run(self) -> StableRunResult:
        all_days = sorted({d for s in self._p.values() for d in s})
        rets: list[float] = []
        dates: list[int] = []
        prev: dict[str, float] = {}
        active = 0
        n = 0
        for i in range(len(all_days) - 1):
            d, nxt = all_days[i], all_days[i + 1]
            dev = {s: series[d] - 1.0 for s, series in self._p.items()
                   if d in series and series[d] > 0}
            w = short_rich_weights(dev, self._th)
            realized = 0.0
            for s, wt in w.items():
                series = self._p[s]
                if nxt in series and series[d] > 0 and series[nxt] > 0:
                    realized += wt * (series[nxt] / series[d] - 1.0)
            turnover = sum(abs(w.get(s, 0.0) - prev.get(s, 0.0)) for s in set(w) | set(prev))
            rets.append(realized - turnover * self._cost / 2.0)
            dates.append(int(nxt))
            active += len(w)
            n += 1
            prev = w
        return StableRunResult(np.asarray(rets, dtype=float), dates,
                               (active / n) if n else 0.0)
```

- [ ] **Step 4: Run, verify PASS** (3 passed).
- [ ] **Step 5: Commit**
```bash
cd /home/jgrusewski/Work/fxhnt
git add src/fxhnt/domain/stablecoin_reversion.py src/fxhnt/application/stablecoin_runner.py tests/unit/test_stablecoin_reversion.py
git commit -m "feat(domain): stablecoin short-rich reversion weights + daily-book runner

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 2: StableRotationForward strategy

**Files:** Create `src/fxhnt/application/stablecoin_strategy.py`; Test `tests/integration/test_stablecoin_strategy.py`.

- [ ] **Step 1: Write failing tests** — `tests/integration/test_stablecoin_strategy.py`:
```python
import json

from fxhnt.application.forward_tracker import ForwardTracker
from fxhnt.application.stablecoin_runner import StableReversionRunner
from fxhnt.application.stablecoin_strategy import StableRotationForward, _iso


def _panel():
    # S0 trades rich (1.01) only on day 159 (the last day) -> no nxt yet at inception
    s0 = {d: 1.00 for d in range(160)}
    s0[159] = 1.01
    return {"S0": s0, "S1": {d: 1.00 for d in range(160)}}


def test_inception_books_nothing(tmp_path):
    strat = StableRotationForward(lambda: _panel())
    path = str(tmp_path / "st.json")
    status = ForwardTracker(strat, path).step()
    assert status.forward_days == 0
    state = json.load(open(path))
    assert state["extra"]["ph"]["killed"] is False


def test_subsequent_books_no_drift(tmp_path):
    p1 = _panel()
    holder = {"p": p1}
    strat = StableRotationForward(lambda: holder["p"], decay_lam=1e9)   # never kill
    path = str(tmp_path / "st.json")
    ForwardTracker(strat, path).step()                                  # inception, last_date=day159
    p2 = {s: dict(series) for s, series in p1.items()}
    for s in p2:
        p2[s][160] = 1.00                                              # S0 reverts 1.01->1.00 on day160
    holder["p"] = p2
    status = ForwardTracker(strat, path).step()
    assert status.booked_today == 1
    res = StableReversionRunner(p2, thresh_bp=50.0, cost_bps=4.0).run()
    booked = next(d["ret"] for d in json.load(open(path))["days"] if d["date"] == _iso(int(res.dates[-1])))
    assert booked == float(res.returns[-1])                            # no drift
    assert booked > 0                                                  # short of a reverting rich stable profits


def test_killed_state_flattens(tmp_path):
    p1 = _panel()
    holder = {"p": p1}
    strat = StableRotationForward(lambda: holder["p"])
    path = str(tmp_path / "st.json")
    ForwardTracker(strat, path).step()
    state = json.load(open(path)); state["extra"]["ph"]["killed"] = True; state["extra"]["ph"]["recover"] = 0
    json.dump(state, open(path, "w"))
    p2 = {s: dict(series) for s, series in p1.items()}
    for s in p2:
        p2[s][160] = 1.00
    holder["p"] = p2
    ForwardTracker(strat, path).step()
    assert json.load(open(path))["days"][-1]["ret"] == 0.0


def test_extra_json_roundtrips(tmp_path):
    strat = StableRotationForward(lambda: _panel())
    path = str(tmp_path / "st.json")
    ForwardTracker(strat, path).step()
    json.dumps(json.load(open(path))["extra"])
```

- [ ] **Step 2: Run, verify FAIL** (ModuleNotFoundError).

- [ ] **Step 3: Implement** `src/fxhnt/application/stablecoin_strategy.py`:
```python
"""Live-booking stablecoin peg-reversion (short-rich) paper track — recomputable series.

Reuses StableReversionRunner on a fresh Binance-spot daily-close panel; applies a PageHinkleyDecay kill
incrementally over new days, carrying detector state in the tracker's `extra`. Short-rich only.
Mirrors the ForwardStrategy contract: advance(last_date, extra) -> (rows, extra)."""
from __future__ import annotations

import datetime as dt
from typing import Any, Callable

from fxhnt.application.stablecoin_runner import StableReversionRunner
from fxhnt.domain.edge_decay import PageHinkleyDecay

_EPOCH = dt.date(1970, 1, 1)


def _iso(epoch_day: int) -> str:
    return (_EPOCH + dt.timedelta(days=int(epoch_day))).isoformat()


class StableRotationForward:
    def __init__(self, load_panel: Callable[[], dict[str, dict[int, float]]], *,
                 thresh_bp: float = 50.0, cost_bps: float = 4.0, periods_per_year: int = 365,
                 decay_lam: float = 0.20, decay_reenter_days: int = 10) -> None:
        self._load_panel = load_panel
        self._th = thresh_bp
        self._cost = cost_bps
        self._ppy = periods_per_year
        self._lam = decay_lam
        self._reenter = decay_reenter_days

    def _series(self) -> tuple[list[int], list[float]]:
        res = StableReversionRunner(self._load_panel(), thresh_bp=self._th, cost_bps=self._cost,
                                    periods_per_year=self._ppy).run()
        return [int(d) for d in res.dates], [float(r) for r in res.returns]

    def advance(self, last_date: str | None,
                extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        dates, raw = self._series()
        if not extra:                                       # inception: fresh PH, book nothing
            ph = PageHinkleyDecay(lam=self._lam, reenter_days=self._reenter)
            rows = [(_iso(d), r) for d, r in zip(dates, raw)]
            return rows, {"ph": ph.to_dict()}
        ph = PageHinkleyDecay.from_dict(extra["ph"])
        cutoff = last_date or ""
        rows: list[tuple[str, float]] = []
        for d, r in zip(dates, raw):
            iso = _iso(d)
            if iso <= cutoff:
                continue
            alive = ph.update(r)
            rows.append((iso, r if alive else 0.0))
        return rows, {"ph": ph.to_dict()}
```

- [ ] **Step 4: Run, verify PASS** (5 passed).
- [ ] **Step 5: Commit**
```bash
cd /home/jgrusewski/Work/fxhnt
git add src/fxhnt/application/stablecoin_strategy.py tests/integration/test_stablecoin_strategy.py
git commit -m "feat(application): StableRotationForward live paper-track strategy (short-rich + decay kill)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 3: Registry entry

**Files:** Modify `src/fxhnt/registry.py`; Test `tests/unit/test_stablecoin_registry.py`.

- [ ] **Step 1: Failing test** — `tests/unit/test_stablecoin_registry.py`:
```python
from fxhnt.registry import STRATEGY_REGISTRY


def test_stablecoin_rotation_registered():
    e = STRATEGY_REGISTRY["stablecoin_rotation"]
    assert e["state_file"] == "stablecoin_rotation_state"
    assert e["sleeve"] == "crypto-stablecoin"
    assert e["venue"] == "binance-spot"
    assert e["gate_spec"] == {"min_days": 60, "min_total_return": 0.0, "min_sharpe": 0.5}
```

- [ ] **Step 2: Run, verify FAIL** (KeyError).

- [ ] **Step 3:** First READ `src/fxhnt/registry.py`. Add this entry inside `STRATEGY_REGISTRY`, immediately after the `"crypto_tstrend"` entry (match indentation):
```python
    "stablecoin_rotation": {
        "display_name": "Stablecoin peg-reversion (short-rich treasury rotation)",
        "sleeve": "crypto-stablecoin",
        "venue": "binance-spot", "state_file": "stablecoin_rotation_state",
        "gate_spec": {"min_days": 60, "min_total_return": 0.0, "min_sharpe": 0.5},
    },
```

- [ ] **Step 4: Run, verify PASS.**
- [ ] **Step 5: Commit**
```bash
cd /home/jgrusewski/Work/fxhnt
git add src/fxhnt/registry.py tests/unit/test_stablecoin_registry.py
git commit -m "feat(registry): register stablecoin_rotation sleeve (gate WAIT 60d)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 4: Dagster wiring

**Files:** Modify `assets.py`, `definitions.py`, `tests/integration/test_orchestration_definitions.py`; Create `tests/integration/test_stablecoin_wiring.py`.

- [ ] **Step 1: Failing test** — `tests/integration/test_stablecoin_wiring.py`:
```python
def test_asset_importable():
    from fxhnt.adapters.orchestration.assets import stablecoin_rotation_nav
    assert stablecoin_rotation_nav is not None


def test_definitions_include_stablecoin():
    from fxhnt.adapters.orchestration.assets import stablecoin_rotation_nav
    from fxhnt.adapters.orchestration.definitions import combined_book_job, defs
    assert stablecoin_rotation_nav in defs.assets
    assert combined_book_job is not None
```

- [ ] **Step 2: Run, verify FAIL** (ImportError).

- [ ] **Step 3:** Add the asset in `assets.py` immediately after `crypto_tstrend_nav`. Match the file's conventions (local imports, `get_settings` if used by siblings — check how `crypto_tstrend_nav`/`unlock_nav` import it and mirror exactly):
```python
@asset
def stablecoin_rotation_nav(context: AssetExecutionContext) -> dict:
    """Paper-forward stablecoin peg-reversion (short-rich daily treasury rotation, Binance spot)."""
    from fxhnt.adapters.data.binance_spot_history import BinanceSpotHistory
    from fxhnt.application.stablecoin_strategy import StableRotationForward

    def load_panel() -> dict:
        h = BinanceSpotHistory()
        return {s: h.daily_close(s) for s in ("FDUSDUSDT", "TUSDUSDT", "USDPUSDT")}

    return _run_paper_tracker(
        context, "stablecoin_rotation_nav",
        lambda: StableRotationForward(load_panel),
        "stablecoin_rotation_state",
    )
```
NOTE: no `crypto_bars` dep — this asset fetches its own spot data. If `_run_paper_tracker`/`AssetExecutionContext` aren't already in scope at this point in the file, they are (used by `crypto_tstrend_nav`/`xsfunding_nav` just above).

- [ ] **Step 4:** Add `stablecoin_rotation_nav` to the `cockpit_forward` deps list in `assets.py`:
`@asset(deps=[combined_forward_nav, sixtyforty_nav, xsfunding_nav, unlock_nav, crypto_tstrend_nav, stablecoin_rotation_nav])`

- [ ] **Step 5:** In `definitions.py` add `stablecoin_rotation_nav,` to: the import block from assets (alphabetical), the `combined_book_job` `selection=[...]`, and the `defs = Definitions(assets=[...])` list.

- [ ] **Step 6:** Update `tests/integration/test_orchestration_definitions.py`:
  - change `assert len(asset_keys) == 8` to `== 9` and add `stablecoin_rotation_nav` to the comment list.
  - after the `crypto_tstrend_nav` presence assert, add: `assert "stablecoin_rotation_nav" in asset_keys, f"missing stablecoin_rotation_nav: {asset_keys}"`
  - add `"stablecoin_rotation_nav"` to the `expected_upstream` set for `cockpit_forward`.

- [ ] **Step 7: Run** `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python -m pytest tests/integration/test_stablecoin_wiring.py tests/integration/test_orchestration_definitions.py -v` — all pass.

- [ ] **Step 8: Commit**
```bash
cd /home/jgrusewski/Work/fxhnt
git add src/fxhnt/adapters/orchestration/assets.py src/fxhnt/adapters/orchestration/definitions.py tests/integration/test_stablecoin_wiring.py tests/integration/test_orchestration_definitions.py
git commit -m "feat(orchestration): wire stablecoin_rotation_nav into nightly job + cockpit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 5: Full verification

- [ ] **Step 1:** `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python -m pytest -q` → all green.
- [ ] **Step 2:** `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python -m mypy src/fxhnt` → "Success".
- [ ] **Step 3:** If fixups were needed, commit them.

---

## Notes for the executor
- Use `.venv/bin/python` for pytest/mypy (`python` is not on PATH).
- **No-drift is load-bearing:** `StableRotationForward` calls `StableReversionRunner` directly — keep `test_subsequent_books_no_drift`.
- **Short-rich only** — `short_rich_weights` must NEVER return a positive (long) weight; `test_never_longs_cheap...` guards the death-spiral exclusion.
- Mirror `crypto_tstrend_nav`'s exact import style for `get_settings`/`_run_paper_tracker` if the asset needs them (this one doesn't use `get_settings` — it hardcodes the universe; fine).
- mypy is strict (`disallow_any_generics`): annotate `dict`/`list` with type args. The `load_panel() -> dict` in the asset mirrors the sibling `crypto_tstrend_nav` which uses `-> dict` with the file's `# type: ignore[type-arg]` convention — match whatever the sibling does.
```
