#!/usr/bin/env python3
"""Event study: Korean exchange listing announcements -> Binance price impact.

Phase 1: scrape BWEnews Telegram public preview (t.me/s/bwenews) back to
CUTOFF, extract Upbit/Bithumb listing events (tickers + UTC timestamp).
Phase 2: for each event, fetch Binance 1m klines and compute forward returns.
Writes events.json and study_results.json to CWD, prints a summary table.
"""
import json
import re
import time
import urllib.request
import urllib.parse
from datetime import datetime, timezone

CUTOFF = datetime(2026, 4, 1, tzinfo=timezone.utc)
MAX_PAGES = 600
UA = {"User-Agent": "Mozilla/5.0"}

LISTING_RE = re.compile(r"(UPBIT LISTING|Upbit 上新|Bithumb Listing|Bithumb上新|Bithumb 上新)", re.I)
TICKER_RE = re.compile(r"\(([A-Z0-9]{2,10})\)")
POST_RE = re.compile(r'data-post="[Bb][Ww][Ee]news/(\d+)"')
TEXT_RE = re.compile(r'js-message_text[^>]*>(.*?)</div>', re.S)
TIME_RE = re.compile(r'<time datetime="([^"]+)"')


def fetch(url):
    req = urllib.request.Request(url, headers=UA)
    for a in range(3):
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                return r.read().decode("utf-8", "replace")
        except Exception:
            if a == 2:
                raise
            time.sleep(3)


def scrape_events():
    events, before, pages = [], None, 0
    while pages < MAX_PAGES:
        url = "https://t.me/s/bwenews" + (f"?before={before}" if before else "")
        html = fetch(url)
        blocks = html.split('tgme_widget_message_wrap')[1:]
        ids = []
        oldest = None
        for b in blocks:
            pm, tm, dm = POST_RE.search(b), TEXT_RE.search(b), TIME_RE.search(b)
            if not (pm and dm):
                continue
            ids.append(int(pm.group(1)))
            ts = datetime.fromisoformat(dm.group(1))
            oldest = min(oldest, ts) if oldest else ts
            if not tm:
                continue
            clean = re.sub(r"<[^>]+>", " ", tm.group(1))
            m = LISTING_RE.search(clean)
            if not m:
                continue
            exch = "upbit" if "upbit" in m.group(1).lower() else "bithumb"
            krw = bool(re.search(r"KRW|원화|韩元", clean))
            for tk in set(TICKER_RE.findall(clean)):
                if tk in ("KRW", "BTC", "USDT"):
                    continue
                events.append({"exchange": exch, "ticker": tk, "krw": krw,
                               "time_utc": ts.isoformat(), "msg_id": int(pm.group(1)),
                               "text": clean[:150].strip()})
        if not ids:
            break
        pages += 1
        before = min(ids)
        if oldest and oldest < CUTOFF:
            break
        time.sleep(0.4)
    # dedup: earliest message per (exchange, ticker) within the window
    events.sort(key=lambda e: e["time_utc"])
    seen, out = {}, []
    for e in events:
        key = (e["exchange"], e["ticker"])
        last = seen.get(key)
        t = datetime.fromisoformat(e["time_utc"])
        if last and (t - last).total_seconds() < 72 * 3600:
            continue
        seen[key] = t
        out.append(e)
    print(f"scraped {pages} pages, {len(out)} deduped events")
    return out


def klines(base, symbol, start_ms, end_ms, interval="1m"):
    q = urllib.parse.urlencode({"symbol": symbol, "interval": interval,
                                "startTime": start_ms, "endTime": end_ms, "limit": 1500})
    try:
        return json.loads(fetch(f"{base}/klines?{q}"))
    except Exception:
        return None


def study(events):
    horizons = [5, 15, 60, 240, 1440]
    results = []
    for e in events:
        t0 = int(datetime.fromisoformat(e["time_utc"]).timestamp() * 1000)
        sym = e["ticker"] + "USDT"
        rows, venue = None, None
        for base, v in [("https://fapi.binance.com/fapi/v1", "perp"),
                        ("https://api.binance.com/api/v3", "spot")]:
            rows = klines(base, sym, t0 - 60000, t0 + 1441 * 60000)
            if rows and len(rows) > 10:
                venue = v
                break
            rows = None
        if not rows:
            e["status"] = "no_binance_market"
            results.append(e)
            continue
        by_min = {r[0]: r for r in rows}
        base_row = by_min.get(t0 - (t0 % 60000)) or rows[0]
        p0 = float(base_row[1])  # open of announcement minute
        rets = {}
        for h in horizons:
            row = by_min.get(t0 - (t0 % 60000) + h * 60000)
            rets[f"ret_{h}m"] = round((float(row[4]) / p0 - 1) * 100, 2) if row else None
        hi60 = max((float(r[2]) for r in rows if t0 <= r[0] <= t0 + 60 * 60000), default=None)
        e.update(status="ok", venue=venue, base_price=p0,
                 max_runup_60m=round((hi60 / p0 - 1) * 100, 2) if hi60 else None, **rets)
        results.append(e)
        time.sleep(0.15)
    return results


def main():
    events = scrape_events()
    json.dump(events, open("events.json", "w"), indent=1)
    results = study(events)
    json.dump(results, open("study_results.json", "w"), indent=1)
    ok = [r for r in results if r.get("status") == "ok"]
    print(f"\n{len(ok)}/{len(results)} events tradeable on Binance")
    for grp_name, grp in [("ALL", ok),
                          ("KRW only", [r for r in ok if r["krw"]]),
                          ("Upbit KRW", [r for r in ok if r["krw"] and r["exchange"] == "upbit"])]:
        if not grp:
            continue
        print(f"\n== {grp_name} (n={len(grp)})")
        for k in ["ret_5m", "ret_15m", "ret_60m", "ret_240m", "ret_1440m", "max_runup_60m"]:
            vals = sorted(r[k] for r in grp if r.get(k) is not None)
            if not vals:
                continue
            n = len(vals)
            mean = sum(vals) / n
            med = vals[n // 2]
            pos = sum(1 for v in vals if v > 0) / n * 100
            print(f"  {k:>14}: mean {mean:+6.2f}%  median {med:+6.2f}%  win {pos:3.0f}%  n={n}")


if __name__ == "__main__":
    main()
