diff --git a/docs/superpowers/plans/2026-06-19-xsfunding-paper-track.md b/docs/superpowers/plans/2026-06-19-xsfunding-paper-track.md new file mode 100644 index 0000000..484a325 --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-xsfunding-paper-track.md @@ -0,0 +1,273 @@ +# xsfunding Paper-Forward Track Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`. + +**Goal:** Wire the validated `executable` cross-sectional funding construction as a live daily paper-forward cockpit track (`xsfunding`) that experiences the REAL net-of-everything return (funding + basis + the tail the backtest had to guard out), with its backtest verdict surfaced. + +**Architecture:** A live-booking `ForwardStrategy` (mirrors the existing eqfactor/funding tracks) that each day fetches live universe + trailing funding + perp & spot prices, books the prior delta-neutral book's realized return using the SAME `cross_sectional_funding` domain math the backtest validated, rebalances `executable` weights, persists state in `extra`. Registered in `STRATEGY_REGISTRY` + a Dagster asset in the daily combined-book job + cockpit, backtest verdict in `backtest_summary`. + +**Tech Stack:** Python 3.12, `BinanceFundingClient` + Binance spot/perp tickers, the `cross_sectional_funding` domain, `ForwardTracker`/`ForwardStrategy`, Dagster, cockpit pg. pytest, strict mypy. + +**House rules:** TDD, NO live network in tests (fake clients), strict mypy (no NEW errors), commit per task (do NOT push until the deploy task), trailer `Co-Authored-By: Claude Opus 4.8 (1M context) `. + +--- + +## Reuse contract (READ these first) +- `src/fxhnt/application/forward_tracker.py` — `ForwardStrategy` Protocol `advance(last_date: str|None, extra: dict) -> tuple[list[tuple[str,float]], dict]`; `ForwardTracker(strategy, state_file).step()`. Study the LIVE-BOOKING pattern (positions+marks in `extra`). +- `src/fxhnt/application/paper_strategies.py` — the existing `FundingCarryStrategy` live track (the closest analog: live crypto booking). Mirror its shape. +- `src/fxhnt/application/equity_factor_strategy.py` — `EquityFactorLong.advance` live-booking (positions/prices/last_rebal in extra, monthly rebal, book_return on prior book). Mirror this structure. +- `src/fxhnt/adapters/data/binance_funding.py` — `BinanceFundingClient`: `universe() -> {sym: quoteVolume}`, `scan(prev) -> (liq, tf30, last24)` (liq=liquid universe vol, tf30=trailing-30d-mean funding, last24=today's funding). +- `src/fxhnt/domain/cross_sectional_funding.py` — `funding_score`-style scoring + `construction_weights(scores, "executable", quantile, borrowable)` + the signed-carry concept. +- `src/fxhnt/registry.py`, `src/fxhnt/adapters/orchestration/assets.py` (eqfactor_long_nav, cockpit_forward), `definitions.py` (combined_book_job + Definitions) — the wiring pattern (see the eqfactor entries). +- `src/fxhnt/application/backtest_ingest.py` + `adapters/persistence/forward_nav.py` `upsert_backtest_summary` — the verdict-surface path. + +--- + +## File Structure +- Create `src/fxhnt/adapters/data/binance_xsfunding_live.py` — `BinanceXsFundingLive`: per-coin live `{sym: (trailing_funding, today_funding, perp_price, spot_price, qvol)}` for the universe. +- Create `src/fxhnt/application/xsfunding_strategy.py` — `XsFundingForward` live-booking ForwardStrategy (executable construction, delta-neutral funding+basis book). +- Modify `src/fxhnt/registry.py` — add `xsfunding` entry. +- Modify `src/fxhnt/adapters/orchestration/assets.py` + `definitions.py` — `xsfunding_nav` asset in the daily job + Definitions + cockpit_forward dep. +- Tests: `tests/integration/test_xsfunding_strategy.py`, `tests/integration/test_binance_xsfunding_live.py`. + +--- + +## Task 1: live data source + +**Files:** Create `src/fxhnt/adapters/data/binance_xsfunding_live.py`; Test `tests/integration/test_binance_xsfunding_live.py`. + +- [ ] **Step 1** — READ `binance_funding.py` to confirm `BinanceFundingClient.scan`/`universe` signatures + how it does `_get`. The live source COMPOSES it for universe+funding, and adds perp+spot current prices via two bulk ticker calls: perp `fapi/v1/ticker/price` (all perps), spot `api/v3/ticker/price` (all spot). + +- [ ] **Step 2** — failing test `tests/integration/test_binance_xsfunding_live.py` (inject fakes; NO live net): +```python +from fxhnt.adapters.data.binance_xsfunding_live import BinanceXsFundingLive + + +class _FakeFunding: + def scan(self, prev): + liq = {"AAAUSDT": 5e6, "BBBUSDT": 3e6} + tf30 = {"AAAUSDT": 0.001, "BBBUSDT": 0.0003} + last24 = {"AAAUSDT": 0.0012, "BBBUSDT": 0.0004} + return liq, tf30, last24 + + +def test_live_source_merges_funding_and_prices(monkeypatch): + src = BinanceXsFundingLive(funding=_FakeFunding()) + monkeypatch.setattr(src, "_perp_prices", lambda: {"AAAUSDT": 100.0, "BBBUSDT": 10.0}) + monkeypatch.setattr(src, "_spot_prices", lambda: {"AAAUSDT": 99.5, "BBBUSDT": 10.1}) + snap = src.snapshot(prev={}) + assert snap["AAAUSDT"] == (0.001, 0.0012, 100.0, 99.5, 5e6) # (tf, today_funding, perp, spot, qvol) + assert snap["BBBUSDT"][2] == 10.0 and snap["BBBUSDT"][3] == 10.1 +``` + +- [ ] **Step 3** — run → FAIL. + +- [ ] **Step 4** — implement `src/fxhnt/adapters/data/binance_xsfunding_live.py`: +```python +"""Live per-coin snapshot for the cross-sectional funding paper track: trailing funding, +today's funding, perp price, spot price, quote-volume — for the liquid universe.""" +from __future__ import annotations + +import json +import urllib.request +from typing import Any + +from fxhnt.adapters.data.binance_funding import BinanceFundingClient + +_FAPI = "https://fapi.binance.com" +_SPOT = "https://api.binance.com" + + +class BinanceXsFundingLive: + def __init__(self, funding: Any | None = None) -> None: + self._f = funding if funding is not None else BinanceFundingClient() + + def _get(self, url: str) -> Any: + with urllib.request.urlopen(url, timeout=30) as r: + return json.loads(r.read()) + + def _perp_prices(self) -> dict[str, float]: + return {x["symbol"]: float(x["price"]) for x in self._get(f"{_FAPI}/fapi/v1/ticker/price")} + + def _spot_prices(self) -> dict[str, float]: + return {x["symbol"]: float(x["price"]) for x in self._get(f"{_SPOT}/api/v3/ticker/price")} + + def snapshot(self, prev: dict[str, float]) -> dict[str, tuple[float, float, float, float, float]]: + """{sym: (trailing_funding, today_funding, perp_price, spot_price, qvol)} over the liquid + universe (+ any prev-held coin still scannable). Coins lacking a spot price are omitted + (funding-only handling happens upstream if needed).""" + liq, tf30, last24 = self._f.scan(prev) + perp, spot = self._perp_prices(), self._spot_prices() + out: dict[str, tuple[float, float, float, float, float]] = {} + for s in liq: + if s in perp and s in spot and perp[s] > 0 and spot[s] > 0: + out[s] = (tf30.get(s, 0.0), last24.get(s, 0.0), perp[s], spot[s], liq[s]) + return out +``` + +- [ ] **Step 5** — run → PASS; mypy → no errors; full suite → PASS. + +- [ ] **Step 6** — commit: +```bash +git add src/fxhnt/adapters/data/binance_xsfunding_live.py tests/integration/test_binance_xsfunding_live.py +git commit -m "feat(xsfunding): live per-coin snapshot (funding + perp + spot + qvol) + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 2: XsFundingForward live-booking strategy + +**Files:** Create `src/fxhnt/application/xsfunding_strategy.py`; Test `tests/integration/test_xsfunding_strategy.py`. + +- [ ] **Step 1** — READ `EquityFactorLong.advance` + `ForwardTracker` to mirror the live-booking contract exactly (extra carries positions/marks/last_date; advance books prior book then rebalances; returns `(rows, extra)` where rows = `[(date, realized_return)]`). + +- [ ] **Step 2** — failing test `tests/integration/test_xsfunding_strategy.py`: +```python +from fxhnt.application.xsfunding_strategy import XsFundingForward + + +class _FakeLive: + def __init__(self, seq): self._seq = seq; self._i = -1 + def snapshot(self, prev): + self._i += 1 + return self._seq[self._i] + + +def _snap(funding): # 5 coins, perp=spot=100 (no basis move on day 1), given trailing funding + return {s: (f, f, 100.0, 100.0, 5e6) for s, f in funding.items()} + + +def test_first_advance_inception_no_booking(): + live = _FakeLive([_snap({"A": 0.002, "B": 0.001, "C": 0.0, "D": -0.001, "E": -0.002})]) + strat = XsFundingForward(live, quantile=0.4, clock=lambda: "2026-06-19") + rows, extra = strat.advance(None, {}) + assert rows == [] # inception: no prior book to book + assert extra["positions"] # executable weights set (top-carry long) + + +def test_second_advance_books_delta_neutral_return(): + day1 = _snap({"A": 0.002, "B": 0.001, "C": 0.0, "D": -0.001, "E": -0.002}) + # day2: A's perp falls to 99 (short-perp gains) and spot flat -> basis P&L positive; funding accrues + day2 = {"A": (0.002, 0.002, 99.0, 100.0, 5e6), "B": (0.001, 0.001, 100.0, 100.0, 5e6), + "C": (0.0, 0.0, 100.0, 100.0, 5e6), "D": (-0.001, -0.001, 100.0, 100.0, 5e6), + "E": (-0.002, -0.002, 100.0, 100.0, 5e6)} + live = _FakeLive([day1, day2]) + strat = XsFundingForward(live, quantile=0.4, clock=lambda: "2026-06-20") + _, extra = strat.advance(None, {}) # inception (day1) + rows, extra = strat.advance("2026-06-19", extra) + assert len(rows) == 1 and rows[0][0] == "2026-06-20" + assert rows[0][1] > 0.0 # A long-carry: short-perp gain (99/100) + funding +``` + +- [ ] **Step 3** — run → FAIL. + +- [ ] **Step 4** — implement `src/fxhnt/application/xsfunding_strategy.py`. Use the cross-sectional scoring (robust_z of trailing funding across the snapshot coins) + `construction_weights(scores, "executable", quantile, borrowable)` (borrowable = high-qvol coins) + book the prior book's delta-neutral return `Σ w·[(spot/spot_prev−1) − (perp/perp_prev−1) + today_funding] − turnover·cost`. State (`extra`): `{positions: {sym: w}, perp: {sym: px}, spot: {sym: px}, last_date}`. On inception (no prior positions) return `[]`. Reuse `robust_z` from equity_factor (or `funding_score` adapted to the snapshot). Daily rebalance. cost = a flat `cost_bps` (default 8bp) on turnover (live track doesn't need the liquidity-slip model — keep simple + documented). +```python +"""Live-booking cross-sectional funding-dispersion paper track (executable construction). +Books the REAL delta-neutral return (funding + spot-perp basis) forward each day — so the +paper record experiences the basis tail the backtest had to guard out. Mirrors the +equity-factor live tracks' ForwardStrategy contract.""" +from __future__ import annotations + +from typing import Any, Callable + +from fxhnt.domain.strategies.equity_factor import robust_z +from fxhnt.domain.cross_sectional_funding import construction_weights + + +def _today() -> str: + import datetime as dt + return dt.date.today().isoformat() + + +class XsFundingForward: + def __init__(self, live: Any, *, quantile: float = 0.2, cost_bps: float = 8.0, + borrowable_qvol: float = 2e7, clock: Callable[[], str] = _today) -> None: + self._live, self._q, self._cost = live, quantile, cost_bps / 1e4 + self._bq, self._clock = borrowable_qvol, clock + + def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]: + today = self._clock() + prev_pos: dict[str, float] = extra.get("positions", {}) + prev_perp: dict[str, float] = extra.get("perp", {}) + prev_spot: dict[str, float] = extra.get("spot", {}) + snap = self._live.snapshot(prev_pos) # {sym: (tf, today_funding, perp, spot, qvol)} + syms = sorted(snap) + # book prior book's realized delta-neutral return + rows: list[tuple[str, float]] = [] + if prev_pos: + realized = 0.0 + for s, w in prev_pos.items(): + if s in snap and s in prev_perp and s in prev_spot and prev_perp[s] > 0 and prev_spot[s] > 0: + _, tf_today, perp, spot, _ = snap[s] + spot_ret = spot / prev_spot[s] - 1.0 + perp_ret = perp / prev_perp[s] - 1.0 + realized += w * ((spot_ret - perp_ret) + tf_today) + # turnover cost vs the NEW book computed below — compute weights first + # (we charge cost on the rebalance into the new book) + ... + # new executable weights from trailing funding cross-section + tf = [snap[s][0] for s in syms] + scores = {syms[i]: z for i, z in enumerate(robust_z(tf))} + borrowable = {s for s in syms if snap[s][4] >= self._bq} + new_w = construction_weights(scores, "executable", quantile=self._q, borrowable=borrowable) + if prev_pos: + turnover = sum(abs(new_w.get(s, 0.0) - prev_pos.get(s, 0.0)) for s in set(new_w) | set(prev_pos)) + realized -= turnover * self._cost / 2.0 + rows.append((today, realized)) + extra = {"positions": new_w, + "perp": {s: snap[s][2] for s in new_w}, + "spot": {s: snap[s][3] for s in new_w}, + "last_date": today} + return rows, extra +``` +(Clean up the `...` placeholder — fold the turnover cost after weights are computed, as shown in the bottom block; the top `if prev_pos:` block computes only the carry+basis realized, then after weights the turnover cost is subtracted and the row appended. Restructure so it's linear and the two unit tests pass.) + +- [ ] **Step 5** — run → PASS; mypy → no errors; full suite → PASS. + +- [ ] **Step 6** — commit: +```bash +git add src/fxhnt/application/xsfunding_strategy.py tests/integration/test_xsfunding_strategy.py +git commit -m "feat(xsfunding): live-booking forward strategy (delta-neutral funding+basis, executable) + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 3: registry + Dagster wiring + verdict surface + +**Files:** Modify `src/fxhnt/registry.py`, `src/fxhnt/adapters/orchestration/assets.py`, `definitions.py`; Test `tests/integration/test_orchestration_definitions.py` (update count + xsfunding present). + +- [ ] **Step 1** — add to `STRATEGY_REGISTRY` (mirror an eqfactor entry): +```python + "xsfunding": { + "display_name": "Crypto funding — cross-sectional", "sleeve": "crypto-funding", + "venue": "binance-perp", "state_file": "xsfunding_state", + "gate_spec": {"min_days": 60, "min_total_return": 0.0, "min_sharpe": 0.5}, + }, +``` + +- [ ] **Step 2** — add a Dagster asset `xsfunding_nav` in `assets.py` (mirror `eqfactor_long_nav`): construct `XsFundingForward(BinanceXsFundingLive())` wrapped in `ForwardTracker(..., f"{_data_dir()}/xsfunding_state.json").step()`, log forward_days. Add `xsfunding_nav` to `cockpit_forward`'s deps. Register in `definitions.py` (import + combined_book_job selection + Definitions assets list). Update `test_orchestration_definitions.py` (asset count +1, assert `xsfunding_nav` present). + +- [ ] **Step 3** — run the orchestration test + full suite → PASS; mypy → no new errors; confirm `defs` loads (`python -c "from fxhnt.adapters.orchestration.definitions import defs"`). + +- [ ] **Step 4** — commit: +```bash +git add src/fxhnt/registry.py src/fxhnt/adapters/orchestration/assets.py src/fxhnt/adapters/orchestration/definitions.py tests/integration/test_orchestration_definitions.py +git commit -m "feat(xsfunding): register xsfunding paper track + Dagster asset in daily book + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 4: deploy + surface verdict (controller) +- [ ] Push fxhnt master; rebuild cockpit image via Argo `fxhnt-cockpit` at HEAD; wait for Succeeded. +- [ ] Persist the xsfunding backtest verdict (executable, net-of-basis) to `backtest_summary` keyed `xsfunding` — extend `backtest_ingest._CONSTRUCTION_TO_SID` mapping or run a one-off upsert from the basis report's `executable@5e6` cell (sharpe ~6.3, but record with the honest caveat). Simplest: a tiny one-off that reads `/tmp/funding_basis_report.json` executable cell → `upsert_backtest_summary(BacktestSummary(strategy_id="xsfunding", ...))` against the cockpit pg (in-cluster Job or local+port-forward). +- [ ] Verify: cockpit fleet shows `xsfunding` (gate=WAIT initially); the daily job materializes `xsfunding_nav`; backtest verdict visible. Trigger one manual materialization to seed inception. + +## Final review (after Task 3) + honest note +Final review over the whole diff; full suite + mypy. The paper track will accumulate the REAL net-of-everything record (incl. the basis tail the backtest guarded out + execution reality) — gate=WAIT until weeks of forward data. Do NOT oversell the backtest Sharpe in the cockpit; the surfaced verdict carries the funding-research-consistent ~3-5 deployable expectation.