Skip to content

KB / framework

Regime-conditional weight overrides

Last verified

The health score’s component weights are not static. When a regime fires that materially changes which signals carry information, score_weights.json::regime_overrides rebalances component maxes — doubling Correlations during an energy shock, zeroing BTC, throttling Sectors. The overrides are declarative; each one names a trigger, the scaling factors, the composition rule, and a required reason string surfaced on the score-breakdown API so operators see WHY the regime rebalanced.

The three composition rules

Each override declares a composition field that selects how its factors combine with any previously-applied overrides on the same component. The dispatch lives in app/signals/regime.py:_apply_regime_overrides.

multiply (default)

Sequential factor multiplication — effective_max = current_max × factor. Matches the pre-C4 behaviour so the two shipped overrides ship identically. Use when you want compounding effects across overrides.

Example. Override A says correlations: 2.0 with multiply, override B says correlations: 1.5 with multiply. Both fire. Base correlations=5 → A lifts to 10 → B lifts to 15. The two factors compound: 5 × 2.0 × 1.5 = 15.

max

Floor enforcement — effective_max = max(current_max, base_max × factor). The factor is applied against the base (pre-override) component max, then compared against the currently-running max. A regime declaring max semantics says “ensure this component’s weight is at least this high; don’t compound with prior overrides.”

Example. Override A already lifted correlations 5 → 12. Override B says correlations: 2.0 with max. The floor is base × 2.0 = 5 × 2.0 = 10. Since the current max (12) already exceeds the floor (10), B passes through unchanged — correlations stays at 12.

Use when an override expresses a structural floor rather than an additive amplification.

additive

Delta accumulation — effective_max = current_max + base_max × (factor - 1.0). Adds (or subtracts, when factor < 1) base-relative points irrespective of any prior override. A regime declaring additive semantics says “add base_max × (factor - 1) to whatever the current max already is.”

Example. Override A already lifted correlations 5 → 12. Override B says correlations: 2.0 with additive. The delta is base × (2.0 - 1.0) = 5 × 1.0 = 5. New max = 12 + 5 = 17. A second additive override of factor 2.0 would add another 5 → 22.

Use when an override should contribute a fixed point allotment irrespective of other regimes also firing.

Currently shipping

Both shipped overrides use multiply so behaviour matches pre-C4 production. Future regime additions (e.g. a CRITICAL_STRESS combo) may pick max or additive.

energy_shock

The doubling on Correlations reflects the empirical observation that during energy stress events correlation breakdowns become the leading signal — risk-on transmission breaks down before breadth or sectors register the move. BTC is zeroed because crypto’s correlation to equity beta becomes incoherent during commodity-driven moves. Sectors are throttled because rotation has less information content when the entire macro tape is being repriced.

energy_relief

The mirror of energy_shock: when oil rolls over, breadth and sectors lead the recovery (capital flows back into the broad market, sector dispersion narrows). Correlations matter less once the macro transmission stabilizes.

Validation (4-week observation, 2026-06-01)

Both shipped overrides completed a four-week live-observation window before being confirmed. The retention bar: an override stays as-is only when, over four weeks of live operation, it (1) fires only on genuine energy_regime transitions — no spurious triggers off transient readings, (2) holds the sum-to-100 invariant on every firing, and (3) produces a rebalanced health-score read that tracks the real shift in signal information content rather than distorting it. An override that fails any of these is retuned or removed.

Outcome: energy_shock and energy_relief both passed. They fired cleanly on real energy-regime moves, never drifted the component-max sum, and the doubled-Correlations / throttled-Sectors rebalancing matched the observed transmission shift during energy stress. Decision: HELD — retained unchanged. No retune required.

Sum-to-100 invariant

Total component max stays 100 after any single shipped override fires. Both energy_shock and energy_relief net to zero by construction (their scales offset). The lint verify-docs.sh:24e enforces this contract for the two shipped overrides — if a future PR introduces drift, the verify-docs gate catches it before merge.

The runtime does NOT enforce sum=100. Per DOCTRINE P8, operators can introduce experimental overrides that don’t sum to 100 (e.g. to test whether a regime warrants more total weight on certain components). The lint is the early-warning, not a runtime gate. The post-override sum is implicit in the score-breakdown response — sum the max fields across components and you have the operative max for the current cycle.

Reading active overrides from the API

GET /api/v1/signals/score/breakdown now returns three new fields:

{
  "active_regime_overrides": ["energy_shock"],
  "regime_override_reasons": {
    "energy_shock": "Energy shock (oil supply disruption) — correlation breakdowns are the dominant transmission channel; BTC/sectors lose signal value when commodity-price stress dominates the macro tape."
  },
  "skipped_regime_overrides": []
}

Adding a new override

  1. Edit app/signals/config/score_weights.json::regime_overrides with the four required fields:
    {
      "your_regime_name": {
        "description": "human-readable summary (existing field)",
        "trigger": {"field": "<signal_key>", "in": ["VALUE_A"]},
        "composition": "multiply",
        "reason": "Required prose explaining WHY this regime rebalances weight.",
        "scales": {"component_key": 1.5}
      }
    }
  2. Pick a compositionmultiply if you want compounding, max if you want a structural floor, additive if you want a fixed point allotment.
  3. Write the reason — non-empty, well-phrased prose. The API surfaces it; the dashboard renders it; the lint blocks the PR if it’s missing.
  4. Decide the sum — if you want the override to keep total=100, design the scales to net to zero. The lint enforces this for the two shipped overrides only; new overrides can break that invariant if the operator is deliberately experimenting (DOCTRINE P8). Document the choice in the description field.
  5. Re-run teststests/test_regime.py::TestRegimeOverrideCompositions covers the three composition rules; extend if your override exercises a new path. verify-docs.sh:24e runs in CI.

See also