73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""Tests for v2 correction events and warning alarm episodes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from app.services.breadth_service import _breadth_from_closes, compute_divergence_series
|
|
from app.services.event_study_service import (
|
|
_percentile,
|
|
alarm_episodes,
|
|
detect_events,
|
|
evaluate_alarms,
|
|
)
|
|
|
|
|
|
def _days(count: int, start: date = date(2021, 1, 1)) -> list[date]:
|
|
return [start + timedelta(days=index) for index in range(count)]
|
|
|
|
|
|
def test_detect_events_uses_rising_edge_and_cooldown():
|
|
closes = [100.0] * 300 + [85.0] * 5 + [100.0] * 50 + [85.0] * 5
|
|
events = detect_events(closes, _days(len(closes)), threshold_pct=15.0, cooldown=40)
|
|
assert [event["index"] for event in events] == [300, 355]
|
|
|
|
|
|
def test_percentile_is_fixed_from_supplied_values():
|
|
values = [float(value) for value in range(0, 101, 10)]
|
|
assert _percentile(values, 50) == 50.0
|
|
assert _percentile(values, 80) == 80.0
|
|
assert _percentile([], 80) is None
|
|
|
|
|
|
def test_alarm_requires_upward_crossing_and_reset():
|
|
dates = _days(10)
|
|
values = [10, 70, 80, 75, 20, 70, 80, 20, 20, 70]
|
|
indicator = dict(zip(dates, values))
|
|
assert alarm_episodes(indicator, dates, threshold=60) == [1, 5, 9]
|
|
|
|
|
|
def test_holdout_start_does_not_invent_crossing_when_already_high():
|
|
dates = _days(6)
|
|
indicator = dict(zip(dates, [10, 70, 80, 80, 20, 70]))
|
|
assert alarm_episodes(indicator, dates, threshold=60, start_index=3) == [5]
|
|
|
|
|
|
def test_evaluate_alarms_counts_episodes_not_alarm_days():
|
|
dates = _days(100)
|
|
result = evaluate_alarms([10, 50, 80], [25, 70], dates, horizon=20)
|
|
assert result["events_warned"] == 2
|
|
assert result["events_missed"] == 0
|
|
assert result["false_alarms"] == 1
|
|
assert result["median_lead_days"] == 17.5
|
|
|
|
|
|
def test_breadth_from_fixed_closes_and_pure_divergence():
|
|
dates = _days(10)
|
|
closes_by_symbol = {
|
|
"A": list(zip(dates, [1.0 + index for index in range(10)])),
|
|
"B": list(zip(dates, [10.0 - index for index in range(10)])),
|
|
"C": list(zip(dates, [5.0] * 10)),
|
|
}
|
|
breadth = _breadth_from_closes(closes_by_symbol, window=3, min_tickers=2)
|
|
assert breadth[dates[2]] == round(1 / 3 * 100, 2)
|
|
|
|
falling_breadth = {dates[index]: 80.0 - index * 3 for index in range(10)}
|
|
rising_benchmark = list(zip(dates, [100.0 + index for index in range(10)]))
|
|
divergence = compute_divergence_series(falling_breadth, rising_benchmark, lookback=3)
|
|
assert divergence[dates[-1]] > 0
|
|
|
|
falling_benchmark = list(zip(dates, [100.0 - index for index in range(10)]))
|
|
no_divergence = compute_divergence_series(falling_breadth, falling_benchmark, lookback=3)
|
|
assert no_divergence[dates[-1]] == 0
|