# Combined risk stack — Design (Phase 2F-A10)

**Date:** 2026-06-23
**Status:** Design (for review)
**Repo:** fxhnt

## Motivation
We established (with data) that the drawdown problem has THREE orthogonal risk questions, each best served
by a different mechanism — and crucially that one of them (edge-health) HURTS if mis-applied:

| Question | Mechanism | Cadence |
|---|---|---|
| "How much do I bet?" | `K(D*)` ex-ante Kelly sizing | continuous (the DD dial) |
| "Is this *specific edge* still alive?" | `WindowedEdgeHealth` per-sleeve | rare / structural |
| "Am I in a sudden catastrophic drawdown *now*?" | `DrawdownKillSwitch` | rare / book-level tail |

**The hard-won discipline (from the A/B):** reactive edge-health as a *continuous book-sizing throttle*
WHIPSAWS a momentum book (cut after a losing window → small into the recovery): killswitch +1856%/Sh2.09
vs edge_health +80%/Sh0.84. So `WindowedEdgeHealth` MUST be used as a **slow, binary, per-sleeve RETIREMENT
gate** ("drop a sleeve whose edge is structurally dead"), NEVER as a daily sizing multiplier. That single
distinction is what makes the three work together instead of fighting.

## Architecture
A new `controller="combined"` mode (alongside `"killswitch"` default and the rejected/inert `"edge_health"`):
```
active   = sleeves NOT structurally retired (WEH retirement gate)
weights  = isv_adaptive_weights(returns, active)          # ISV over survivors (natural re-normalization)
leverage = vol_target_leverage(weights, kelly_fraction=K) # K(D*) sizing; default K=1.0 (current)
book     = killswitch-gated(weights · leverage)           # existing DrawdownKillSwitch as the tail backstop
```
The ONLY behavioural delta vs `"killswitch"` is the retirement gate filtering `active`. When all sleeves are
healthy (the current 2-edge book), `combined == killswitch` exactly (gate inert) — it cannot hurt the current
curve; its value is future-proofing (auto-drop a sleeve that later dies).

## The retirement gate — per-sleeve hysteresis state machine (the part to sanity-check)
State per sleeve: `status ∈ {active, retired}`, counters `below_run`, `above_run`, and a `WindowedEdgeHealth`.
Each day, feed every sleeve its **SHADOW** daily return (the kill-proof `unit_shadow_positions` series — alive
even while retired, so recovery is detectable), then:
```
weh.update(shadow_ret); h = weh.health()
if status == active:
    below_run = below_run + 1 if h < retire_floor else 0
    if below_run >= retire_window:  status = retired; below_run = above_run = 0
else:  # retired
    above_run = above_run + 1 if h >= readmit_floor else 0
    if above_run >= readmit_window: status = active;  below_run = above_run = 0
active_sleeves = [s for s if status == active]
```
Hysteresis (`readmit_floor > retire_floor`) + long confirmation windows make it structural, not tactical —
**it cannot whipsaw daily** (a sleeve must be dead `retire_window` days to drop, recovered `readmit_window`
days to return). Bootstrap: a young sleeve has `health()==1.0` → never retired early. All-retired → empty
book (0 exposure) is acceptable (don't trade dead edges).

### Parameters (fixed v1 defaults; ISV-adaptation is future work)
- `retire_floor = 0.15`, `retire_window = 30`   (≈ a month of structural deadness to drop)
- `readmit_floor = 0.50`, `readmit_window = 30`  (clear hysteresis gap; a month of recovery to return)
- WEH: `window=63` (v1b defaults); `kelly_fraction = 1.0` (current sizing — `kelly_for_dd_budget(D*)` available)
- `kill_dd / reenter_dd`: existing killswitch defaults (0.25 / 0.12)

## Integration (live==replay)
- Add `controller="combined"` to `paper_risk_overlay` (stateless replay) + `IncrementalRiskOverlay` (stateful).
  Retirement state (per-sleeve WEH + counters + status) is maintained incrementally in `IncrementalRiskOverlay`
  and replayed from scratch in `paper_risk_overlay` — the existing pattern for the killswitch + per-sleeve WEH.
- The gate reuses the active-set convention (cf. `marginal_gate_active`): it produces `active_sleeves`, which
  feeds `isv_adaptive_weights` exactly as today. Retired sleeves get weight 0.
- `controller="killswitch"` stays **bit-identical** (default; existing tests unchanged).

## Validation (the honest test — don't measure on in-sample where nothing dies)
1. **Synthetic dead-sleeve test (the decisive one):** two sleeves — A healthy throughout, B healthy then a
   sustained structural decay (then optionally a recovery). Assert:
   - `combined` retires B within ~`retire_window` of the decay onset; `killswitch`-alone keeps allocating to B.
   - final NAV(`combined`) > NAV(`killswitch`-alone) on the dying-sleeve scenario (the gate earns its keep).
   - if B recovers, `combined` re-admits it within ~`readmit_window` (no permanent exile).
2. **Inert-on-healthy:** on the real 2-edge book (both healthy), `combined` ≡ `killswitch` (no retirements) —
   proves it doesn't touch the current curve.
3. **live==replay golden:** `IncrementalRiskOverlay(controller="combined")` == `paper_risk_overlay(...,
   controller="combined")` per date, exact.
4. **No-whipsaw guard:** a sleeve that merely *dips* below `retire_floor` for `< retire_window` days is NOT
   retired (distinguishes structural death from a tactical drawdown).

## Out of scope (future, only if the forward gate shows need)
- ISV-adaptive gate params (windows/floors derived from data variance).
- Re-normalization policy alternatives (currently: ISV over survivors).
- Wiring the gate into the live snapshot default (stays behind the flag until the synthetic test + a forward
  period justify flipping the default).

## Acceptance criteria
- Retirement gate is a pure, unit-tested hysteresis state machine (retire/re-admit windows + bootstrap + dip-not-death).
- `controller="killswitch"` bit-identical (backward-compat; existing tests pass).
- `controller="combined"` wired in BOTH overlays; live==replay golden passes.
- Synthetic dead-sleeve test passes (combined > killswitch-alone on a dying sleeve; re-admits on recovery).
- Inert-on-healthy: combined ≡ killswitch on the real 2-edge book.
- Full suite green; no `*.dbn` staged.
