# Capacity-Aware Capital Allocator — 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:** Upgrade the existing B2a allocation engine so it weights edges by inverse-vol (risk-parity, capped), caps each edge at its honest-reference capacity ceiling, and sizes from the honest-reference backtest (not the too-short forward record) — with the forward gate remaining the live-deploy switch.

**Architecture:** A focused change to `allocation_engine.compute_allocation` (equal-weight → inverse-vol capped) + a per-strategy capacity ceiling cap in `target_capital`, fed by a new provider that maps each deploy strategy to its honest-reference return series + capacity ceiling. `record_allocation` sources returns from that provider instead of `nav_history`. Everything else (forward-gating, cross-venue, funding-decay, killswitch, vol-target, $ conversion) is reused unchanged.

**Tech Stack:** Python 3.12, numpy (already used in `allocation_engine`), the merged honest-reference modules (`honest_reference`, `honest_gates`), pytest.

## Global Constraints

- **Upgrade, not a new system.** Reuse `AllocationPolicy`, `full_history_vol`, `equal_weight_marginal_prune`, `killswitch_active`, `target_capital`, `record_allocation`, `StrategyAllocationRow`, the rebalancers. Do not duplicate.
- **Scope = paper.** No change to the `capital_ceiling` ($35k human gate) or going live; no new execution path. Forward-gating (deploy + promoted) stays the live-deploy switch.
- **Sizing basis = the honest-reference backtest series**, NOT `nav_history` (8-day forward, fails `min_history_days`). The forward gate is the live-deploy switch only.
- **Weighting = inverse-vol (risk-parity), capped at `max_strategy_weight`** (concentration prudence — a young, not-yet-forward-proven fund must not bet on one edge).
- **Capacity ceiling per deploy strategy**, from the honest-reference: `bybit_4edge` = book-level (its sleeves, tstrend excluded — retired standalone), `positioning` = its sleeve curve, `multistrat` (IBKR) = a documented high **equity default** ($10M) — do NOT fabricate a crypto number for equity.
- `capacity_min_sharpe` default **1.0**; bump `ALLOCATION_POLICY_VERSION` on any policy field change.
- Tests run `pytest -o addopts=""` (repo `addopts` injects `-n`/xdist, absent locally). Use `python3`.

## File Structure

- `src/fxhnt/application/allocation_engine.py` — add `inverse_vol_weights`, `capacity_ceiling_from_curve`; change `compute_allocation`'s base weights; add the capacity cap to `target_capital`.
- `src/fxhnt/application/allocation_honest_inputs.py` — NEW: `honest_allocation_inputs(store, spread_bps, unlock_events, *, deploy_sids, target_aum, ...) -> tuple[dict[str, dict[int,float]], dict[str,float]]` returning (per-strategy honest return series, per-strategy capacity ceiling). The deploy-strategy → honest-reference mapping lives here.
- `src/fxhnt/application/allocation_policy.py` — add `capacity_min_sharpe`, `equity_default_ceiling`; bump version.
- `src/fxhnt/application/allocation_ingest.py` — `record_allocation` sources returns + ceilings from the provider.
- Tests: `tests/unit/test_allocation_engine.py` (extend), `tests/unit/test_allocation_honest_inputs.py`, `tests/integration/test_record_allocation_capacity.py`.

---

### Task 1: `inverse_vol_weights` — risk-parity base weights

**Files:** Modify `src/fxhnt/application/allocation_engine.py`; Test `tests/unit/test_allocation_engine.py`.

**Interfaces:**
- Consumes: `full_history_vol(returns) -> float` (already in the module).
- Produces: `inverse_vol_weights(returns_by_strategy: dict[str, dict[str,float]]) -> dict[str,float]` (∝ 1/full_history_vol, summing to 1; zero-vol strategy → weight 0; all-degenerate → equal weight).

- [ ] **Step 1: Write the failing test**
```python
# tests/unit/test_allocation_engine.py
from fxhnt.application.allocation_engine import inverse_vol_weights

def test_inverse_vol_weights_favour_low_vol_and_sum_to_one():
    rbs = {"lo": {"1": 0.001, "2": -0.001, "3": 0.001},
           "hi": {"1": 0.10,  "2": -0.10,  "3": 0.10}}
    w = inverse_vol_weights(rbs)
    assert abs(sum(w.values()) - 1.0) < 1e-9 and w["lo"] > w["hi"]
```
- [ ] **Step 2: Run** `python3 -m pytest tests/unit/test_allocation_engine.py::test_inverse_vol_weights_favour_low_vol_and_sum_to_one -v -o addopts=""` → FAIL (undefined).
- [ ] **Step 3: Implement** (add near `full_history_vol`):
```python
def inverse_vol_weights(returns_by_strategy: dict[str, dict[str, float]]) -> dict[str, float]:
    """Risk-parity base: weight each strategy ∝ 1/full_history_vol, normalized to sum 1. Zero/degenerate-vol
    strategy → 0; all-degenerate → equal weight. Reuses the ex-ante full-history vol (never trailing)."""
    inv = {s: (1.0 / v if (v := full_history_vol(r)) > 0.0 else 0.0) for s, r in returns_by_strategy.items()}
    total = sum(inv.values())
    if total <= 0.0:
        n = len(returns_by_strategy)
        return {s: 1.0 / n for s in returns_by_strategy} if n else {}
    return {s: v / total for s, v in inv.items()}
```
- [ ] **Step 4: Run** the test → PASS.
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat(alloc): inverse-vol (risk-parity) base weights"`

---

### Task 2: `capacity_ceiling_from_curve` — the ceiling from a capacity curve

**Files:** Modify `src/fxhnt/application/allocation_engine.py`; Test `tests/unit/test_allocation_engine.py`.

**Interfaces:**
- Produces: `capacity_ceiling_from_curve(curve: dict[float,float], min_sharpe: float) -> float | None` — the LARGEST aum whose honest Sharpe ≥ `min_sharpe`; `None` if the curve is empty or the first (smallest) AUM already fails (edge unviable even at the smallest size); the largest tested AUM if it never drops below (unbounded in-range).

- [ ] **Step 1: Write the failing test**
```python
def test_capacity_ceiling_is_last_aum_above_min_sharpe():
    from fxhnt.application.allocation_engine import capacity_ceiling_from_curve
    curve = {35_000: 3.5, 100_000: 2.5, 350_000: 0.3, 1_000_000: -2.5}   # xsfunding-shaped
    assert capacity_ceiling_from_curve(curve, 1.0) == 100_000            # last AUM with Sh >= 1.0
    robust = {35_000: 2.9, 1_000_000: 2.6, 10_000_000: 1.6}
    assert capacity_ceiling_from_curve(robust, 1.0) == 10_000_000        # never drops -> top of range
    assert capacity_ceiling_from_curve({35_000: 0.2}, 1.0) is None       # unviable even at smallest
    assert capacity_ceiling_from_curve({}, 1.0) is None
```
- [ ] **Step 2: Run** → FAIL.
- [ ] **Step 3: Implement**
```python
def capacity_ceiling_from_curve(curve: dict[float, float], min_sharpe: float) -> float | None:
    """Largest AUM whose honest Sharpe >= min_sharpe (the capacity ceiling). None if empty or the edge is
    unviable even at the smallest tested AUM. Report the ceiling next to the curve so the threshold is visible."""
    ok = [aum for aum in sorted(curve) if curve[aum] >= min_sharpe]
    return max(ok) if ok else None
```
- [ ] **Step 4: Run** → PASS.
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat(alloc): capacity ceiling from an honest capacity curve"`

---

### Task 3: `compute_allocation` uses inverse-vol (capped), not equal-weight

**Files:** Modify `src/fxhnt/application/allocation_engine.py`; Test `tests/unit/test_allocation_engine.py`.

**Interfaces:** Consumes `inverse_vol_weights` (Task 1). `compute_allocation` signature unchanged.

- [ ] **Step 1: Write the failing test** — the survivor base is inverse-vol capped, not 1/n:
```python
def test_compute_allocation_uses_inverse_vol_base_capped():
    from fxhnt.application.allocation_engine import compute_allocation
    from fxhnt.application.allocation_policy import AllocationPolicy
    # two survivors, very different vol; both pass the (disabled) marginal gate; enough history
    n = 400
    rbs = {"lo": {str(i): (0.002 if i % 2 else -0.002) for i in range(n)},
           "hi": {str(i): (0.05 if i % 2 else -0.05) for i in range(n)}}
    pol = AllocationPolicy(marginal_gate=False, min_history_days=60, max_strategy_weight=0.9)
    w = compute_allocation(rbs, pol)
    assert w["lo"] > w["hi"]           # low-vol gets more (was equal under the old code)
```
- [ ] **Step 2: Run** → FAIL (old code gives equal base → `w["lo"] == w["hi"]`).
- [ ] **Step 3: Implement** — in `compute_allocation`, replace the equal-weight `base` line
```python
    base = {s: min(1.0 / n, policy.max_strategy_weight) for s in survivors}
```
with inverse-vol capped:
```python
    rp = inverse_vol_weights({s: eligible[s] for s in survivors})
    base = {s: min(rp.get(s, 0.0), policy.max_strategy_weight) for s in survivors}
```
(Keep everything else — the NO-renormalise cap behavior, killswitch, vol-target K — identical.)
- [ ] **Step 4: Run** the new test AND the full existing allocation suite `python3 -m pytest tests/unit/test_allocation_engine.py -v -o addopts=""`. Some existing equal-weight assertions may need updating to the inverse-vol expectation — update them to the correct new values, do NOT weaken. Expected: all pass.
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat(alloc): compute_allocation weights by inverse-vol (capped) not equal-weight"`

---

### Task 4: Honest-reference allocation inputs provider (series + ceilings per deploy strategy)

**Files:** Create `src/fxhnt/application/allocation_honest_inputs.py`; Test `tests/unit/test_allocation_honest_inputs.py`.

**Interfaces:**
- Consumes: `honest_sleeve_returns` (`honest_reference`), `book_returns`/`risk_parity_weights` (`honest_gates`), `sleeve_weights_and_contributions` (`bybit_paper_weights`), `capacity_curve` (`honest_reference`), `capacity_ceiling_from_curve` (Task 2).
- Produces: `honest_allocation_inputs(store, spread_bps, unlock_events, *, deploy_sids, target_aum, aums, participation_cap=0.03, impact_k=1.0, min_sharpe=1.0, equity_default_ceiling=10_000_000.0) -> tuple[dict[str, dict[int,float]], dict[str,float]]` → (returns_by_strategy, capacity_ceiling_by_strategy). Pure (store reads only).

- [ ] **Step 1: Confirm the mapping (read, no code yet).** Read `honest_report.build_honest_report` for how sleeves → honest series + capacity curves are produced; confirm `bybit_4edge`'s sleeves in the registry (`["tstrend","unlock","xsfunding","positioning"]`) and that `tstrend` has no standalone store series (excluded). Note the mapping to write:
  - `positioning` → its own `honest_sleeve_returns` series + `capacity_ceiling_from_curve` of its capacity curve.
  - `bybit_4edge` → the risk-parity **book** of its available crypto sleeves (`xsfunding`, `unlock`, `positioning`; `tstrend` excluded) via `book_returns`; ceiling from the book's capacity curve.
  - `multistrat` (or any non-crypto deploy sid) → `equity_default_ceiling`, series left to the caller's fallback (out of v1 crypto scope; deploy set is currently crypto-only).
- [ ] **Step 2: Write the failing test** with a fake `_FeatureStore` (model on `tests/integration/test_honest_reference.py`'s fake store) for `deploy_sids={"positioning","bybit_4edge"}`:
```python
def test_provider_returns_series_and_ceilings_per_deploy_strategy():
    from fxhnt.application.allocation_honest_inputs import honest_allocation_inputs
    store = _fake_store()                      # supports xsfunding + positioning sleeves
    aums = [35_000, 100_000, 350_000, 1_000_000, 10_000_000]
    series, ceilings = honest_allocation_inputs(store, {}, [], deploy_sids={"positioning", "bybit_4edge"},
                                                target_aum=35_000, aums=aums)
    assert set(series) == {"positioning", "bybit_4edge"} and all(len(s) > 1 for s in series.values())
    assert set(ceilings) == {"positioning", "bybit_4edge"}       # each has a ceiling (float or None)
```
- [ ] **Step 3: Run** → FAIL (module missing).
- [ ] **Step 4: Implement** the provider per the Step-1 mapping: for each crypto deploy sid, build the honest series at `target_aum` and the capacity curve across `aums` → ceiling; for a non-crypto sid, ceiling = `equity_default_ceiling` (series omitted, caller falls back). Return `(series, ceilings)`.
- [ ] **Step 5: Run** the test → PASS. **Commit** `git add -A && git commit -m "feat(alloc): honest-reference series+ceiling provider per deploy strategy"`

---

### Task 5: `record_allocation` sizes from the provider + applies the capacity cap

**Files:** Modify `src/fxhnt/application/allocation_engine.py` (`target_capital` capacity cap) and `src/fxhnt/application/allocation_ingest.py`; Test `tests/integration/test_record_allocation_capacity.py`.

**Interfaces:** `target_capital(target_weights, equity, policy, *, capacity_ceiling=None)` — new optional per-strategy dollar ceiling.

- [ ] **Step 1: Write the failing test** for the capacity cap in `target_capital`:
```python
def test_target_capital_caps_at_per_strategy_capacity():
    from fxhnt.application.allocation_engine import target_capital
    from fxhnt.application.allocation_policy import AllocationPolicy
    pol = AllocationPolicy(capital_ceiling=1_000_000.0)
    w = {"cap_limited": 0.5, "robust": 0.5}
    d = target_capital(w, equity=1_000_000.0, policy=pol,
                       capacity_ceiling={"cap_limited": 100_000.0})   # cap_limited breaks at $100k
    assert d["cap_limited"] <= 100_000.0 and d["robust"] > d["cap_limited"]
```
- [ ] **Step 2: Run** → FAIL (no `capacity_ceiling` param).
- [ ] **Step 3: Implement** the cap in `target_capital` (apply `min(dollars, capacity_ceiling[s])` per strategy after the existing `capital_ceiling` scale; `None`/missing → no cap). Then wire `record_allocation`: build `returns` and `ceilings` from `honest_allocation_inputs(...)` (target_aum = the equity) instead of `nav_history`, and pass `capacity_ceiling=ceilings` to `target_capital`. Keep the deploy+promoted gating, funding-decay, and killswitch exactly as-is.
- [ ] **Step 4: Run** the target_capital test AND an integration test of `record_allocation` against a fake store + a small in-memory DB (model on existing `allocation_ingest` tests if present; else assert the written `StrategyAllocationRow`s respect the ceilings). Expected: PASS.
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat(alloc): record_allocation sizes from honest-reference + caps at capacity (spec 4b/4c)"`

---

### Task 6: Policy knobs + capacity-ceiling visibility

**Files:** Modify `src/fxhnt/application/allocation_policy.py`; Modify the honest-report/cockpit display; Test `tests/unit/test_allocation_policy.py`.

**Interfaces:** `AllocationPolicy` gains `capacity_min_sharpe: float = 1.0` and `equity_default_ceiling: float = 10_000_000.0`; `ALLOCATION_POLICY_VERSION` bumped to 3.

- [ ] **Step 1: Write the failing test**
```python
def test_policy_has_capacity_knobs_and_version_bumped():
    from fxhnt.application.allocation_policy import AllocationPolicy, ALLOCATION_POLICY_VERSION
    p = AllocationPolicy()
    assert p.capacity_min_sharpe == 1.0 and p.equity_default_ceiling == 10_000_000.0
    assert ALLOCATION_POLICY_VERSION >= 3
```
- [ ] **Step 2: Run** → FAIL.
- [ ] **Step 3: Implement** — add the two fields to `AllocationPolicy`; bump `ALLOCATION_POLICY_VERSION = 3`. Surface the per-strategy capacity ceiling in `build_honest_report`'s output (a `"capacity_ceiling"` per sleeve/book next to its curve) so the threshold choice is visible (spec §8).
- [ ] **Step 4: Run** the test + the honest-report test (confirm the new key is present, existing shape intact). Expected: PASS.
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat(alloc): capacity policy knobs + ceiling shown next to the curve (spec 6/8)"`

---

## Self-Review

**Spec coverage:** §4a inverse-vol → Tasks 1,3. §4b capacity cap → Tasks 2,5. §4c sizing-from-backtest → Tasks 4,5. §5 granularity (book/sleeve/equity-default) → Task 4. §6 forward-gating → unchanged (reused; Task 5 keeps deploy+promoted). §7 scope (paper, no live) → all tasks read-only-ish (record_allocation writes only the allocation table it already owns; no new execution). §8 ceiling visibility → Task 6. §9 decisions → knobs Task 6.

**Placeholder scan:** Tasks 1-3,6 have complete code. Task 4 Step 1 is a deliberate READ (the deploy-strategy→honest mapping must be confirmed against the real registry/store before coding — the crux), then Steps 4 gives the mapping to implement; Task 5 Step 3 describes the wiring against the confirmed provider signature. Acceptable — these are integration tasks whose exact store fixtures depend on Task 4's confirmed API, not hidden work.

**Type consistency:** honest series are `dict[int,float]` (epoch-day keyed, matching `honest_sleeve_returns`); `full_history_vol`/`compute_allocation` take `dict[str,float]` (date-string keyed) — **the provider (Task 4) must return the series in the shape `compute_allocation` expects (str-keyed dates)** or convert at the seam; call this out in Task 4/5 and add an assertion. Weights/ceilings are `dict[str,float]`.

---

## Execution Handoff

Plan complete and saved to `docs/superpowers/plans/2026-07-15-capacity-aware-capital-allocator.md`. Two execution options:
1. **Subagent-Driven (recommended)** — fresh subagent per task, review between tasks (this is what caught the honest-reference bugs).
2. **Inline Execution** — execute in this session with checkpoints.

Which approach?
