from app.analysis.alerts import AlertEngine from app.analysis.confluence import cluster_levels from app.analysis.levels import Level, LevelKind, Side from app.bars.models import Timeframe def level(id_: str, price: float, weight: float): return Level(id_, LevelKind.MA, Timeframe.D1, Side.RESISTANCE, weight, 1, id_, 100, price, 0, None, 0, 100, 100, False, False) def test_oscillation_fires_once_until_separation_and_cooldown(): engine = AlertEngine(min_score=6, cooldown_seconds=900) levels = [level("a", 100, 3), level("b", 100.1, 4)] cluster = cluster_levels(levels, 100, 100, 1) assert len(engine.evaluate(cluster, 100, 1, 0, "/ES")) == 1 assert engine.evaluate(cluster, 100.2, 1, 60, "/ES") == [] assert engine.evaluate(cluster, 100, 1, 901, "/ES") == [] far_cluster = cluster_levels(levels, 100, 103, 1) assert engine.evaluate(far_cluster, 103, 1, 902, "/ES") == [] assert len(engine.evaluate(cluster, 100, 1, 903, "/ES")) == 1 def test_score_threshold_blocks_two_daily_mas_at_default_calibration(): engine = AlertEngine(min_score=28) cluster = cluster_levels([level("a", 100, 12), level("b", 100.1, 12)], 100, 100, 1) assert engine.evaluate(cluster, 100, 1, 0, "/ES") == [] def test_a_third_level_joining_the_zone_does_not_re_alert(): # Membership churns constantly as levels drift in and out of tolerance. # Suppression is by proximity precisely so this reads as one zone. engine = AlertEngine(min_score=6, cooldown_seconds=900) two = cluster_levels([level("a", 100, 3), level("b", 100.1, 4)], 100, 100, 1) assert len(engine.evaluate(two, 100, 1, 0, "/ES")) == 1 three = cluster_levels( [level("a", 100, 3), level("b", 100.1, 4), level("c", 100.2, 5)], 100, 100, 1 ) assert engine.evaluate(three, 100, 1, 60, "/ES") == [] def test_a_genuinely_separate_zone_still_alerts_during_cooldown(): # The cooldown is per zone, not global: a level far away is new information. engine = AlertEngine(min_score=6, cooldown_seconds=900) near = cluster_levels([level("a", 100, 3), level("b", 100.1, 4)], 100, 100, 1) assert len(engine.evaluate(near, 100, 1, 0, "/ES")) == 1 far = cluster_levels([level("c", 120, 3), level("d", 120.1, 4)], 100, 120, 1) assert len(engine.evaluate(far, 120, 1, 60, "/ES")) == 1