| """Tests for the 5-stage detection cascade.""" |
| import pytest |
|
|
| from flash_crash_watchdog.cascade import DetectionCascade |
| from flash_crash_watchdog.lob import OrderBookSnapshot, PriceLevel |
| from flash_crash_watchdog.tick import Tick |
|
|
|
|
| def make_normal_tick(ts: int = 1000) -> Tick: |
| """A tick that should NOT trigger an alert.""" |
| return Tick( |
| book=OrderBookSnapshot( |
| timestamp_ms=ts, |
| bids=[PriceLevel(99.5, 1.0)], |
| asks=[PriceLevel(100.5, 1.0)], |
| ), |
| symbol="BTCUSDT", |
| ) |
|
|
|
|
| def make_anomalous_tick(ts: int = 1000) -> Tick: |
| """A tick with extreme OBI that SHOULD trigger Stage 1.""" |
| return Tick( |
| book=OrderBookSnapshot( |
| timestamp_ms=ts, |
| bids=[PriceLevel(99.5, 50.0)], |
| asks=[PriceLevel(100.5, 0.1)], |
| ), |
| symbol="BTCUSDT", |
| ) |
|
|
|
|
| def test_stage1_rejects_normal_ticks(): |
| """Stage 1 should reject normal ticks quickly.""" |
| from flash_crash_watchdog.models.stage1_statistical import Stage1Statistical |
| s1 = Stage1Statistical() |
| |
| for i in range(200): |
| score, passed = s1.score(make_normal_tick(ts=1000 + i)) |
| |
| assert score < 0.5 |
| assert not passed |
|
|
|
|
| def test_cascade_processes_tick(): |
| """The full cascade should process a tick without error.""" |
| cascade = DetectionCascade.from_config("configs/pipeline.yml") |
| |
| for i in range(100): |
| result = cascade.process_tick(make_normal_tick(ts=1000 + i)) |
| assert result is None |
| assert cascade.stats.ticks_total == 100 |
|
|
|
|
| def test_cascade_stats(): |
| """Cascade stats should be tracked correctly.""" |
| cascade = DetectionCascade.from_config("configs/pipeline.yml") |
| for i in range(50): |
| cascade.process_tick(make_normal_tick(ts=1000 + i)) |
| stats = cascade.stats |
| assert stats.ticks_total == 50 |
| assert stats.total_latency_ms > 0 |
|
|