# Paper book: ISV-adaptive vol budget — Design (Phase 2F-A3h)

**Date:** 2026-06-22
**Status:** Design (approved)
**Repo:** fxhnt
**Follows:** A3g (fixed 30% full-Kelly overlay). Makes the vol BUDGET itself state-adaptive.

## Motivation

A3g set a fixed 30% full-Kelly vol budget. The adaptive-budget A/B sweep on the corrected curve showed an
**ISV-adaptive budget bounded [25%, 35%] beats both fixed 30% and fixed 35% on Sharpe** (3.39 vs 3.24/3.28),
at +221% / −14.5% DD. The win is NOT from pressing into strength — it's from **easing the budget toward 25%
during weak patches** (when trailing book Sharpe softens) and running near the 35% ceiling when the edge is
firing. User cap: 35% ("40% is massive"); this design never exceeds it.

## Decisions (from the sweep + user)

- **Adaptive target-vol scaled by trailing book Sharpe**, bounded **[25%, 35%]**, reference Sharpe **2.0**
  (the "eager" winner — reaches the 35% ceiling once the trailing book Sharpe ≥ 2, eases to 25% as it →0).
  Bootstrap to 30% until enough history.
- **Same signal in BOTH sizing paths** — `paper_book_returns` (per historical date, from the running book
  series it already builds) AND `paper_risk_overlay` (latest date, from the full prior book series) — so live
  `/paper` == `/paper/replay` stays exact.
- **Causal:** the budget for date d uses the trailing realized book returns STRICTLY BEFORE d.
- Everything else from A3g unchanged: full-Kelly, dormant wide −25/−12 killswitch, marginal-gate OFF.

## Components

1. **`adaptive_target_vol(book_returns, *, vol_min=_VOL_MIN, vol_max=_VOL_MAX, sh_ref=_VOL_SH_REF,
   base=_VOL_BASE, win=_VOL_LOOKBACK, min_obs=_VOL_MIN_OBS) -> float`** (pure, `paper_book.py`):
   - `w = book_returns[-win:]`; if `len(w) < min_obs` → return `base` (bootstrap).
   - `sd = pstdev(w)`; `ts = mean(w)/sd·√periods_per_year` if `sd>0` else `0.0` (trailing ann. book Sharpe).
   - `frac = clip(ts / sh_ref, 0.0, 1.0)`; return `vol_min + (vol_max − vol_min)·frac`.
   - Constants: `_VOL_MIN=0.25`, `_VOL_MAX=0.35`, `_VOL_SH_REF=2.0`, `_VOL_BASE=0.30` (rename of the A3g
     `_TARGET_VOL=0.30` → kept as the bootstrap base), `_VOL_MIN_OBS=20`, reuse `_VOL_LOOKBACK=60`,
     `_PERIODS_PER_YEAR=365`.
2. **`paper_book_returns`:** in the per-date loop, after `out[d]` is set, compute
   `tv = adaptive_target_vol(list(out.values()))` and pass `target_vol=tv` to `vol_target_leverage` (replacing
   the fixed `target_vol`). The book series `out` it already accumulates is the causal trailing signal.
3. **`paper_risk_overlay`:** after `bret = paper_book_returns(returns_by_sleeve)`, compute
   `tv = adaptive_target_vol(list(bret.values()))` and use it for the latest-date
   `vol_target_leverage(weights, returns_by_sleeve, target_vol=tv, ...)`. (Killswitch replay over `bret`
   unchanged.) This matches what `paper_book_returns` would compute for the next date → consistent.
4. `vol_target_leverage` itself is the unchanged primitive (still takes an explicit `target_vol`).

## Data flow

Per date (both paths): trailing realized book returns (strictly prior, from the series `paper_book_returns`
builds / the `bret` the overlay gets) → `adaptive_target_vol` → bounded [25%,35%] budget → `vol_target_leverage`
→ leverage. Bootstrap 30% until 20 obs. The adaptive budget rides near 35% in strong stretches, eases to 25%
in weak ones; the dormant −25/−12 kill backstops a true crash.

## Risks / safety

- **Read-only/additive w.r.t. accounting + strategy mechanics:** only the vol BUDGET becomes adaptive (a
  function of the book's own trailing returns). A3f equity accounting, shadow signal, ISV weights, killswitch,
  and the `vol_target_leverage` primitive are unchanged. No book_allocator/sleeve/broker change.
- **Bounded — never "massive":** hard `[vol_min=0.25, vol_max=0.35]` clip; `_MAX_LEVERAGE=3.0` still caps
  notional; the budget can only move within the user's ceiling.
- **Procyclicality** (the honest risk of any performance-scaled budget): mitigated because (a) the win comes
  from EASING in weak patches, not pressing into tops; (b) it's bounded to a narrow [25,35] band; (c) the
  vol-target + dormant kill remain. Validated on the corrected curve (Sharpe 3.39 > fixed) — but it is ONE
  ~1yr sample; documented, not a walk-forward claim.
- **Causality:** `adaptive_target_vol` consumes only the strictly-prior book series (`out` through d−1 /
  `bret` over prior dates). No look-ahead.
- **Consistency:** identical `adaptive_target_vol` call in both paths → live `/paper` == `/paper/replay`.
- **Degenerate/cold:** `<min_obs` → base 0.30; zero-vol window → ts 0 → budget vol_min (25%); never NaN
  (guarded `sd>0`).

## Out of scope

Fee/slippage model; walk-forward / multi-window validation of the adaptive rule; per-sleeve adaptive budgets;
re-enabling the marginal-gate; adding xsfunding to replay.

## Acceptance criteria

- `adaptive_target_vol`: bootstrap (`<min_obs` → base 0.30); strong trailing Sharpe (≥ sh_ref) → vol_max
  (0.35); weak/negative → vol_min (0.25); monotone in trailing Sharpe; bounded [0.25,0.35]; never NaN;
  unit-tested.
- `paper_book_returns` + `paper_risk_overlay` both use it on the strictly-prior book series; causal; the two
  paths produce identical budgets for identical history (live==replay); unit-tested.
- Full suite green (overlay tests updated for the adaptive budget where they implied a fixed one).
- Re-run backfill in-cluster → `/paper/replay` curve ≈ the sweep's ADAPT[25→35] row (~+220% gross, Sharpe
  ~3.4, DD ~−14.5%), capped at the 35% budget; `/paper` live agrees; verified post-deploy.
- Read-only/additive (accounting/strategy mechanics); no `*.dbn` staged.
