# Drawdown controller v1 — Design (Phase 2F-A9)

**Date:** 2026-06-22
**Status:** Design (approved — build v1 core)
**Repo:** fxhnt

## Motivation / reframe
We proved (Kelly sweep): **drawdown scales with how much the book buys; Sharpe is ~leverage-invariant**
(kelly 0.25→−24%DD/Sh1.6, 1.0→−33%/2.2, 1.5→−39%/2.1). So DD is a SIZING CHOICE, not an edge risk.
The current `DrawdownKillSwitch` is the wrong tool: it's binary and fires on PRICE drawdown (which includes
recoverable market moves) → **whipsaw** (it made the −98.9% bug WORSE). An excellent controller is ~90%
ex-ante sizing + a surgical reaction that fires only on **edge decay**, never on raw price-DD.

Two different questions, two different mechanisms:
1. "How much DD do I budget?" → set by LEVERAGE ex-ante (Sharpe-invariant).
2. "Is this DD recoverable noise or is the edge dying?" → the only thing worth reacting to → react to
   EDGE-HEALTH (Page-Hinkley on realized PnL drift), per sleeve, continuously.

## Architecture — multiplicative, per-sleeve sizing stack
For each sleeve `s`, day `t`:  `L[s,t] = K(D*) · V[s,t] · H[s,t]`  (then a no-trade hysteresis band — v2).
Replaces the portfolio `DrawdownKillSwitch`. Floored (never to cash; `H ≥ θ_min > 0`) + the existing
kill-proof unit shadow book keeps the signal alive. Lives in BOTH `paper_risk_overlay` (snapshot) and
`IncrementalRiskOverlay` (backfill) so live==replay; selectable via a `controller` flag for A/B.

## Components (v1 core)
1. **`K(D*)` — ex-ante DD budget → base Kelly** (`kelly_for_dd_budget(dd_budget)` in paper_book.py):
   invert the empirical curve `DD ≈ −21% − 12%·kelly` → `kelly = clip((|D*|−21)/12, kelly_min, max)`.
   Calibrate the slope/intercept from a backfill leverage-sweep (don't hardcode blindly — fit on the data).
   Default budget `D* = −25%`. This is the PRIMARY control (~80% of the effect).

2. **`V[s,t]` — asymmetric vol-target ramp** (the "stop buying more when calm" fix):
   keep `vol_target_leverage` (inverse-vol), but in the STATEFUL overlay cap the UP-move per step:
   `L = min(L_raw, L_prev·(1+up_rate))` for increases; decreases apply immediately (`L = L_raw`).
   `up_rate` small (e.g. 0.15/day). Down-fast/up-slow prevents the pro-cyclical over-buy that creates the
   spike-drawdown. (Up-ramp state is per-sleeve in the overlay.)

3. **`H[s,t]` — per-sleeve continuous edge-health** (the anti-whipsaw layer; reuse `PageHinkleyDecay`):
   feed each sleeve's realized SHADOW daily return into a per-sleeve Page-Hinkley; derive a CONTINUOUS
   health `θ = clip(1 − max(0, (m_max − m_t))/lam, θ_min, 1)` (1 = healthy; → θ_min as PH approaches its
   kill threshold `lam`; recovers via the existing reenter logic). Multiply leverage by `θ`. This de-sizes
   on EDGE DRIFT (sustained negative PnL), not on a single down-day → no whipsaw; per-sleeve so a decaying
   sleeve is cut without touching a healthy one. `θ_min` e.g. 0.0–0.1 (floored, not hard zero).
   - Extend `PageHinkleyDecay` with a pure `health() -> float` (the continuous θ from m_max/m_t/lam),
     leaving its existing binary `update()` intact (ts_trend/stablecoin still use it).

## Integration + A/B
- Add `controller: str = "killswitch"` (default keeps current behavior) selectable to `"edge_health"`
  on `paper_risk_overlay` + `IncrementalRiskOverlay`. `edge_health` replaces the `killed` cut with the
  `H` (and `V`) stack; `K(D*)` sets `kelly_fraction`. Both code paths identical → live==replay.
- The snapshot + backfill take the controller choice from one place (a setting / the `REPLAYABLE` config)
  so live and replay use the SAME controller.

## Validation (the judge is the forward gate; controllers overfit)
- **Backfill A/B**: run the 2-edge book under `killswitch` vs `edge_health` (same data, NET). Report for
  each: return, Sharpe, maxDD, **#kill/de-size episodes + their avg length** (whipsaw proxy), time-in-cash.
  PASS for edge_health: maxDD ≈ the chosen `D*` budget, Sharpe ≥ killswitch's, FEWER/shorter whipsaws.
- **Ablation**: K(D*) alone vs +V vs +H — each layer's marginal effect.
- **Forward gate**: run the live forward under `edge_health`; the OOS record is the real judge. Don't add
  v2 layers unless the forward shows a need.

## The honest limit (documented, not solved here)
Sizing controls DD ABOVE the directional floor (~−24%): a long-crypto trend book in a −70% bear loses
regardless of size until the signal flips. Going below the floor needs an EXPOSURE change (faster signal
flip / tail hedge) = an EDGE change, OUT OF SCOPE. The controller targets `D*` above the floor.

## Out of scope (v2, only if the forward gate shows a need)
- `G[t]` Grossman-Zhou hard-floor guardrail (`clip(1−DD/D*_hard, g_min, 1)`).
- Hysteresis / no-trade band on `L` changes (turnover/cost-churn control — the "must buy first" cost).
- Full ISV-adaptive params (up_rate, lam, θ_min, lookbacks derived from data variance).
- Forward-looking vol forecast (GARCH/EWMA) for `V`.

## Acceptance criteria
- `kelly_for_dd_budget`, asymmetric-ramp, and per-sleeve `PageHinkleyDecay.health()` are pure + unit-tested.
- `controller="killswitch"` is **bit-identical** to current behavior (backward-compat; existing tests pass).
- `controller="edge_health"` wired in BOTH paper_risk_overlay + IncrementalRiskOverlay (live==replay; a
  golden test asserts they agree per date).
- Backfill A/B table produced (killswitch vs edge_health: return/Sharpe/maxDD/whipsaw-count).
- Full suite green; no `*.dbn` staged.
