Drawdown-Risk Lens (20-day, transparent weighted z-score composite)
Last verified
A probabilistic drawdown-risk gauge over the credit / inflation / rate LEVELS the full-history study flagged for the platform’s multi-week drawdown lane, plus a validated positioning input — dealer net gamma. Renders as the “Drawdown-Risk Lens · 20d” block in the regime card with a 0–100 score, a low / elevated / high band, and the per-leg contributions. The 15-year re-audit (U1, 2026-07-13) upgraded it to a validated cross-regime drawdown-severity gauge — but the edge is between-regime severity, never within-episode timing (see the caveats).
This is not a directional predictor, not a timing trigger, and not the validated logistic from the research. It is a transparent weighted z-score composite, chosen deliberately for explainability and graceful degradation over fitted accuracy.
What it measures
Each of the nine macro levels is standardized against its trailing baseline (default ~2 trading years), oriented by economic risk-direction, and averaged into a single number. The dealer-gamma leg adds a positioning read on top. Higher = more stress + more fragile dealer positioning relative to the recent norm; lower = calmer.
The dealer-gamma positioning leg
An important positioning input in the platform’s drawdown research is dealer net-gamma positioning (a “dealer-gamma analytics” feed). When option dealers sit at historically low (more-negative) net gamma, they are short gamma — forced to sell into a falling market and buy into a rising one, which amplifies drawdowns. The leg is net_gex_rank: the 252-day trailing percentile rank of the aggregate dealer net-gamma exposure. A low rank (historically negative gamma) reads as elevated drawdown risk; a high rank (positive gamma, stabilizing dealers) reads as calmer.
The discovery-era read (rank-vs-drawdown IC ≈ +0.25 non-overlapping, on two episodes) did not survive the 15-year re-validation. The U2 re-audit (2026-07-13) halved the non-OL rank-IC to ≈ +0.10 to +0.11 at 15y power (boot-CI lower bound ≤ 0), and each macro leg (nfci / hy_oas / gz_spread / ofr_fsi) individually out-ICs it — so on rank-IC the gamma leg is now mid-pack, not the strongest. What did transfer (and to the 2011–2020 out-of-sample span) is the quintile tail-lift: ~5–6× Q20/Q80 separation on the −5%-drawdown days, monotone. So the leg’s value is tail-capture + positioning-class diversification + freshness, not day-ordering rank-IC. Its weight was re-sized down from 2× to 1.5× accordingly (the residual premium pays for tail-lift, not IC; 1.0 is defensible on strict IC parity).
The rank needs a trailing-history warm-up; on a cold start (under ~6 months of gamma history) the leg abstains rather than report a misleading mid-rank.
Why this shape, not a fitted model
The full-history study (.research/SYNTHESIS.md) validated the multi-week drawdown signal as a parsimonious L2 logistic over seven of these macro features (the composite later added gz_spread + ofr_fsi_funding). That model is sklearn-based, and the platform’s prod image deliberately ships no machine-learning runtime — scikit-learn and its peers live in requirements-dev.txt and the no-Dockerfile-reference lint (verify-docs.sh:27) blocks them from ever being COPYed into a production container.
The deployable shape is therefore the transparent composite. Trade-offs, on the record:
- Transparent + auditable. Every contribution is a z-score the operator can recompute by hand. The card surfaces the per-leg contributions so it never moves without showing why.
- Weighted, not fitted. The macro levels are equal-weight (1.0 each) with four config-listed exceptions — the funding-stress leg carries 1.5 (highest forward-drawdown IC of any tested component);
ebp+gz_spreadcarry 0.5 each (the GZ spread is predicted-default plus EBP by construction, so weighting both at 1.0 double-counted the EBP information; the split keeps the pair’s combined credit-premium mass at one leg, backtested within noise or better on all horizons); and the Treasury-curve leg carries 0.0 — it is oriented for a 12–18-month recession lead, mis-horizoned against the 10–20-day target, and was the only leg with a wrong-signed standalone relationship to forward drawdown (it flipped the whole composite negative in the 2022 bear); zero-weighting it improved the drawdown read on every horizon and both validation episodes, so it is kept visible in the breakdown at weight 0. The dealer-gamma leg carries a 1.5× over-weight (re-sized down from 2× by the U2 15y re-validation). This is a coarse, hand-set weighting — it encodes “gamma is a diversifying, tail-lift-strong positioning input” without smuggling in an opaque single-bear fit. - Degrades gracefully. A missing feature is just dropped from the weighted average; the gamma leg abstains until it has enough history; below four available legs the gauge suppresses itself (
status: insufficient_data) rather than report off a thin set.
The nine macro inputs + risk-orientation
The orientation table lives in app/signals/drawdown_risk.py:_FEATURE_ORIENTATION and is documented inline with the rationale. Summary:
| Feature | Orientation | Reading |
|---|---|---|
hy_oas |
+1 | High-yield credit-spread widening = credit stress. The official series publishes next-morning (T-1 on display); the leg prefers a same-day level nowcast of the print (the stale value + a fixed coefficient times today’s duration-isolated high-yield ETF excess return) when available — the leg’s input_source field says which input fed it (nowcast / stale_print). Input-freshening only: weight, orientation, and baseline are unchanged. |
ebp |
+1 | Excess bond premium = above-fundamentals default-risk compensation |
nfci |
+1 | Positive NFCI = tighter-than-average financial conditions |
real_yield_10y |
+1 | Higher real yields = tighter policy / valuation pressure |
t10y3m_spread |
−1 | Curve inversion (low/negative) precedes recessions — but by 12–18 months, mis-horizoned against the 10–20-day target. Zero-weighted (the only wrong-signed leg; see the weighting note above); orientation shown for context but inert on the score. |
breakeven_5y |
+1 | Elevated inflation expectations → tightening risk (regime-specific) |
breakeven_10y |
+1 | Same — long-end inflation expectations |
gz_spread |
+1 | Wider Gilchrist-Zakrajšek parent credit spread = credit stress (weight 0.5 — see the double-count split note above; ebp is its predicted-default subcomponent) |
ofr_fsi_funding |
+1 | Higher OFR funding-stress subindex = funding-market stress (weight 1.5 — the highest forward-drawdown IC of any tested component) |
A +1 feature contributes +1·z to the mean (positive z = up = more risk). A −1 feature contributes −1·z (so a deeply inverted curve, where z is very negative, still pushes risk up).
The dealer-gamma leg’s risk-z is derived from the percentile rank instead of a level z-score: oriented_z = (0.5 − rank) × √12, so a rank of 0 (most-negative gamma) maps to a strongly positive risk-z and a rank of 1 (most-positive gamma) to a strongly negative one. The √12 scaling puts a uniform [0,1] rank on a unit-variance footing comparable to the level z-scores — so the leg’s weight, not an accidental scale mismatch, is the lever.
How the gauge is computed
For each available macro feature with a non-zero baseline standard deviation:
z = (level − trailing_mean) / trailing_stdoriented_z = z × orientation,weight = feature_weights.get(leg, 1.0)(most legs 1.0; funding-stress 1.5;ebpandgz_spread0.5 each per the double-count split above;t10y3m_spread0.0 per the wrong-signed / mis-horizoned note above — computed and shown, contributes 0)
For the dealer-gamma leg (when enough history exists):
oriented_z = (0.5 − net_gex_rank) × √12,weight = 1.5(re-sized from 2.0 by U2, 2026-07-13)
Then:
mean_z = Σ(oriented_z × weight) / Σ(weight)over available legsscore = 100 / (1 + exp(−mean_z))— a logistic squash with steepness1.0, somean_z = 0maps to50,+1σto ~73,+2σto ~88,−1σto ~27.- Band: low under
+0.5σ, elevated between+0.5σand+1.25σ, high at or above+1.25σ.
Each leg’s response carries a contribution field — its signed share of mean_z — so the card shows how much each input moved the gauge. If fewer than four legs are populated the gauge returns status: insufficient_data and the dashboard suppresses the block.
Caveats — read these before you act on it
- Cross-regime validated composite, but per-leg trust is uneven (U1, 15y). The 15-year re-audit validated the composite as a cross-regime drawdown-severity gauge — the HIGH band fired in every 2011–2026 bear with 0 calm-year false fires, and it beats VIX-only at the IC level (partial IC ≈ +0.14, survives BH-FDR). But leg-level trust is uneven: only 4/10 legs (nfci, ofr_fsi_funding, hy_oas, gz_spread) survive per-leg BH-FDR at 15y, the hy_oas survival is partly synthetic-substrate inflation (FLAG-U1a, re-graded forward on real data), and the breakevens are regime-fragile — wrong-signed pre-2021 (FLAG-U1b). Treat the composite band as trustworthy; treat any single leg’s contribution with the per-leg caveat in mind.
- Between-regime severity, NOT within-episode timing (U1 bound + U4). The edge is between-regime discrimination. Inside a named drawdown episode the composite reads CONTRARIAN (7/8 negative within-episode IC) — a HIGH print mid-episode is a severity/state read, never a step-aside-before-the-drop timer. And as a thresholded top-decile tail-capture flag (rather than an IC-level read) the composite does NOT beat VIX-only under causal expanding-window thresholds (U4, 2026-07-14) — VIX-only dominates every causal comparison. The beats-VIX result is an IC-level claim only; the gauge is coincident, not leading.
- The gamma leg’s rank-IC is mid-pack at 15y power. The non-overlapping out-of-sample sample behind the discovery-era gamma weight was ~57–116 observations (overlapping 20-day windows autocorrelate, inflating the nominal count); at full 15y power the rank-IC halved to ≈ +0.10/+0.11 (U2). The 1.5× weight (re-sized from 2× by U2) is sized for the leg’s quintile tail-lift + positioning-class diversification, not its day-ordering rank-IC. Treat the leg as a diversifying tail-capture tilt, not the strongest input.
- Hand-set weights, not fitted. The fitted logistic moved the AUC; this composite does not. Treat the gauge as situational awareness, not a quantitative probability.
- The breakeven sign is regime-specific. 2022 was an inflation-driven drawdown; elevated breakevens read as risk here. A deflationary bear would invert that. The orientation is the load-bearing assumption of the gauge — flag the gauge as suspect if the regime story changes.
- It does not contribute to the health score. This is a separate risk-side read; the 19-component health score is unchanged.
Where it lives in the code
- Pure helper:
app/signals/drawdown_risk.py(no DB, no network, no ML). Takes the macro levels + baselines and the pre-computednet_gex_rank. - Route:
GET /api/v1/signals/drawdown-risk(computes trailing baselines + current levels fromdaily_signals, computes the 252d gamma rank from thegexhistory, calls the helper). Schema-versioned byDRAWDOWN_RISK_SCHEMA_VERSION. - UI: rendered in the regime card on
/directly below the regime line, above the historical base-rate block. - Tests:
tests/test_drawdown_risk.pypins macro orientation, banding, the dealer-gamma leg’s orientation + over-weight + abstain-on-thin-history, per-leg contributions, the insufficient-data fallback, and a non-mocked route test that drives the gamma leg from seededgexhistory.