docs(xsfunding): paper-forward track implementation plan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-19 07:38:51 +02:00
parent a54a59c57e
commit e7c9946aac

View File

@@ -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) <noreply@anthropic.com>`.
---
## 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) <noreply@anthropic.com>"
```
---
## 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_prev1) (perp/perp_prev1) + 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) <noreply@anthropic.com>"
```
---
## 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) <noreply@anthropic.com>"
```
---
## 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.