feat: replace regime monitor with v2 methodology

This commit is contained in:
2026-07-15 09:02:56 +02:00
parent fd21067a40
commit 1d5b1489be
17 changed files with 1599 additions and 1535 deletions
+28 -40
View File
@@ -1,52 +1,40 @@
"""Tests for the regime quadrant classification + hysteresis (anti-flicker)."""
"""Tests for v2 State/Warning quadrant hysteresis and basket reseeding keys."""
from __future__ import annotations
from app.services.alert_service import _classify_quadrant, _parse_quadrant_log_key, _quadrant_log_key
from app.services.alert_service import (
_classify_quadrant,
_parse_quadrant_log_key,
_quadrant_log_key,
)
# Quadrant ids: 1=① hot&brittle (regime low, warning high), 2=② transition
# (both high), 3=③ healthy (both low), 4=④ real downturn (regime high, warning low).
# Dividers: regime 40, early-warning 60; margin 5.
def test_fresh_classification_uses_60_60_boundaries():
assert _classify_quadrant(20, 90, None) == "1"
assert _classify_quadrant(70, 90, None) == "2"
assert _classify_quadrant(20, 30, None) == "3"
assert _classify_quadrant(70, 30, None) == "4"
def test_fresh_classification():
assert _classify_quadrant(20, 90, None) == "1" # low regime, high warning
assert _classify_quadrant(70, 90, None) == "2" # both high
assert _classify_quadrant(20, 30, None) == "3" # both low
assert _classify_quadrant(70, 30, None) == "4" # high regime, low warning
def test_warning_axis_hysteresis():
assert _classify_quadrant(20, 62, prev="3") == "3"
assert _classify_quadrant(20, 66, prev="3") == "1"
assert _classify_quadrant(20, 58, prev="1") == "1"
assert _classify_quadrant(20, 54, prev="1") == "3"
def test_hysteresis_holds_inside_deadband():
# From ③ (both low): early-warning nudging just past 60 stays ③ until it
# clears 60 + margin (65).
assert _classify_quadrant(20, 62, prev="3") == "3" # within deadband → no flip
assert _classify_quadrant(20, 66, prev="3") == "1" # clears 65 → flips to ①
def test_hysteresis_sticky_when_already_high():
# From ① (warning high): a dip below 60 keeps ① until it drops past 60 - margin (55).
assert _classify_quadrant(20, 58, prev="1") == "1" # still high (deadband)
assert _classify_quadrant(20, 54, prev="1") == "3" # drops past 55 → back to ③
def test_hysteresis_on_regime_axis():
# From ③: regime rising past 40 stays ③ until it clears 45.
assert _classify_quadrant(43, 30, prev="3") == "3"
assert _classify_quadrant(46, 30, prev="3") == "4"
# From ④: regime easing keeps ④ until below 35.
assert _classify_quadrant(37, 30, prev="4") == "4"
assert _classify_quadrant(34, 30, prev="4") == "3"
def test_state_axis_hysteresis():
assert _classify_quadrant(63, 30, prev="3") == "3"
assert _classify_quadrant(66, 30, prev="3") == "4"
assert _classify_quadrant(57, 30, prev="4") == "4"
assert _classify_quadrant(54, 30, prev="4") == "3"
def test_boundary_sitting_does_not_flip():
# A point parked exactly on both dividers keeps whatever quadrant it had.
for q in ("1", "2", "3", "4"):
assert _classify_quadrant(40, 60, prev=q) == q
for quadrant in ("1", "2", "3", "4"):
assert _classify_quadrant(60, 60, prev=quadrant) == quadrant
def test_quadrant_log_key_keeps_previous_values():
key = _quadrant_log_key("3", 32.4, 54.6)
assert _parse_quadrant_log_key(key) == ("3", 32.4, 54.6)
# Existing pre-value keys still parse so old installs do not need migration.
assert _parse_quadrant_log_key("3") == ("3", None, None)
def test_quadrant_key_carries_basket_hash_and_parses_legacy_keys():
key = _quadrant_log_key("3", 32.4, 54.6, "abc123")
assert _parse_quadrant_log_key(key) == ("abc123", "3", 32.4, 54.6)
assert _parse_quadrant_log_key("3:32.4:54.6") == (None, "3", 32.4, 54.6)
assert _parse_quadrant_log_key("3") == (None, "3", None, None)