"""Test empirique : les cotes Polymarket BTC 15m apportent-elles une info au-dela du spot Binance ?
Stdlib uniquement. Usage: python pm_test.py [n_windows] [asset] [duration]
"""
import json, math, sys, time, urllib.request
from concurrent.futures import ThreadPoolExecutor

N = int(sys.argv[1]) if len(sys.argv) > 1 else 288
ASSET = sys.argv[2] if len(sys.argv) > 2 else "btc"
DUR = sys.argv[3] if len(sys.argv) > 3 else "15m"
STEP = {"5m": 300, "15m": 900}[DUR]
SYMBOL = {"btc": "BTCUSDT", "eth": "ETHUSDT", "sol": "SOLUSDT", "xrp": "XRPUSDT"}[ASSET]


def get(url, tries=4):
    for i in range(tries):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "research/0.1"})
            with urllib.request.urlopen(req, timeout=20) as r:
                return json.loads(r.read())
        except Exception as e:
            if i == tries - 1:
                raise
            time.sleep(1.5 * (i + 1))


now = int(time.time())
last_closed = now // STEP * STEP - 2 * STEP  # marge pour la resolution
starts = [last_closed - i * STEP for i in range(N)]


def fetch_window(ts):
    try:
        ms = get(f"https://gamma-api.polymarket.com/markets?slug={ASSET}-updown-{DUR}-{ts}&closed=true")
        if not ms:
            return None
        m = ms[0]
        prices = json.loads(m["outcomePrices"])
        if prices not in (["1", "0"], ["0", "1"]):
            return None
        up_tok = json.loads(m["clobTokenIds"])[0]
        meta = (m.get("events") or [{}])[0].get("eventMetadata") or {}
        h = get(f"https://clob.polymarket.com/prices-history?market={up_tok}&startTs={ts-1200}&endTs={ts+STEP}&fidelity=1")
        return {"ts": ts, "up": 1 if prices[0] == "1" else 0, "ptb": meta.get("priceToBeat"),
                "hist": [(p["t"], p["p"]) for p in h.get("history", [])]}
    except Exception as e:
        return None


with ThreadPoolExecutor(8) as ex:
    wins = [w for w in ex.map(fetch_window, starts) if w]
print(f"fenetres resolues recuperees: {len(wins)}/{N}")

# Binance 1m klines couvrant toute la periode (+ 2h pour la vol)
kl = {}
t0 = (min(starts) - 7200) * 1000
end = (last_closed + STEP) * 1000
while t0 < end:
    rows = get(f"https://api.binance.com/api/v3/klines?symbol={SYMBOL}&interval=1m&startTime={t0}&limit=1000")
    if not rows:
        break
    for r in rows:
        kl[r[0] // 1000] = (float(r[1]), float(r[4]))  # open, close
    t0 = rows[-1][0] + 60000


def close_at(t):  # dernier close dispo a l'instant t (minute complete)
    m = (t // 60) * 60 - 60
    return kl.get(m, (None, None))[1]


def open_at(t):
    return kl.get((t // 60) * 60, (None, None))[0]


def rv_per_min(t, lookback=60):
    rets = []
    for k in range(lookback):
        a, b = kl.get(t - 60 * (k + 2)), kl.get(t - 60 * (k + 1))
        if a and b:
            rets.append(math.log(b[1] / a[1]))
    if len(rets) < 20:
        return None
    mu = sum(rets) / len(rets)
    return math.sqrt(sum((r - mu) ** 2 for r in rets) / (len(rets) - 1))


def ncdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))


def pm_at(hist, t):
    v = None
    for ht, p in hist:
        if ht <= t:
            v = p
        else:
            break
    return v


def clip(p):
    return min(max(p, 1e-3), 1 - 1e-3)


def logit(p):
    p = clip(p)
    return math.log(p / (1 - p))


def brier(ps, ys):
    return sum((p - y) ** 2 for p, y in zip(ps, ys)) / len(ys)


def logloss(ps, ys):
    return -sum(y * math.log(clip(p)) + (1 - y) * math.log(1 - clip(p)) for p, y in zip(ps, ys)) / len(ys)


def acc(ps, ys):
    return sum((p >= 0.5) == (y == 1) for p, y in zip(ps, ys)) / len(ys)


def fit_logreg(X, y, iters=3000, lr=0.1):
    w = [0.0] * (len(X[0]) + 1)
    n = len(y)
    for _ in range(iters):
        g = [0.0] * len(w)
        for xi, yi in zip(X, y):
            z = w[0] + sum(a * b for a, b in zip(w[1:], xi))
            p = 1 / (1 + math.exp(-max(min(z, 30), -30)))
            e = p - yi
            g[0] += e
            for j, xv in enumerate(xi):
                g[j + 1] += e * xv
        w = [wi - lr * gi / n for wi, gi in zip(w, g)]
    return w


ys_all = [w["up"] for w in wins]
print(f"taux de 'Up' realise: {sum(ys_all)/len(ys_all):.3f}")
print()
print("checkpoint | n | Brier PM | Brier spot | Brier TA | LogLoss PM | LogLoss spot | Acc PM | Acc spot | coef logit(PM) / logit(spot) (regression jointe)")

# Checkpoints : minutes avant/apres le debut de la fenetre
mins = [-5, 0, 1, 3, 5, 7, 10, 12] if DUR == "15m" else [-2, 0, 1, 2, 3, 4]
for mnt in mins:
    t = wins[0]["ts"]  # placeholder
    P, S, T, Y = [], [], [], []
    for w in wins:
        t = w["ts"] + 60 * mnt
        pm = pm_at(w["hist"], t)
        k = open_at(w["ts"])  # proxy Binance du prix d'ouverture
        st = close_at(t) if mnt > 0 else None
        sig = rv_per_min(w["ts"] + 60 * max(mnt, 0))
        if pm is None or k is None or sig is None:
            continue
        rem = (STEP - 60 * max(mnt, 0)) / 60
        if mnt > 0 and st is not None:
            ps = ncdf(math.log(st / k) / (sig * math.sqrt(rem)))
        else:
            ps = 0.5
        # TA naif : momentum 15 min + RSI14 1m -> proba via petite pente
        c_now = close_at(w["ts"] + 60 * max(mnt, 0))
        c_prev = close_at(w["ts"] + 60 * max(mnt, 0) - STEP)
        mom = math.log(c_now / c_prev) / (sig * math.sqrt(STEP / 60)) if c_now and c_prev else 0
        pt = ncdf(0.15 * mom) if mnt <= 0 else ps  # TA ne compte qu'avant l'ouverture
        P.append(pm); S.append(ps); T.append(pt); Y.append(w["up"])
    globals().setdefault("DEV", {}).setdefault(mnt, []).append(abs(pm - 0.5))
    if len(Y) < 30:
        continue
    X = [[logit(p), logit(s)] for p, s in zip(P, S)] if mnt > 0 else [[logit(p), logit(t_)] for p, t_ in zip(P, T)]
    wts = fit_logreg(X, Y)
    print(f"t{mnt:+d}m | {len(Y)} | {brier(P,Y):.4f} | {brier(S,Y):.4f} | {brier(T,Y):.4f} | {logloss(P,Y):.4f} | {logloss(S,Y):.4f} | {acc(P,Y):.3f} | {acc(S,Y):.3f} | {wts[1]:+.2f} / {wts[2]:+.2f}")

# Lead-lag minute par minute: dLogit(PM) a t vs rendement Binance a t+1 (et inverse)
xs_lead, ys_lead, xs_lag, ys_lag, xs_co, ys_co = [], [], [], [], [], []
for w in wins:
    for m in range(1, STEP // 60 - 2):
        t = w["ts"] + 60 * m
        p0, p1, p2 = pm_at(w["hist"], t - 60), pm_at(w["hist"], t), pm_at(w["hist"], t + 60)
        c0, c1, c2 = close_at(t - 60), close_at(t), close_at(t + 60)
        if None in (p0, p1, p2, c0, c1, c2):
            continue
        dl = logit(p1) - logit(p0)
        dl_next = logit(p2) - logit(p1)
        xs_lead.append(dl); ys_lead.append(math.log(c2 / c1))        # PM mene le spot ?
        xs_lag.append(math.log(c1 / c0)); ys_lag.append(dl_next)     # spot mene PM ?
        xs_co.append(math.log(c1 / c0)); ys_co.append(dl)            # contemporain


def corr(a, b):
    n = len(a); ma, mb = sum(a) / n, sum(b) / n
    cov = sum((x - ma) * (y - mb) for x, y in zip(a, b))
    va = math.sqrt(sum((x - ma) ** 2 for x in a)); vb = math.sqrt(sum((y - mb) ** 2 for y in b))
    return cov / (va * vb) if va and vb else float("nan")


print()
print(f"corr( dPM[t] , retour spot[t->t+1] ) = {corr(xs_lead, ys_lead):+.3f}   (n={len(xs_lead)})  -> PM predit-il le spot ?")
print(f"corr( retour spot[t-1->t] , dPM[t->t+1] ) = {corr(xs_lag, ys_lag):+.3f}   (n={len(xs_lag)})  -> PM suit-il le spot avec retard ?")
print(f"corr( retour spot[t-1->t] , dPM[t-1->t] ) = {corr(xs_co, ys_co):+.3f}   (n={len(xs_co)})  -> contemporain")
print()
for k, v in DEV.items():
    if k <= 0:
        print(f"ecart moyen |PM-0.5| a t{k:+d}m : {sum(v)/len(v):.4f}")
