"""v3lpown.py (A1.26, sup-to-a1 v22): LP OWNER at the +5 min block on the dust-guarded mc5 book (same trades and exits as A1.25 base:
6 s fill, stop 0.5x, 4 h, next-block exits, own buy kept, pull = 0). For each trade: who holds the in-range liquidity at the signal block.
Holder = NFT owner (ownerOf at the signal block) for position-manager mints, else the Mint owner. Classes by the LARGEST holder of in-range
units: EOA (deployer or fresh wallet), CONTRACT (locker / kit / other), BURNED (zero or dead address). Also the largest holder's share.
Uses: SKIP worst class (picked on H1) and SIZE TILT (1.5x burned or contract, 0.5x EOA, equal total stake per half). Pick H1, test H2.
Early warning: NFT Transfer / Approval (by tokenId) on the largest position before the pull Burn."""
import collections, statistics as st, datetime, json, time, math, numpy as np
exec(open("/root/fwa1/v3dead.py").read().split("# new buyer wallets")[0])
alld = sorted({day(p["t"]) for p in allp}); D1 = [d for d in alld if d < day(CUT)]; D2 = [d for d in alld if d >= day(CUT)]
PM = "0x73991a25c818bf1f1128deaab1492d45638de0d3"
T_MINT = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde"
T_BURN = "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c"
T_INC = "0x3067048beee31b25b2f1681f88dac838c8bba36af25bfb2b7cf7473a5847e35f"
T_APP = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
T_APA = "0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31"
DEAD = {"0x" + "0" * 40, "0x000000000000000000000000000000000000dead", "0xdead000000000000000000000000000000000000"}
def batch(calls, size=20):
    out = []
    for a in range(0, len(calls), size):
        g = calls[a:a + size]
        r = post([dict(jsonrpc="2.0", id=k, method=m, params=pr) for k, (m, pr) in enumerate(g)])
        d = {x["id"]: x.get("result") for x in (r if isinstance(r, list) else [])}
        out += [d.get(k) for k in range(len(g))]
        time.sleep(0.2)
    return out
def a40(t): return "0x" + t[-40:]
TR = [(p, s) for p, s in SET[100]]
B300 = {p["pool"]: p["fb"] + int(300 / SPB) for p, s in TR}
END = {p["pool"]: p["fb"] + int((306 + 4 * 3600 + 60) / SPB) for p, s in TR}
# 1. Mint + Burn logs with topics, creation block to end
jobs = []
for p, s in TR:
    cb = meta[p["pool"]][4]
    for f in range(cb, END[p["pool"]] + 1, 50000): jobs.append((p["pool"], f, min(f + 49999, END[p["pool"]])))
res = batch([("eth_getLogs", [{"address": pl, "fromBlock": hex(f), "toBlock": hex(t), "topics": [[T_MINT, T_BURN]]}]) for pl, f, t in jobs])
LOG = collections.defaultdict(list); fails = 0
for (pl, f, t), r in zip(jobs, res):
    if r is None: fails += 1; continue
    LOG[pl] += r
print("mint/burn logs", sum(len(v) for v in LOG.values()), "failed calls", fails, flush=True)
# 2. tick at the signal block (last swap sqrtPrice)
def tick_at(pool, b):
    row = None
    for DB in (D, E):
        r = DB.execute("SELECT b, sq FROM sw WHERE pool=? AND b<=? ORDER BY b DESC, li DESC LIMIT 1", (pool, b)).fetchone()
        if r and (row is None or r[0] > row[0]): row = r
    if not row: return None
    P = (int(row[1], 16) / 2 ** 96) ** 2
    return math.floor(math.log(P) / math.log(1.0001)) if P > 0 else None
def s24(h):
    v = int(h, 16)
    return v - (1 << 256) if v >= 1 << 255 else v
# 3. PM mints: tokenId from the tx receipt
pmtx = sorted({l["transactionHash"] for pl, L in LOG.items() for l in L if l["topics"][0] == T_MINT and a40(l["topics"][1]) == PM and int(l["blockNumber"], 16) <= B300[pl]})
rc = dict(zip(pmtx, batch([("eth_getTransactionReceipt", [h]) for h in pmtx])))
POS = {}
for pl, L in LOG.items():
    pos = []
    for l in L:
        b = int(l["blockNumber"], 16)
        if l["topics"][0] != T_MINT or b > B300[pl]: continue
        own = a40(l["topics"][1]); tl, tu = s24(l["topics"][2]), s24(l["topics"][3]); amt = int(l["data"][2 + 64:2 + 128], 16)
        tid = None
        if own == PM:
            r = rc.get(l["transactionHash"]) or {}
            for x in r.get("logs", []):
                if x["address"].lower() == PM and x["topics"][0] == T_INC and int(x["data"][2:66], 16) == amt: tid = int(x["topics"][1], 16)
        pos.append(dict(b=b, own=own, tl=tl, tu=tu, amt=amt, tid=tid))
    POS[pl] = pos
need = [(pl, q["tid"]) for pl, v in POS.items() for q in v if q["tid"] is not None]
own_at = batch([("eth_call", [{"to": PM, "data": "0x6352211e" + "%064x" % t}, hex(B300[pl])]) for pl, t in need])
NFTOWN = {(pl, t): (a40(o) if o and len(o) >= 42 else None) for (pl, t), o in zip(need, own_at)}
# 4. holders, in-range units, class
H = {}
for pl, v in POS.items():
    tk = tick_at(pl, B300[pl]); u = collections.defaultdict(int)
    for q in v:
        if tk is not None and not (q["tl"] <= tk < q["tu"]): continue
        h = NFTOWN.get((pl, q["tid"])) if q["own"] == PM else q["own"]
        u[h or "unknown"] += q["amt"]
    H[pl] = u
addrs = sorted({a for u in H.values() for a in u if a not in DEAD and a != "unknown"})
code = dict(zip(addrs, batch([("eth_getCode", [a, "latest"]) for a in addrs])))
def cls(a):
    if a in DEAD: return "BURNED"
    if a == "unknown": return "UNKNOWN"
    c = code.get(a)
    return "EOA" if c in ("0x", None, "") else "CONTRACT"
INFO = {}
for p, s in TR:
    u = H.get(p["pool"], {}); tot = sum(u.values())
    if not tot: INFO[p["pool"]] = ("NONE", 0, None); continue
    top = max(u, key=u.get); INFO[p["pool"]] = (cls(top), u[top] / tot, top)
# 5. early warning: NFT moves on the largest position's tokenIds before the pull
pullb = {p["pool"]: (p["fb"] + int(CACHE[p["pool"]][1] / SPB)) if CACHE[p["pool"]][1] is not None else None for p, s in TR}
wj = []
for p, s in TR:
    pl = p["pool"]; top = INFO[pl][2]
    tids = ["0x%064x" % q["tid"] for q in POS.get(pl, []) if q["tid"] is not None and NFTOWN.get((pl, q["tid"])) == top]
    if not tids: continue
    a = min(q["b"] for q in POS[pl]); z = END[pl]
    for f in range(a, z + 1, 50000): wj.append((pl, f, min(f + 49999, z), tids))
wr = batch([("eth_getLogs", [{"address": PM, "fromBlock": hex(f), "toBlock": hex(t), "topics": [[T_TR, T_APP], None, None, tids]}]) for pl, f, t, tids in wj])
MOVES = collections.defaultdict(list)
for (pl, f, t, tids), r in zip(wj, wr):
    for l in r or []:
        if l["topics"][0] == T_TR and int(l["topics"][1], 16) == 0: continue          # the mint itself
        MOVES[pl].append((int(l["blockNumber"], 16), "transfer" if l["topics"][0] == T_TR else "approve", a40(l["topics"][2])))
# 6. money by class
def bucket(sh): return "100%" if sh >= 0.999 else "90-99%" if sh >= 0.9 else "<90%"
def S(R, days):
    R = [r for r in R if day(r[0]["t"]) in days]; v = [x for p, x, w in R]
    if not R: return dict(n=0, mean=0, usd=0, w3=0, top2=0)
    pnl = sorted(((x - 1) * w for p, x, w in R), reverse=True)
    return dict(n=len(R), mean=st.mean(v), usd=sum(pnl) / len(days), w3=worst3(R, days), top2=sum(pnl[2:]) / len(days))
def worst3(R, days):
    pn = collections.defaultdict(float)
    for p, x, w in R: pn[day(p["t"])] += (x - 1) * w
    dl = sorted(datetime.date(2026, int(d[:2]), int(d[3:])) for d in days)
    cal = [dl[0] + datetime.timedelta(j) for j in range((dl[-1] - dl[0]).days + 1)]
    v = [pn.get(c.strftime("%m-%d"), 0.0) for c in cal]
    return min(sum(v[i:i + 3]) for i in range(max(1, len(v) - 2)))
def pulled(p, s): pt = CACHE[p["pool"]][1]; return pt is not None and pt <= s["te"] + 4 * 3600
def fmt(d): return "n%d mean %.2fx $%+.0f/day worst3 $%+.0f no-top2 $%+.0f/day" % (d["n"], d["mean"], d["usd"], d["w3"], d["top2"]) if d["n"] else "n0"
print("\nCLASS of largest in-range holder at the +5 min block (trades | pull rate | mean x | $/day $100 | $/day $1k):")
for half, days in (("H1", D1), ("H2", D2)):
    for c in ("EOA", "CONTRACT", "BURNED", "UNKNOWN", "NONE"):
        T = [(p, s) for p, s in TR if INFO[p["pool"]][0] == c and day(p["t"]) in days]
        if not T: continue
        k = {p["pool"]: s for p, s in SET[1000]}
        a = S([(p, s["base"], 100) for p, s in T], days); b = S([(p, k[p["pool"]]["base"], 1000) for p, s in T if p["pool"] in k], days)
        print("  %s %-8s n%3d pull %3.0f%% mean %.2fx $%+.0f/day $%+.0f/day at $1k" % (half, c, len(T), 100 * sum(pulled(p, s) for p, s in T) / len(T), a["mean"], a["usd"], b["usd"]))
print("\nLARGEST HOLDER SHARE (all classes):")
for half, days in (("H1", D1), ("H2", D2)):
    for bk in ("100%", "90-99%", "<90%"):
        T = [(p, s) for p, s in TR if INFO[p["pool"]][0] not in ("NONE",) and bucket(INFO[p["pool"]][1]) == bk and day(p["t"]) in days]
        if T: print("  %s %-7s n%3d pull %3.0f%% mean %.2fx" % (half, bk, len(T), 100 * sum(pulled(p, s) for p, s in T) / len(T), st.mean(s["base"] for p, s in T)))
# kit cross tab
try:
    KIT = json.load(open("/root/fwa1/v3/kit.json"))
    print("\nclass x kit (all days):", dict(collections.Counter((INFO[p["pool"]][0], KIT.get(p["pool"], "?")) for p, s in TR)))
except Exception as e: print("kit.json", e)
# uses
for usd in (100, 1000):
    T = SET[usd]
    base = [(p, s["base"], usd) for p, s in T]
    h1c = {c: st.mean([s["base"] for p, s in T if INFO[p["pool"]][0] == c and day(p["t"]) in D1] or [9e9]) for c in ("EOA", "CONTRACT", "BURNED", "UNKNOWN")}
    worst = min(h1c, key=h1c.get)
    skip = [(p, s["base"], usd) for p, s in T if INFO[p["pool"]][0] != worst]
    def tilt(days):
        W = [(p, s, 1.5 if INFO[p["pool"]][0] in ("BURNED", "CONTRACT") else 0.5 if INFO[p["pool"]][0] == "EOA" else 1.0) for p, s in T if day(p["t"]) in days]
        m = st.mean(w for p, s, w in W) if W else 1
        return [(p, s["base"], usd * w / m) for p, s, w in W]
    tl = tilt(D1) + tilt(D2)
    print("\n== $%d ==  worst class on H1: %s (H1 mean %.2fx)" % (usd, worst, h1c[worst]))
    for lab, R in (("BASE", base), ("SKIP " + worst, skip), ("SIZE TILT", tl)):
        a, b = S(R, D1), S(R, D2); bb = S(base, D2)
        ok = b["usd"] > bb["usd"] and b["w3"] >= bb["w3"] and b["top2"] > bb["top2"]
        print("  %-14s H1 %s | H2 %s %s" % (lab, fmt(a), fmt(b), "" if lab == "BASE" else ("PASS" if ok else "FAIL")))
print("\nEARLY WARNING (NFT transfer/approve on the largest position before the pull Burn):")
pl_pulled = [(p, s) for p, s in TR if pulled(p, s)]
hit = [(p, [m for m in MOVES[p["pool"]] if m[0] < pullb[p["pool"]]]) for p, s in pl_pulled]
print("  pulled trades %d, with a move before the pull %d" % (len(pl_pulled), sum(1 for p, m in hit if m)))
for p, m in hit:
    if m: print("   %s %s lead %.0f s before pull" % (p["pool"], m[0][1], (pullb[p["pool"]] - m[0][0]) * SPB))
fa = [(p, s) for p, s in TR if not pulled(p, s) and MOVES[p["pool"]]]
print("  not-pulled trades with a move (false alarms): %d of %d" % (len(fa), len(TR) - len(pl_pulled)))
json.dump({p["pool"]: [INFO[p["pool"]][0], INFO[p["pool"]][1], INFO[p["pool"]][2]] for p, s in TR}, open("/root/fwa1/v3/lpown.json", "w"))
