# Honest-Inputs Load-Once/Reprice Refactor Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Checkbox (`- [ ]`) steps.

**Goal:** Refactor `allocation_honest_inputs` so each crypto sleeve's AUM-INDEPENDENT panels (weights + turnover) are loaded ONCE and repriced across all capacity-curve AUMs, instead of a full panel reload per AUM — so the nightly can use a wider AUM grid without OOM-killing on the book deploy set.

**Architecture:** Split the current load+reprice `_sleeve_series`/`_book_series`/`_deploy_series` into a load step (`_sleeve_inputs` → (wbd, turnover), heavy, once) and a reprice step (reuse `honest_reference.honest_sleeve_returns` / `capacity_curve`, cheap, per AUM). `honest_allocation_inputs` loads once per sid then reprices for base + the whole curve. Byte-identical honest series/ceilings; only the compute schedule changes. Then widen the nightly AUM grid.

**Tech Stack:** Python 3.12, pytest. The honest-reference REPORT path (`honest_report.py`) already does load-once/reprice — this brings the allocation provider to the same pattern (and reuses `capacity_curve`).

## Global Constraints

- **Byte-identity:** for the same store/spread/unlock_events/aums, the NEW `honest_allocation_inputs` must return the SAME `returns_by_strategy` (ISO-keyed) and the SAME `ceilings` as the OLD one. The series math is unchanged — same `wbd`, same `turnover`, same `honest_sleeve_returns(...)` calls; only WHEN the panel is loaded changes.
- **Load-once invariant:** `store.read_panel(...)` must be called ONCE per distinct sleeve (1 for a sleeve sid, 3 for the `bybit_4edge` book) regardless of how many AUMs are in the grid — NOT once per (sleeve × AUM). Enforce with a test that counts `read_panel` calls.
- `_BOOK_SLEEVES = ("xsfunding", "unlock", "positioning")`, `_CRYPTO_DEPLOY_SIDS = {positioning, xsfunding, unlock, bybit_4edge}` — unchanged. tstrend still excluded (no standalone store series).
- The old `_sleeve_series`/`_book_series`/`_deploy_series` become dead after the refactor → DELETE them (wire-or-delete; no dead code). They are used ONLY inside `allocation_honest_inputs.py` (verified — the report path has its own impl).
- Reuse `honest_reference.capacity_curve` for the sleeve reprice (do not reimplement the per-AUM loop).
- Commit trailer: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`.

---

### Task 1: load-once/reprice refactor of the provider

**Files:**
- Modify: `src/fxhnt/application/allocation_honest_inputs.py`
- Test: `tests/unit/test_allocation_honest_inputs.py`

**Interfaces:**
- Consumes: `sleeve_weights_and_contributions` (bybit_paper_weights), `honest_sleeve_returns` + `capacity_curve` (honest_reference), `book_returns` + `risk_parity_weights` + `sharpe_of` (honest_gates), `capacity_ceiling_from_curve` (allocation_engine).
- Produces (new internal helpers, replacing the old ones):
  - `_sleeve_inputs(store, sleeve, unlock_events) -> tuple[dict, dict] | None`
  - `_book_curve(store, spread_bps, unlock_events, aums, *, participation_cap, impact_k) -> dict[float, dict[int,float]]`
  - `_deploy_curve(store, sid, spread_bps, unlock_events, aums, *, participation_cap, impact_k) -> dict[float, dict[int,float]]`
  - `honest_allocation_inputs(...)` — SAME public signature and return type.

- [ ] **Step 1: Write the load-once-count test first** (extend `test_allocation_honest_inputs.py`). Wrap the fake store so `read_panel` increments a counter; call `honest_allocation_inputs(deploy_sids={"bybit_4edge"}, target_aum=100_000, aums=[35_000,100_000,350_000,1_000_000,10_000_000])` (5 AUMs) and assert `read_panel` was called exactly **3 times** (one per book sleeve), NOT 3×(1+5). Also add a sleeve case: `deploy_sids={"positioning"}` with the same 5 AUMs → `read_panel` called exactly **1 time**.

Look at the existing test file's fake store / `_seed(...)` pattern and mirror it — wrap or subclass the store used there to count `read_panel`.

- [ ] **Step 2: Run — expect FAIL** (current code reloads per AUM → 18 and 6 calls)

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/unit/test_allocation_honest_inputs.py -q`
Expected: the new count tests FAIL (too many read_panel calls); the existing series/ceiling tests still PASS.

- [ ] **Step 3: Implement the refactor.** Replace `_sleeve_series`, `_book_series`, `_deploy_series` with:

```python
def _sleeve_inputs(store: Any, sleeve: str,
                   unlock_events: list[dict[str, Any]] | None) -> tuple[dict, dict] | None:
    """AUM-INDEPENDENT heavy inputs for a sleeve: (weights_by_day, turnover_panel). Load ONCE, reprice across
    AUMs via honest_sleeve_returns/capacity_curve — the turnover panel read is the memory cost, so it must NOT
    be reloaded per AUM. None when the sleeve is uncomputable (e.g. unlock with no events). READ-ONLY."""
    wbd = sleeve_weights_and_contributions(store, sleeve, unlock_events=unlock_events)
    if not wbd:
        return None
    symbols = sorted({s for row in wbd.values() for s in row})
    turnover = store.read_panel(symbols, "turnover")
    return wbd, turnover


def _book_curve(store: Any, spread_bps: dict[str, float], unlock_events: list[dict[str, Any]] | None,
                aums, *, participation_cap: float, impact_k: float) -> dict[float, dict[int, float]]:
    """{aum: risk-parity BOOK series} across `aums`, loading each `_BOOK_SLEEVES` sleeve's inputs ONCE then
    repricing per AUM. Empty sleeves (uncomputable) are dropped; an AUM with no computable sleeve -> {}."""
    loaded = {sleeve: inp for sleeve in _BOOK_SLEEVES
              if (inp := _sleeve_inputs(store, sleeve, unlock_events)) is not None}
    out: dict[float, dict[int, float]] = {}
    for aum in aums:
        per: dict[str, dict[int, float]] = {}
        for sleeve, (wbd, turnover) in loaded.items():
            series = honest_sleeve_returns(wbd, turnover, spread_bps, aum=aum,
                                           participation_cap=participation_cap, impact_k=impact_k)
            if series:
                per[sleeve] = series
        out[aum] = book_returns(per, risk_parity_weights(per)) if per else {}
    return out


def _deploy_curve(store: Any, sid: str, spread_bps: dict[str, float],
                  unlock_events: list[dict[str, Any]] | None, aums, *,
                  participation_cap: float, impact_k: float) -> dict[float, dict[int, float]]:
    """{aum: honest series} for a crypto deploy `sid` across `aums`, loading AUM-independent inputs ONCE.
    `bybit_4edge` -> the risk-parity book; a sleeve sid -> its own series (reuses `capacity_curve`); other -> {}."""
    if sid == "bybit_4edge":
        return _book_curve(store, spread_bps, unlock_events, aums,
                           participation_cap=participation_cap, impact_k=impact_k)
    if sid in _CRYPTO_DEPLOY_SIDS:
        inputs = _sleeve_inputs(store, sid, unlock_events)
        if inputs is None:
            return {aum: {} for aum in aums}
        wbd, turnover = inputs
        return capacity_curve(wbd, turnover, spread_bps, aums=aums,
                              participation_cap=participation_cap, impact_k=impact_k)
    return {aum: {} for aum in aums}
```

Rewrite the loop body of `honest_allocation_inputs` (keep the signature + the non-crypto branch) so it loads/reprices once:

```python
    for sid in deploy_sids:
        if sid not in _CRYPTO_DEPLOY_SIDS:
            ceilings[sid] = equity_default_ceiling      # equity / non-crypto — caller falls back; no series
            continue
        # AUM-independent panels loaded ONCE inside _deploy_curve; base + curve are repriced from them.
        all_aums = sorted({float(target_aum), *(float(a) for a in aums)})
        curve_series = _deploy_curve(store, sid, spread_bps, unlock_events, all_aums,
                                     participation_cap=participation_cap, impact_k=impact_k)
        base = curve_series.get(float(target_aum), {})
        if base:
            returns_by_strategy[sid] = {_iso(day): ret for day, ret in base.items()}
        curve = {aum: sharpe_of(curve_series.get(float(aum), {})) for aum in aums}
        ceilings[sid] = capacity_ceiling_from_curve(curve, min_sharpe)
```

Update imports (add `capacity_curve` from honest_reference) and the module docstring (deploy→honest mapping now describes the load-once/reprice; the report path already does this).

**Note on float keying:** the curve dict is keyed by the AUM values passed to `_deploy_curve`. Build `all_aums` and look up with the SAME float() cast on both sides (as shown) so `target_aum` and grid entries match by key even if one is int and one is float.

- [ ] **Step 4: Run — expect PASS** (count tests pass; series/ceiling tests unchanged and still pass = byte-identity held)

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

- [ ] **Step 5: Confirm no other consumer of the deleted helpers**

Run: `cd /home/jgrusewski/Work/fxhnt && grep -rn "_sleeve_series\|_book_series\|_deploy_series" src/ tests/ | grep -v "docstring\|# "`
Expected: no CODE references remain (only prose/docstring mentions, which you should also update if they name the removed functions).

- [ ] **Step 6: Commit**

```bash
git add src/fxhnt/application/allocation_honest_inputs.py tests/unit/test_allocation_honest_inputs.py
git commit -m "refactor(alloc): load-once/reprice honest inputs (panels loaded once, repriced per AUM)

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

---

### Task 2: widen the nightly AUM grid + full suite green

**Files:**
- Modify: `src/fxhnt/application/allocation_ingest.py`

- [ ] **Step 1:** In `record_allocation`, widen the capacity-curve grid back to characterize the book's break with headroom (now cheap — one load, many reprices): `aums=[35_000, 100_000, 350_000]` → `aums=[35_000, 100_000, 350_000, 1_000_000, 10_000_000]`. Update the leading comment: the load-once/reprice provider (Task 1) makes grid length cheap (repricing, not reloading), so the wider grid no longer OOMs; it restores capacity resolution above the book's ~$350k break so growth past it is detectable.

- [ ] **Step 2: Full unit suite**

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

- [ ] **Step 3: Allocation/honest integration**

Run: `cd /home/jgrusewski/Work/fxhnt && .venv/bin/python3 -m pytest tests/integration -k "allocation or honest or funding" -q`
Expected: all pass (2 skipped ok).

- [ ] **Step 4: Commit**

```bash
git add src/fxhnt/application/allocation_ingest.py
git commit -m "feat(alloc): widen nightly capacity-curve AUM grid (load-once makes it cheap)

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

## Post-implementation (controller-run, in-cluster)
Deploy (git-sync), re-run `record_allocation` with the honest store: confirm (1) NO OOM with the widened 5-AUM grid on the book deploy set, (2) `read_panel` load-once holds (the whole point), (3) the book ceiling is resolved over the wider grid (still ~$350k or refined), (4) deployment still ~$75k (75%) — byte-identical sizing, just a wider capacity probe.

## Self-Review
- Byte-identity: same wbd/turnover/honest_sleeve_returns → identical series+ceilings (only load schedule changed); existing series/ceiling tests are the guard.
- Load-once: enforced by the read_panel-count test (invariant, not a comment).
- Dead code: old three helpers deleted; grep gate in Task 1 Step 5.
- Reuse: `capacity_curve` reused for the sleeve path (DRY).
