# Stablecoin Peg-Reversion (Treasury-Rotation) Paper-Track Sleeve — Design

**Date:** 2026-06-21
**Status:** Design (pending approval)
**Repo:** fxhnt

## Motivation

The crypto-FX dislocation hunt found a validated, bounded, uncorrelated edge: **short-rich stablecoin
peg-reversion**. When a stablecoin trades *above* $1, mint/redeem arbitrage (forced flow) pulls it back
to parity; selling the rich stable and rebuying on reversion harvests that. Validation (foxhunt probes
`scripts/surfer/crypto_stablecoin_*.py`):

- **Spike** (daily): edge grows with dislocation size (+11→+23→+33 bp as threshold 25→50bp), concentrated
  in less-arbed stables; small <10bp gaps die at cost (HFT-arbed), big >50bp gaps survive even 10bp cost.
- **Hardened** (non-overlapping, UST included, slippage-scaled, long/short split): **SHORT-rich** = mean
  +24.7 bp/trade, hit 68-70%, worst only −148bp, SR~4, positive 5/6 years, robust to all filters. Only 8%
  of events on BTC-crash days → idiosyncratic/uncorrelated. **LONG-cheap = death-spiral trap** (UST −75%,
  stops gap through) → EXCLUDED.
- **Execution** (1h, next-bar-open fills + spread): NOT latency-gated — rich deviations persist for hours
  (TUSD 90% still rich at +8h), so next-open fills + 2bp spread still net +18-36bp, hit 85-93%. Live book:
  spreads 0.1-1bp, deep enough for small capital. USDC too tight (never rich); DAI/BUSD delisted (empty book).

It's a small, uncorrelated **yield overlay on stablecoin cash** (capacity = cash balance), not a return
engine. Deployed as a treasury rotation it needs no borrow.

## Goal

Add `stablecoin_rotation` as a live paper-track sleeve that books a **daily-rebalanced short-rich
stablecoin reversion** return from inception via the nightly Dagster job, surfacing in the cockpit with
`gate=WAIT` (60 days, Sharpe ≥ 0.5). No capital until forward-proven.

## Non-Goals

- **No LONG-cheap leg** — death-spiral trap (UST→0); excluded by design.
- **No intraday execution** in this track — the daily-rebalance book is the honest conservative proxy
  (the daily hardened test confirmed +25bp/SR~4); a future intraday executor could capture more, separately.
- No order/treasury-execution bridge here (deferred until gate=GO, analogous to `xsfunding-orders`).
- No new adaptive sizing — fixed equal-risk-per-rich-name; the edge is bounded so no continuous controller.

## Key design decisions

1. **Daily-rebalanced short-rich book** (mirrors `xsfunding`'s daily-book + `crypto_tstrend`'s
   recomputable-series wiring). Each day: short (w<0) every stablecoin currently trading rich beyond a
   threshold, equal-weight; book the realized daily return minus turnover cost. Flat on days with no rich
   name (honest — it's episodic).
2. **Universe = live-tradeable, deviation-prone liquid stables**: `FDUSDUSDT, TUSDUSDT, USDPUSDT`
   (configurable). USDC excluded (reverts in ~2h, never rich); DAI/BUSD excluded (delisted, empty books).
3. **Recomputable series, reuse the existing spot source.** `BinanceSpotHistory.daily_close(sym)` returns
   `{epoch_day: close}` per pair (test-injectable). The strategy recomputes the full daily book each night;
   the `ForwardTracker` books only days strictly after `last_date` (same pattern as `crypto_tstrend`).
4. **PageHinkleyDecay kill reused** (`lam=0.20`) as cheap insurance — bounded edge, but consistent with
   the book's edge-decay discipline; configurable.

## Architecture (mirrors crypto_tstrend; generic tracker/registry/cockpit reused)

### 1. Domain (NEW) — `src/fxhnt/domain/stablecoin_reversion.py`

```python
def short_rich_weights(dev: dict[str, float], thresh: float) -> dict[str, float]:
    """dev = {sym: close-1.0} as-of a day. Return equal-weight SHORT weights on names with dev>thresh
    (rich), summing to gross 1 (e.g. -1/k each over k rich names); {} if none rich. Long-cheap is NEVER
    taken (death-spiral trap)."""
```

### 2. Runner (NEW) — `src/fxhnt/application/stablecoin_runner.py`

```python
@dataclass(frozen=True)
class StableRunResult:
    returns: np.ndarray
    dates: list[int]          # epoch-day each daily book return is realized on
    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: ...
    def run(self) -> StableRunResult:
        """Walk the union of days; each day d→nxt: dev_i = close_i[d]-1; w = short_rich_weights(dev,thresh);
        realized = Σ w_i·(close_i[nxt]/close_i[d]-1) − turnover(w,prev)·cost; book on nxt."""
```

### 3. Strategy (NEW) — `src/fxhnt/application/stablecoin_strategy.py`

```python
class StableRotationForward:   # ForwardStrategy: advance(last_date, extra) -> (rows, extra)
    def __init__(self, load_panel: Callable[[], dict[str, dict[int, float]]], *,
                 thresh_bp=50.0, cost_bps=4.0, periods_per_year=365,
                 decay_lam=0.20, decay_reenter_days=10) -> None: ...
    # recomputable: run StableReversionRunner on fresh panel → (dates, returns);
    # inception → warm PH fresh/alive, book nothing; subsequent → for new days apply PH kill
    #   (booked = ret if alive else 0.0), carry ph-state in extra. (No vol overlay — bounded edge.)
```

The Dagster asset builds `load_panel` from `BinanceSpotHistory().daily_close` over the universe:
```python
def load_panel():
    h = BinanceSpotHistory()
    return {s: h.daily_close(s) for s in ("FDUSDUSDT", "TUSDUSDT", "USDPUSDT")}
```

### 4. Registry — `src/fxhnt/registry.py`
```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},
},
```

### 5. Dagster wiring — `assets.py` + `definitions.py`
- New `@asset stablecoin_rotation_nav` via `_run_paper_tracker(...)` (no `crypto_bars` dep — it fetches its
  own spot data; best-effort transient handling already in `_run_paper_tracker`).
- Add to `cockpit_forward` deps, the `combined_book_job` selection, and `defs.assets`.
- Update `test_orchestration_definitions.py` asset count (8 → 9).

### 6. Tests (mirror crypto_tstrend)
- `tests/unit/test_stablecoin_reversion.py` — `short_rich_weights`: shorts only rich names, equal-weight
  gross 1, empty when none rich, NEVER longs a cheap name.
- `tests/integration/test_stablecoin_strategy.py` — fake `load_panel`: inception books nothing; a rich
  name → next-day booked return matches hand-computed `Σw·r − cost`; no-drift vs `StableReversionRunner`;
  PH kill flattens + carries; `extra` JSON round-trips.
- `tests/integration/test_stablecoin_wiring.py` — asset importable; `defs` includes it.

## Data flow
```
nightly job → stablecoin_rotation_nav
  → StableRotationForward(load_panel = BinanceSpotHistory.daily_close over {FDUSD,TUSD,USDP})
      → StableReversionRunner.run() → daily short-rich book returns + dates
      → per new day: PH kill → booked
  → ForwardTracker.step() → stablecoin_rotation_state.json
  → cockpit_forward → cockpit DB (gate=WAIT until 60d & Sharpe≥0.5)
```

## Error handling
- Network/transient: handled by `_run_paper_tracker` (logged, state unchanged, job not blocked).
- A pair returning empty/short history: skipped that run (excluded from the day's universe); book 0 if none.
- Genuine bugs propagate.

## Risks / honest caveats
- **Small capacity / episodic** — ~modest trades/yr, flat on no-rich days; a yield overlay, not an engine.
- **Daily proxy understates** the intraday edge (conservative — fine for a paper track).
- **Stress-depeg book thinning** unmodeled (calm-state spreads used); the rich side is the calmer side, but
  a real cascade can widen spreads — the forward track is the truth-teller.
- **Cost assumption** `cost_bps=4` (≈2bp spread rt + slippage buffer); the forward record validates it.

## Acceptance criteria
- `stablecoin_rotation` registered; nightly job materializes `stablecoin_rotation_state.json` (days-list);
  `cockpit_forward` depends on it; cockpit shows it `gate=WAIT`, booking from inception.
- `short_rich_weights` never longs a cheap name (death-spiral guard); shorts only rich, equal-weight.
- No-drift: strategy's daily return == `StableReversionRunner` value for the same date (test).
- PH kill flattens + re-arms; `extra` JSON-safe.
- All new + existing tests green (definitions count 8→9); mypy clean.
- `thresh_bp`, `cost_bps`, `decay_lam` adjustable at the asset composition root.
```
