# Positioning Per-Coin Gross Cap 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:** Add an ex-ante per-coin gross-weight cap (0.07) to the `positioning` sleeve so no single coin dominates the book, then re-inception + backfill the forward anchor so the live gate keeps running against the capped construction.

**Architecture:** The cap is applied to the FORMING weights in `positioning_weights` (before returns), so it is strictly causal (no lookahead). It threads through the existing seams (`_book_breakdown` → `positioning_returns_from_store` → `_positioning_series` → `sleeve_returns_from_store`) as an optional `coin_gross_cap` param that defaults to `None` (OFF → byte-identical to today). It is switched ON at 0.07 only in `bybit_book_persist.py`. The registry `positioning` + `bybit_4edge` definition versions bump to re-inception; `inception_t0` pins the existing OOS start (2026-07-06) so the forward track backfills the window with the capped construction instead of resetting the gate to 0.

**Tech Stack:** Python 3.12, numpy-free pure math, pytest (`-n auto`), TimescaleFeatureStore (Postgres, table `bybit_features`).

## Global Constraints

- The cap is applied to WEIGHTS (in `positioning_weights`), NOT to returns/contributions — it must be ex-ante/causal (the cap decision cannot see the coin's realized return).
- `coin_gross_cap` default is `None` (OFF) at EVERY seam. With it OFF, every existing test and the whole backtest must be byte-identical to today. The OFF path must not add per-term division drift.
- After clipping `|w| ≤ coin_gross_cap`, RENORMALIZE the day's weights to unit gross (`Σ|w| = 1`), matching `positioning_weights`' existing unit-gross contract.
- The validated cap value is **0.07** (full-history sweep 1295d: Sharpe 2.60→2.69, maxDD −32.3%→−29.9%, per-year Sharpe non-decreasing every year). Do NOT use 0.04 (falsified: Sharpe drops to 2.43).
- Re-inception uses the EXISTING pattern: bump `version` in `definition.params.version`, and set `inception_t0: {"date": "2026-07-06", "version": <new>}` (outside `params`, NOT hashed) so the anchor backdates and the forward window is recomputed, not reset. See `forward_definition.inception_override`.
- `positioning` is a CONSTITUENT of `bybit_4edge`; both registry entries' definition versions must bump together (the book's sleeve construction changed).
- Never touch real money / execution. This is a construction change to the modeled+forward track only.
- Run tests with `uv run --extra dev python -m pytest ...`. The positioning eval integration tests need no extra services (they use a fake store).

---

### Task 1: Add the ex-ante `coin_gross_cap` to `positioning_weights`

**Files:**
- Modify: `src/fxhnt/application/bybit_positioning_eval.py:78-104` (the `positioning_weights` function)
- Test: `tests/integration/test_bybit_positioning_eval.py`

**Interfaces:**
- Consumes: nothing new.
- Produces: `positioning_weights(ratio_by_day, *, direction="contrarian", coin_gross_cap: float | None = None) -> dict[int, dict[str, float]]`. When `coin_gross_cap` is a float, each day's weights are clipped to `|w| ≤ coin_gross_cap` then renormalized to unit gross. When `None`, output is byte-identical to today.

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

Add to `tests/integration/test_bybit_positioning_eval.py`:

```python
def test_coin_gross_cap_clips_and_renormalizes() -> None:
    # One coin dominates (retail extreme) -> uncapped weight far exceeds 0.07; cap must clip + renormalize.
    ratio = {0: {"DOMUSDT": 0.95, "AUSDT": 0.50, "BUSDT": 0.50, "CUSDT": 0.05}}
    uncapped = positioning_weights(ratio)[0]
    assert max(abs(w) for w in uncapped.values()) > 0.07     # DOM dominates uncapped
    capped = positioning_weights(ratio, coin_gross_cap=0.07)[0]
    assert max(abs(w) for w in capped.values()) <= 0.07 + 1e-9   # no coin exceeds the cap
    assert abs(sum(abs(w) for w in capped.values()) - 1.0) < 1e-9  # still unit gross
    # sign structure preserved: DOM still short (over-long crowd), C still long (over-short)
    assert capped["DOMUSDT"] < 0.0 and capped["CUSDT"] > 0.0


def test_coin_gross_cap_none_is_byte_identical() -> None:
    ratio = {0: {"AAAUSDT": 0.9, "BBBUSDT": 0.5, "CCCUSDT": 0.1}}
    assert positioning_weights(ratio, coin_gross_cap=None) == positioning_weights(ratio)
```

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

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py::test_coin_gross_cap_clips_and_renormalizes -v`
Expected: FAIL with `TypeError: positioning_weights() got an unexpected keyword argument 'coin_gross_cap'`

- [ ] **Step 3: Implement the cap in `positioning_weights`**

Replace the body of `positioning_weights` (lines 91-104) so the signature gains `coin_gross_cap` and, after the existing unit-gross normalization, applies the clip+renormalize when the cap is set:

```python
def positioning_weights(ratio_by_day: dict[int, dict[str, float]], *,
                        direction: str = "contrarian",
                        coin_gross_cap: float | None = None) -> dict[int, dict[str, float]]:
    """`{day: {coin: w}}` market-neutral cross-sectional weights from the long/short positioning signal.

    `direction="contrarian"` (DEFAULT) SHORTs the high-buyRatio coins (retail over-long → fade) and LONGs the
    low-buyRatio coins (retail over-short). `direction="momentum"` is the EXACT SIGN-FLIP: LONG the over-long
    / SHORT the over-short (informed-flow continuation). Construction per day:
      1. DEMEAN the per-day buyRatio across coins (so the book is dollar/market-neutral, Σw ≈ 0);
      2. apply the DIRECTION sign — contrarian = −demeaned (short the rich/over-long), momentum = +demeaned;
      3. normalise to UNIT GROSS (Σ|w| = 1);
      4. (OPTIONAL) if `coin_gross_cap` is set, CLIP each |w| to the cap and RENORMALISE to unit gross —
         an EX-ANTE per-coin concentration limit (applied to the FORMING weight, so it is strictly causal:
         it cannot see the coin's realized return). Validated at 0.07 (full-history sweep: Sharpe 2.60→2.69,
         maxDD −32.3%→−29.9%, per-year non-decreasing) — caps the LABUSDT-class implosions (a 10.3% long leg
         that did −59%/−79.5% on 2026-07-06/07) without over-throttling the edge (0.04 falsified: Sharpe→2.43).

    Because the gross is identical for both directions, `momentum` weights are the exact per-coin negation of
    `contrarian`. A day with <2 coins, or zero gross after demeaning, yields no weights (skipped). Pure."""
    if direction not in ("contrarian", "momentum"):
        raise ValueError(f"direction must be 'contrarian' or 'momentum', got {direction!r}")
    sign = -1.0 if direction == "contrarian" else 1.0
    out: dict[int, dict[str, float]] = {}
    for day, row in ratio_by_day.items():
        if len(row) < 2:
            continue
        mean = st.mean(row.values())
        raw = {c: sign * (v - mean) for c, v in row.items()}
        gross = sum(abs(w) for w in raw.values())
        if gross <= 0.0:
            continue
        w = {c: x / gross for c, x in raw.items()}
        if coin_gross_cap is not None:
            w = {c: max(-coin_gross_cap, min(coin_gross_cap, x)) for c, x in w.items()}
            capped_gross = sum(abs(x) for x in w.values())
            if capped_gross <= 0.0:
                continue
            w = {c: x / capped_gross for c, x in w.items()}
        out[int(day)] = w
    return out
```

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py -v`
Expected: PASS (the two new tests + all existing positioning_weights tests still green)

- [ ] **Step 5: Commit**

```bash
git add src/fxhnt/application/bybit_positioning_eval.py tests/integration/test_bybit_positioning_eval.py
git commit -m "feat(positioning): ex-ante per-coin gross cap in positioning_weights (default off)"
```

---

### Task 2: Thread `coin_gross_cap` through `_book_breakdown` and `positioning_returns_from_store`

**Files:**
- Modify: `src/fxhnt/application/bybit_positioning_eval.py:140-166` (`_book_breakdown` signature + the `positioning_weights` call at line 177)
- Modify: `src/fxhnt/application/bybit_positioning_eval.py:287-303` (`positioning_returns_from_store` signature + its `_book_breakdown` call)
- Test: `tests/integration/test_bybit_positioning_eval.py`

**Interfaces:**
- Consumes: `positioning_weights(..., coin_gross_cap=...)` from Task 1.
- Produces:
  - `_book_breakdown(store, *, universe, cost_bps, direction, capacity_capital=None, participation=0.10, turnover_window=30, tail_cap_k=None, book_vol_window=60, coin_gross_cap=None) -> dict`
  - `positioning_returns_from_store(store, *, universe=None, cost_bps=5.5, direction="contrarian", capacity_capital=None, tail_cap_k=None, coin_gross_cap=None) -> dict[int, float]`

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

Add to `tests/integration/test_bybit_positioning_eval.py` (reuse the module's existing fake store fixture — grep the file for the store used by `test_capacity_haircut_caps_gain_only_at_scale`, e.g. a `_FakeStore`/`_ReadOnlyStore` builder, and construct one where a single coin has an extreme long_ratio + a large adverse next-day return):

```python
def test_coin_gross_cap_reduces_single_coin_loss_in_book(positioning_store_with_shock):
    # positioning_store_with_shock: one coin (retail over-short) craters next day; uncapped it dominates the
    # book loss, capped its weight (and thus its loss contribution) is bounded.
    store = positioning_store_with_shock
    uncapped = positioning_returns_from_store(store, cost_bps=0.0)
    capped = positioning_returns_from_store(store, cost_bps=0.0, coin_gross_cap=0.07)
    shock_day = min(uncapped, key=lambda d: uncapped[d])       # the worst day uncapped
    assert capped[shock_day] > uncapped[shock_day]             # the cap made the worst day less bad
```

If the module has no reusable multi-coin shock fixture, build the store inline in the test from a `long_ratio` panel (one coin at ratio 0.95, three near 0.5) + a `close` panel where the over-short coin drops ~50% the next day, mirroring the `test_capacity_haircut_*` store construction in the same file.

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

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py::test_coin_gross_cap_reduces_single_coin_loss_in_book -v`
Expected: FAIL with `TypeError: positioning_returns_from_store() got an unexpected keyword argument 'coin_gross_cap'`

- [ ] **Step 3: Thread the param through both functions**

In `_book_breakdown`, add `coin_gross_cap: float | None = None` to the signature (after `book_vol_window: int = 60`), and change the weight-forming call (currently line 177):

```python
    weights_by_day = positioning_weights(_ratio_by_day(long_ratio), direction=direction,
                                         coin_gross_cap=coin_gross_cap)
```

In `positioning_returns_from_store`, add `coin_gross_cap: float | None = None` to the signature (after `tail_cap_k`) and pass it through:

```python
    return _book_breakdown(store, universe=universe, cost_bps=cost_bps, direction=direction,
                           capacity_capital=capacity_capital, tail_cap_k=tail_cap_k,
                           coin_gross_cap=coin_gross_cap)["net"]
```

Update the `positioning_returns_from_store` docstring's param note to add: `coin_gross_cap` (None default) applies the ex-ante per-coin concentration cap (see `positioning_weights`).

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/fxhnt/application/bybit_positioning_eval.py tests/integration/test_bybit_positioning_eval.py
git commit -m "feat(positioning): thread coin_gross_cap through book_breakdown + returns_from_store"
```

---

### Task 3: Thread `coin_gross_cap` through the sleeve seam (`_positioning_series` + `sleeve_returns_from_store`)

**Files:**
- Modify: `src/fxhnt/application/bybit_book_eval.py:226-240` (`_positioning_series`)
- Modify: `src/fxhnt/application/bybit_book_eval.py:243-278` (`sleeve_returns_from_store` signature + the positioning branch)
- Test: `tests/integration/test_bybit_positioning_eval.py` (or `tests/integration/test_web_new_edges.py` if that is where sleeve_returns_from_store is exercised — grep first)

**Interfaces:**
- Consumes: `positioning_returns_from_store(..., coin_gross_cap=...)` from Task 2.
- Produces:
  - `_positioning_series(store, *, universe, cost_bps, direction="contrarian", capacity_capital=None, tail_cap_k=None, coin_gross_cap=None) -> dict[int, float]`
  - `sleeve_returns_from_store(store, edge, *, universe=None, cost_bps=0.0, unlock_events=None, stablecoin_spot_panel=None, capacity_capital=None, tail_cap_k=None, coin_gross_cap=None) -> dict[int, float]`. The `coin_gross_cap` is forwarded ONLY to the `positioning` branch (other edges ignore it).

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

Add (same test file, reuse the store from Task 2's fixture/inline construction):

```python
def test_sleeve_returns_positioning_forwards_coin_gross_cap(positioning_store_with_shock):
    from fxhnt.application.bybit_book_eval import sleeve_returns_from_store
    store = positioning_store_with_shock
    uncapped = sleeve_returns_from_store(store, "positioning", cost_bps=0.0)
    capped = sleeve_returns_from_store(store, "positioning", cost_bps=0.0, coin_gross_cap=0.07)
    shock_day = min(uncapped, key=lambda d: uncapped[d])
    assert capped[shock_day] > uncapped[shock_day]     # cap forwarded -> worst day improved
```

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

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py::test_sleeve_returns_positioning_forwards_coin_gross_cap -v`
Expected: FAIL with `TypeError: sleeve_returns_from_store() got an unexpected keyword argument 'coin_gross_cap'`

- [ ] **Step 3: Thread through both functions**

In `_positioning_series` (line 226), add `coin_gross_cap: float | None = None` to the signature (after `tail_cap_k`) and forward it:

```python
    return positioning_returns_from_store(store, universe=universe, cost_bps=cost_bps, direction=direction,
                                          capacity_capital=capacity_capital, tail_cap_k=tail_cap_k,
                                          coin_gross_cap=coin_gross_cap)
```

In `sleeve_returns_from_store` (line 243), add `coin_gross_cap: float | None = None` to the signature (after `tail_cap_k`), and change ONLY the positioning branch (line 276) to forward it:

```python
    if edge == "positioning":
        # ... existing comment about raw store + universe unchanged ...
        return _positioning_series(store, universe=universe, cost_bps=cost_bps,
                                   capacity_capital=capacity_capital, tail_cap_k=tail_cap_k,
                                   coin_gross_cap=coin_gross_cap)
```

Leave the tstrend / unlock / stablecoin / xsfunding branches unchanged (they never receive `coin_gross_cap`).

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/fxhnt/application/bybit_book_eval.py tests/integration/test_bybit_positioning_eval.py
git commit -m "feat(positioning): forward coin_gross_cap through sleeve_returns_from_store"
```

---

### Task 4: Turn the cap ON at 0.07 in the live precompute + add the constant

**Files:**
- Modify: `src/fxhnt/application/bybit_forward_track.py:47` (add `POSITIONING_COIN_GROSS_CAP` next to `POSITIONING_TAIL_CAP_K`)
- Modify: `src/fxhnt/application/bybit_book_persist.py:77-84` (pass the cap into `sleeve_returns_from_store`)
- Test: `tests/integration/test_bybit_paper_reconciliation.py` (or the persist test — grep for a test that calls `_persist_bybit_book` / `persist_bybit_sleeve_returns`; if none exists at unit level, assert the constant is wired via a focused test in `test_bybit_positioning_eval.py`)

**Interfaces:**
- Consumes: `sleeve_returns_from_store(..., coin_gross_cap=...)` from Task 3.
- Produces: `POSITIONING_COIN_GROSS_CAP = 0.07` in `bybit_forward_track.py`; the live `_persist_bybit_book` sleeve computation applies it to the positioning sleeve.

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

Add to `tests/integration/test_bybit_positioning_eval.py`:

```python
def test_positioning_coin_gross_cap_constant_is_007() -> None:
    from fxhnt.application.bybit_forward_track import POSITIONING_COIN_GROSS_CAP
    assert POSITIONING_COIN_GROSS_CAP == 0.07
```

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

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py::test_positioning_coin_gross_cap_constant_is_007 -v`
Expected: FAIL with `ImportError: cannot import name 'POSITIONING_COIN_GROSS_CAP'`

- [ ] **Step 3: Add the constant + wire it into the precompute**

In `src/fxhnt/application/bybit_forward_track.py`, directly after line 47 (`POSITIONING_TAIL_CAP_K = 4.0`):

```python
# Ex-ante per-coin gross-weight cap for the positioning sleeve — no single coin's |weight| may exceed this
# fraction of the book's unit gross. Validated 2026-07-20 over the full 1295-day history (Sharpe 2.60→2.69,
# maxDD −32.3%→−29.9%, per-year Sharpe non-decreasing every year); caps LABUSDT-class single-coin implosions
# (a 10.3% long leg that did −59%/−79.5% on 2026-07-06/07) without over-throttling (0.04 falsified → 2.43).
POSITIONING_COIN_GROSS_CAP = 0.07
```

In `src/fxhnt/application/bybit_book_persist.py`, update the import (line 78) and the sleeve computation (lines 80-83):

```python
        from fxhnt.application.bybit_forward_track import POSITIONING_COIN_GROSS_CAP, POSITIONING_TAIL_CAP_K
        sleeve_rets = {
            s: sleeve_returns_from_store(store, s, universe=universe, cost_bps=cost_bps,
                                         unlock_events=unlock_events,
                                         capacity_capital=settings.paper_capital,
                                         tail_cap_k=POSITIONING_TAIL_CAP_K,
                                         coin_gross_cap=POSITIONING_COIN_GROSS_CAP)
            for s in _DEFAULT_BYBIT_SLEEVES}
```

(`coin_gross_cap` is ignored by every sleeve except positioning — Task 3 only forwards it in the positioning branch — so passing it for all sleeves in the dict comprehension is correct and harmless.)

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_positioning_eval.py -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/fxhnt/application/bybit_forward_track.py src/fxhnt/application/bybit_book_persist.py tests/integration/test_bybit_positioning_eval.py
git commit -m "feat(positioning): enable coin_gross_cap=0.07 in the live bybit book precompute"
```

---

### Task 5: Bump definition versions to re-inception positioning + bybit_4edge (backdated anchor, no gate reset)

**Files:**
- Modify: `src/fxhnt/registry.py` (the `positioning` entry ~line 249-259 and the `bybit_4edge` entry ~line 198-211; also `bybit_4edge_levered` ~line 229-241 which shares the sleeve construction)
- Test: `tests/` — grep for `test_forward_definition` / a registry-consistency test; add an assertion that the bumped versions + inception_t0 are consistent

**Interfaces:**
- Consumes: the capped construction from Task 4 (the version bump signals the construction changed).
- Produces: `positioning`, `bybit_4edge`, `bybit_4edge_levered` definitions at `version: 3` with `inception_t0: {"date": "2026-07-06", "version": 3}`.

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

Add `tests/unit/test_positioning_cap_reinception.py`:

```python
from fxhnt.registry import STRATEGY_REGISTRY


def test_positioning_and_book_bumped_to_v3_backdated():
    for sid in ("positioning", "bybit_4edge", "bybit_4edge_levered"):
        d = STRATEGY_REGISTRY[sid]["definition"]
        assert d["params"]["version"] == 3, f"{sid} version not bumped to 3"
        ov = d.get("inception_t0")
        assert ov and ov["version"] == 3, f"{sid} inception_t0 not re-pinned to v3"
        # backdated to the existing OOS start, NOT reset to today -> the forward window is recomputed, not lost
        assert ov["date"] == "2026-07-06"
```

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

Run: `uv run --extra dev python -m pytest tests/unit/test_positioning_cap_reinception.py -v`
Expected: FAIL (versions are currently 2)

- [ ] **Step 3: Bump the three registry entries**

In each of the `positioning`, `bybit_4edge`, and `bybit_4edge_levered` entries in `src/fxhnt/registry.py`, change `"version": 2` to `"version": 3` inside `definition.params`, and change the `inception_t0` to `{"date": "2026-07-06", "version": 3}`. Note: `positioning`'s existing `inception_t0` date is `2026-07-06`; `bybit_4edge` / `bybit_4edge_levered` currently use `2026-07-07` — for this backfill set ALL three to `2026-07-06` (the positioning shock's first day) so the whole book re-warms over the same window. Add a comment on each:

```python
                       "inception_t0": {"date": "2026-07-06", "version": 3},  # v3: positioning coin_gross_cap=0.07 (2026-07-20 backfill)
```

- [ ] **Step 4: Run test to verify it passes**

Run: `uv run --extra dev python -m pytest tests/unit/test_positioning_cap_reinception.py tests/ -k "forward_definition or registry" -v`
Expected: PASS (the new test + any existing registry/definition consistency tests)

- [ ] **Step 5: Commit**

```bash
git add src/fxhnt/registry.py tests/unit/test_positioning_cap_reinception.py
git commit -m "feat(positioning): re-inception positioning+bybit_4edge v3 (coin_gross_cap, backdated 2026-07-06)"
```

---

### Task 6: Full-suite verification + lint/type gate

**Files:** none (verification task)

**Interfaces:** consumes all prior tasks.

- [ ] **Step 1: Run the full positioning + book + forward suite**

Run: `uv run --extra dev python -m pytest tests/ -k "positioning or book or forward or reconcil or bybit" -q`
Expected: all pass (no regression; the OFF-path byte-identity tests from Tasks 1-3 guarantee unchanged behavior everywhere the cap is not enabled).

- [ ] **Step 2: Lint + type-check the changed files**

Run: `uv run --extra dev ruff check src/fxhnt/application/bybit_positioning_eval.py src/fxhnt/application/bybit_book_eval.py src/fxhnt/application/bybit_book_persist.py src/fxhnt/application/bybit_forward_track.py src/fxhnt/registry.py`
Then: `uv run --extra dev --extra orchestration mypy src/fxhnt/application/bybit_positioning_eval.py src/fxhnt/application/bybit_book_eval.py`
Expected: no NEW errors on changed lines (pre-existing whole-file findings that are not in the diff are acceptable — confirm via `git diff --unified=0 ... | grep '^+' | awk 'length>120'` returning nothing).

- [ ] **Step 3: Commit any lint fixes** (only if Step 2 surfaced issues on changed lines)

```bash
git add -A
git commit -m "chore(positioning): lint/type fixes for coin_gross_cap"
```

---

## Post-merge deploy + backfill (OPERATOR steps — outside the coding plan, run after merge)

These are runtime/prod steps, NOT code, listed so the operator has the full picture. Do them after the branch is merged to master and pushed:

1. **Deploy the code:** `git push origin master` (the Dagster K8sRunLauncher git-syncs master HEAD; a code-only change needs no image rebuild). Rollout-restart the dagster daemon if it caches code: `kubectl --context=$BIZ -n foxhunt rollout restart deploy/dagster-daemon`.
2. **Backfill the anchor:** the re-inception is AUTOMATIC on the next `combined_book_daily` run (23:30 UTC) — the bumped version + backdated `inception_t0` cause `forward_engine` to reset the anchor to `t0=2026-07-06` and recompute the forward window with the capped construction. Do NOT run `migrate-forward-anchors` (re-inception is automatic — see `reference_fxhnt_prod_deploy_mechanics`).
3. **Refresh the backtest ref:** the `bybit_book_precompute` asset re-persists the `positioning` / `bybit_4edge` / `bybit_4edge_levered` `backtest_summary` from the capped `_persist_bybit_book` in the same nightly run, so the gate reconciles the forward track against the capped (capturable) reference.
4. **Verify next morning:** query the prod DB (port-forward svc/postgres) — `positioning` forward should re-show ~13 days (recomputed, capped), the 2026-07-06/07 daily returns should be materially less negative than −7.32%/−6.89%, and the gate should read WAIT "building" (not reset to 0) or its real verdict.

---

## Self-Review

**1. Spec coverage:** cap in weights (T1) → threaded through book (T2) → sleeve seam (T3) → enabled live (T4) → re-inception+backfill (T5) → verified (T6). The "backfill not reset" constraint is implemented via `inception_t0` backdating in T5 + operator step 2. Covered.

**2. Placeholder scan:** every code step shows the exact code; the only deferred item is the shock-store fixture in T2/T3 which is explicitly instructed to reuse the existing `test_capacity_haircut_*` store construction in the same file (a real, named pattern, not a placeholder). Acceptable — the implementer has a concrete reference.

**3. Type consistency:** `coin_gross_cap: float | None = None` is identical across `positioning_weights`, `_book_breakdown`, `positioning_returns_from_store`, `_positioning_series`, `sleeve_returns_from_store`. Constant `POSITIONING_COIN_GROSS_CAP = 0.07` in `bybit_forward_track.py`, imported in `bybit_book_persist.py`. Versions all → 3, `inception_t0.date` all → "2026-07-06". Consistent.

---

### Task 7: Thread `coin_gross_cap` into the LIVE forward-nav path (Critical fix from final review)

**Why:** The final whole-branch review found the cap threads to the reconciliation REF (via `_persist_bybit_book`) but NOT to the live FORWARD NAV (built by `BybitFourEdgeStrategy`), so the gate would compare a capped ref against an uncapped forward and read a healthy book as "diverging below backtest" (reproduced: forward −25% vs ref −12.5% on a LABUSDT shock). Fail-safe (WAIT, never false-PASS) but defeats the branch's purpose. This threads the cap the same way `positioning_tail_cap_k` is already threaded.

**Files:**
- Modify: `src/fxhnt/application/bybit_forward_track.py` (`BybitFourEdgeStrategy.__init__` ~135-157, `advance` ~168-172)
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (builders at 105-122, 128-143, 149-158, 164-169; nightly call sites 868-876, 916-924, 957-965)
- Modify: `src/fxhnt/adapters/orchestration/migration_builders.py` (builders at 106-117, 127-141, 148-156)
- Modify: `src/fxhnt/application/bybit_positioning_eval.py:280` (`positioning_weights_and_contributions` — the Important SSOT-attribution fix)
- Test: `tests/integration/test_positioning_forward_track.py` (+ the attribution invariant in `tests/integration/test_bybit_paper_reconciliation.py`)

**Interfaces:**
- Consumes: `POSITIONING_COIN_GROSS_CAP = 0.07` (Task 4), `sleeve_returns_from_store(..., coin_gross_cap=...)` (Task 3), `positioning_weights(..., coin_gross_cap=...)` (Task 1).
- Produces: `BybitFourEdgeStrategy(..., positioning_coin_gross_cap: float | None = None, ...)`; the four `assets.py` builders + three `migration_builders.py` builders gain a `positioning_coin_gross_cap` param forwarded to the strategy; the three nightly call sites pass `POSITIONING_COIN_GROSS_CAP`.

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

Add to `tests/integration/test_positioning_forward_track.py` — a shock store where the forward NAV's worst day is materially less negative when the cap is threaded (mirror the shock-store construction already used in the positioning tests):

```python
def test_forward_nav_applies_coin_gross_cap() -> None:
    """The forward NAV (BybitFourEdgeStrategy store-read path) must apply the per-coin cap when given, so it
    reconciles against the capped ref instead of diverging. Shock store: one over-short coin craters ~50%."""
    from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
    from fxhnt.application.bybit_forward_track import BybitFourEdgeStrategy

    _DAY = 86_400
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    px = {"LABUSDT": 100.0, "AAAUSDT": 100.0, "BBBUSDT": 100.0, "CCCUSDT": 100.0}
    for d in range(4):
        store.write_features("LABUSDT", [(d * _DAY, {"long_ratio": 0.05, "close": px["LABUSDT"]})])
        store.write_features("AAAUSDT", [(d * _DAY, {"long_ratio": 0.50, "close": px["AAAUSDT"]})])
        store.write_features("BBBUSDT", [(d * _DAY, {"long_ratio": 0.48, "close": px["BBBUSDT"]})])
        store.write_features("CCCUSDT", [(d * _DAY, {"long_ratio": 0.52, "close": px["CCCUSDT"]})])
        if d == 2:
            px["LABUSDT"] *= 0.50
    uncapped_rows, _ = BybitFourEdgeStrategy(store, sleeves=["positioning"]).advance(None, {})
    capped_rows, _ = BybitFourEdgeStrategy(
        store, sleeves=["positioning"], positioning_coin_gross_cap=0.07).advance(None, {})
    store.close()
    assert uncapped_rows and capped_rows, "forward strategy produced no rows — fixture broken"
    u = dict(uncapped_rows); c = dict(capped_rows)
    worst = min(u, key=lambda k: u[k])
    assert c[worst] > u[worst], f"cap must reduce the forward worst day: uncapped={u[worst]}, capped={c[worst]}"
```

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

Run: `uv run --extra dev python -m pytest tests/integration/test_positioning_forward_track.py::test_forward_nav_applies_coin_gross_cap -v`
Expected: FAIL with `TypeError: BybitFourEdgeStrategy.__init__() got an unexpected keyword argument 'positioning_coin_gross_cap'`

- [ ] **Step 3: Add the param to `BybitFourEdgeStrategy` and thread it in `advance`**

In `__init__` (after `positioning_tail_cap_k: float | None = None,` on line 142) add:
```python
                 positioning_coin_gross_cap: float | None = None,
```
And in the body (after `self._positioning_tail_cap_k = positioning_tail_cap_k` line 154):
```python
        self._positioning_coin_gross_cap = positioning_coin_gross_cap
```
In `advance`'s store-read `sleeve_returns_from_store(...)` call (lines 168-172), add the kwarg:
```python
                series = sleeve_returns_from_store(
                    self._store, sleeve, universe=self._universe, cost_bps=self._cost_bps,
                    unlock_events=self._unlock_events, stablecoin_spot_panel=self._stablecoin_spot_panel,
                    capacity_capital=self._positioning_capacity_capital,
                    tail_cap_k=self._positioning_tail_cap_k,
                    coin_gross_cap=self._positioning_coin_gross_cap)
```

- [ ] **Step 4: Thread through every builder (parallel to `positioning_tail_cap_k`)**

In `src/fxhnt/adapters/orchestration/assets.py`, for EACH of the four builders (`build_bybit_4edge_forward_nav`, `build_bybit_4edge_levered_forward_nav`, and the two single-sleeve builders at 149 and 164): add a `positioning_coin_gross_cap=None` param to the signature (next to `positioning_tail_cap_k=None`) and forward `positioning_coin_gross_cap=positioning_coin_gross_cap` into the `BybitFourEdgeStrategy(...)` call. For the THREE nightly call sites (868-876, 916-924, 957-965) that already pass `positioning_tail_cap_k=POSITIONING_TAIL_CAP_K`: add `positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP` (import it alongside `POSITIONING_TAIL_CAP_K`).

Do the identical change in `src/fxhnt/adapters/orchestration/migration_builders.py` for its three builders that pass `positioning_tail_cap_k=POSITIONING_TAIL_CAP_K` (106-117, 127-141, 148-156): add `positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP` to each `BybitFourEdgeStrategy(...)` call, importing the constant.

- [ ] **Step 5: Fix the attribution SSOT (the Important finding)**

In `src/fxhnt/application/bybit_positioning_eval.py`, `positioning_weights_and_contributions` (line 263): add `coin_gross_cap: float | None = None` to its signature and forward it in the `positioning_weights(...)` call at line 280:
```python
    weights_by_day = positioning_weights(_ratio_by_day(long_ratio), direction=direction,
                                         coin_gross_cap=coin_gross_cap)
```
Then thread it through `sleeve_weights_and_contributions` in `src/fxhnt/application/bybit_paper_weights.py` (the caller at ~80-84) so the paper-book per-symbol weights match the capped sleeve return (preserving the `Σ weight·return == sleeve_returns_from_store` invariant documented at `bybit_paper_weights.py:9`). Default None everywhere.

- [ ] **Step 6: Run the tests + verify no regression**

Run: `uv run --extra dev python -m pytest tests/integration/test_positioning_forward_track.py tests/integration/test_bybit_paper_reconciliation.py tests/integration/test_bybit_positioning_eval.py -v`
Expected: all pass (new forward-nav cap test passes; existing reconciliation invariant test still passes with default-None on both sides).

- [ ] **Step 7: Commit**

```bash
git add -A
git commit -m "fix(positioning): thread coin_gross_cap into the LIVE forward-nav path + attribution (final-review Critical)"
```

---

### Task 8: Cap the LIVE paper-book POSITIONS + the report-ref path (Critical + Important from Task-7 review)

**Why:** The Task-7 review traced two more uncapped paths:
- **Critical:** the live paper-book position sizing (`combined_symbol_weights*` / `latest_raw_sleeve_weights` → `derive_and_persist_bybit_paper_book` → the nightly `bybit_paper_book` asset) never passes the positioning haircuts, so the fund's ACTUAL persisted positions for the positioning sleeve are sized uncapped even though the reconciled return is capped. It turns out capacity + tail_cap were ALSO never threaded here (pre-existing gap). Operator decision (2026-07-20): thread ALL THREE haircuts so executed positions match the reconciled returns (full SSOT).
- **Important:** `bybit_4edge_backtest_summary` + its caller `report_backtest_summary` pass `positioning_tail_cap_k` but no `positioning_coin_gross_cap` — a real, independently-callable uncapped ref path (masked in the scheduled flow only by the `_BYBIT_BOOK_SIDS` special-case).

**Files:**
- Modify: `src/fxhnt/application/bybit_paper_book.py` — `combined_symbol_weights` (~48), `combined_symbol_weights_and_returns` (~105), `latest_raw_sleeve_weights` (~80), and `derive_and_persist_bybit_paper_book` (the persist driver): add `positioning_capacity_capital`, `positioning_tail_cap_k`, `positioning_coin_gross_cap` (all `float | None = None`) and forward into every `sleeve_weights_and_contributions(...)` call.
- Modify: `src/fxhnt/adapters/orchestration/assets.py` — `build_bybit_paper_book` (~180): add the 3 params, forward into `derive_and_persist_bybit_paper_book`; nightly call site (~1105): pass `positioning_capacity_capital=s.paper_capital, positioning_tail_cap_k=POSITIONING_TAIL_CAP_K, positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP` (import the two constants).
- Modify: `src/fxhnt/application/bybit_forward_track.py` — `bybit_4edge_backtest_summary` (~210): add `positioning_coin_gross_cap: float | None = None` next to `positioning_tail_cap_k`, forward into the `BybitFourEdgeStrategy(...)` call.
- Modify: `src/fxhnt/application/backtest_refs_report.py` — `report_backtest_summary` (~42): import `POSITIONING_COIN_GROSS_CAP`, pass `positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP` into `bybit_4edge_backtest_summary(...)`.
- Test: `tests/integration/test_bybit_paper_reconciliation.py` (or the paper-book test file — grep for a test that calls `combined_symbol_weights_and_returns` / `derive_and_persist_bybit_paper_book`).

**Interfaces:**
- Consumes: `sleeve_weights_and_contributions(..., positioning_capacity_capital=None, positioning_tail_cap_k=None, positioning_coin_gross_cap=None)` (Task 7 added coin_gross_cap; VERIFY it also has capacity/tail params — if `sleeve_weights_and_contributions` does NOT yet accept capacity_capital/tail_cap_k, add them there too, forwarding into its `positioning_weights`/underlying call, same as coin_gross_cap).
- Produces: the 3 haircut params on `combined_symbol_weights`, `combined_symbol_weights_and_returns`, `latest_raw_sleeve_weights`, `derive_and_persist_bybit_paper_book`, `build_bybit_paper_book`; `positioning_coin_gross_cap` on `bybit_4edge_backtest_summary` + `report_backtest_summary`. All default None.

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

Add a paper-book position test: on a LABUSDT-shock store, the positioning sleeve's persisted per-symbol weight for the crashing coin is smaller when `positioning_coin_gross_cap=0.07` is threaded through `combined_symbol_weights_and_returns` than without.

```python
def test_paper_book_positions_capped_by_coin_gross_cap() -> None:
    """The live paper-book per-symbol weights must honor the per-coin cap so executed positions match the
    capped reconciled return (SSOT). On a store with one dominant positioning coin, the capped weight for that
    coin is strictly smaller than uncapped."""
    from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
    from fxhnt.application.bybit_paper_book import combined_symbol_weights_and_returns
    _DAY = 86_400
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    for d in range(4):
        store.write_features("LABUSDT", [(d * _DAY, {"long_ratio": 0.02, "close": 100.0})])
        for i, sym in enumerate(("AAAUSDT", "BBBUSDT", "CCCUSDT", "DDDUSDT")):
            store.write_features(sym, [(d * _DAY, {"long_ratio": 0.45 + 0.02 * i, "close": 100.0})])
    w_unc, _r, _d = combined_symbol_weights_and_returns(store, sleeves=("positioning",))
    w_cap, _r2, _d2 = combined_symbol_weights_and_returns(
        store, sleeves=("positioning",), positioning_coin_gross_cap=0.07)
    store.close()
    assert w_unc.get("positioning") and w_cap.get("positioning"), "no positioning weights — fixture broken"
    lab_unc = abs(w_unc["positioning"].get("LABUSDT", 0.0))
    lab_cap = abs(w_cap["positioning"].get("LABUSDT", 0.0))
    assert lab_unc > 0.0
    assert lab_cap < lab_unc, f"cap must shrink the dominant coin's position: unc={lab_unc}, cap={lab_cap}"
```

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

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_paper_reconciliation.py::test_paper_book_positions_capped_by_coin_gross_cap -v`
Expected: FAIL with `TypeError: combined_symbol_weights_and_returns() got an unexpected keyword argument 'positioning_coin_gross_cap'`

- [ ] **Step 3: Thread the 3 haircuts through the paper-book weight functions**

For `combined_symbol_weights`, `combined_symbol_weights_and_returns`, and `latest_raw_sleeve_weights` in `bybit_paper_book.py`: add `positioning_capacity_capital: float | None = None`, `positioning_tail_cap_k: float | None = None`, `positioning_coin_gross_cap: float | None = None` to each signature, and forward all three into their `sleeve_weights_and_contributions(...)` call(s) (as `capacity_capital=`/`tail_cap_k=`/`positioning_coin_gross_cap=` per that function's actual param names — grep `def sleeve_weights_and_contributions` for the exact names). If `sleeve_weights_and_contributions` lacks capacity_capital/tail_cap_k params, add them there first (forwarding into its positioning path), then here.

- [ ] **Step 4: Thread through the persist driver + builder**

`derive_and_persist_bybit_paper_book` (bybit_paper_book.py): add the 3 params, forward into its `combined_symbol_weights_and_returns(...)` call. `build_bybit_paper_book` (assets.py:180): add the 3 params, forward into `derive_and_persist_bybit_paper_book(...)`. Nightly call site (assets.py:~1105): pass `positioning_capacity_capital=s.paper_capital, positioning_tail_cap_k=POSITIONING_TAIL_CAP_K, positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP` (import both constants from `bybit_forward_track`).

- [ ] **Step 5: Fix the report-ref path (Important)**

`bybit_4edge_backtest_summary` (bybit_forward_track.py:~210): add `positioning_coin_gross_cap: float | None = None` next to `positioning_tail_cap_k`, forward into `BybitFourEdgeStrategy(..., positioning_coin_gross_cap=positioning_coin_gross_cap)`. `report_backtest_summary` (backtest_refs_report.py:~42): import `POSITIONING_COIN_GROSS_CAP`, pass `positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP` into the `bybit_4edge_backtest_summary(...)` call.

- [ ] **Step 6: Run tests + no regression**

Run: `uv run --extra dev python -m pytest tests/integration/test_bybit_paper_reconciliation.py tests/integration/test_positioning_forward_track.py tests/unit/test_backtest_refs_report.py tests/integration/test_bybit_positioning_eval.py -v`
Expected: all pass. Then confirm no site was missed: `grep -c positioning_tail_cap_k` vs `grep -c positioning_coin_gross_cap` should MATCH in bybit_paper_book.py, assets.py, bybit_forward_track.py, backtest_refs_report.py.

- [ ] **Step 7: Commit**

```bash
git add -A
git commit -m "fix(positioning): cap live paper-book positions + report-ref path (Task-7-review Critical+Important)"
```
