# Book Representation + Venue-Cap Knob Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) tracking.

**Goal:** Switch the crypto deploy target from per-sleeve (collapses to positioning, ~18% deployed) to the diversified `bybit_4edge` BOOK (measured 75% deployed at vol 5.2% / maxDD −3.2% — 4× more at lower risk), and add a `max_venue_weight` fund-config knob so per-venue concentration (75% on Bybit) is cappable at live.

**Architecture:** Registry re-points the deploy tier to `bybit_4edge` (positioning/xsfunding/unlock become tracked constituents); the honest-inputs provider re-adds the book series dispatch (keeping the sleeve generalization). A new `max_venue_weight` on `AllocationPolicy` + `apply_venue_caps` engine function caps each venue's total gross; it is a fund_config column set via `fxhnt fund-config`.

**Tech Stack:** Python 3.12, SQLAlchemy 2.0, typer, pytest. In-cluster measurement already done (book = 75% deploy; funding-stress check PASSED).

## Global Constraints

- Basis: the 2026-07-16 measurements — book (`_book_series` risk-parity of {xsfunding,unlock,positioning}) @ $100k: vol 0.0523, maxDD −0.0324, Sharpe 0.161/pd, deploys **75%** at target_vol 0.25 (K hits max_leverage 1.5). Funding-stress check PASSED (crises in-sample, smoothness legitimate).
- `max_venue_weight` DEFAULT = **1.0** (no cap → byte-identical to today's deployment). It caps the fraction of `equity` deployable as gross to any single venue.
- `record_allocation(policy=None)` and `apply_venue_caps(..., max_venue_weight=1.0)` MUST be byte-identical to today (no-op at the default).
- `fund_config` read fallback + partial-upsert semantics unchanged; a missing `max_venue_weight` column/value → default 1.0 (never crash the nightly). Column added to the EXISTING prod table via an idempotent ALTER (mirror `forward_nav._ensure_summary_policy_columns`: Postgres `ADD COLUMN IF NOT EXISTS`, SQLite PRAGMA-check).
- Bump `ALLOCATION_POLICY_VERSION` 3 → 4 (policy shape changed).
- Marginal LOO gate, ex-ante full-history-vol K, anti-reactive invariant: UNTOUCHED.
- Commit trailer: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`.

---

### Task A: switch the crypto deploy target to the `bybit_4edge` book

**Files:**
- Modify: `src/fxhnt/registry.py`
- Modify: `src/fxhnt/application/allocation_honest_inputs.py`
- Modify: `src/fxhnt/adapters/web/templates/sim.html`
- Test: `tests/unit/test_allocation_honest_inputs.py`, `tests/unit/test_registry_tiers.py`

**Interfaces:**
- Produces: deploy set = `{bybit_4edge}`; provider sizes `bybit_4edge`→`_book_series`, `{positioning,xsfunding,unlock}`→`_sleeve_series`.

- [ ] **Step 1: Update the tier tests to the target state (write the expectation first)**

In `tests/unit/test_registry_tiers.py`: `_DEPLOY` back to `{"bybit_4edge"}` (positioning is now a research constituent). Update the test name/docstring accordingly (deploy tier = exactly the bybit_4edge book).

- [ ] **Step 2: Run — expect FAIL** (registry still has positioning deploy / bybit_4edge research)

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit/test_registry_tiers.py -q`
Expected: FAIL.

- [ ] **Step 3: Registry edits** (`registry.py`):
  - `bybit_4edge`: `"tier": "research"` → **`"tier": "deploy"`**. Rewrite its leading comment: it is the fund's DEPLOYED crypto book (risk-parity of its sleeves; measured 75% deploy @ 0.25 vol, funding-stress-checked); constituents tracked separately as reference.
  - `positioning`: `"tier": "deploy"` → **`"tier": "research"`** (constituent of the book; still forward-tracked as reference).
  - `xsfunding`: **REMOVE** `"promote_on_pass": True` (it is a book constituent, not a standalone deploy edge — must not auto-promote and double-count the book). Keep `tier: research`.
  - `unlock`: **REMOVE** `"promote_on_pass": True`. Keep `tier: research`.

- [ ] **Step 4: Provider edits** (`allocation_honest_inputs.py`):
  - `_CRYPTO_DEPLOY_SIDS = frozenset({"positioning", "xsfunding", "unlock", "bybit_4edge"})`.
  - `_deploy_series(store, sid, ...)`:
    ```python
    if sid == "bybit_4edge":
        return _book_series(store, spread_bps, unlock_events, aum=aum,
                            participation_cap=participation_cap, impact_k=impact_k)
    if sid in _CRYPTO_DEPLOY_SIDS:
        return _sleeve_series(store, sid, spread_bps, unlock_events, aum=aum,
                              participation_cap=participation_cap, impact_k=impact_k)
    return {}
    ```
  - Update the module docstring's deploy→honest mapping: `bybit_4edge`→the risk-parity BOOK (the deploy target), the three sleeves→their own sleeve series (available if ever deployed standalone).

- [ ] **Step 5: sim.html** — revert the label: `bybit_4edge` span `· reference` → `· deploy`; and the two comments (lines ~9, ~20) `REFERENCE book`/`(reference, default)` → `DEPLOY book`/`(deploy, default)` (it is the deploy book again).

- [ ] **Step 6: Update `test_allocation_honest_inputs.py`** — the pre-existing tests from the prior task asserted `_CRYPTO_DEPLOY_SIDS == {positioning,xsfunding,unlock}` and `bybit_4edge` not a crypto deploy sid. Update to the new set (includes `bybit_4edge`) and assert `bybit_4edge` now gets the BOOK series (non-empty, ISO-keyed) via the provider. Keep the sleeve-series assertions.

- [ ] **Step 7: Run — expect PASS**

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit/test_allocation_honest_inputs.py tests/unit/test_registry_tiers.py -q`
Expected: PASS.

- [ ] **Step 8: Commit**

```bash
git add src/fxhnt/registry.py src/fxhnt/application/allocation_honest_inputs.py src/fxhnt/adapters/web/templates/sim.html tests/unit/test_allocation_honest_inputs.py tests/unit/test_registry_tiers.py
git commit -m "feat(alloc): switch crypto deploy target to the bybit_4edge book (75% deploy vs 18% per-sleeve)

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

---

### Task B: venue-cap in the allocation engine + policy

**Files:**
- Modify: `src/fxhnt/application/allocation_policy.py` (`max_venue_weight` field, version bump)
- Modify: `src/fxhnt/application/allocation_engine.py` (`apply_venue_caps`)
- Modify: `src/fxhnt/application/allocation_ingest.py` (wire it into `record_allocation`)
- Test: `tests/unit/test_venue_caps.py`

**Interfaces:**
- Produces: `AllocationPolicy.max_venue_weight: float = 1.0`; `apply_venue_caps(dollars, venue_by_sid, max_venue_weight, equity) -> dict[str,float]` (per-venue gross cap, sign-preserving, no-op at ≥1.0).

- [ ] **Step 1: Write failing tests**

```python
# tests/unit/test_venue_caps.py
from fxhnt.application.allocation_engine import apply_venue_caps

def test_no_cap_at_default_is_identity():
    d = {"a": 60000.0, "b": -30000.0}
    v = {"a": "bybit-perp", "b": "ibkr-equity"}
    assert apply_venue_caps(dict(d), v, 1.0, 100000.0) == d

def test_single_venue_over_cap_scales_down_sign_preserving():
    # both on bybit-perp, gross 75k, cap 0.6*100k=60k -> scale 0.8
    d = {"a": 50000.0, "b": -25000.0}
    v = {"a": "bybit-perp", "b": "bybit-perp"}
    out = apply_venue_caps(dict(d), v, 0.6, 100000.0)
    assert round(out["a"], 2) == 40000.0 and round(out["b"], 2) == -20000.0

def test_under_cap_venue_untouched():
    d = {"a": 40000.0, "b": 40000.0}
    v = {"a": "bybit-perp", "b": "ibkr-equity"}   # each venue 40k < 60k cap
    assert apply_venue_caps(dict(d), v, 0.6, 100000.0) == d

def test_missing_venue_treated_as_own_bucket():
    d = {"a": 80000.0}
    v = {}                                          # sid with no venue -> its own ("") bucket
    out = apply_venue_caps(dict(d), v, 0.6, 100000.0)
    assert round(out["a"], 2) == 60000.0
```

- [ ] **Step 2: Run — expect FAIL**

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit/test_venue_caps.py -q`
Expected: FAIL.

- [ ] **Step 3: Implement `apply_venue_caps`** (`allocation_engine.py`):

```python
def apply_venue_caps(dollars: dict[str, float], venue_by_sid: dict[str, str],
                     max_venue_weight: float, equity: float) -> dict[str, float]:
    """Cap each venue's total deployed gross at `max_venue_weight * equity` (sign-preserving, proportional
    scale-down within the venue). `max_venue_weight >= 1.0` (or non-finite equity) -> no-op. A sid missing from
    `venue_by_sid` falls in the "" bucket (its own venue)."""
    if not dollars or max_venue_weight >= 1.0 or not math.isfinite(equity) or equity <= 0.0:
        return dollars
    cap = max_venue_weight * equity
    by_venue: dict[str, list[str]] = {}
    for sid in dollars:
        by_venue.setdefault(venue_by_sid.get(sid, ""), []).append(sid)
    for sids in by_venue.values():
        gross = sum(abs(dollars[s]) for s in sids)
        if gross > cap and gross > 0.0:
            scale = cap / gross
            for s in sids:
                dollars[s] *= scale
    return dollars
```

- [ ] **Step 4: Policy** (`allocation_policy.py`): add `max_venue_weight: float = 1.0  # cap on the fraction of equity deployed as gross to any single venue (1.0 = no cap)` after `max_strategy_weight`; bump `ALLOCATION_POLICY_VERSION = 3` → `4`.

- [ ] **Step 5: Wire into `record_allocation`** (`allocation_ingest.py`, after `dollars = target_capital(...)`):

```python
    venue_by_sid = {sid: STRATEGY_REGISTRY.get(sid, {}).get("venue", "") for sid in dollars}
    dollars = apply_venue_caps(dollars, venue_by_sid, getattr(pol, "max_venue_weight", 1.0), equity)
```

(import `apply_venue_caps` from allocation_engine; `STRATEGY_REGISTRY` is already imported.)

- [ ] **Step 6: Run — expect PASS** (venue tests + existing allocation tests still green)

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit/test_venue_caps.py tests/unit/test_compute_allocation.py tests/unit/test_record_allocation_policy.py -q`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add src/fxhnt/application/allocation_policy.py src/fxhnt/application/allocation_engine.py src/fxhnt/application/allocation_ingest.py tests/unit/test_venue_caps.py
git commit -m "feat(alloc): max_venue_weight per-venue gross cap (default 1.0 = no-op)

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

---

### Task C: `max_venue_weight` as a fund_config knob (end-to-end)

**Files:**
- Modify: `src/fxhnt/adapters/persistence/cockpit_models.py` (`FundConfigRow.max_venue_weight`)
- Modify: `src/fxhnt/application/fund_config.py` (field + validation + idempotent ALTER + read/write/seed)
- Modify: `src/fxhnt/cli.py` (`--max-venue-weight` flag on `fund-config set`; show it)
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (`cockpit_forward` replace adds `max_venue_weight`)
- Test: `tests/unit/test_fund_config.py`, `tests/unit/test_fund_config_cli.py`

**Interfaces:**
- Consumes: `apply_venue_caps` (Task B), `AllocationPolicy.max_venue_weight` (Task B).
- Produces: `FundConfig.max_venue_weight` (default 1.0); `fxhnt fund-config set --max-venue-weight`.

- [ ] **Step 1: Write failing tests** (extend `test_fund_config.py`):

```python
def test_max_venue_weight_default_and_roundtrip():
    import os
    p = "./.pytest_fc_venue.db"
    if os.path.exists(p): os.remove(p)
    from fxhnt.application.fund_config import read_fund_config, write_fund_config, seed_fund_config
    seed_fund_config(f"sqlite:///{p}")
    assert read_fund_config(f"sqlite:///{p}").max_venue_weight == 1.0
    write_fund_config(f"sqlite:///{p}", max_venue_weight=0.6)
    cfg = read_fund_config(f"sqlite:///{p}")
    assert cfg.max_venue_weight == 0.6 and cfg.target_vol == 0.20   # partial-override: vol unchanged
    os.remove(p)

def test_max_venue_weight_validation():
    import pytest, os
    p = "./.pytest_fc_venue_val.db"
    if os.path.exists(p): os.remove(p)
    from fxhnt.application.fund_config import write_fund_config
    for bad in (0.0, -0.1, 1.5):
        with pytest.raises(ValueError):
            write_fund_config(f"sqlite:///{p}", max_venue_weight=bad)
    if os.path.exists(p): os.remove(p)
```

Also extend the fallback test to assert `read_fund_config(absent).max_venue_weight == 1.0`.

- [ ] **Step 2: Run — expect FAIL**

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit/test_fund_config.py -q`
Expected: FAIL.

- [ ] **Step 3: Model + module** :
  - `cockpit_models.FundConfigRow`: add `max_venue_weight: Mapped[float] = mapped_column(Float)`.
  - `fund_config.py`: add `max_venue_weight: float` to the `FundConfig` dataclass; `_defaults()` → `max_venue_weight=1.0`; `_validate` accepts `max_venue_weight` and enforces `0 < max_venue_weight <= 1.0`; `write_fund_config` gains the `max_venue_weight=None` kwarg (partial-overlay like the others — including the "at least one field" guard); `seed_fund_config` inserts `max_venue_weight=1.0`.
  - **Idempotent ALTER** for the existing prod table — add an `_ensure_fund_config_columns(engine)` helper (mirror `forward_nav._ensure_summary_policy_columns`) that adds `max_venue_weight DOUBLE PRECISION` (Postgres `ADD COLUMN IF NOT EXISTS`; SQLite PRAGMA-check) and BACKFILLS existing row(s) to 1.0 where NULL; call it in `write_fund_config`/`seed_fund_config` right after `create_all` (so an existing seeded table gains the column without losing the operator's target_vol). A fresh table gets the column from `create_all`, so the ALTER is a no-op there.

- [ ] **Step 4: CLI** (`cli.py`): `fund-config set` gains `max_venue_weight: float = typer.Option(None, "--max-venue-weight", help="Max fraction of equity deployed to any single venue (0,1]. 1.0 = no cap.")`; pass it to `write_fund_config`; include it in both `show` and `set` echo lines.

- [ ] **Step 5: Wire the nightly** (`assets.py` `cockpit_forward`): the `dataclasses.replace(...)` gains `max_venue_weight=_cfg.max_venue_weight`.

- [ ] **Step 6: Run — expect PASS**

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit/test_fund_config.py tests/unit/test_fund_config_cli.py -q`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add src/fxhnt/adapters/persistence/cockpit_models.py src/fxhnt/application/fund_config.py src/fxhnt/cli.py src/fxhnt/adapters/orchestration/assets.py tests/unit/test_fund_config.py tests/unit/test_fund_config_cli.py
git commit -m "feat(fund-config): max_venue_weight knob (fxhnt fund-config set --max-venue-weight)

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

---

### Task D: full-suite green

- [ ] **Step 1:** `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit -q` → all pass.
- [ ] **Step 2:** `.venv/bin/python3 -m pytest tests/integration -k "allocation or honest or funding" -q` → all pass.
- [ ] **Step 3:** commit any fixes (else skip).

## Post-implementation (controller-run)
Deploy (git-sync), `fxhnt migrate-cockpit` (adds the column, backfills 1.0), re-measure in-cluster: confirm the deploy set is `{bybit_4edge}` and it deploys ~75% at target_vol 0.25 with `max_venue_weight=1.0`; then demonstrate the knob (`fxhnt fund-config set --max-venue-weight 0.6` → book caps to 60%). Cockpit local verify.

## Self-Review
- Coverage: book switch → Task A; venue-cap engine → Task B; venue-cap knob end-to-end → Task C; green → Task D.
- Byte-identity guards: `apply_venue_caps` no-op at 1.0; policy=None unchanged; fund_config fallback/ALTER default 1.0.
- Type consistency: `max_venue_weight`, `apply_venue_caps`, `FundConfig` fields used identically across tasks.
