Terrain Regime Dictionary — Terrain 1.0 (Risk-First Ladder)
Status: Phase A shipped (this branch:feat/terrain-1.0-phase-a). Supersedes the prior
direction-conditioned dictionary in full — see §10. Swing horizon only.
Source of truth: docs/superpowers/specs/2026-07-04-terrain-1.0-risk-first-taxonomy-design.md
(design). This document is the product-facing dictionary derived from it, kept in sync with the
shipped implementation in api/app/services/backtester/{terrain_state,regime_tagger, regime_effectiveness,strategy_family,terrain_affinity}.py.
1. Identity & purpose
Terrain is Sequency’s regime classification/detection system. Terrain 1.0 is one plain-language read of “what risk regime are we in, and how big should we size” — shared by the prop desk and the app. It is a risk/context/allocation layer, not alpha. Terrain is explicitly not a direction predictor. It never names or implies a market-direction call (“the market is going up/down”), and no rung, qualifier, or affinity entry is a long/short gate. It describes regime severity, not regime direction.1.1 Phasing
- Phase A (this document, shipped). The taxonomy + evidence substrate: the rung derivation, the
regime_archivetagger, the rung-keyed accrual with family/sleeve columns, the affinity table as descriptive evidence-gated desk display, and the app’s rung + posture labels. No live sizing/gating — sizing posture is a descriptive label, not an enforced multiplier. - Phase B (separate spec, later). The per-sleeve multipliers (§4) and the risk controls (§6) as an enforced execution-layer policy. Depends on a live borrow/locate feed and on emitted-PIT accrual + operator approval. Not built yet — §4 and §6 below describe the target policy so it isn’t lost, not what runs today.
1.2 Advisory vs enforced — and who enforcement is allowed to apply to
This is the load-bearing product principle behind the phasing, and it governs every Terrain surface:- Advisory (Phase A, and the default everywhere). Terrain describes the regime and shows a recommended posture (“Stress · rising → suggested: beta down, edge near-full but gross-capped”). It changes what you see, not what the book holds. No order is blocked, no size is set. A human (or PM) reads it and decides.
- Enforced (Phase B). The rung becomes a live control input to the sizing/risk engine: per-sleeve exposure caps, order gating, the book-level short-vol gross ceiling, the borrow-gated edge cap, and the total-equity kill-switch execute mechanically, with no human in the loop. The label stops being a readout and starts constraining the book.
- The firm’s own prop book — the firm enforcing its own risk on its own capital.
- A user operating under a risk policy they opted into — the funded-live-testing model: the user’s own limits, unbypassably enforced once they set them. Mechanism is ours; the values/limits are theirs.
2. The ladder — 5 rungs, ordered by severity
Rung =derive_terrain_rung(shock, vol_pctile) (api/app/services/backtester/terrain_state.py:40),
a pure function of two raw regime_archive columns. No translation layer — the rung name is the
canonical value.
Evaluation order (unambiguous, matches the code exactly):
Unavailable (NULL) → Shock (raw
shock = 1) → Stress (vol_pctile ≥ 90) → Elevated (vol_pctile ≥ 70) → Calm
(vol_pctile ≤ 25) → Normal (else). The table above lists rungs by severity for reading; this
cascade is the authoritative precedence.
Shock is an overlay, not a strictly-higher-severity rung. shock fires independent of vol
level (46/86 shock days have vol_pctile < 90 in the historical corpus), so it is a distinct
event tag, not “more volatile than Stress.” It gets its own rung so its P&L accrues with its own
lineage — even though the canonical risk-state axis used elsewhere in the regime engine
(regime_v2.py) currently folds shock into its own stress bucket rather than carrying it
separately.
2.2 Trajectory qualifier — rising / steady / falling
Sourced from the regime engine’s volatility_change axis (expanding/stable/compressing),
mapped 1:1 by derive_trajectory (terrain_state.py:55) to rising/steady/falling. Fires on
Δvol_pctile ≥ +15 (rising) or ≤ −15 (falling) vs. the prior session; otherwise steady. Needs the
prior-session vol_pctile (swing prior-session binding). Renders as e.g. Elevated · rising.
Descriptive and forward-useful (“risk climbing → lighten ahead of the next rung”) — never a
price-direction claim.
Unreachable cells are deleted, not hidden: shock forces volatility_change = expanding, so
Shock · do not exist. Stress · falling is unreachable via
volatility_change alone (would need a prior vol_pctile ≥ 105 against a ~99.6 practical
ceiling) — the “risk receding out of Stress” case is instead re-keyed to a rung
down-transition (rung(T−1) = stress ∧ rung(T) ∈ {elevated, normal}), computed from the prior
snapshot already loaded rather than from the trajectory axis.
2.3 stretched modifier — advisory only, never a trim signal
derive_stretched (terrain_state.py:62) is a sign-agnostic alias of the regime engine’s
directional_pressure extension: upside_extended or downside_extended (from slope_pctile ≥ 80 or ≤ 20) both map to stretched = True. It is off the sizing dial (historically
anti-predictive, IC −0.012, fires on ~45% of sessions). It is advisory/entry-timing/limit-placement
context only. Note: stretched is the entry condition for the reversal/edge sleeve, never a
trim signal on any sleeve.
2.4 Deliberately absent
Any direction-conditioned label (bullish/bearish, up/down). Breadth and correlation axes, and intraday market-structure context, remainunavailable — there is no proven point-in-time source
for them yet. They are future richness, out of Terrain 1.0.
3. The two-sleeve model (governs sizing)
Sizing is governed by sleeve (mechanism), not strategy-family name. Every strategy family maps deterministically to a sleeve viastrategy_family.family_of / sleeve_of
(api/app/services/backtester/strategy_family.py). Critically, the cross-sectional relative-
strength family’s momentum leg and its reversal/liquidity-provision leg are opposite-signed and
must never share a row — the family currently maps as a single unit to beta (its momentum
character); splitting the reversal leg out into edge is an explicit Phase-B policy concern, not
something the family map does today.
“Do not vol-target the edge” — kept in sign, replaced in mechanism. Do not use realized vol
as the edge’s governor (that sells its best regime — the Nagel-wrong-sign mistake); do govern it
by gross ceiling + loss budget + age cap + borrow feasibility. Scoped to the reversal/
liquidity-provision mechanism only — the momentum leg is vol-managed like any beta exposure.
The firm’s deployed cross-sectional book is a market-neutral, vol-scaled blend of both legs. For a
risk overlay it must eventually be decomposed so each leg routes to its own sleeve; that
decomposition is Phase-B scope.
4. Sizing posture (per rung) — descriptive, not enforced
Two independent multipliers are needed — a single dial cannot say “beta max-down AND edge up.” Sizing is conceived as a continuous multiplier under the rung label (rung names are for playbook/comms), with hysteresis (§6) — none of that is enforced in Phase A. What ships today are labels:terrain_state.label() returns a single descriptive posture
string per rung (shown to both desk and app), plus a per-sleeve breakdown available desk-only via
terrain_affinity.affinity_for(rung)["sizing"].
Notes: Stress and Shock share the same
vol_pctile-derived rung condition; the shock flag is
used only to freeze the edge sleeve (no new adds), not to set a different size. “Scale into the
flush” is deliberately removed as a posture — scaling a short-vol book into a multi-week grind
(observed episode length up to 83 days) is a known way liquidity-provision books die. Shock adds
are pre-funded long-gamma only, scaled on realized-vol stabilization, never on price level.
The generic single-line posture actually rendered on both surfaces today
(terrain_state.RUNG_POSTURE_DESCRIPTIVE, app-facing companion
terrain_state.RUNG_APP_CAUTION) is intentionally coarser than the table above — it never
mentions a sleeve by name (see §9).
5. Strategy-family affinity (desk-only, evidence-gated)
Conditioned on rung; desk-only — the app never shows sleeve/family economics (§9). Transcribed verbatim from the design spec intoapi/app/services/backtester/terrain_affinity.py (AFFINITY
dict, served via affinity_for(rung)). E = evidence-backed (a gate-passing corpus cell in
backtest_regime_effectiveness, or standard literature); I = insufficient / hand-authored prior
(evidence pending) — the conservative default for any cell the source did not explicitly mark E.
Corpus PF/PoP figures below are panel-derived from a single thin batch (2026-06-28) — treat as
priors, not certified numbers, until the promotion gate (§8) clears them.
The entire Shock row is forced
[I] (0 emitted shock days) even where the underlying rule
reads as a hard risk rule rather than a statistical claim — “favorable/preferred” for any family
requires the promotion gate to have cleared, never labels alone. Do not read Shock’s “kill
beta” as license to avoid all directional exposure indiscriminately: it protects the
market-neutral long and short legs of the edge sleeve, not a blanket direction call.
Qualifier modulation
· rising→ sleeve-split. Beta pre-emptively de-risks; the edge sleeve engages / prepares to harvest — opposite actions, must be rendered per-sleeve, never as one blended posture. Annotate as one session lagged (“as of prior close,” not intraday).- Rung down-transition (out of Stress) → beta re-engages, edge budget normalizes.
stretched→ advisory only; an anti-signal for a trim on the edge sleeve (i.e. it argues against cutting the edge sleeve early, not for entering a directional trade).
6. Phase-B risk controls (documented target policy — not enforced today)
These are the mechanisms Phase B’s enforced sizing policy is designed around. None of them run in Phase A; they are recorded here so the design isn’t lost between specs:- Book-level short-vol gross ceiling. A shared, non-increasing net-vega/short-vol gross budget
across the entire edge sleeve — one budget, so the “several favored families in Stress” reading
doesn’t hide that they are correlated short-vol bets on the same underlying risk. Track
aggregate net book gamma/vega across every rung; keep
trend_following’s convex use as the offset so net book gamma does not go monotonically short as risk climbs. - Edge circuit breaker. Per-episode realized-loss sub-budget + episode-age cap (from
regime_archive.episode_day; observed max 83 days — a grind, not a guaranteed fast reversal). De-gross on loss/time, not on vol. - Kill-switch & all loss budgets on total book equity (funded trailing-drawdown basis) — letting the edge run uncapped while cutting beta can increase worst-case drawdown, since tails are positively correlated across sleeves in a real crisis.
- Short-leg feasibility as a first-class sizing input. “Edge runs in stress” needs a borrowable short leg; there is no live borrow-rate/locate/hard-to-borrow/short-ban feed today. Cap edge gross by borrowable short notional + realized spread, never by vol. When the short leg is unborrowable, degrade to a neutrality-aware reduced book — never run the naked long leg.
- Hysteresis. Schmitt-trigger boundaries (enter Elevated ≥ 70 / exit ≤ 60; enter Stress ≥ 90 / exit ≤ 80) + an N-observation debounce + asymmetric cut-fast/add-slow behavior + a continuous size multiplier underneath the discrete rung label.
Unavailableguard.vol_pctile IS NULL(a data outage during a real event) must refuse to emit and de-risk — never fall through to Normal at full size.
7. Sourcing, derivation, provenance
- Source table:
regime_archive(the canonical regime substrate) — columnsvol_pctile,shock,slope_pctile,episode_day,direction,volatility,grid_cell,variant,logic_version(api/app/services/backtester/resolve_regime.py). Not the legacy 2×3 grid projection table that fed the superseded prior dictionary (§10) — that projection is a shadow/ diagnostic artifact of the canonical substrate, not an independent source. derive_terrain_rung(shock, vol_pctile) → rung(terrain_state.py:40) is the §2 ordered cascade, reading the rawshockcolumn directly.- Trajectory and
stretchedare derived one layer up, fromregime_v2.py’svolatility_change/directional_pressureaxes — themselves computed from the sameregime_archivepercentiles (vol_pctiledelta andslope_pctilerespectively), never from a separately-sourced signal. - PIT / provenance:
variant='emitted'(prior-session bound) is the only gating-eligible read for anything that would size or restrict live risk;variant='corrected'is diagnostics/backfill only. The backtester’s rung tagger (regime_tagger.tag_trades_with_terrain_rung) deliberately usesvariant="corrected"— it is result-time attribution on completed trades, not a simulation input, so it is explicitly out of the emitted-PIT gating path. Every read carries explicit(variant, logic_version)pins;resolve_regime.pyhard-raises rather than silently defaulting on an unrecognized pair. - Swing horizon only. Intraday regime resolution hard-raises; macro is metadata-only context, never sized.
- Calibration honesty. As of Phase A, only a handful of emitted-PIT sessions exist (0 at Stress, 0 at Shock). The design specifies how rungs would gate under a richer emitted history (deterministic risk-overlay override / borrow-from-Elevated / wider-CI) — no sizing multiplier is hard-coded to corrected-history rung behavior.
8. Accrual & evidence
- Taxonomy
regime_taxonomy = 'terrain_1_0',regime_cell = rung(calm/normal/elevated/stress/shock) inbacktest_regime_effectiveness(regime_effectiveness.REGIME_TAXONOMY_TERRAIN_1_0) — so the Shock rung finally accrues its own lineage, distinct from the two prior taxonomies (terrain_v1grid,terrain_v23-axis) that continue to accrue in parallel for backward comparison. strategy_family+sleevecolumns on every row (regime_effectiveness. REGIME_EFFECTIVENESS_COLUMNS), populated fromstrategy_family.family_of/sleeve_of— a deterministic, explicit-token strategy-id classifier (not an order-dependent substring guess). Required because a strategy’sstrategy_kindalone (rule_driven/cross_sectional) cannot represent the 6-family enum.- ClickHouse migration
clickhouse/061_terrain_1_0.sql: addsstrategy_familyandsleeve(bothLowCardinality(String) DEFAULT '') tobacktest_regime_effectiveness, and drops the shipped prior-dictionary accrual rows (DELETE WHERE regime_taxonomy = 'terrain_state') — miscalibrated, wrong-taxonomy data that would otherwise confuse the evidence gate. Forward-only, idempotent, operator-approved clean slate. - Promotion gate (
terrain_state.affinity_evidence_status, keys onmodels.regime_vnext.EvidenceStatus): a (rung, family) cell graduatesinsufficient → exploratory → validatedonly at ≥ 200 trades ∧ ≥ 20 independent episodes ∧ ≥ 3 calendar years, winsorized profit factor, with sign agreement against the hand-authored prior. A powered cell that contradicts the prior is flaggedrejected— evidence can tighten or downgrade confidence, but it never silently inverts a hand-authored call. Until a cell clears the gate it rendersinsufficient/exploratorywith no raw PF/win-rate shown as certification.
9. Surfaces & versioning
- One enum, two rendering maps.
terrain_state.RUNGSis the single canonical key set; desk and app currently share identical display strings (RUNG_DESK_DISPLAY/RUNG_APP_DISPLAY— no fork in Phase A) and the same descriptiveposturestring. What differs is what else is attached:- App: rung + trajectory +
stretchedflag + one caution line (RUNG_APP_CAUTION) + the generic posture string. Never sleeve/edge economics, family affinity, proprietary performance figures, or affinity framed as a forward prediction. - Desk: everything the app gets, plus sleeve/family economics
(
terrain_affinity.affinity_for) and the underlyingregime_archiveaxis internals. Aggregate net book gamma/vega (§6) is Phase-B — not computed anywhere today.
- App: rung + trajectory +
- Version: Terrain 1.0 is the product name. Internal accrual data retains its historical
taxonomy labels (
terrain_v1,terrain_v2,terrain_1_0) as lineage — those are engine- substrate generations, not product versions users ever see.
10. Migration — this supersedes the prior direction-conditioned dictionary
Terrain 1.0 replaces, in place, the dictionary that named regimes by a bull/bear × calm/volatile grid (with a shock sub-cell carved out of the bearish-high-vol corner). Two verified facts made that the wrong foundation to keep building on:- The grid is legacy/shadow. The canonical engine derives risk-first axes from
regime_archiveraw percentiles; the grid projection is a stale, diagnostic-only view of that substrate, not an independent source of truth. - A direction-conditioned name reintroduces exactly the framing the regime engine’s own design rejected. Regime is a description of “market conditions under which strategy-family return distributions differ” — not a market-direction predictor — and must never translate into a long/short gate. The direction axis those state names encoded was a percentile-relative slope tercile that misclassified roughly 40% of a genuinely bullish window as bearish.
terrain_state.py—derive_terrain_rung(fromregime_archive) replaces the old grid-based state derivation; risk-rung display/caution/posture maps replace the old direction-conditioned maps.regime_tagger.py—tag_trades_with_terrain_rungreadsregime_archive(shock+vol_pctile) and stamps the rung, alongside (not instead of) the pre-existing grid tagger kept for theterrain_v1comparison taxonomy.regime_effectiveness.py— a third taxonomy,terrain_1_0(rung-keyed), plusstrategy_familyandsleevecolumns, accrues alongside the two prior taxonomies.- This document — rewritten as the risk-first ladder dictionary you are reading.
- The shipped direction-conditioned accrual rows were dropped (§8’s ClickHouse migration) — miscalibrated, wrong-taxonomy data that would have confused the evidence gate. Operator-approved clean slate; no revert.
11. Out of scope / open items
- Breadth & correlation rungs — no point-in-time source yet; future work.
- Intraday / macro horizons — not point-in-time-wired; swing only at launch.
- Emitted-PIT calibration of multipliers — data-gated (a handful of emitted sessions today). Terrain 1.0 ships deterministic cascade rules + hand-authored affinity priors, not certified multipliers.
- Live borrow/locate/short-ban feed — required before the §6 short-leg governor can run; until it lands, any future edge-sleeve enforcement must cap conservatively and hard-block the naked-long degradation path.
- Cross-sectional leg split (momentum vs. reversal/liquidity-provision) — the family map
currently routes the whole cross-sectional family to the
betasleeve; splitting the reversal leg intoedgeis Phase-B policy work, not done in Phase A.