@@ -21,6 +21,9 @@ class Event:
2121 entity_match_type : str = "unverified"
2222 match_evidence : str = ""
2323 relationship_type : str = "unverified"
24+ legacy_compatibility : bool = False
25+ compatibility_reason : str = ""
26+ legacy_provenance : str = ""
2427
2528
2629@dataclass (frozen = True )
@@ -38,6 +41,9 @@ class EventReturn:
3841 benchmark_symbol : str
3942 benchmark_return_pct : float | None
4043 abnormal_return_pct : float | None
44+ compatibility_used : bool = False
45+ compatibility_reason : str = ""
46+ legacy_provenance : str = ""
4147
4248 def to_row (self ) -> dict [str , object ]:
4349 return {
@@ -54,6 +60,9 @@ def to_row(self) -> dict[str, object]:
5460 "benchmark_symbol" : self .benchmark_symbol ,
5561 "benchmark_return_pct" : "" if self .benchmark_return_pct is None else f"{ self .benchmark_return_pct :.6f} " ,
5662 "abnormal_return_pct" : "" if self .abnormal_return_pct is None else f"{ self .abnormal_return_pct :.6f} " ,
63+ "compatibility_used" : str (self .compatibility_used ).lower (),
64+ "compatibility_reason" : self .compatibility_reason ,
65+ "legacy_provenance" : self .legacy_provenance ,
5766 }
5867
5968
@@ -65,9 +74,14 @@ def parse_date(value: str) -> date:
6574 return date .fromisoformat (value .strip ())
6675
6776
68- def load_events (path : str | Path ) -> list [Event ]:
77+ def load_events (path : str | Path , historical_compatibility : bool = False , compatibility_reason : str = "" ) -> list [Event ]:
6978 events : list [Event ] = []
70- for row in read_csv_rows (path ):
79+ rows = read_csv_rows (path )
80+ legacy_schema = bool (rows ) and "relationship_type" not in rows [0 ]
81+ if historical_compatibility and not compatibility_reason .strip ():
82+ raise ValueError ("compatibility_reason is required for historical compatibility" )
83+ for row in rows :
84+ legacy_compatibility = historical_compatibility and legacy_schema
7185 events .append (
7286 Event (
7387 event_id = row ["event_id" ],
@@ -81,6 +95,9 @@ def load_events(path: str | Path) -> list[Event]:
8195 entity_match_type = row .get ("entity_match_type" , "unverified" ) or "unverified" ,
8296 match_evidence = row .get ("match_evidence" , "" ),
8397 relationship_type = row .get ("relationship_type" , "unverified" ) or "unverified" ,
98+ legacy_compatibility = legacy_compatibility ,
99+ compatibility_reason = compatibility_reason if legacy_compatibility else "" ,
100+ legacy_provenance = str (path ) if legacy_compatibility else "" ,
84101 )
85102 )
86103 return events
@@ -130,12 +147,14 @@ def compute_event_returns(
130147 prices : PriceTable ,
131148 windows : tuple [int , ...] = (1 , 5 , 20 ),
132149 benchmark_symbol : str = "SPY" ,
150+ historical_compatibility : bool = False ,
133151) -> list [EventReturn ]:
134152 results : list [EventReturn ] = []
135153 benchmark_symbol = benchmark_symbol .upper ()
136154
137155 for event in events :
138- if event .relationship_type not in VERIFIED_RELATIONSHIPS :
156+ use_legacy = historical_compatibility and event .legacy_compatibility
157+ if event .relationship_type not in VERIFIED_RELATIONSHIPS and not use_legacy :
139158 continue
140159 symbol_dates = sorted_dates (prices , event .symbol )
141160 base_date = first_trading_date_on_or_after (symbol_dates , event .event_date )
@@ -175,6 +194,9 @@ def compute_event_returns(
175194 benchmark_symbol = benchmark_symbol ,
176195 benchmark_return_pct = benchmark_return ,
177196 abnormal_return_pct = abnormal_return ,
197+ compatibility_used = use_legacy ,
198+ compatibility_reason = event .compatibility_reason if use_legacy else "" ,
199+ legacy_provenance = event .legacy_provenance if use_legacy else "" ,
178200 )
179201 )
180202
@@ -196,10 +218,18 @@ def run_event_study(
196218 output_path : str | Path ,
197219 windows : tuple [int , ...] = (1 , 5 , 20 ),
198220 benchmark_symbol : str = "SPY" ,
221+ historical_compatibility : bool = False ,
222+ compatibility_reason : str = "" ,
199223) -> list [EventReturn ]:
200- events = load_events (events_path )
224+ events = load_events (events_path , historical_compatibility = historical_compatibility , compatibility_reason = compatibility_reason )
201225 prices = load_prices (prices_path )
202- results = compute_event_returns (events , prices , windows = windows , benchmark_symbol = benchmark_symbol )
226+ results = compute_event_returns (
227+ events ,
228+ prices ,
229+ windows = windows ,
230+ benchmark_symbol = benchmark_symbol ,
231+ historical_compatibility = historical_compatibility ,
232+ )
203233 write_csv_rows (
204234 output_path ,
205235 [
@@ -216,6 +246,9 @@ def run_event_study(
216246 "benchmark_symbol" ,
217247 "benchmark_return_pct" ,
218248 "abnormal_return_pct" ,
249+ "compatibility_used" ,
250+ "compatibility_reason" ,
251+ "legacy_provenance" ,
219252 ],
220253 [result .to_row () for result in results ],
221254 )
@@ -229,6 +262,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
229262 parser .add_argument ("--output" , required = True , help = "Output CSV path." )
230263 parser .add_argument ("--windows" , default = "1,5,20" , help = "Comma-separated trading-day offsets." )
231264 parser .add_argument ("--benchmark" , default = "SPY" , help = "Benchmark symbol for abnormal returns." )
265+ parser .add_argument ("--historical-compatibility" , action = "store_true" , help = "Explicitly analyze legacy event CSVs." )
266+ parser .add_argument ("--compatibility-reason" , default = "" , help = "Required audit reason for legacy compatibility." )
232267 return parser
233268
234269
@@ -240,6 +275,8 @@ def main(argv: list[str] | None = None) -> None:
240275 output_path = args .output ,
241276 windows = parse_windows (args .windows ),
242277 benchmark_symbol = args .benchmark ,
278+ historical_compatibility = args .historical_compatibility ,
279+ compatibility_reason = args .compatibility_reason ,
243280 )
244281
245282
0 commit comments