# Bybit Execution Leg (C2) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make the existing Bybit-testnet leg B2a/B3-driven (deploy the `bybit_4edge` envelope, flatten on $0/de-funded) and record an envelope-basis, capital-flow-excluded, funding/fee/slippage-net `bybit_4edge_exec` observed track via A's `run_track`, reconciling executed-vs-modeled against `bybit_4edge`.

**Architecture:** Pure helpers carry the tested logic (funding query, exec-return math, observed-track recording); the two Dagster assets (`bybit_testnet_reconcile`, `bybit_testnet_record`) become thin callers. The reconcile sizes the book to `min(strategy_allocation[bybit_4edge], testnet_equity)` and measures realized funding (via a new adapter method) + fees (from fills); the record computes the envelope-basis exec return and books the observed track. Reuses `plan_orders`, `slippage.py`, `BybitTestnetRepo`, `run_track`, and C1's exec-record shape.

**Tech Stack:** Python 3.13, ccxt (Bybit), frozen dataclasses, SQLAlchemy 2.0 (SQLite tests / Postgres+Timescale prod), Dagster assets, pytest, ruff.

## Global Constraints

- Envelope = `min(ForwardNavRepo.current_allocation().get("bybit_4edge", 0.0), testnet_equity)`; `current_allocation()` is ALREADY B3-funding-filtered (a de-funded/killswitched `bybit_4edge` is absent → 0.0 → the book FLATTENS to cash).
- Size the book to the envelope by passing `equity=envelope` to `plan_orders` (targets = `weight·envelope`), NOT `adapter.equity()`.
- The executed return is **envelope-basis** (÷ `envelope_prior`, never full equity), **capital-flow-excluded** (a B2a re-allocation resets the basis, never enters the return), and **funding/fee/slippage-net**: `exec_return = (price_pnl + funding − fees) / envelope_prior` where `price_pnl` marks the PRIOR held positions (prior marks → today marks), `funding` is realized funding on the held book over the period, `fees` are the trading fees paid this run. First run → no prior book → record nothing (inception).
- Funding is measured explicitly (Approach X, spec §3): a new `BybitExecution.funding_since(since_ms)` sums realized funding; `realized_pnl: 0.0` in the reconcile dict is REPLACED by measured `realized_funding` + `realized_fees`.
- NO capital moved to real money: testnet only; kill-switch, idempotent NAV marker, resting-order abort, per-fill slippage — all UNCHANGED. Real-money Bybit is C3.
- `bybit_4edge_exec` reconciliation reference = the modeled `bybit_4edge` via `CockpitBacktestRefProvider._REF_ALIAS`.
- Book-state for the exec track lives under `ForwardNavRepo` key `bybit_4edge_exec_pos`, SEPARATE from `run_track`'s own `bybit_4edge_exec` book-state (mirrors C1's `multistrat_exec_pos`).
- Marks source: `_testnet_marks` = `{symbol: latest_close}`. Positions are signed base qty (`adapter.positions()` convention).

---

### Task 1: `BybitExecution.funding_since` — realized-funding query

**Files:**
- Modify: `src/fxhnt/adapters/broker/bybit.py` (add method to `BybitExecution`)
- Test: `tests/unit/test_bybit_funding_since.py`

**Interfaces:**
- Produces: `BybitExecution.funding_since(since_ms: int) -> float` — net realized funding in USDT since `since_ms` epoch-ms (received > 0, paid < 0), summed across the account's funding events.

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

Create `tests/unit/test_bybit_funding_since.py`:

```python
from fxhnt.adapters.broker.bybit import BybitExecution


class _FakeCcxt:
    def __init__(self, rows):
        self._rows = rows
        self.calls = []

    def set_sandbox_mode(self, _):  # constructor calls this
        pass

    def fetch_funding_history(self, symbol=None, since=None, limit=None):
        self.calls.append({"symbol": symbol, "since": since, "limit": limit})
        return self._rows


def _exec(rows):
    ex = BybitExecution.__new__(BybitExecution)      # bypass __init__ (no network/keys)
    ex._x = _FakeCcxt(rows)                           # inject the fake ccxt client
    return ex


def test_funding_since_sums_signed_amounts():
    # ccxt funding rows: {"amount": ...} (received > 0, paid < 0)
    ex = _exec([{"amount": 1.5}, {"amount": -0.4}, {"amount": 0.9}])
    assert abs(ex.funding_since(1_700_000_000_000) - 2.0) < 1e-9
    assert ex._x.calls[0]["since"] == 1_700_000_000_000


def test_funding_since_empty_is_zero():
    assert _exec([]).funding_since(0) == 0.0
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/unit/test_bybit_funding_since.py -q`
Expected: FAIL — `AttributeError: 'BybitExecution' object has no attribute 'funding_since'`.

- [ ] **Step 3: Implement the method**

In `src/fxhnt/adapters/broker/bybit.py`, add to `BybitExecution` (after `positions`):

```python
    def funding_since(self, since_ms: int) -> float:
        """Net realized funding in USDT since `since_ms` (epoch-ms): sum of the account's funding payments
        (received > 0, paid < 0). Bybit perps settle funding periodically; this is the funding component of
        the executed book's return (several 4-edge sleeves harvest it). Paginated via ccxt's `since`."""
        rows = self._call(self._x.fetch_funding_history, None, since_ms, None)
        return float(sum(float(r.get("amount") or 0.0) for r in (rows or [])))
```

(`_call` is the existing retry wrapper; `fetch_funding_history(symbol, since, limit)` is ccxt's unified signature — `symbol=None` returns all symbols on Bybit.)

- [ ] **Step 4: Run to verify pass**

Run: `python -m pytest tests/unit/test_bybit_funding_since.py -q`
Expected: PASS (2 passed).

- [ ] **Step 5: ruff + commit**

Run: `ruff check src/fxhnt/adapters/broker/bybit.py tests/unit/test_bybit_funding_since.py`
```bash
git add src/fxhnt/adapters/broker/bybit.py tests/unit/test_bybit_funding_since.py
git commit -m "feat(bybit): funding_since — realized funding query for the executed return (C2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 2: Pure crypto exec-return helpers (`bybit_exec_record.py`)

**Files:**
- Create: `src/fxhnt/application/bybit_exec_record.py`
- Test: `tests/unit/test_bybit_exec_record.py`

**Interfaces:**
- Produces:
  - `crypto_envelope(repo, equity) -> float` = `min(repo.current_allocation().get("bybit_4edge", 0.0), equity)`.
  - `compute_crypto_exec_return(pos_prior: dict, today_marks: dict[str,float], funding: float, fees: float) -> float | None`.
  - `build_crypto_pos_state(positions, marks, envelope, funding_cursor_ts) -> dict`.
  - `apply_fills(current: dict[str,float], fills: list[dict]) -> dict[str,float]` — the post-rebalance book (signed qty updated by each successful fill).

- [ ] **Step 1: Write the failing tests (load-bearing invariants)**

Create `tests/unit/test_bybit_exec_record.py`:

```python
from fxhnt.application.bybit_exec_record import (
    build_crypto_pos_state, compute_crypto_exec_return, crypto_envelope,
)


def _state(positions, marks, envelope, cursor=0):
    return build_crypto_pos_state(positions, marks, envelope, cursor)


def test_inception_and_flat_return_none():
    assert compute_crypto_exec_return({}, {"BTCUSDT": 100.0}, 0.0, 0.0) is None          # no prior book
    flat = _state({"BTCUSDT": 1.0}, {"BTCUSDT": 100.0}, 0.0)                              # envelope 0
    assert compute_crypto_exec_return(flat, {"BTCUSDT": 100.0}, 0.0, 0.0) is None


def test_envelope_basis_undiluted():
    # $10k envelope, book long 100 units entered at 100 -> mark 101 = +$100 price pnl. Return = 100/10000 = 1%,
    # regardless of any (irrelevant) larger testnet balance — the divisor is the ENVELOPE.
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 101.0}, 0.0, 0.0)
    assert abs(r - 0.01) < 1e-9


def test_funding_is_return_not_flow():
    # flat marks, +$50 funding on a $10k envelope -> +0.5% (funding is edge return).
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 100.0}, 50.0, 0.0)
    assert abs(r - 0.005) < 1e-9


def test_fees_reduce_return():
    # flat marks, $30 fees on a $10k envelope -> -0.3%.
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 100.0}, 0.0, 30.0)
    assert abs(r - (-0.003)) < 1e-9


def test_missing_today_mark_falls_back_to_prior():
    prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
    assert compute_crypto_exec_return(prior, {}, 0.0, 0.0) == 0.0                          # no price move


class _Repo:
    def __init__(self, alloc):
        self._alloc = alloc

    def current_allocation(self):
        return self._alloc


def test_crypto_envelope_caps_at_equity_and_zero_when_absent():
    assert crypto_envelope(_Repo({"bybit_4edge": 8_000.0}), 5_000.0) == 5_000.0            # capped at equity
    assert crypto_envelope(_Repo({"bybit_4edge": 8_000.0}), 20_000.0) == 8_000.0           # alloc binds
    assert crypto_envelope(_Repo({}), 20_000.0) == 0.0                                      # de-funded/absent -> flatten


def test_apply_fills_builds_post_rebalance_book():
    from fxhnt.application.bybit_exec_record import apply_fills
    current = {"BTCUSDT": 1.0, "ETHUSDT": -2.0}
    fills = [{"symbol": "BTCUSDT", "side": "BUY", "qty": 0.5},      # 1.0 + 0.5 = 1.5
             {"symbol": "ETHUSDT", "side": "BUY", "qty": 2.0},      # -2.0 + 2.0 = 0.0 -> dropped
             {"symbol": "SOLUSDT", "side": "SELL", "qty": 3.0},     # new short -3.0
             {"symbol": "BTCUSDT", "error": True, "qty": 9.0}]      # error row ignored
    assert apply_fills(current, fills) == {"BTCUSDT": 1.5, "SOLUSDT": -3.0}
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/unit/test_bybit_exec_record.py -q`
Expected: FAIL — `ModuleNotFoundError: No module named 'fxhnt.application.bybit_exec_record'`.

- [ ] **Step 3: Implement the helpers**

Create `src/fxhnt/application/bybit_exec_record.py`:

```python
"""Sub-project C2 — record the EXECUTED Bybit crypto track and bridge B2a/B3's dollar allocation to the venue.

`crypto_envelope` = the B2a (B3-funding-filtered) dollars for `bybit_4edge`, capped at the testnet equity
(absent/de-funded -> 0 -> flatten). `compute_crypto_exec_return` measures the executed book's daily return on
the ENVELOPE (not the full testnet equity), as the P&L of the HELD positions since the last rebalance PLUS
realized funding MINUS fees (funding is edge return; a re-allocation is a capital flow, not a gain). Mirrors
C1's multistrat_exec_record, extended for crypto funding/fees. No new execution engine, no new return store."""
from __future__ import annotations

import datetime as dt
from typing import Any


def crypto_envelope(repo: Any, equity: float) -> float:
    return min(float(repo.current_allocation().get("bybit_4edge", 0.0)), float(equity))


def build_crypto_pos_state(positions: dict[str, float], marks: dict[str, float], envelope: float,
                           funding_cursor_ts: int) -> dict[str, Any]:
    return {"positions": {s: float(q) for s, q in positions.items()},
            "marks": {s: float(m) for s, m in marks.items()},
            "envelope": float(envelope), "funding_cursor_ts": int(funding_cursor_ts)}


def compute_crypto_exec_return(pos_prior: dict[str, Any], today_marks: dict[str, float],
                               funding: float, fees: float) -> float | None:
    """Envelope-basis, flow-excluded, funding/fee-net executed return:
        (price_pnl + funding - fees) / envelope_prior
    where price_pnl marks the PRIOR held positions (prior marks -> today marks). None on inception / flat."""
    if not pos_prior:
        return None                                              # first run / no prior book -> inception
    envelope = float(pos_prior.get("envelope", 0.0))
    if envelope <= 0.0:
        return None                                              # flat (de-funded/killswitched) -> no return
    positions: dict[str, float] = pos_prior.get("positions", {})
    prior_marks: dict[str, float] = pos_prior.get("marks", {})
    val_prior = sum(q * prior_marks.get(s, 0.0) for s, q in positions.items())
    val_today = sum(q * today_marks.get(s, prior_marks.get(s, 0.0)) for s, q in positions.items())
    price_pnl = val_today - val_prior
    return (price_pnl + float(funding) - float(fees)) / envelope


def apply_fills(current: dict[str, float], fills: list[dict[str, Any]]) -> dict[str, float]:
    """The post-rebalance book = `current` signed base qty updated by each SUCCESSFUL fill's signed filled qty
    (BUY > 0, SELL < 0). Error rows are ignored; near-zero residual positions are dropped. This is the real
    placed book (reflects snapping/partial fills) — the next run's return basis."""
    out = dict(current)
    for f in fills:
        if f.get("error"):
            continue
        signed = float(f["qty"]) * (1.0 if f["side"] == "BUY" else -1.0)
        out[f["symbol"]] = out.get(f["symbol"], 0.0) + signed
    return {s: q for s, q in out.items() if abs(q) > 1e-12}
```

- [ ] **Step 4: Run to verify pass**

Run: `python -m pytest tests/unit/test_bybit_exec_record.py -q`
Expected: PASS (7 passed).

- [ ] **Step 5: ruff + commit**

Run: `ruff check src/fxhnt/application/bybit_exec_record.py tests/unit/test_bybit_exec_record.py`
```bash
git add src/fxhnt/application/bybit_exec_record.py tests/unit/test_bybit_exec_record.py
git commit -m "feat(bybit): pure envelope-basis funding-net crypto exec-return helpers (C2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 4: `record_bybit_exec_track` — book the observed track via A's run_track

> **Depends on Task 3** (the `bybit_4edge_exec` registry entry) — `run_track` reads `track_definition(sid)` from `STRATEGY_REGISTRY`, so the registry entry MUST exist before this task's test runs.

**Files:**
- Modify: `src/fxhnt/application/bybit_exec_record.py` (add the booking function + strategy shim)
- Test: `tests/integration/test_record_bybit_exec_track.py`

**Interfaces:**
- Consumes: `ForwardNavRepo` (`run_track` target + `set_book_state`), `build_crypto_pos_state` (Task 2).
- Produces: `record_bybit_exec_track(repo, exec_return, new_positions, today_marks, envelope, funding_cursor_ts, today, at) -> None` — books `exec_return` as an observed `forward_record` for `bybit_4edge_exec` and carries the next basis under `bybit_4edge_exec_pos`.

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

Create `tests/integration/test_record_bybit_exec_track.py`:

```python
import datetime as dt

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.bybit_exec_record import record_bybit_exec_track


def test_records_observed_return_and_carries_basis(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}")
    repo.migrate()
    at = dt.datetime(2026, 7, 10)
    # inception: exec_return None -> a forward_record is booked with no return row, basis is carried.
    record_bybit_exec_track(repo, None, {"BTCUSDT": 1.0}, {"BTCUSDT": 100.0}, 10_000.0, 1_700_000_000_000,
                            "2026-07-10", at)
    st = repo.get_book_state("bybit_4edge_exec_pos")
    assert st["envelope"] == 10_000.0
    assert st["positions"] == {"BTCUSDT": 1.0}
    assert st["funding_cursor_ts"] == 1_700_000_000_000
    # second run books a real return.
    record_bybit_exec_track(repo, 0.01, {"BTCUSDT": 2.0}, {"BTCUSDT": 101.0}, 10_000.0, 1_700_100_000_000,
                            "2026-07-11", dt.datetime(2026, 7, 11))
    hist = repo.nav_history("bybit_4edge_exec")
    assert any(abs(r.ret - 0.01) < 1e-9 for r in hist)
    assert repo.get_book_state("bybit_4edge_exec_pos")["positions"] == {"BTCUSDT": 2.0}
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/integration/test_record_bybit_exec_track.py -q`
Expected: FAIL — `ImportError: cannot import name 'record_bybit_exec_track'`.

- [ ] **Step 3: Implement the booking function**

Append to `src/fxhnt/application/bybit_exec_record.py`:

```python
class _CryptoExecBookingStrategy:
    """Trivial observed ForwardStrategy for run_track: books today's pre-computed executed return (or nothing
    on inception). The return is computed by the record asset (it has the marks/funding/fees)."""
    def __init__(self, exec_return: float | None) -> None:
        self._r = exec_return

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        if self._r is None:
            return [], extra
        return [(str(extra.get("_today")), float(self._r))], extra


def record_bybit_exec_track(repo: Any, exec_return: float | None, new_positions: dict[str, float],
                            today_marks: dict[str, float], envelope: float, funding_cursor_ts: int,
                            today: str, at: dt.datetime) -> None:
    """Book `exec_return` as an observed forward_record for `bybit_4edge_exec` via A's run_track (anchor +
    record + nav/summary), then carry the next run's basis (positions/marks/envelope/funding cursor) under the
    book-state key `bybit_4edge_exec_pos` (SEPARATE from run_track's own book_state)."""
    from fxhnt.application.forward_engine import run_track
    run_track(repo, "bybit_4edge_exec", _CryptoExecBookingStrategy(exec_return), today=today, at=at)
    repo.set_book_state("bybit_4edge_exec_pos",
                        build_crypto_pos_state(new_positions, today_marks, envelope, funding_cursor_ts), at=at)
```

- [ ] **Step 4: Run to verify pass**

Run: `python -m pytest tests/integration/test_record_bybit_exec_track.py -q`
Expected: PASS (1 passed). (`run_track` reads the `bybit_4edge_exec` registry definition — guaranteed present because Task 3 ran first.)

- [ ] **Step 5: ruff + commit**

Run: `ruff check src/fxhnt/application/bybit_exec_record.py tests/integration/test_record_bybit_exec_track.py`
```bash
git add src/fxhnt/application/bybit_exec_record.py tests/integration/test_record_bybit_exec_track.py
git commit -m "feat(bybit): record_bybit_exec_track — book the executed crypto track via A run_track (C2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 3: Register `bybit_4edge_exec` observed track + reconciliation ref alias

**Files:**
- Modify: `src/fxhnt/registry.py` (add the `bybit_4edge_exec` entry, after `bybit_4edge`)
- Modify: `src/fxhnt/application/forward_ingest.py` (`_REF_ALIAS`)
- Test: `tests/unit/test_registry_bybit_4edge_exec.py`

**Interfaces:**
- Produces: `STRATEGY_REGISTRY["bybit_4edge_exec"]` (observed, venue `bybit-testnet`, tier research, gate_type reconciliation); `CockpitBacktestRefProvider._REF_ALIAS["bybit_4edge_exec"] == "bybit_4edge"`.

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

Create `tests/unit/test_registry_bybit_4edge_exec.py`:

```python
from fxhnt.application.forward_ingest import CockpitBacktestRefProvider
from fxhnt.registry import STRATEGY_REGISTRY


def test_bybit_4edge_exec_is_observed_reconciliation_track():
    m = STRATEGY_REGISTRY["bybit_4edge_exec"]
    assert m["definition"]["record_mode"] == "observed"
    assert m["venue"] == "bybit-testnet"
    assert m["tier"] == "research"
    assert m["gate_spec"]["gate_type"] == "reconciliation"


def test_exec_reconciliation_ref_aliases_to_modeled_bybit_4edge():
    assert CockpitBacktestRefProvider._REF_ALIAS["bybit_4edge_exec"] == "bybit_4edge"
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/unit/test_registry_bybit_4edge_exec.py -q`
Expected: FAIL — `KeyError: 'bybit_4edge_exec'`.

- [ ] **Step 3: Add the registry entry**

In `src/fxhnt/registry.py`, immediately AFTER the `"bybit_4edge"` entry, add:

```python
    # The EXECUTED 4-edge book (sub-project C2): the real Bybit-TESTNET track — its daily return is the
    # executed book's envelope-basis P&L (fills + slippage + funding - fees), recorded by the
    # bybit_testnet_reconcile/record assets. OBSERVED (fills are observed, not recomputable). Its gate
    # reconciles the executed track against the MODELED bybit_4edge via the ref-provider alias (execution
    # decay shows as divergence). min_forward_days 21 mirrors the modeled book's window; crypto is 24/7 so
    # the default max_gap_days (1) applies.
    "bybit_4edge_exec": {
        "display_name": "Bybit 4-edge book (EXECUTED, testnet)", "sleeve": "crypto-bybit",
        "venue": "bybit-testnet", "state_file": "bybit_4edge_exec_state",
        "tier": "research", "execution": "paper",
        "gate_spec": {"gate_type": "reconciliation", "min_forward_days": 21},
        "definition": {"params": {"source": "bybit-testnet-fills", "models": "bybit_4edge"},
                       "version": 1, "record_mode": "observed"},
    },
```

- [ ] **Step 4: Add the ref alias**

In `src/fxhnt/application/forward_ingest.py`, change:
```python
    _REF_ALIAS = {"multistrat_exec": "multistrat"}
```
to:
```python
    _REF_ALIAS = {"multistrat_exec": "multistrat", "bybit_4edge_exec": "bybit_4edge"}
```

- [ ] **Step 5: Run to verify pass + ruff + commit**

Run: `python -m pytest tests/unit/test_registry_bybit_4edge_exec.py tests/unit/test_registry_multistrat_exec.py -q`
Expected: PASS.
Run: `ruff check src/fxhnt/registry.py src/fxhnt/application/forward_ingest.py tests/unit/test_registry_bybit_4edge_exec.py`
```bash
git add src/fxhnt/registry.py src/fxhnt/application/forward_ingest.py tests/unit/test_registry_bybit_4edge_exec.py
git commit -m "feat(bybit): register bybit_4edge_exec observed track + reconciliation ref alias (C2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 5: Wire the assets — envelope bridge (reconcile) + exec-track recording (record)

**Files:**
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (`bybit_testnet_reconcile`, `bybit_testnet_record`)
- Test: `tests/integration/test_bybit_testnet_b2a_wiring.py`

**Interfaces:**
- Consumes: `crypto_envelope`, `compute_crypto_exec_return`, `record_bybit_exec_track` (Tasks 2-3), `BybitExecution.funding_since` (Task 1), `ForwardNavRepo` (`current_allocation`, `get_book_state`).

This task extracts the pure decision into a helper so it is testable without a live Dagster context, then makes the assets thin callers.

- [ ] **Step 1: Write the failing test (the pure wiring helper)**

Create `tests/integration/test_bybit_testnet_b2a_wiring.py`:

```python
import datetime as dt

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.bybit_exec_record import (
    compute_crypto_exec_return, crypto_envelope, record_bybit_exec_track,
)


class _Repo:
    def __init__(self, alloc):
        self._alloc = alloc

    def current_allocation(self):
        return self._alloc


def test_envelope_flattens_when_bybit_4edge_defunded():
    # de-funded/absent bybit_4edge -> envelope 0 -> the book flattens (plan_orders would size everything to 0).
    assert crypto_envelope(_Repo({}), 50_000.0) == 0.0
    assert crypto_envelope(_Repo({"bybit_4edge": 0.0}), 50_000.0) == 0.0


def test_end_to_end_envelope_basis_exec_return_recorded(tmp_path):
    # Simulate the reconcile->record data flow with a real repo (no Dagster/broker): inception, then a day
    # with +$100 price pnl + $50 funding - $20 fees on a $10k envelope behind a large (irrelevant) equity.
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}")
    repo.migrate()
    at0 = dt.datetime(2026, 7, 10)
    # inception run: no prior book -> None return, carry basis (100 units @ 100, envelope 10k).
    prior = repo.get_book_state("bybit_4edge_exec_pos")
    assert compute_crypto_exec_return(prior, {"BTCUSDT": 100.0}, 0.0, 0.0) is None
    record_bybit_exec_track(repo, None, {"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0,
                            1_700_000_000_000, "2026-07-10", at0)
    # next run: marks 100->101 (+$100), funding +$50, fees $20 -> (100+50-20)/10000 = +1.30%.
    prior = repo.get_book_state("bybit_4edge_exec_pos")
    r = compute_crypto_exec_return(prior, {"BTCUSDT": 101.0}, 50.0, 20.0)
    assert abs(r - 0.013) < 1e-9
    record_bybit_exec_track(repo, r, {"BTCUSDT": 100.0}, {"BTCUSDT": 101.0}, 10_000.0,
                            1_700_100_000_000, "2026-07-11", dt.datetime(2026, 7, 11))
    assert any(abs(row.ret - 0.013) < 1e-9 for row in repo.nav_history("bybit_4edge_exec"))
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/integration/test_bybit_testnet_b2a_wiring.py -q`
Expected: FAIL until Tasks 2-3 are present (they are, if done in order) — this test uses only their helpers, so it should PASS once they exist. If it PASSES immediately, that is expected (it pins the data-flow contract the assets must follow); proceed to wire the assets in Steps 3-4 and keep this test green.

- [ ] **Step 3: Wire `bybit_testnet_reconcile` (envelope + funding + fees + positions_after)**

In `src/fxhnt/adapters/orchestration/assets.py` `bybit_testnet_reconcile`, make these changes (keep the kill-switch, idempotency, abort, and per-fill slippage untouched):

(a) Add imports at the top of the function's import block:
```python
    from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
    from fxhnt.application.bybit_exec_record import apply_fills, crypto_envelope
```

(b) After `equity = adapter.equity()` (the non-kill-switch path, ~line 1211) and BEFORE `plan_orders`, compute the envelope and the prior funding cursor:
```python
    fwd_repo = ForwardNavRepo(s.operational_dsn)
    fwd_repo.migrate()
    envelope = crypto_envelope(fwd_repo, equity)
    prior_state = fwd_repo.get_book_state("bybit_4edge_exec_pos") or {}
    funding_cursor_prior = int(prior_state.get("funding_cursor_ts", 0))
    realized_funding = adapter.funding_since(funding_cursor_prior) if funding_cursor_prior else 0.0
```
Change BOTH `plan_orders(weights, current, equity, ...)` calls (the kill-switch branch AND the live branch) to size to the envelope:
```python
    intents = plan_orders(weights, current, envelope, marks,   # size to the B2a/B3 envelope, not full equity
                          limits, min_order_usd=s.bybit_exec.min_order_usd, max_gross=s.bybit_exec.max_gross)
```

(c) In the fill loop, accumulate fees; after the loop compute the post-rebalance book from the fills, and put the new fields into the return dict:
```python
    realized_fees = sum(float(f.get("fee") or 0.0) for f in fills if not f.get("error"))
    positions_after = apply_fills(current, fills)   # the real placed book (tested helper), next-run basis
```
Replace the return dict's `"realized_pnl": 0.0,` and add the new fields:
```python
    return {
        "run_date": run_date, "placed": placed, "killed": False, "noop": False,
        "equity": equity, "envelope": envelope,
        "realized_funding": realized_funding, "realized_fees": realized_fees,
        "realized_pnl": realized_funding - realized_fees,
        "gross": gross, "mean_exec_slippage_bps": mean_exec, "mean_timing_drift_bps": mean_drift,
        "as_of_day": as_of_day, "fills": fills,
        "positions_after": positions_after, "marks": marks,
        "funding_cursor_prior": funding_cursor_prior,
    }
```
(The kill-switch / no-op / aborted early returns are UNCHANGED — they carry no envelope/exec fields, so the record step skips recording, exactly as it already skips fills/NAV for those.)

- [ ] **Step 4: Wire `bybit_testnet_record` (compute + book the exec track)**

In `bybit_testnet_record`, AFTER the existing `repo.upsert_testnet_nav(...)` call (keep it — `bybit_testnet_nav` stays), add:
```python
    import datetime as _dt2

    from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
    from fxhnt.application.bybit_exec_record import compute_crypto_exec_return, record_bybit_exec_track

    fwd_repo = ForwardNavRepo(s.operational_dsn)
    fwd_repo.migrate()
    pos_prior = fwd_repo.get_book_state("bybit_4edge_exec_pos")
    exec_return = compute_crypto_exec_return(
        pos_prior or {}, recon["marks"], recon["realized_funding"], recon["realized_fees"])
    now_ms = int(now.timestamp() * 1000)
    record_bybit_exec_track(
        fwd_repo, exec_return, recon["positions_after"], recon["marks"], recon["envelope"],
        now_ms, run_date, _dt2.datetime.now(_dt2.UTC).replace(tzinfo=None))
    context.log.info(f"bybit_testnet_record: bybit_4edge_exec return="
                     f"{'inception' if exec_return is None else f'{exec_return*100:.3f}%'} "
                     f"(envelope=${recon['envelope']:.0f}, funding=${recon['realized_funding']:.2f}, "
                     f"fees=${recon['realized_fees']:.2f})")
```
(`recon` already gained `marks`, `realized_funding`, `realized_fees`, `envelope`, `positions_after` in Step 3. The early `noop/killed/aborted` guard at the top of `record` returns BEFORE this block, so a killed/no-op run records no exec track — a killed run is a visible gap, matching the existing NAV behavior.)

- [ ] **Step 5: Run the wiring test + the C2 helper tests + ruff**

Run: `python -m pytest tests/integration/test_bybit_testnet_b2a_wiring.py tests/integration/test_record_bybit_exec_track.py tests/unit/test_bybit_exec_record.py tests/unit/test_bybit_funding_since.py tests/unit/test_registry_bybit_4edge_exec.py -q`
Expected: PASS.
Run: `ruff check src/fxhnt/adapters/orchestration/assets.py src/fxhnt/application/bybit_exec_record.py tests/integration/test_bybit_testnet_b2a_wiring.py`
Expected: clean. (`assets.py` may have PRE-EXISTING ruff violations unrelated to this diff — do NOT fix those; only your added lines must be clean.)

- [ ] **Step 6: Commit**

```bash
git add src/fxhnt/adapters/orchestration/assets.py tests/integration/test_bybit_testnet_b2a_wiring.py
git commit -m "feat(bybit): B2a/B3-envelope-driven testnet leg + record bybit_4edge_exec observed track (C2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Final verification (after all tasks)

- [ ] Full suite: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest -q -p no:cacheprovider`
  Expected: all green (prior baseline 1933 + the new C2 tests).
- [ ] `ruff check src/fxhnt/application/bybit_exec_record.py src/fxhnt/adapters/broker/bybit.py src/fxhnt/registry.py src/fxhnt/application/forward_ingest.py` — no new violations from C2 files.
