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
- Trigger:
energy_regimein["SHOCK", "CRISIS", "SHOCK_UP", "RISING"] - Composition:
multiply - Scales:
correlations: 2.0,btc: 0.0,sectors: 0.6 - Net delta:
+5 -3 -2 = 0— total max stays 100. - Reason (surfaced on API): 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.
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
- Trigger:
energy_regimein["FALLING", "SHOCK_DOWN"] - Composition:
multiply - Scales:
correlations: 0.6,breadth_50d: 1.14,sectors: 1.2 - Net delta:
-2 +1 +1 = 0— total max stays 100. - Reason (surfaced on API): Energy relief — restored breadth and sectors lead the risk-on recovery; correlations matter less as the cross-asset transmission stabilizes after oil rolls over.
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": []
}
active_regime_overrides— declaration order of every override that fired AND applied successfully.regime_override_reasons—{name: reason}map. Use this to render “rebalanced for energy_shock — correlation breakdowns dominate” alongside the per-component weight changes.skipped_regime_overrides— diagnostic list of overrides whose declaredcompositionwas unsupported. Empty in normal operation; populated only when a future operator-edit introduces a typo. Per DOCTRINE P0 the skip is loud, not silent.
Adding a new override
- Edit
app/signals/config/score_weights.json::regime_overrideswith 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} } } - Pick a composition —
multiplyif you want compounding,maxif you want a structural floor,additiveif you want a fixed point allotment. - 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.
- 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.
- Re-run tests —
tests/test_regime.py::TestRegimeOverrideCompositionscovers the three composition rules; extend if your override exercises a new path.verify-docs.sh:24eruns in CI.
See also
- Health Score — the 19-component additive score these overrides rebalance.
- Regimes — how
energy_regimeitself is classified upstream. app/signals/regime.py:_apply_regime_overrides— the dispatch implementation.app/signals/config/score_weights.json::regime_overrides— the JSON declarations.