feat: sharpen the event study — more events, fair baseline, per-event view
The first run gave only 2 events (N=2 is anecdote, not evidence) and an unfairly weak coincident baseline, so the +42d lead couldn't be trusted. This makes the measurement meaningful: - More, cleaner events: default drawdown threshold 15%→10%, and dedup switched from "recover to the high" to a rising-edge + cooldown (40d), so distinct drawdowns each register instead of merging. - Fair comparison: each indicator now warns at its OWN 80th percentile instead of a shared absolute 60, removing the artifact that muted the coincident baseline. - Per-event breakdown (date · depth · breadth lead · coincident lead) so a median over a tiny sample can't hide an apples-to-oranges comparison — you see whether both warned on the same drawdown. - Surface precision/recall (best row) + base rate per indicator — the honest edge read, not just lead time. Re-run the Event Study job to regenerate the cached report in the new shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ from datetime import date, timedelta
|
||||
|
||||
from app.services.breadth_service import _breadth_from_closes, compute_divergence_series
|
||||
from app.services.event_study_service import (
|
||||
_lead,
|
||||
_percentile,
|
||||
detect_events,
|
||||
event_centered,
|
||||
signal_centered,
|
||||
@@ -40,6 +42,29 @@ def test_detect_events_two_after_recovery():
|
||||
assert len(events) == 2
|
||||
|
||||
|
||||
def test_detect_events_cooldown_suppresses_close_recross():
|
||||
# Dips below threshold then re-crosses only a few bars later.
|
||||
closes = [100.0] * 300 + [85.0] * 3 + [100.0] * 3 + [85.0] * 3
|
||||
dates = _days(len(closes))
|
||||
assert len(detect_events(closes, dates, threshold_pct=15.0, cooldown=40)) == 1
|
||||
assert len(detect_events(closes, dates, threshold_pct=15.0, cooldown=3)) == 2
|
||||
|
||||
|
||||
def test_percentile_interpolation():
|
||||
vals = [float(v) for v in range(0, 101, 10)] # 0,10,...,100
|
||||
assert _percentile(vals, 50) == 50.0
|
||||
assert _percentile(vals, 80) == 80.0
|
||||
assert _percentile([], 50) is None
|
||||
|
||||
|
||||
def test_lead_earliest_crossing():
|
||||
dates = _days(200)
|
||||
t0 = 120
|
||||
indicator = {dates[i]: (70.0 if t0 - 30 <= i <= t0 else 10.0) for i in range(len(dates))}
|
||||
assert _lead(indicator, t0, dates, pre=60, threshold=60.0) == 30
|
||||
assert _lead(indicator, t0, dates, pre=60, threshold=80.0) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event-centered lead time
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user