# Paper book: risk-overlay re-tune — Implementation Plan (Phase 2F-A3g)

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

**Goal:** Re-tune the live paper-book risk overlay to fit the validated edges (per the A/B sweep on the
corrected curve): 30% full-Kelly vol-target, a dormant wide (−25/−12) killswitch, marginal-gate OFF.

**Architecture:** Constants + two call-site removals in `application/paper_book.py`. The overlay functions
(`vol_target_leverage`, `DrawdownKillSwitch`, `paper_risk_overlay`, `paper_book_returns`) are unchanged in
shape — only the parameter constants and the (removed) marginal-gate step change. Both sizing paths stay
consistent (live == replay).

**Tech Stack:** Python 3.12, pytest. **Repo:** `~/Work/fxhnt`, branch `feat/paper-overlay-retune`.
**Spec:** `docs/superpowers/specs/2026-06-22-paper-overlay-retune-design.md`.

**Verified context:**
- Constants in `paper_book.py`: `_TARGET_VOL=0.12, _KELLY_FRACTION=0.5, _MAX_LEVERAGE=3.0, _VOL_LOOKBACK=60,
  _PERIODS_PER_YEAR=365, _KILL_DD=0.15, _REENTER_DD=0.07`.
- `paper_risk_overlay` has `active = marginal_gate_active(returns_by_sleeve, active_sleeves)` before
  `weights = isv_adaptive_weights(returns_by_sleeve, active)`.
- `paper_book_returns` per-date loop has `active = [s for s,r in before_d.items() if r]` then
  `active = marginal_gate_active(before_d, active)` then `w_held = isv_adaptive_weights(before_d, active)`.
- Only one test asserts the gate-via-overlay behavior: `tests/unit/test_paper_risk_overlay.py::
  test_overlay_drops_dead_sleeve_from_weights`. `vol_target_leverage`/`DrawdownKillSwitch`/`marginal_gate_active`
  unit tests pass explicit params (not the module defaults), so the constant change does not affect them.

---

## Task 1: re-tune constants + disable marginal-gate in both paths

**Files:** Modify `src/fxhnt/application/paper_book.py`; Test update `tests/unit/test_paper_risk_overlay.py`

- [ ] **Step 1: Update the affected test FIRST** in `tests/unit/test_paper_risk_overlay.py`. Replace
  `test_overlay_drops_dead_sleeve_from_weights` (which asserted the gate benched a dead sleeve via the
  overlay) with a test asserting the gate is now OFF — the dead sleeve receives a (small, inverse-vol) weight:
```python
def test_overlay_no_longer_gates_dead_sleeve():
    # marginal-gate is OFF in the overlay (A3g): a dead sleeve is NOT benched here; it still receives an
    # inverse-vol weight. (The gate fn keeps its own direct unit tests in test_marginal_gate_active.)
    from fxhnt.application.paper_book import paper_risk_overlay
    good = {i + 1: v for i, v in enumerate([0.02, 0.01, 0.03, 0.00] * 15)}
    dead = {i + 1: v for i, v in enumerate([-0.02, -0.01, -0.03, 0.00] * 15)}
    w, _lev, _killed = paper_risk_overlay({"good": good, "dead": dead}, ["good", "dead"])
    assert w.get("dead", 0.0) > 0.0          # not benched anymore
    assert abs(sum(w.values()) - 1.0) < 1e-9
```
- [ ] **Step 2:** Run → FAIL (current overlay still gates the dead sleeve to 0).
- [ ] **Step 3a: Update the constants** in `paper_book.py`:
  `_TARGET_VOL = 0.30`, `_KELLY_FRACTION = 1.0`, `_KILL_DD = 0.25`, `_REENTER_DD = 0.12`.
  (Leave `_MAX_LEVERAGE=3.0`, `_VOL_LOOKBACK=60`, `_PERIODS_PER_YEAR=365`, `_L_MIN/_L_MAX/_PHI/_KAPPA`.)
- [ ] **Step 3b: Remove the marginal-gate call from `paper_risk_overlay`:** delete the
  `active = marginal_gate_active(returns_by_sleeve, active_sleeves)` line and weight directly over
  `active_sleeves`:
```python
    weights = isv_adaptive_weights(returns_by_sleeve, active_sleeves)
```
- [ ] **Step 3c: Remove the marginal-gate call from `paper_book_returns`:** in the per-date loop, delete the
  `active = marginal_gate_active(before_d, active)` line, keeping:
```python
        active = [s for s, r in before_d.items() if r]
        w_held = isv_adaptive_weights(before_d, active)
```
- [ ] **Step 3d:** Add a one-line note to the `paper_risk_overlay` docstring: overlay is 30% full-Kelly
  vol-target + a dormant wide (−25/−12) killswitch; marginal-gate intentionally OFF for the current small
  book (the `marginal_gate_active` fn is retained + unit-tested, re-enable when the book grows).
- [ ] **Step 4:** Run → PASS: `uv run pytest tests/unit/test_paper_risk_overlay.py -q`.
- [ ] **Step 5: Regression:** `uv run pytest tests/unit/test_marginal_gate_active.py tests/unit/test_vol_target_leverage.py tests/unit/test_drawdown_killswitch.py tests/unit/test_isv_adaptive_weights.py tests/integration/test_paper_backfill.py tests/integration/test_paper_snapshot.py tests/integration/test_paper_compounding.py -q` → all green (these use explicit params or assert shape, so the constant change is safe; report any that need a value update).
- [ ] **Step 6: Commit** (NEVER `git add -A`; never stage `*.dbn`):
```
git add src/fxhnt/application/paper_book.py tests/unit/test_paper_risk_overlay.py
git status --short | grep -i '\.dbn' && echo "ABORT: dbn staged" || echo "ok"
git commit -m "feat(paper): re-tune overlay (30% full-Kelly vol-target, wide dormant kill, gate off)"
```
End commit body with: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`

---

## Task 2: Full suite + finish + deploy + re-backfill + verify the re-tuned curve

- [ ] **Step 1:** `cd ~/Work/fxhnt && uv run pytest -q` → all green (update any remaining test that hard-coded
  the old overlay constants/gate behavior, preserving intent; report it).
- [ ] **Step 2:** `git status --short | grep -i '\.dbn'` must print nothing.
- [ ] **Step 3:** **superpowers:finishing-a-development-branch** — merge `feat/paper-overlay-retune` → master,
  push (auto-deploys via Gitea→Argo). Confirm cockpit workflow Succeeded + pods rolled.
- [ ] **Step 4:** Re-run backfill in-cluster:
  `kubectl exec deploy/dagster -c daemon -n foxhunt -- fxhnt paper-backfill --days 365`.
- [ ] **Step 5: VERIFY** — the re-tuned curve matches the sweep's 30%-full-Kelly row (~+150–200% gross, Sharpe
  ~3, DD ~−14%):
```bash
kubectl exec deploy/dagster -c daemon -n foxhunt -- python -c "
from sqlalchemy import create_engine, text
import statistics as st, math
from fxhnt.config import get_settings
e=create_engine(get_settings().operational_dsn)
with e.connect() as c:
    nav=[float(x[0]) for x in c.execute(text('select equity from paper_nav order by run_date'))]
    rets=[nav[i]/nav[i-1]-1 for i in range(1,len(nav)) if nav[i-1]]
    peak=0.0; mdd=0.0
    for v in nav:
        peak=max(peak,v); mdd=min(mdd, v/peak-1)
    sh=(st.mean(rets)/st.pstdev(rets)*math.sqrt(365)) if st.pstdev(rets)>0 else 0
    print(f'final/start: {nav[-1]:.0f}/{nav[0]:.0f} = {(nav[-1]/nav[0]-1)*100:.1f}%  Sharpe {sh:.2f}  maxDD {mdd*100:.1f}%')
"
```
  Confirm a strongly profitable curve (~Sharpe 3, ~+150–200%, DD ~−14%). Load `https://dashboard.fxhnt.ai/paper`
  + `/paper/replay`; report final equity, Sharpe, max-DD, and that `/paper` (live) agrees with the curve.

---

## Self-review notes
- Pure config change: 4 constants + 2 call-site removals; overlay fn shapes unchanged.
- Gate removed from BOTH `paper_risk_overlay` and `paper_book_returns` → live == replay preserved.
- Wide kill (−25/−12) is dormant at 30% vol (vol-target caps DD below threshold) = free tail-insurance; kept.
- `marginal_gate_active` fn + its tests retained (re-enablable); only the call sites removed.
- A3f equity accounting + shadow signal + sizing mechanics untouched.
- NEVER `git add -A`; verify no `*.dbn` staged before each commit.

## Acceptance criteria (from spec)
- constants 0.30/1.0/0.25/0.12; overlay tests green. ✅ T1
- marginal-gate not called by overlay/book-returns; the gate-via-overlay test updated. ✅ T1
- full suite green. ✅ T1, T2
- re-backfill → re-tuned profitable curve (~Sharpe 3, ~+150–200%, ~−14% DD); /paper agrees; verified. ✅ T2
- accounting/strategy mechanics read-only; no `*.dbn`. ✅ T2
