TeX 53.6%
Python 46.2%
1"""Stage D.1 — refine split candidates into high-confidence events + exclusion windows.23Stage C flagged 442k ticker-days with overnight ratios near rational split ratios4(over-inclusive). Here we keep events where:5 * the overnight ratio prev_close/first_open is within TOL of a canonical ratio6 r in {2,3,4,5,6,7,8,10,20,3/2} (forward) or 1/r (reverse); tighter TOL for 3/2;7 * both adjacent days have RTH dollar volume >= MIN_DV (kills dead-OTC noise);8 * prev_close >= MIN_PRICE.910Outputs:11 outputs/diagnostics/stage_d/split_events.parquet (ticker, date, ratio, kind)12 outputs/diagnostics/stage_d/split_windows.parquet (ticker, date) — event day ± WINDOW days13"""1415from __future__ import annotations1617from pathlib import Path1819import polars as pl2021ROOT = Path(__file__).resolve().parents[1]22DAILY = ROOT / "data" / "metadata" / "daily"23OUT = ROOT / "outputs" / "diagnostics" / "stage_d"2425CANONICAL = [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 20.0]26TOL, TOL_32 = 0.015, 0.00827MIN_DV = 50_000.028MIN_PRICE = 1.029WINDOW = 1 # exclusion: event day +/- 1 trading day303132def main() -> None:33 OUT.mkdir(parents=True, exist_ok=True)34 d = (35 pl.scan_parquet(str(DAILY / "daily_*.parquet"))36 .filter(pl.col("rth_last_close").is_not_null() & (pl.col("rth_volume") > 0))37 .select("ticker", "date", "rth_first_open", "rth_last_close", "rth_dollar_vol")38 .sort("ticker", "date")39 .with_columns(40 pl.col("rth_last_close").shift(1).over("ticker").alias("prev_close"),41 pl.col("rth_dollar_vol").shift(1).over("ticker").alias("prev_dv"),42 pl.col("date").shift(1).over("ticker").alias("prev_date"),43 )44 .filter(45 pl.col("prev_close").is_not_null()46 & ((pl.col("date") - pl.col("prev_date")).dt.total_days() <= 7)47 & (pl.col("prev_dv") >= MIN_DV)48 & (pl.col("rth_dollar_vol") >= MIN_DV)49 & (pl.col("prev_close") >= MIN_PRICE)50 )51 .with_columns((pl.col("prev_close") / pl.col("rth_first_open"))52 .alias("overnight_ratio"))53 )5455 cond = None56 exprs = []57 for r in CANONICAL + [1.5]:58 tol = TOL_32 if r == 1.5 else TOL59 for ratio, kind in [(r, "split"), (1.0 / r, "reverse_split")]:60 c = (pl.col("overnight_ratio") / ratio - 1).abs() < tol61 cond = c if cond is None else (cond | c)62 exprs.append(63 pl.when((pl.col("overnight_ratio") / ratio - 1).abs() < tol)64 .then(pl.lit(f"{kind}:{r:g}")).otherwise(None))6566 events = (67 d.filter(cond)68 .with_columns(pl.coalesce(exprs).alias("event"))69 .select("ticker", "date", "overnight_ratio", "event",70 "prev_close", "rth_first_open")71 .collect()72 )73 events.write_parquet(OUT / "split_events.parquet")7475 # exclusion windows: event date +/- WINDOW *observed* trading days per ticker76 dates = (77 pl.scan_parquet(str(DAILY / "daily_*.parquet"))78 .select("ticker", "date").unique().sort("ticker", "date")79 .with_columns(pl.int_range(pl.len()).over("ticker").alias("idx"))80 .collect()81 )82 ev = events.join(dates, on=["ticker", "date"], how="left")83 win = []84 for off in range(-WINDOW, WINDOW + 1):85 win.append(ev.select("ticker", (pl.col("idx") + off).alias("idx")))86 windows = (87 pl.concat(win).unique()88 .join(dates, on=["ticker", "idx"], how="inner")89 .select("ticker", "date").unique().sort("ticker", "date")90 )91 windows.write_parquet(OUT / "split_windows.parquet")9293 print(f"events={events.height} "94 f"(splits={(events['event'].str.starts_with('split')).sum()}, "95 f"reverse={(events['event'].str.starts_with('reverse')).sum()}) "96 f"window_days={windows.height}")979899if __name__ == "__main__":100 main()101