# Phase 0b — Venue Consolidation to Bybit + IBKR 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:** Make Bybit (crypto) + IBKR (tradfi) the only venues for execution AND data — migrate the crypto
edges that work on Bybit (xsfunding, unlock) onto Bybit data, retire the ones that don't (crypto_tstrend
standalone, stablecoin), and remove Binance + Alpaca entirely.

**Architecture:** Migrate first (xsfunding/unlock → Bybit standalone tracks, template = `positioning`),
then remove (Alpaca exec, Binance exec, Binance data + `features` table, vestigial `state_file`). The
`bybit_4edge` deploy book is unchanged (it already runs on Bybit).

**Tech Stack:** Python 3.12, Dagster assets, SQLAlchemy 2.0, Timescale/Postgres, pytest.

**Base:** branch off `feat/unified-db-backtest-architecture` (0a, unmerged) as
`feat/phase0b-venue-consolidation`. Spec:
`docs/superpowers/specs/2026-07-13-phase0b-venue-consolidation-bybit-ibkr.md`.

## Global Constraints

- **A crypto strat survives only if it works on Bybit** (instruments listed + edge holds). Decided per edge
  in the spec — do not re-litigate.
- **Real money errs false-WAIT, never false-FUND.** Migrated tracks re-inception on cutover (new Bybit
  basis) — a VISIBLE deterministic re-inception, never a silent reset. Their Bybit refs must exist before
  the gate can PASS. The 0a reconciliation invariant test MUST stay green.
- **Single source of truth**; no dead code / stubs / TODO; fix every review Minor in-loop.
- **Keep the Broker port** with `IBKRBroker` as the sole implementation (do not delete the abstraction).
- **`bybit_4edge` deploy book is UNCHANGED** — crypto_tstrend stays a sleeve in the book; 0b retires only its
  redundant *standalone* track. Do not touch `_DEFAULT_BYBIT_SLEEVES`.
- **Binance stays gone**: after migration, nothing (data or exec) may read Binance or the `features` table.
- Verify the cockpit LOCALLY in a real browser before finishing (`./scripts/dev-cockpit-local.sh` or a
  seeded sqlite DB).

---

## Task 1: Generic Bybit single-sleeve track builder + migrate `xsfunding` → Bybit

**Files:**
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (add `build_bybit_sleeve_forward_nav`; refactor
  `build_positioning_forward_nav` to use it; rewrite `xsfunding_nav` asset)
- Modify: `src/fxhnt/application/bybit_forward_track.py` (`bybit_report_sid_kwargs`: add `xsfunding`)
- Modify: `src/fxhnt/cli.py` (`_BYBIT_BOOK_SIDS`: add `"xsfunding"`)
- Modify: `src/fxhnt/registry.py` (`xsfunding` entry: venue + backtest.kind + params)
- Test: `tests/unit/test_bybit_sleeve_forward_builder.py`

**Interfaces:**
- Consumes: `BybitFourEdgeStrategy(store, sleeves=[...], unlock_events=…, positioning_capacity_capital=…,
  positioning_tail_cap_k=…)`, `forward_engine.run_track(repo, sid, strategy, today, at)`,
  `TimescaleFeatureStore(dsn, table="bybit_features")`, `liquid_universe(store)`.
- Produces: `build_bybit_sleeve_forward_nav(repo, store, sleeve, *, today, at, universe=None, cost_bps=5.5,
  unlock_events=None, positioning_capacity_capital=None, positioning_tail_cap_k=None) -> dict`.

- [ ] **Step 1: Write the failing test** — the generic builder runs a single-sleeve Bybit strategy through
  the deterministic engine and returns a track dict, and `build_positioning_forward_nav` still works via it.

```python
# tests/unit/test_bybit_sleeve_forward_builder.py
import datetime as dt
from fxhnt.adapters.orchestration.assets import build_bybit_sleeve_forward_nav, build_positioning_forward_nav
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore


def _seed(store, days=40):
    import math
    for i in range(6):
        px, rows, sign = 100.0, [], (1.0 if i % 2 == 0 else -1.0)
        for d in range(days):
            px *= 1 + sign * 0.01 + 0.003 * math.cos(0.4 * d + i)
            rows.append((d * 86400, {"close": px, "funding": 0.001 * sign, "spot_close": px,
                                     "long_ratio": 0.5 + 0.1 * sign}))
        store.write_features(f"SYM{i:02d}USDT", rows)


def test_generic_builder_books_a_single_sleeve_track(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}"); repo.migrate()
    store = TimescaleFeatureStore("sqlite://", table="bybit_features"); _seed(store)
    out = build_bybit_sleeve_forward_nav(repo, store, "xsfunding",
                                         today="1970-02-09", at=dt.datetime(2026, 1, 1), cost_bps=0.0)
    assert out["days"] >= 1 and "t0" in out
    store.close()
```

- [ ] **Step 2: Run to verify it fails** — `.venv/bin/python -m pytest tests/unit/test_bybit_sleeve_forward_builder.py -q` → FAIL (import error).

- [ ] **Step 3: Add the generic builder + refactor positioning to use it** (`assets.py`). Insert before
  `build_positioning_forward_nav`:

```python
def build_bybit_sleeve_forward_nav(repo, store, sleeve, *, today, at, universe=None, cost_bps=5.5,
                                   unlock_events=None, positioning_capacity_capital=None,
                                   positioning_tail_cap_k=None):  # type: ignore[no-untyped-def]
    """Plain (Dagster-free, unit-testable) driver for a SINGLE Bybit sleeve as its own forward track: build
    the one-sleeve `BybitFourEdgeStrategy` over `store` (bybit_features) and run it through the deterministic
    engine (DB anchor + recompute). The DB (forward_nav + anchor) is the SSOT — no JSON state. Shared by the
    per-edge Bybit tracks (xsfunding / unlock / positioning)."""
    from fxhnt.application.bybit_forward_track import BybitFourEdgeStrategy
    from fxhnt.application.forward_engine import run_track
    strategy = BybitFourEdgeStrategy(
        store, universe=universe, cost_bps=cost_bps, sleeves=[sleeve], unlock_events=unlock_events,
        positioning_capacity_capital=positioning_capacity_capital, positioning_tail_cap_k=positioning_tail_cap_k)
    return run_track(repo, sleeve, strategy, today=today, at=at)
```
Then replace `build_positioning_forward_nav`'s body with a thin delegation:
```python
def build_positioning_forward_nav(repo, store, *, today, at, universe=None, cost_bps=5.5,
                                  positioning_capacity_capital=None, positioning_tail_cap_k=None):
    """POSITIONING single-sleeve Bybit track — thin wrapper on `build_bybit_sleeve_forward_nav` (keeps the
    capturability haircuts). See that function."""
    return build_bybit_sleeve_forward_nav(
        repo, store, "positioning", today=today, at=at, universe=universe, cost_bps=cost_bps,
        positioning_capacity_capital=positioning_capacity_capital, positioning_tail_cap_k=positioning_tail_cap_k)
```

- [ ] **Step 4: Run — the builder test + the existing positioning tests pass** —
  `.venv/bin/python -m pytest tests/unit/test_bybit_sleeve_forward_builder.py tests/integration/test_positioning_forward_track.py -q` → PASS.

- [ ] **Step 5: Rewrite the `xsfunding_nav` asset** (Bybit, mirroring `positioning_nav`). Replace the current
  Binance body (`XsFundingForward(BinanceXsFundingLive())` + `persist_backtest_ref=True`) with:

```python
@asset(deps=[bybit_warehouse])   # match positioning_nav's freshness dep (use the same bybit warehouse asset dep)
def xsfunding_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Paper-forward cross-sectional funding-dispersion — the SINGLE xsfunding sleeve as its own edge, on
    BYBIT (`bybit_features`), the venue it would trade. Deterministic engine (DB anchor + recompute); its
    reconciliation ref is written by the weekly Bybit book persist (`_persist_bybit_book`), like positioning."""
    import datetime as _dt
    import urllib.error
    from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
    from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
    from fxhnt.application.bybit_liquidity import liquid_universe
    from fxhnt.config import get_settings
    s = get_settings()
    store = TimescaleFeatureStore(s.operational_dsn, table="bybit_features")
    try:
        universe = liquid_universe(store) or None
        repo = ForwardNavRepo(s.operational_dsn); repo.migrate()
        today = _dt.datetime.now(_dt.UTC).strftime("%Y-%m-%d")
        at = _dt.datetime.now(_dt.UTC).replace(tzinfo=None)
        out = build_bybit_sleeve_forward_nav(repo, store, "xsfunding", today=today, at=at,
                                             universe=universe, cost_bps=s.cost_bps_per_turnover)
        context.log.info(f"xsfunding_nav: T0={out['t0']}, {out['days']}d through {out['last_date']} "
                         f"(reinception={out['reinception']})")
        return {"inception": out["t0"], "last_date": out["last_date"], "forward_days": out["days"]}
    except (urllib.error.URLError, TimeoutError, ConnectionError, OSError) as e:
        context.log.warning(f"xsfunding_nav step failed (record unchanged): {e}")
        return {"inception": None, "last_date": None, "forward_days": 0}
    finally:
        store.close()
```
*(Use the SAME `deps=[...]` and the same bybit warehouse-refresh asset that `positioning_nav` depends on —
read positioning_nav's decorator and match it exactly; if positioning_nav has no explicit dep, match that.)*

- [ ] **Step 6: Wire xsfunding's Bybit ref.** (a) `bybit_forward_track.py` `bybit_report_sid_kwargs`: add
  `if strategy_id == "xsfunding": return {"sleeves": ["xsfunding"], "strategy_id": "xsfunding"}`. (b)
  `cli.py`: `_BYBIT_BOOK_SIDS = ("bybit_4edge", "bybit_4edge_levered", "positioning", "xsfunding")`. (c)
  `registry.py` `xsfunding`: set `"venue": "bybit-perp"`, and `definition.backtest.kind` from `"sleeve"` →
  `"report"` (its ref now comes from the Bybit book persist, like positioning); update
  `definition.params` to the single-sleeve Bybit construction (mirror `positioning`'s params shape).

- [ ] **Step 7: Update the invariant test expectation** — `tests/unit/test_backtest_invariant.py` already
  enumerates the registry; confirm it still passes (xsfunding is now `report`-kind, non-`none`, and has a
  provider via `_BYBIT_BOOK_SIDS`). Run `.venv/bin/python -m pytest tests/unit/test_backtest_invariant.py -q`.

- [ ] **Step 8: Run the affected suites + commit** —
  `.venv/bin/python -m pytest tests/unit/test_bybit_sleeve_forward_builder.py tests/unit/test_backtest_invariant.py tests/integration/test_bybit_persist_sleeve_ret_cli.py -q` → PASS, then:
```bash
git add src/fxhnt/adapters/orchestration/assets.py src/fxhnt/application/bybit_forward_track.py src/fxhnt/cli.py src/fxhnt/registry.py tests/unit/test_bybit_sleeve_forward_builder.py
git commit -m "feat(0b): migrate xsfunding to a Bybit single-sleeve forward track (venue-consistent ref)"
```

---

## Task 2: Migrate `unlock` → Bybit (reuse the builder; carry the unlock calendar)

**Files:**
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (rewrite `unlock_nav`)
- Modify: `src/fxhnt/application/bybit_forward_track.py` (`bybit_report_sid_kwargs`: add `unlock`)
- Modify: `src/fxhnt/cli.py` (`_BYBIT_BOOK_SIDS`: add `"unlock"`)
- Modify: `src/fxhnt/registry.py` (`unlock` entry)
- Test: extend `tests/unit/test_bybit_sleeve_forward_builder.py`

**Interfaces:**
- Consumes: `build_bybit_sleeve_forward_nav` (Task 1), `load_unlock_events(operational_unlock_repo(s), path)`,
  `_data_dir()`.

- [ ] **Step 1: Add a failing test** that the builder threads `unlock_events` into the unlock sleeve
  (monkeypatch `BybitFourEdgeStrategy` to capture kwargs, assert `sleeves==["unlock"]` and
  `unlock_events` passed through). Run → FAIL.

- [ ] **Step 2: Rewrite the `unlock_nav` asset** — Bybit single-sleeve via `build_bybit_sleeve_forward_nav`
  with `sleeve="unlock"`, loading the calendar the SAME way the Bybit book does
  (`unlock_events = load_unlock_events(operational_unlock_repo(s), f"{_data_dir()}/unlock_calendar.json")`,
  `[]` on failure — never fabricate), store = `TimescaleFeatureStore(dsn, table="bybit_features")`, NO
  `persist_backtest_ref` (ref comes from `_persist_bybit_book`). Mirror the `xsfunding_nav` shape from Task 1.

- [ ] **Step 3: Wire unlock's Bybit ref** — `bybit_report_sid_kwargs`: add
  `if strategy_id == "unlock": return {"sleeves": ["unlock"], "strategy_id": "unlock"}`;
  `_BYBIT_BOOK_SIDS += ("unlock",)`; `registry.py` `unlock`: `venue` → `bybit-perp`, `backtest.kind`
  `recompute-replay` → `report`, params → single-sleeve Bybit construction.

- [ ] **Step 4: Confirm `_persist_bybit_book` writes both new refs** — it loops `_BYBIT_BOOK_SIDS`, which now
  includes xsfunding + unlock; `bybit_4edge_backtest_summary(sleeves=["unlock"], strategy_id="unlock",
  sleeve_returns=sleeve_rets)` reuses the already-computed sleeve series (unlock is in `_DEFAULT_BYBIT_SLEEVES`,
  so `sleeve_rets["unlock"]` exists — verify). No extra compute.

- [ ] **Step 5: Run + commit** —
  `.venv/bin/python -m pytest tests/unit/test_bybit_sleeve_forward_builder.py tests/unit/test_backtest_invariant.py -q` → PASS, then commit:
```bash
git commit -am "feat(0b): migrate unlock to a Bybit single-sleeve forward track"
```

---

## Task 3: Align the Bybit reconciliation cost basis (single-source 5.5)

**Files:**
- Modify: `src/fxhnt/config.py` (add `bybit_cost_bps: float = 5.5`)
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (`positioning_nav`, `xsfunding_nav`, `unlock_nav`: pass `cost_bps=s.bybit_cost_bps`, NOT `s.cost_bps_per_turnover`)
- Modify: `src/fxhnt/adapters/orchestration/migration_builders.py` (the Bybit-sleeve recompute builders: `cost_bps=settings.bybit_cost_bps`)
- Modify: `src/fxhnt/cli.py` (`_persist_bybit_book` + `bybit-persist-sleeve-ret`/`backtest-refs` `--cost-bps`: default from `settings.bybit_cost_bps`)
- Modify: `src/fxhnt/registry.py` (the Bybit-sleeve tracks' `params.cost_bps` 10.0→5.5, update the "mirrors Settings.cost_bps_per_turnover" comment → `bybit_cost_bps`, BUMP `definition_version` 1→2 for a clean visible re-inception)
- Test: `tests/unit/test_bybit_cost_basis_consistent.py`

**Interfaces:**
- Produces: `Settings.bybit_cost_bps` (= 5.5) — the SINGLE Bybit-perp per-turnover reconciliation cost, read by both the forward tracks AND their `_persist_bybit_book` refs.

**Why:** the standalone Bybit sleeve tracks charged `cost_bps_per_turnover=10.0` while their reconciliation ref (`_persist_bybit_book`) + the deploy book use 5.5 (the MEASURED liquid-book cost — the "mirage" finding: Sharpe 2.11 at 5.5bp drag). Forward-charged-more-than-ref biases the gate toward WAIT (safe, but a cost artifact could permanently WAIT a real edge). One correct Bybit cost, single-sourced, enforced by an invariant test. `cost_bps_per_turnover` (10) stays for the equity/tradfi tracks (different venue) — do NOT change multistrat.

- [ ] **Step 1: Write the failing invariant test**

```python
# tests/unit/test_bybit_cost_basis_consistent.py
from fxhnt.config import Settings
from fxhnt.registry import STRATEGY_REGISTRY

_BYBIT_SLEEVE_SIDS = ("positioning", "xsfunding", "unlock")   # standalone Bybit sleeve tracks


def test_bybit_sleeve_tracks_use_the_single_bybit_cost():
    c = Settings().bybit_cost_bps
    assert c == 5.5
    for sid in _BYBIT_SLEEVE_SIDS:
        params = STRATEGY_REGISTRY[sid]["definition"]["params"]
        assert params["cost_bps"] == c, (
            f"{sid} registry cost_bps={params['cost_bps']} != bybit_cost_bps={c} — "
            f"track and reconciliation ref would use different costs")
```

- [ ] **Step 2: Run to verify it fails** — `.venv/bin/python -m pytest tests/unit/test_bybit_cost_basis_consistent.py -q` → FAIL.

- [ ] **Step 3: Add `bybit_cost_bps = 5.5` to `Settings`** (config.py), documented as the measured Bybit-perp liquid-book per-turnover reconciliation cost (single source for tracks + refs).

- [ ] **Step 4: Repoint the tracks + refs + builders to `bybit_cost_bps`.** In `assets.py`, the three Bybit sleeve assets pass `cost_bps=s.bybit_cost_bps`. In `migration_builders.py`, the Bybit-sleeve builders use `settings.bybit_cost_bps`. In `cli.py`, `_persist_bybit_book`'s `cost_bps` default + the `--cost-bps` option defaults resolve from `get_settings().bybit_cost_bps`. VERIFY `bybit_4edge`/`bybit_4edge_levered` forward tracks: if their asset already uses 5.5 they are consistent (leave); if any Bybit forward track passes `cost_bps_per_turnover`, repoint it too — the invariant is "every Bybit forward track and its ref use `bybit_cost_bps`".

- [ ] **Step 5: Registry — set `cost_bps: 5.5` + bump `version` 1→2** on the Bybit sleeve tracks (positioning/xsfunding/unlock; and `carry` for consistency if present, though dormant), update the mirror comment. The param change re-hashes the definition → deterministic re-inception; the version bump is the documented protocol for a cost change (visible, not silent). Do NOT touch multistrat/equity cost_bps.

- [ ] **Step 6: Run the invariant + the migration/regression suites** —
`.venv/bin/python -m pytest tests/unit/test_bybit_cost_basis_consistent.py tests/unit/test_backtest_invariant.py tests/integration/test_bybit_persist_sleeve_ret_cli.py tests/unit/test_bybit_sleeve_forward_builder.py -q` → PASS.

- [ ] **Step 7: Commit**

```bash
git commit -am "fix(0b): single-source the Bybit reconciliation cost (bybit_cost_bps=5.5) — tracks + refs now reconcile on one measured cost basis"
```

Post-deploy note (runbook): the cost change re-inceptions the affected Bybit tracks (version 2) — a VISIBLE deterministic re-inception; include `migrate-forward-anchors` for them.

## Task 4: Retire `crypto_tstrend` standalone track + `stablecoin_rotation`

**Files:**
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (delete `crypto_tstrend_nav`, `stablecoin_rotation_nav`
  assets + remove from the Definitions asset list)
- Modify: `src/fxhnt/registry.py` (delete `crypto_tstrend`, `stablecoin_rotation` entries)
- Delete: `src/fxhnt/application/stablecoin_strategy.py`, `src/fxhnt/application/stablecoin_runner.py` (if no
  other consumer — grep first)
- Test: `tests/unit/test_retired_tracks_gone.py`

**Interfaces:** none produced. **Consumes:** nothing new.

- [ ] **Step 1: Verify no other consumer** — `grep -rn "stablecoin_strategy\|stablecoin_runner\|StableRotationForward\|StableReversionRunner\|crypto_tstrend_nav" src/fxhnt/ | grep -v test`. Anything outside the assets/registry above is a blocker — report it. (The `crypto_tstrend` SLEEVE in `_DEFAULT_BYBIT_SLEEVES` / `bybit_book_eval` is the DEPLOY BOOK — it MUST remain; only the standalone `crypto_tstrend_nav` track + registry entry go.)

- [ ] **Step 2: Write the guard test**:
```python
# tests/unit/test_retired_tracks_gone.py
from fxhnt.registry import STRATEGY_REGISTRY
from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES


def test_standalone_binance_tracks_retired():
    assert "crypto_tstrend" not in STRATEGY_REGISTRY   # standalone track gone
    assert "stablecoin_rotation" not in STRATEGY_REGISTRY


def test_tstrend_stays_in_the_deploy_book():
    assert "crypto_tstrend" in _DEFAULT_BYBIT_SLEEVES   # the BOOK sleeve is untouched
```

- [ ] **Step 3: Run to verify it fails** (entries still present) → FAIL.

- [ ] **Step 4: Delete** the two assets (remove their `@asset` defs + drop them from the `Definitions(assets=[...])` list), the two registry entries, and the two stablecoin modules (if Step 1 clean). Leave `_DEFAULT_BYBIT_SLEEVES` untouched.

- [ ] **Step 5: Run the guard + full suite slice** —
  `.venv/bin/python -m pytest tests/unit/test_retired_tracks_gone.py -q && .venv/bin/python -c "from fxhnt.adapters.orchestration.assets import defs; print('assets load')"` → PASS. Fix any now-dangling import.

- [ ] **Step 6: Commit** — `git commit -am "feat(0b): retire crypto_tstrend standalone track + stablecoin_rotation (not tradeable on Bybit); book sleeve unchanged"`.

---

## Task 5: Remove the Alpaca execution surface (keep Broker port, IBKR sole impl)

**Files:**
- Delete: `src/fxhnt/adapters/broker/alpaca.py`, `infra/k8s/jobs/fxhnt-alpaca-rebalancer.yaml`,
  `infra/k8s/secrets/alpaca-credentials.yaml`
- Modify: `src/fxhnt/config.py` (delete `AlpacaSettings` + the `alpaca` field), `src/fxhnt/cli.py` (remove
  `--broker` option + all `alpaca` branches from `execute-multistrat` (cli.py:257–325) and the peer command
  at cli.py:206–223 — IBKR implicit)
- Test: `tests/unit/test_no_alpaca.py`

**Interfaces:** the Broker port stays; `IBKRBroker` becomes the only construction path.

- [ ] **Step 1: Grep the Alpaca surface** — `grep -rn "alpaca\|Alpaca\|AlpacaBroker\|AlpacaSettings" src/fxhnt/ infra/ | grep -v test`. Enumerate every site; each must be deleted or repointed to IBKR.

- [ ] **Step 2: Guard test**:
```python
# tests/unit/test_no_alpaca.py
import subprocess, pathlib
def test_no_alpaca_symbols_in_src():
    root = pathlib.Path(__file__).parents[1] / "src" / "fxhnt"
    hits = subprocess.run(["grep", "-rin", "alpaca", str(root)], capture_output=True, text=True).stdout
    assert not hits.strip(), f"residual Alpaca references:\n{hits}"
def test_broker_port_ibkr_only():
    from fxhnt.adapters.broker.ibkr import IBKRBroker   # the sole impl still imports
    assert IBKRBroker is not None
```
*(Adjust the IBKRBroker import to its real path — grep `class IBKRBroker`.)*

- [ ] **Step 3: Run → FAIL** (alpaca refs present).

- [ ] **Step 4: Delete + rewire** — remove `alpaca.py`, `AlpacaSettings` + `alpaca` field, the `--broker`
  option and its `alpaca` branches (execute-multistrat + the peer command become IBKR-only; keep the Broker
  port abstraction and `IBKRBroker`). Delete the two manifests. `kubectl delete cronjob/fxhnt-alpaca-rebalancer
  secret/alpaca-credentials -n foxhunt` + the superseded `multistrat-rebalancer` CronJob (locate its manifest
  via `grep -rn multistrat-rebalancer infra/`; delete file + in-cluster object) — record these as MANUAL
  cluster ops in the commit body (not run by CI).

- [ ] **Step 5: Run guard + cli load** —
  `.venv/bin/python -m pytest tests/unit/test_no_alpaca.py -q && .venv/bin/python -c "from fxhnt.cli import app; print('cli OK')"` → PASS.

- [ ] **Step 6: Commit** — `git commit -am "feat(0b): remove dead Alpaca execution; IBKR is the sole equity executor (Broker port kept)"`.

---

## Task 6: Remove the Binance execution surface

**Files:**
- Delete: `src/fxhnt/adapters/exchange/binance_ccxt.py`
- Modify: `src/fxhnt/cli.py` (delete `crypto-rebalance` (4746), `crypto-positions` (4768), `crypto-flatten`
  (4776) commands), `src/fxhnt/application/crypto_execution.py` (remove Binance coupling — delete the module
  if Binance was its only backend; grep first), `src/fxhnt/config.py` (remove execution-only `BinanceSettings`
  fields)
- Test: `tests/unit/test_no_binance_execution.py`

- [ ] **Step 1: Grep** — `grep -rn "binance_ccxt\|BinanceCcxt\|crypto-rebalance\|crypto-positions\|crypto-flatten\|crypto_execution" src/fxhnt/ | grep -v test`. Determine whether `crypto_execution.py` has any non-Binance use (if not → delete the module).

- [ ] **Step 2: Guard test** asserting the three CLI commands are gone (`crypto-rebalance` / `-positions` /
  `-flatten` not in `app`'s registered commands) and `binance_ccxt` unimportable. Run → FAIL.

- [ ] **Step 3: Delete** `binance_ccxt.py`, the three CLI commands, the Binance coupling in
  `crypto_execution.py` (or the whole module), and the execution-only `BinanceSettings` fields. Keep any
  Binance *data* settings for now (Task 6 removes them once the ingests are gone).

- [ ] **Step 4: Run guard + cli load** → PASS. **Step 5: Commit** —
  `git commit -am "feat(0b): remove dead Binance execution surface (binance_ccxt + crypto-rebalance/positions/flatten CLI)"`.

---

## Task 7: Remove Binance DATA + retire the `features` table (depends on Tasks 1,2,4)

**Files:**
- Delete: `src/fxhnt/adapters/exchange/binance_funding_history.py`,
  `src/fxhnt/adapters/data/binance_spot_history.py`, `src/fxhnt/adapters/data/binance_xsfunding_live.py`
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (delete `crypto_bars` (263), `crypto_funding` (322),
  `crypto_spot` (345) ingest assets + remove from the Definitions list), `src/fxhnt/application/bybit_edges_eval.py`
  (remove/repoint its Binance imports), `src/fxhnt/config.py` (remove the now-unused Binance data settings +
  `warehouse_table="features"` default if `features` is dropped — confirm bybit paths hardcode `bybit_features`)
- Test: `tests/unit/test_no_binance_data.py`

**Interfaces:** none produced. This is the "Binance fully gone" guard. Depends on the crypto migration (Tasks 1,2) + retirement (Task 4).

- [ ] **Step 1: Prove nothing live reads `features` / Binance** — `grep -rn "BinanceFundingHistory\|BinanceSpotHistory\|BinanceXsFundingLive\|binance_funding_history\|binance_spot_history\|binance_xsfunding_live\|crypto_bars\|crypto_funding\|crypto_spot" src/fxhnt/ | grep -v test`. After Tasks 1–3, the only hits should be the definitions being deleted here + `bybit_edges_eval.py` (repoint/remove). Any live *reader* of the `features` table (`get_feature_store` with the default table on a live path) is a BLOCKER — report it. (The bybit book/tracks all pass `table="bybit_features"` explicitly.)

- [ ] **Step 2: Guard test**:
```python
# tests/unit/test_no_binance_data.py
import subprocess, pathlib
def test_no_binance_symbols_in_src():
    root = pathlib.Path(__file__).parents[1] / "src" / "fxhnt"
    hits = subprocess.run(["grep", "-rin", "binance", str(root)], capture_output=True, text=True).stdout
    assert not hits.strip(), f"residual Binance references:\n{hits}"
```
- [ ] **Step 3: Run → FAIL.** **Step 4:** Delete the three adapters + three ingest assets (+ Definitions
  entries) + `bybit_edges_eval.py` Binance imports + the Binance data settings. If the `features` table is
  now unreferenced, drop its default (`warehouse_table` becomes `bybit_features`, or the setting is removed if
  every consumer hardcodes the table) — confirm no path relied on the `features` default. Leave a one-time
  `DROP TABLE features` as a documented MANUAL cluster op in the commit body (not auto-run).

- [ ] **Step 5: Run guard + full suite** —
  `.venv/bin/python -m pytest tests/unit/test_no_binance_data.py -q && .venv/bin/python -m pytest tests/ -q` → PASS (full suite green — this is the venue-cutover checkpoint).

- [ ] **Step 6: Commit** — `git commit -am "feat(0b): remove Binance data (adapters + ingests) + retire the features table — crypto is 100% Bybit"`.

---

## Task 8: Remove the vestigial registry `state_file` field

**Files:**
- Modify: `src/fxhnt/registry.py` (delete `state_file` from all entries + the header comment lines 3,5)
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (`_run_paper_tracker` (401): drop the `state_file`
  param + all call sites)
- Investigate: `src/fxhnt/cli.py:470` (forward-track CLI `(state_file, build_strategy)` map),
  `src/fxhnt/adapters/orchestration/migration_builders.py` (one-time migration seed)
- Test: `tests/unit/test_no_registry_state_file.py`

- [ ] **Step 1: Determine migration completeness + CLI dependency.** `grep -rn "state_file" src/fxhnt/ | grep -v test`. Two non-registry consumers: (a) `migration_builders.py` (the ONE-TIME deterministic-forward-state seed — if every track is already on a DB anchor in prod, this is fully vestigial → delete the module); (b) `cli.py:470` forward-track CLI. Read cli.py:470 — if the CLI recomputes from the DB anchor (0a SSOT) and only needs a `ForwardTracker` PATH it constructs itself, the registry `state_file` is not needed there; if it reads `entry["state_file"]`, move that basename into the CLI locally (or derive `f"{sid}_state"`) so removing the registry field doesn't break it. Decide per finding; record it in the commit body.

- [ ] **Step 2: Guard test**:
```python
# tests/unit/test_no_registry_state_file.py
from fxhnt.registry import STRATEGY_REGISTRY
def test_no_state_file_field():
    assert all("state_file" not in e for e in STRATEGY_REGISTRY.values())
```
- [ ] **Step 3: Run → FAIL.** **Step 4:** Remove the `state_file` field from every entry + the header
  comment; drop the `state_file` param from `_run_paper_tracker` and all call sites; apply the Step-1
  decision for the CLI + `migration_builders.py` (delete the module if the migration is complete, else keep
  it self-contained). The JSON `ForwardTracker` + its `.bak` crash-recovery stay (used by `fxhnt forward-track`).

- [ ] **Step 5: Run guard + forward suites** —
  `.venv/bin/python -m pytest tests/unit/test_no_registry_state_file.py tests/integration/test_forward_gate_e2e.py -q && .venv/bin/python -c "from fxhnt.cli import app; print('cli OK')"` → PASS.

- [ ] **Step 6: Commit** — `git commit -am "feat(0b): remove vestigial registry state_file field (DB anchor is the forward SSOT)"`.

---

## Task 9: Cockpit-local verify + whole-branch review

**Files:** none (verification task).

- [ ] **Step 1: Local cockpit verify (real browser).** Seed a sqlite cockpit DB (or use
  `./scripts/dev-cockpit-local.sh` against prod, read-only) and confirm in a browser: `xsfunding` and
  `unlock` strategy pages render as **Bybit** tracks (venue `bybit-perp`) with their reconciliation gates;
  `crypto_tstrend` and `stablecoin_rotation` detail pages are GONE (404, not a 500); the `bybit_4edge`
  deploy book + `/paper` bybit view are unchanged. Read the actual rendered values — do not claim off logs.

- [ ] **Step 2: Full suite green** — `.venv/bin/python -m pytest tests/ -q` → all pass. Confirm the 0a
  reconciliation invariant + the four new guard tests (retired-tracks, no-alpaca, no-binance-data,
  no-state-file) are included and green.

- [ ] **Step 3: Whole-branch review** — dispatch the final code-reviewer over
  `git merge-base feat/unified-db-backtest-architecture HEAD`..HEAD with the spec's Global Constraints as the
  lens (crypto 100% Bybit; no Binance/Alpaca; Broker port kept; book unchanged; deterministic re-inception;
  invariant green). Fix Critical/Important in one fix wave; record Minors and fix them in-loop.

- [ ] **Step 4: Finish** — use superpowers:finishing-a-development-branch (verify tests → present merge/PR
  options). Note the post-deploy runbook in the PR body: the `migrate-forward-anchors` step for the
  re-inceptioned xsfunding/unlock tracks + the two MANUAL cluster ops (drop `features` table; delete the
  Alpaca/multistrat CronJobs + `alpaca-credentials` secret).

---

## Self-Review notes
- **Spec coverage:** A→xsfunding (T1) + unlock (T2); B→retire (T3); C→Alpaca (T4); D→Binance-exec (T5);
  E→Binance-data (T6); F→state_file (T7); §7 0a-interaction folded into T1/T2 (ref store flips to
  bybit_features; xsfunding leaves the inline sleeve path); verification+cockpit (T8). All spec sections map.
- **Ordering:** migrate (T1–T2) + retire (T3) BEFORE Binance-data removal (T6) — enforced by T6's Step-1
  block. Exec removals (T4–T5) are independent and can run any time after T1–T3.
- **Type consistency:** `build_bybit_sleeve_forward_nav(repo, store, sleeve, *, …) -> dict` used by T1/T2 +
  positioning wrapper. `_BYBIT_BOOK_SIDS` extended in T1/T2. `bybit_report_sid_kwargs(sid)` extended in T1/T2.
