#!/usr/bin/env python3
"""ROT A2 Task 2: does flow lead price? Pre-registered 7 Oct 2026 (rules A2.R1..A2.R9 in registry.csv).
Price target: chain native token vs BTC (log ratio). Flow series below. All signals trailing-only, acted on with 1 day delay.
Outputs: out/lead_report.csv (per series x chain), out/lead_summary.csv (per series)."""
import pandas as pd, numpy as np, os, json
D = os.path.dirname(os.path.abspath(__file__))
os.makedirs(f"{D}/out", exist_ok=True)
df = pd.read_csv(f"{D}/data/flows_daily.csv.gz")
df["date"] = pd.to_datetime(df["date"])
TOK = {"Ethereum":"ETH","Solana":"SOL","BSC":"BNB","Arbitrum":"ARB","Tron":"TRX","Sui":"SUI","Avalanche":"AVAX",
       "Sonic":"S","TON":"TON","Polygon":"POL","Aptos":"APT","OP Mainnet":"OP","Hyperliquid L1":"HYPE"}
REV = {v: k for k, v in TOK.items()}
def wide(metric, src=None):
    x = df[df.metric == metric]
    if src: x = x[x.source == src]
    return x.pivot_table(index="date", columns="entity", values="value", aggfunc="sum").sort_index().asfreq("D")

px = wide("close_usd")
rel = np.log(px.div(px["BTC"], axis=0)).drop(columns=["BTC"])
rel.columns = [REV.get(c, c) for c in rel.columns]
rel = rel[[c for c in rel.columns if c in TOK]]

def share(w):
    w = w[[c for c in w.columns if c in TOK or c == "Base"]]
    return w.div(w.sum(axis=1, min_count=3), axis=0)

stab = wide("stable_supply_usd").ffill(limit=3)
dex = wide("dex_volume_usd").rolling(7, min_periods=5).mean()
fee = wide("fees_usd").rolling(7, min_periods=5).mean()
pad = df[df.metric == "pad_fees_usd"].copy(); pad["chain"] = pad.entity.str.split("|").str[1]
padw = pad.pivot_table(index="date", columns="chain", values="value", aggfunc="sum").sort_index().asfreq("D").fillna(0)
padw = padw.rename(columns={"solana": "Solana", "bsc": "BSC", "base": "Base", "ethereum": "Ethereum", "arbitrum": "Arbitrum", "tron": "Tron", "sui": "Sui", "avax": "Avalanche", "hyperliquid": "Hyperliquid L1", "sonic": "Sonic", "ton": "TON", "polygon": "Polygon", "aptos": "Aptos", "optimism": "OP Mainnet"})
fund = wide("funding_daily"); fund.columns = [REV.get(c, c) for c in fund.columns]
oi = wide("oi_contracts", "bybit"); oi.columns = [REV.get(c, c) for c in oi.columns]
cexv = wide("quote_vol_usd"); cexv.columns = [REV.get(c, c) for c in cexv.columns]

SERIES = {  # id: (name, frame where higher = more money arriving)
 "A2.R1": ("stable supply share, 7d change", share(stab).diff(7)),
 "A2.R2": ("stable supply own growth 7d %", np.log(stab).diff(7)),
 "A2.R3": ("DEX volume share (7d avg), 7d change", share(dex).diff(7)),
 "A2.R4": ("fees share (7d avg), 7d change", share(fee).diff(7)),
 "A2.R5": ("launchpad+app fees heat 7d/28d", (padw.rolling(7).mean() / padw.rolling(28).mean()).where(padw.rolling(28).mean() > 1000)),
 "A2.R6": ("funding 7d mean (crowding, expect NEGATIVE lead)", fund.rolling(7, min_periods=5).mean()),
 "A2.R7": ("open interest contracts 7d log change", np.log(oi.replace(0, np.nan)).diff(7)),
 "A2.R8": ("CEX spot volume share 7d change", share(cexv.rolling(7, min_periods=5).mean()).diff(7)),
 "A2.R9": ("DEX volume share level vs its 90d mean", share(dex) - share(dex).rolling(90, min_periods=60).mean()),
}
H = 14  # forward horizon for "beats BTC"
fwd = rel.shift(-H - 1) - rel.shift(-1)          # return from t+1 (1 day delay) to t+1+H
past = rel - rel.shift(H)
def z(s, n=90): return (s - s.rolling(n, min_periods=60).mean()) / s.rolling(n, min_periods=60).std()

def price_events(r):
    # chain starts beating BTC: 14d rel return crosses above +10%, first time in 14 days
    p = (r - r.shift(14)); on = (p > 0.10) & (p.shift(1) <= 0.10)
    ev, last = [], None
    for d in on[on].index:
        if last is None or (d - last).days > 14: ev.append(d)
        last = d
    return ev

rows = []
for sid, (name, X) in SERIES.items():
    for ch in rel.columns:
        if ch not in X.columns: continue
        x = X[ch]; r = rel[ch]
        ok = x.notna() & r.notna()
        if ok.sum() < 200: continue
        zx = z(x)
        # 1) lead-lag correlation: corr(x_t, rel return over [t+L, t+L+7]) for L in -21..21
        cc = {}
        for L in range(-21, 22):
            y = r.shift(-L - 7) - r.shift(-L)
            m = pd.concat([x, y], axis=1).dropna()
            if len(m) > 150: cc[L] = m.iloc[:, 0].corr(m.iloc[:, 1], method="spearman")
        if not cc: continue
        Lbest = max(cc, key=lambda k: abs(cc[k]))
        # 2) signal fires: z >= 2, first in 14 days; hit = price event within 1..30 days after
        fire = (zx >= 2) & (zx.shift(1) < 2); fires, last = [], None
        for d in fire[fire].index:
            if last is None or (d - last).days > 14: fires.append(d)
            last = d
        ev = price_events(r)
        evs = pd.Series(1, index=pd.DatetimeIndex(ev)) if ev else pd.Series(dtype=float)
        def hit(d): return any(1 <= (e - d).days <= 30 for e in ev)
        valid = r.dropna().index
        valid = valid[(valid >= x.dropna().index.min()) & (valid <= valid.max() - pd.Timedelta(days=31))]
        base = np.mean([hit(d) for d in valid[::3]]) if len(valid) else np.nan
        fh = [hit(d) for d in fires if d in valid]
        # signed lead: for each price event, nearest fire within +-30d; positive = flow first
        leads = []
        for e in ev:
            ds = [(e - f).days for f in fires if abs((e - f).days) <= 30]
            if ds: leads.append(min(ds, key=abs))
        # forward rel return after fire (1 day delay), by era
        fr = fwd[ch].reindex(fires)
        era = lambda a, b: fr[(fr.index >= a) & (fr.index < b)].dropna()
        e1, e2 = era("2019-01-01", "2023-01-01"), era("2023-01-01", "2027-01-01")
        allfwd = fwd[ch].dropna()
        rows.append(dict(rule=sid, series=name, chain=ch, n_days=int(ok.sum()), lag_best=Lbest, corr_best=round(cc[Lbest], 3),
            corr_L0=round(cc.get(0, np.nan), 3), corr_L7=round(cc.get(7, np.nan), 3), corr_Lm7=round(cc.get(-7, np.nan), 3),
            fires=len(fires), hit_rate=round(np.mean(fh), 3) if fh else np.nan, base_rate=round(base, 3),
            lift=round(np.mean(fh) / base, 2) if fh and base else np.nan,
            med_signed_lead=float(np.median(leads)) if leads else np.nan, n_lead=len(leads),
            fwd14_after_fire=round(fr.mean(), 4) if len(fr.dropna()) else np.nan, fwd14_any=round(allfwd.mean(), 4),
            fwd_2019_22=round(e1.mean(), 4) if len(e1) else np.nan, n_2019_22=len(e1),
            fwd_2023_26=round(e2.mean(), 4) if len(e2) else np.nan, n_2023_26=len(e2)))
rep = pd.DataFrame(rows); rep.to_csv(f"{D}/out/lead_report.csv", index=False)
g = rep.groupby(["rule", "series"])
summ = g.agg(chains=("chain", "count"), med_lead=("med_signed_lead", "median"), med_lag_best=("lag_best", "median"),
             med_corr_L7=("corr_L7", "median"), med_hit=("hit_rate", "median"), med_base=("base_rate", "median"),
             med_lift=("lift", "median"), share_lift_gt1=("lift", lambda s: round((s > 1).mean(), 2)),
             fires=("fires", "sum"), edge14=("fwd14_after_fire", "median"), any14=("fwd14_any", "median"),
             edge_19_22=("fwd_2019_22", "median"), edge_23_26=("fwd_2023_26", "median")).reset_index()
summ.to_csv(f"{D}/out/lead_summary.csv", index=False)
pd.set_option("display.width", 250); pd.set_option("display.max_columns", 30)
print(summ.to_string(index=False))
