65dd53baa3
Fires once when the regime monitor shifts quadrant (regime index x early warning), so you don't have to watch the tab. Two guards against spam: - Hysteresis: each axis only flips once the value crosses its divider by a margin, so a point parked on a boundary keeps its quadrant instead of flip-flopping day to day. - Cooldown: a genuine change stays quiet for a few days after the last alert. Seeds the baseline silently on first run; reuses the existing Telegram dispatch + AlertLog. New per-trigger toggle in Admin → Alerts (on by default). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 lines
1.9 KiB
Python
46 lines
1.9 KiB
Python
"""Tests for the regime quadrant classification + hysteresis (anti-flicker)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.services.alert_service import _classify_quadrant
|
|
|
|
|
|
# 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():
|
|
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_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_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
|