Files
set50-system/backend/tests/test_event_study.py
Kunthawat Greethong ead9aeb25c chore: pre-existing in-tree work (event-study/research/vintages/prices + migration script + integrity docs)
Committing the prior uncommitted working-tree state that predates this session's
data-source work (was already modified/untracked at session start) so the tree
is clean before push. Includes: event-study + research report integrity/forward
observation work, prices tests, research hash migration script, and the
2026-08-23/24 engineering-log + test-evidence notes. Verified green as part of
the full 362-test suite.
2026-08-29 09:19:24 +07:00

133 lines
6.9 KiB
Python

import unittest
from app.event_study import EventStudyError, assess_backtest_readiness, run_event_study
class EventStudyTests(unittest.TestCase):
def test_readiness_blocks_when_independent_events_are_insufficient(self):
result = assess_backtest_readiness([{"vintage_id": "v1"}], min_events=12)
self.assertEqual(result["status"], "blocked")
self.assertEqual(result["reason"], "insufficient_vintages")
self.assertEqual(result["available_events"], 1)
self.assertEqual(result["required_events"], 12)
def test_readiness_rejects_boolean_min_events(self):
with self.assertRaisesRegex(EventStudyError, "min_events"):
assess_backtest_readiness([], min_events=True)
def test_readiness_does_not_count_revisions_as_independent_events(self):
entries = [
{"vintage_id": "v1", "source_id": "bot", "published_at": "2026-01-01T08:00:00+07:00"},
{"vintage_id": "v1-revised", "source_id": "bot", "published_at": "2026-01-01T08:00:00+07:00"},
{"vintage_id": "v2", "source_id": "bot", "published_at": "2026-02-01T08:00:00+07:00"},
]
result = assess_backtest_readiness(entries, min_events=3)
self.assertEqual(result["status"], "blocked")
self.assertEqual(result["available_events"], 2)
def test_readiness_canonicalizes_release_timezone_before_deduplication(self):
entries = [
{"vintage_id": "v1", "source_id": "bot", "published_at": "2026-01-01T08:00:00+07:00"},
{"vintage_id": "v1-revised", "source_id": "bot", "published_at": "2026-01-01T01:00:00Z"},
]
result = assess_backtest_readiness(entries, min_events=2)
self.assertEqual(result["available_events"], 1)
self.assertEqual(result["status"], "blocked")
def test_event_study_applies_windowed_returns_and_costs(self):
events = [
{
"event_id": "v1",
"published_at": "2026-01-01T08:00:00+07:00",
"signals": [
{"symbol": "AOT", "target_weight": 0.5},
{"symbol": "PTT", "target_weight": -0.5},
],
},
{
"event_id": "v2",
"published_at": "2026-01-01T08:00:00+07:00",
"signals": [
{"symbol": "AOT", "target_weight": 0.5},
{"symbol": "PTT", "target_weight": -0.5},
],
},
]
prices = {
"AOT": [{"date": "2026-01-01", "close": 100}, {"date": "2026-01-02", "close": 102}, {"date": "2026-01-05", "close": 104}],
"PTT": [{"date": "2026-01-01", "close": 100}, {"date": "2026-01-02", "close": 99}, {"date": "2026-01-05", "close": 98}],
}
result = run_event_study(events, prices, benchmark_prices=[{"date": "2026-01-01", "close": 100}, {"date": "2026-01-02", "close": 101}, {"date": "2026-01-05", "close": 102}], windows=(1,), cost_bps=100, min_events=2)
window = result["windows"]["1"]
self.assertEqual(result["status"], "ready")
self.assertEqual(result["event_count"], 2)
self.assertGreater(window["gross_return"], window["net_return"])
self.assertEqual(window["cost_bps"], 100)
self.assertIsNotNone(window["benchmark_return"])
def test_event_study_rejects_missing_price_series(self):
events = [{"event_id": "v1", "published_at": "2026-01-01T08:00:00+07:00", "signals": [{"symbol": "AOT", "target_weight": 1.0}]}]
with self.assertRaisesRegex(EventStudyError, "missing prices"):
run_event_study(events, {}, windows=(1,), min_events=1)
def test_event_study_uses_next_trading_session_as_default_anchor(self):
events = [{"event_id": "v1", "published_at": "2026-01-01T08:00:00+07:00", "signals": [{"symbol": "AOT", "target_weight": 1.0}]}]
prices = {
"AOT": [
{"date": "2026-01-01", "close": 100},
{"date": "2026-01-02", "close": 105},
{"date": "2026-01-05", "close": 110},
]
}
result = run_event_study(events, prices, windows=(1,), min_events=1)
self.assertAlmostEqual(result["windows"]["1"]["gross_return"], 110 / 105 - 1, places=8)
def test_event_study_does_not_skip_first_session_after_non_trading_event_date(self):
events = [{"event_id": "weekend", "published_at": "2026-01-03T08:00:00+07:00", "signals": [{"symbol": "AOT", "target_weight": 1.0}]}]
prices = {
"AOT": [
{"date": "2026-01-05", "close": 100},
{"date": "2026-01-06", "close": 101},
{"date": "2026-01-07", "close": 102},
]
}
result = run_event_study(events, prices, windows=(1,), min_events=1)
self.assertAlmostEqual(result["windows"]["1"]["gross_return"], 101 / 100 - 1, places=8)
def test_event_study_requires_known_at_for_point_in_time_prices(self):
events = [{"event_id": "pit", "published_at": "2026-01-01T08:00:00+07:00", "signals": [{"symbol": "AOT", "target_weight": 1.0}]}]
prices = {
"AOT": [
{"session_date": "2026-01-01", "close": 100},
{"session_date": "2026-01-02", "close": 105, "known_at": "2026-01-02T17:00:00+07:00"},
]
}
with self.assertRaisesRegex(EventStudyError, "known_at"):
run_event_study(events, prices, windows=(1,), min_events=1, require_price_known_at=True)
def test_event_study_rejects_price_known_after_session_as_lookahead(self):
events = [{"event_id": "lookahead", "published_at": "2026-01-01T08:00:00+07:00", "signals": [{"symbol": "AOT", "target_weight": 1.0}]}]
prices = {
"AOT": [
{"session_date": "2026-01-01", "close": 100, "known_at": "2026-01-01T17:00:00+07:00", "market_timezone": "Asia/Bangkok"},
{"session_date": "2026-01-02", "close": 105, "known_at": "2026-01-03T09:00:00+07:00", "market_timezone": "Asia/Bangkok"},
]
}
with self.assertRaisesRegex(EventStudyError, "known_at"):
run_event_study(events, prices, windows=(1,), min_events=1, require_price_known_at=True)
def test_event_study_checks_known_at_in_market_timezone(self):
events = [{"event_id": "timezone-lookahead", "published_at": "2026-01-01T08:00:00+07:00", "signals": [{"symbol": "AOT", "target_weight": 1.0}]}]
prices = {
"AOT": [
{"session_date": "2026-01-01", "close": 100, "known_at": "2026-01-01T18:30:00Z", "market_timezone": "Asia/Bangkok"},
{"session_date": "2026-01-02", "close": 105, "known_at": "2026-01-02T17:00:00+07:00", "market_timezone": "Asia/Bangkok"},
]
}
with self.assertRaisesRegex(EventStudyError, "known_at"):
run_event_study(events, prices, windows=(1,), min_events=1, require_price_known_at=True)
if __name__ == "__main__":
unittest.main()