Skip to main content

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_archive tagger, 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.
Enforcement scope — legitimate for exactly two subjects, and no others:
  1. The firm’s own prop book — the firm enforcing its own risk on its own capital.
  2. 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.
Terrain is advisory for app users by default and is never a platform-imposed restriction on what a user may trade. This follows the standing product principle — intelligence platform, not a broker; the user owns their risk; “safety” means the user’s own policy, not one imposed on them. Any Phase-B enforcement path must key off “whose policy is this?” and refuse to globally gate a user who hasn’t opted in.

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, remain unavailable — 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 via strategy_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 into api/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:
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Unavailable guard. 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) — columns vol_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 raw shock column directly.
  • Trajectory and stretched are derived one layer up, from regime_v2.py’s volatility_change/directional_pressure axes — themselves computed from the same regime_archive percentiles (vol_pctile delta and slope_pctile respectively), 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 uses variant="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.py hard-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) in backtest_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_v1 grid, terrain_v2 3-axis) that continue to accrue in parallel for backward comparison.
  • strategy_family + sleeve columns on every row (regime_effectiveness. REGIME_EFFECTIVENESS_COLUMNS), populated from strategy_family.family_of / sleeve_of — a deterministic, explicit-token strategy-id classifier (not an order-dependent substring guess). Required because a strategy’s strategy_kind alone (rule_driven / cross_sectional) cannot represent the 6-family enum.
  • ClickHouse migration clickhouse/061_terrain_1_0.sql: adds strategy_family and sleeve (both LowCardinality(String) DEFAULT '') to backtest_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 on models.regime_vnext.EvidenceStatus): a (rung, family) cell graduates insufficient → exploratory → validated only 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 flagged rejected — evidence can tighten or downgrade confidence, but it never silently inverts a hand-authored call. Until a cell clears the gate it renders insufficient/exploratory with no raw PF/win-rate shown as certification.

9. Surfaces & versioning

  • One enum, two rendering maps. terrain_state.RUNGS is 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 descriptive posture string. What differs is what else is attached:
    • App: rung + trajectory + stretched flag + 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 underlying regime_archive axis internals. Aggregate net book gamma/vega (§6) is Phase-B — not computed anywhere today.
  • 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:
  1. The grid is legacy/shadow. The canonical engine derives risk-first axes from regime_archive raw percentiles; the grid projection is a stale, diagnostic-only view of that substrate, not an independent source of truth.
  2. 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.
What changed, concretely:
  • terrain_state.pyderive_terrain_rung (from regime_archive) replaces the old grid-based state derivation; risk-rung display/caution/posture maps replace the old direction-conditioned maps.
  • regime_tagger.pytag_trades_with_terrain_rung reads regime_archive (shock + vol_pctile) and stamps the rung, alongside (not instead of) the pre-existing grid tagger kept for the terrain_v1 comparison taxonomy.
  • regime_effectiveness.py — a third taxonomy, terrain_1_0 (rung-keyed), plus strategy_family and sleeve columns, 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 beta sleeve; splitting the reversal leg into edge is Phase-B policy work, not done in Phase A.