"""Builds the stock-session panel the paper's tests read. Thresholds are fixed here,
before any result is read, and every robustness variant is reported."""
import re, gzip, pickle, sys
import numpy as np, pandas as pd

MIN_PRICE = 1.0
MIN_DV = 100_000.0
SHOCK_ABS = 0.05
SHOCK_Z = 3.0
SAMPLE_START = "2025-10-20"
SAMPLE_END = "2026-11-02"
LAW = re.compile(r"\bclass action\b|\b(shareholder|investor|stockholder)s? (alert|notice|reminder|deadline)\b|\blead plaintiff\b|\bsecurities (fraud|litigation)\b|\binvestigation on behalf\b|\blaw (firm|offices)\b|\bpomerantz\b|\brosen law\b|\blevi & korsinsky\b|\bbragar eagel\b|\bfaruqi\b|\bhagens berman\b|\bkirby mcinerney\b|\bglancy\b|\brobbins geller\b", re.I)
NONCOMMON = re.compile(r"\bwarrants?\b|\bunits?\b|\brights?\b|\bpreferred\b|\bdepositary shares? rep|\bnotes? due\b|\bdebentures?\b|\betf\b|\bfund\b|\bacquisition corp", re.I)
RULES = [
 ("analyst", r"\b(upgraded|downgraded) by\b|\binitiated coverage\b|\bprice target\b"),
 ("listing", r"\bdelist|\bnasdaq (listing|notice|minimum)|\bnyse (notice|listing)|\breverse (stock |share )?split\b|\bshare consolidation\b|\blisting (update|compliance|requirement)|\bregain(s|ed)? compliance\b"),
 ("offering", r"\b(public|registered direct|secondary|equity|stock|share) offering\b|\bregistered direct\b|\bprivate placement\b|\bcapital raise\b|\bequity financing\b|\bat-the-market\b|\bconvertible (senior |promissory )?notes?\b|\bwarrant (inducement|exercise)\b"),
 ("mna", r"\bacqui(re|res|red|sition)\b|\bmerger\b|\bdefinitive agreement\b|\bbusiness combination\b|\b(take|go|going)[- ]private\b|\bstrategic (review|alternatives)\b|\bcarve-out\b|\bshare exchange\b"),
 ("clinical", r"\btopline\b|\bphase (1|2|3|i|ii|iii)\b|\bclinical\b|\bprimary endpoint|\bfda\b|\bpdufa\b|\b510\(k\)"),
 ("earnings", r"\b(quarter|q[1-4]|fiscal|full[- ]year|half[- ]year|half of fiscal year)\b.*\b(results|earnings)\b|\bearnings\b|\bfinancial results\b"),
 ("guidance", r"\bguidance\b|\boutlook\b|\bpreliminary (results|revenue)\b"),
 ("capital", r"\bdividend\b|\bbuyback\b|\b(share|stock) repurchase\b"),
 ("legal", r"\blawsuit\b|\blitigation\b|\bcourt\b|\bsettlement\b|\bpatent\b"),
 ("contract", r"\bcontract\b|\bpartner(s|ship)?\b|\bcollaborat|\baward(s|ed)?\b|\borders?\b|\bagreement\b|\blicens(e|ing)\b|\bsupply\b"),
 ("leadership", r"\b(ceo|cfo|chief executive|chief financial)\b|\bappoints?\b|\bsteps down\b"),
]
RULES = [(k, re.compile(p, re.I)) for k, p in RULES]
ANALYST_CATS = {17, 18, 19, 20, 21}

def classify(title, cat):
    if cat in ANALYST_CATS: return "analyst"
    for k, r in RULES:
        if r.search(title): return k
    if cat == 10: return "mna"
    return "other"

def load():
    c = pd.read_csv("data/companies.tsv", sep="\t", encoding="latin-1", dtype=str, keep_default_na=False, quoting=3)
    c["id"] = c["id"].astype(int)
    c["ticker"] = c["ticker"].str.strip().str.upper()
    c["common"] = (c["is_etf"] != "1") & ~c["name"].fillna("").str.contains(NONCOMMON)
    b = pd.read_csv("data/bars.csv.gz", dtype={"sym": str})
    b = b[b.t <= SAMPLE_END]
    close = b.pivot(index="t", columns="sym", values="c").sort_index()
    opn = b.pivot(index="t", columns="sym", values="o").reindex(close.index)
    vol = b.pivot(index="t", columns="sym", values="v").reindex(close.index)
    vw = b.pivot(index="t", columns="sym", values="vw").reindex(close.index)
    sess = close.index[close["SPY"].notna()]
    close, opn, vol, vw = [x.reindex(sess) for x in (close, opn, vol, vw)]
    return c, close, opn, vol, vw

def et_session(ts_utc, sessions):
    et = pd.to_datetime(ts_utc, utc=True, errors="coerce").dt.tz_convert("America/New_York")
    d = et.dt.strftime("%Y-%m-%d")
    mins = et.dt.hour * 60 + et.dt.minute
    idx = np.searchsorted(sessions, d.values)
    idx = np.where(mins.values >= 16 * 60, np.searchsorted(sessions, d.values, side="right"), idx)
    return et, d, mins, idx

def build():
    c, close, opn, vol, vw = load()
    sessions = close.index.values.astype(str)
    syms = close.columns.values
    # drop thin series and impossible daily moves (unadjusted reverse splits, bad prints)
    ret = close / close.shift(1) - 1
    ret = ret.where(ret.abs() < 3)
    spy = ret["SPY"]
    ar = ret.sub(spy, axis=0)
    sig = ar.rolling(60, min_periods=20).std().shift(1)
    dv = (close * vol).rolling(20, min_periods=10).median().shift(1)
    pc = close.shift(1)
    nc = pd.read_csv("data/nc.tsv", sep="\t", header=None, names="nid cid at parser cat imp".split())
    nc = nc[nc["at"].str.startswith(("2025", "2026"))]
    et, d, mins, idx = et_session(nc["at"], sessions)
    nc["et"] = et; nc["etd"] = d; nc["mins"] = mins; nc["si"] = idx
    nc = nc.merge(c[["id", "ticker"]], left_on="cid", right_on="id", how="left")
    nc = nc[nc.si < len(sessions)]
    n_tick = nc.groupby("nid")["cid"].transform("nunique")
    nc["single"] = n_tick == 1
    npr = pd.read_csv("data/npr.tsv", sep="\t", encoding="latin-1", quoting=3, dtype={"ticker": str})
    npr["law"] = npr["title"].fillna("").str.contains(LAW)
    cat = nc.drop_duplicates("nid").set_index("nid")["cat"]
    npr["cat"] = npr["newsitem_id"].map(cat)
    npr["catalyst"] = [classify(t if isinstance(t, str) else "", (None if pd.isna(k) else int(k))) for t, k in zip(npr["title"], npr["cat"])]
    et2, _, mins2, si2 = et_session(npr["event_at"], sessions)
    npr["et"] = et2; npr["mins"] = mins2; npr["si"] = si2
    npr = npr[npr.si < len(sessions)]
    pickle.dump(dict(c=c, close=close, opn=opn, vol=vol, vw=vw, ret=ret, ar=ar, sig=sig, dv=dv, pc=pc, spy=spy, nc=nc, npr=npr, sessions=sessions), open("panel.pkl", "wb"), protocol=4)
    print("panel", close.shape, "nc", len(nc), "npr", len(npr), "sessions", sessions[0], sessions[-1])

if __name__ == "__main__":
    build()
