# Faithful delta-neutral carry — Implementation Plan (Phase 2F-A5)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. TDD. `- [ ]` steps.

**Goal:** Fix the xsfunding carry artifact (Sharpe 30.9) by modeling the REAL delta-neutral return
(`basis + funding`) — ingest spot closes, build a delta-neutral return panel, feed it to the carry sleeve.
Sizing stays the existing ISV-adaptive overlay ("match the system").

**Architecture:** Spot stored as `feature='spot_close'` in the warehouse (mirror funding). A
`delta_neutral_return_panel(spot, perp, funding)` = `(spot_ret − perp_ret) + funding` (funding-only fallback).
The backfill/snapshot feed that to the existing `sleeve_daily_carry` (unchanged: `Σ w·x / gross`) in place of
raw funding. Read-only/additive; overlay sizing untouched.

**Tech Stack:** Python 3.12, pytest, DuckDB, httpx. **Repo:** `~/Work/fxhnt`, branch `feat/faithful-deltaneutral-carry`.
**Spec:** `docs/superpowers/specs/2026-06-22-faithful-deltaneutral-carry-design.md`.

**Verified context:**
- `BinanceSpotHistory().daily_close(symbol) -> {epoch_day: close}` (`adapters/data/binance_spot_history.py`),
  per-symbol retry/never-raise. Reuse for spot ingest.
- Feature store: `write_features_bulk(items)` idempotent per (symbol, ts-range, feature); `crypto_funding_panel`
  (feature='funding') at duckdb_feature_store.py:123 — MIRROR for spot. `crypto_close_panel` = perp closes.
- A4 wiring: `_paper_backfill_sleeves` builds `XsFundingReplay` (carry, return_mode="carry"); the
  `paper_backfill` command builds `funding_by_date` and passes it to `backfill_paper_book(..., funding_by_date=)`;
  carry sleeves book `sleeve_daily_carry(prev_shadow, funding_by_date[date])`. We REPLACE that input with the
  delta-neutral return.
- `funding_ingest.py`/`crypto-funding-ingest` CLI/`crypto_funding` asset (A4) — MIRROR for spot.

---

## Task 1: spot ingest (adapter reuse) + `crypto_spot_panel` + CLI + asset

**Files:** Create `src/fxhnt/application/spot_ingest.py`; Modify `duckdb_feature_store.py`, `cli.py`,
`adapters/orchestration/assets.py` (+ definitions); Test `tests/integration/test_spot_ingest.py` +
`tests/integration/test_crypto_spot_panel.py`

- [ ] **Step 1: Failing tests**
```python
# tests/integration/test_crypto_spot_panel.py
from fxhnt.adapters.warehouse.duckdb_feature_store import DuckDbFeatureStore
def test_crypto_spot_panel_reads_spot_close(tmp_path):
    st = DuckDbFeatureStore(str(tmp_path/"w.duckdb"))
    st.write_features("BTCUSDT", [(86400, {"spot_close": 100.0}), (172800, {"spot_close": 110.0})])
    st.write_features("BTCUSDT", [(86400, {"close": 99.0})])     # perp close must NOT leak
    assert st.crypto_spot_panel()["BTCUSDT"] == {1: 100.0, 2: 110.0}
```
```python
# tests/integration/test_spot_ingest.py
from fxhnt.adapters.warehouse.duckdb_feature_store import DuckDbFeatureStore
from fxhnt.application.spot_ingest import ingest_spot
class FakeFetcher:
    def daily_close(self, sym, start_ms=0): return {1: 100.0, 2: 110.0}
def test_ingest_spot_writes_spot_close_idempotent(tmp_path):
    st = DuckDbFeatureStore(str(tmp_path/"w.duckdb"))
    assert ingest_spot(st, FakeFetcher(), ["BTCUSDT"]) == 1
    assert st.crypto_spot_panel()["BTCUSDT"] == {1: 100.0, 2: 110.0}
    ingest_spot(st, FakeFetcher(), ["BTCUSDT"])              # re-run
    assert st.crypto_spot_panel()["BTCUSDT"] == {1: 100.0, 2: 110.0}
def test_ingest_spot_skips_empty(tmp_path):
    st = DuckDbFeatureStore(str(tmp_path/"w.duckdb"))
    class E:
        def daily_close(self, s, start_ms=0): return {}
    assert ingest_spot(st, E(), ["XUSDT"]) == 0
```
- [ ] **Step 2:** Run → FAIL.
- [ ] **Step 3a:** `crypto_spot_panel(*, suffix='USDT')` in duckdb_feature_store.py — exact mirror of
  `crypto_funding_panel` but `feature='spot_close'`.
- [ ] **Step 3b:** `src/fxhnt/application/spot_ingest.py`:
```python
"""Ingest Binance SPOT daily-close history into the warehouse feature store as feature='spot_close'
(the long leg of the delta-neutral carry). Idempotent per (symbol, day). One-shot / single-writer."""
from __future__ import annotations
from typing import Any, Protocol
class _SpotFetcher(Protocol):
    def daily_close(self, symbol: str, start_ms: int = 0) -> dict[int, float]: ...
def ingest_spot(store: Any, fetcher: _SpotFetcher, symbols: list[str]) -> int:
    items: list[tuple[str, list[tuple[int, dict[str, float]]]]] = []
    for sym in symbols:
        series = fetcher.daily_close(sym)
        if not series:
            continue
        rows = [(int(day) * 86_400, {"spot_close": float(c)}) for day, c in sorted(series.items())]
        if rows:
            items.append((sym, rows))
    if not items:
        return 0
    store.write_features_bulk(items)
    return len(items)
```
- [ ] **Step 3c:** CLI `crypto-spot-ingest [--days 400]` in cli.py (mirror `crypto-funding-ingest`: open
  `DuckDbFeatureStore(s.warehouse_path)`, `symbols = sorted(store.crypto_close_panel())`, `BinanceSpotHistory()`,
  `ingest_spot(store, ...)`, echo count). Dagster asset `crypto_spot` (mirror `crypto_funding`, deps=[crypto_bars],
  daily/single-writer) + register in definitions.py (update the asset-count test).
- [ ] **Step 4:** PASS: `uv run pytest tests/integration/test_spot_ingest.py tests/integration/test_crypto_spot_panel.py -q`.
- [ ] **Step 5:** Regression `uv run pytest -k "warehouse or ingest or asset or funding or spot" -q`.
- [ ] **Step 6:** Commit `feat(carry): ingest spot closes (feature=spot_close) + crypto_spot_panel + CLI/asset`.

---

## Task 2: `delta_neutral_return_panel` + feed it to the carry sleeve

**Files:** Modify `src/fxhnt/application/funding_panel.py`, `src/fxhnt/cli.py`,
`src/fxhnt/adapters/orchestration/assets.py`; Test `tests/integration/test_delta_neutral_return.py`

- [ ] **Step 1: Failing test**
```python
# tests/integration/test_delta_neutral_return.py
from fxhnt.application.funding_panel import delta_neutral_return_panel
def test_dn_is_basis_plus_funding():
    spot = {"A": {1: 100.0, 2: 110.0}}; perp = {"A": {1: 100.0, 2: 121.0}}; funding = {"A": {2: 0.001}}
    dn = delta_neutral_return_panel(spot, perp, funding)
    # day2: spot_ret 0.10, perp_ret 0.21 -> basis -0.11 + funding 0.001 = -0.109
    assert abs(dn["A"][2] - (-0.109)) < 1e-9
    assert 1 not in dn["A"]                          # no prior close for day 1 -> no dn
def test_dn_funding_only_fallback_when_no_spot():
    perp = {"A": {1: 100.0, 2: 110.0}}; funding = {"A": {2: 0.02}}
    dn = delta_neutral_return_panel({}, perp, funding)
    assert abs(dn["A"][2] - 0.02) < 1e-9             # no spot -> funding-only
```
- [ ] **Step 2:** Run → FAIL.
- [ ] **Step 3a:** Add to `funding_panel.py`:
```python
def delta_neutral_return_panel(spot_panel, perp_panel, funding_panel):
    """{sym: {day: dn_ret}} where dn_ret(d) = (spot_ret - perp_ret) + funding(d). Per coin/day:
    requires a perp close at d AND d-1; spot leg used when spot has d AND d-1, else funding-only
    (the live strategy's documented fallback). Pure."""
    out: dict[str, dict[int, float]] = {}
    for sym, pdays in perp_panel.items():
        sp = spot_panel.get(sym, {}); fnd = funding_panel.get(sym, {}); row: dict[int, float] = {}
        for d in pdays:
            if (d - 1) not in pdays or pdays[d - 1] <= 0:
                continue
            perp_ret = pdays[d] / pdays[d - 1] - 1.0
            f = fnd.get(d, 0.0)
            if (d in sp) and ((d - 1) in sp) and sp[d - 1] > 0:
                spot_ret = sp[d] / sp[d - 1] - 1.0
                row[d] = (spot_ret - perp_ret) + f
            else:
                row[d] = f                                  # funding-only fallback
        if row:
            out[sym] = row
    return out
```
- [ ] **Step 3b:** In `cli.py` `paper_backfill`: build the spot panel
  (`DuckDbFeatureStore(s.warehouse_path).crypto_spot_panel()`) and replace the `funding_by_date` construction
  with a delta-neutral one: `dn = delta_neutral_return_panel(spot_panel, panel, funding_panel)` then invert to
  `dn_by_date = {iso:{sym: dn[sym][day]}}`; pass `funding_by_date=dn_by_date` to `backfill_paper_book`. (`panel`
  is the perp close panel already built; `funding_panel = crypto_funding_panel()`.)
- [ ] **Step 3c:** Mirror in the snapshot assembly (`assets.py _assemble_paper_snapshot_inputs` / wherever the
  carry funding-for-as_of would be provided) so the live snapshot books the delta-neutral return for as_of —
  live==replay. If the snapshot doesn't yet pass a carry input, wire it to pass the as_of delta-neutral return
  per symbol (latest dn from the panels). REPORT exactly what you wired.
- [ ] **Step 4:** PASS: `uv run pytest tests/integration/test_delta_neutral_return.py tests/integration/test_paper_backfill_cli.py -q`.
- [ ] **Step 5:** Regression `uv run pytest -k "paper or backfill or carry or funding or snapshot" -q`.
- [ ] **Step 6:** Commit `feat(carry): delta_neutral_return_panel (basis+funding) feeds the xsfunding carry sleeve`.

---

## Task 3: deploy + ingest spot + re-backfill + HONEST 3-edge verify

- [ ] **Step 1:** `uv run pytest -q` → green. **Step 2:** no `*.dbn` staged.
- [ ] **Step 3:** **superpowers:finishing-a-development-branch** — merge → master, push (auto-deploy). Confirm
  workflow Succeeded + pods rolled. (If git.fxhnt.ai:22 is down again: `kubectl rollout restart
  deploy/tailscale-gitlab-proxy -n foxhunt`, wait ready, retry push.)
- [ ] **Step 4:** Ingest spot (funding already ingested in A4; re-run if stale):
  `kubectl exec deploy/dagster -c daemon -n foxhunt -- fxhnt crypto-spot-ingest --days 400`.
- [ ] **Step 5:** Re-backfill: `kubectl exec deploy/dagster -c daemon -n foxhunt -- fxhnt paper-backfill --days 365`.
- [ ] **Step 6: VERIFY HONESTLY** — the success bar is "plausible + non-dominating", NOT a target number:
  - xsfunding **standalone Sharpe** — must drop from 30.9 to a plausible band (~1–4). Report it.
  - 3-edge nav return/Sharpe/maxDD + per-sleeve standalone + the OOS H1/H2 split + cost-drag.
  - **If xsfunding Sharpe is STILL absurd (>~6) after the basis term**, that proves the daily book can't
    represent carry → recommend the live-only fallback (revert xsfunding from the replay). Say so plainly.
  - `/paper` + `/paper/replay` render. Report all numbers honestly (incl. whether the basis fix worked).

---

## Self-review notes
- The fix is the RETURN (basis+funding), not the sizing — sizing stays the existing ISV-adaptive overlay
  ("match the system").
- `sleeve_daily_carry` unchanged; it's fed the delta-neutral return instead of raw funding.
- Funding-only fallback (no spot) = the live strategy's documented behavior.
- Gross in the book (post-hoc cost lens), matching the directional sleeves.
- Honest exit: if Sharpe still absurd, carry → live-only (pre-agreed).
- NEVER `git add -A`; verify no `*.dbn` staged.

## Acceptance criteria (from spec)
- spot ingest + crypto_spot_panel; idempotent; tested. ✅ T1
- delta_neutral_return_panel (basis+funding, funding-only fallback); tested. ✅ T2
- carry sleeve fed the delta-neutral return (basis injects variance); backfill+snapshot; tested. ✅ T2
- in-cluster ingest spot → re-backfill → xsfunding Sharpe plausible OR honest live-only verdict; reported. ✅ T3
- read-only/additive; sizing unchanged; no `*.dbn`. ✅ T3
