"""v3dead.py (A1.25, sup-to-a1 v21): DEAD COIN EXIT on the dust-guarded mc5 book.
Base: +5 min $250k-$1M, dust guard, fill = pool state 6 s after the signal, stop 0.5x (on 0.01+ ETH swaps), 4 h time stop, pull = 0,
every exit sold at the END OF THE NEXT BLOCK (state 0.15 s after the trigger), own buy kept in pool WETH, pool fee + 1% each way + gas.
Dead exit: at check time T after the fill (+10/+20/+30/+60 min), sell if price < X of fill price (0.7/0.85/1.0)
AND new buyer wallets in the last 10 min <= K (0/3/10), or with no buyer test.
New buyer wallet = first time an address receives the token (Transfer logs), not the pool, not a router (100+ swap recipients in the DBs).
Pick on H1 ($/day at $100), test once on H2: pass if H2 $/day up AND worst 3 days not worse AND still up without the top 2 coins."""
import collections, statistics as st, datetime, json, time, urllib.request, sqlite3, numpy as np
exec(open("/root/fwa1/v3cheap.py").read().split("def series_h")[0])
src = open("/root/fwa1/v3pull.py").read().split("\n"); exec(next(l for l in src if l.startswith("K ="))); exec(next(l for l in src if l.startswith("U =")))
T_TR = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
NB = 0.15
def post(body):
    for i in range(5):
        try:
            rq = urllib.request.Request(U, json.dumps(body).encode(), {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"})
            return json.load(urllib.request.urlopen(rq, timeout=90))
        except Exception as e:
            err = e; time.sleep(3 * (i + 1))
    raise err
ROUT = set()
for DB in (D, E):
    ROUT |= {r[0] for r in DB.execute("SELECT rcp FROM sw GROUP BY rcp HAVING COUNT(*) >= 100")}
    ROUT |= {r[0] for r in DB.execute("SELECT snd FROM sw GROUP BY snd HAVING COUNT(*) >= 100")}
print("routers excluded", len(ROUT), flush=True)
def setup(p, usd):
    pth, pull, pf, sup = CACHE[p["pool"]]
    i0 = state(pth, 300)
    if i0 is None or pth[i0][4] < 0.1 or not any(x[2] for x in pth[:i0 + 1]): return None
    if not Lat(pth, i0): return None
    mc = pth[i0][1] ** 2 * sup * p["eu"]
    if not (2.5e5 <= mc < 1e6): return None
    i = state(pth, 300 + FD); L = Lat(pth, i)
    if not L: return None
    eu = p["eu"]; gas = 0.05 / eu; x = usd / eu; q = buy(pth[i][1], L, x * (1 - pf) * 0.99 - gas)
    def val(k):
        Lk = Lat(pth, k) or L
        out = min(sell(pth[k][1], Lk, q), 0.9 * (pth[k][4] + x))
        return max(0, out * (1 - pf) * 0.99 - gas) / x
    def exit_at(t):
        if pull is not None and pull <= t + NB: return 0.0
        return val(state(pth, t + NB))
    te = 300 + FD; end = te + 4 * H; base = None; tb = end
    for k in range(i + 1, len(pth)):
        t = pth[k][0]
        if t > end: break
        if pull is not None and pull <= t: base, tb = 0.0, pull; break
        if pth[k][3] >= 0.01 and val(k) <= 0.5: base, tb = exit_at(t), t; break
    if base is None:
        if pull is not None and pull <= end: base, tb = 0.0, pull
        else: base = val(state(pth, end))
    return dict(pth=pth, i=i, te=te, base=base, tb=tb, exit_at=exit_at, mc=mc)
SET = {100: [], 1000: []}
for p in allp:
    for usd in (100, 1000):
        s = setup(p, usd)
        if s: SET[usd].append((p, s))
print("trades", len(SET[100]), flush=True)
# new buyer wallets from token Transfer logs, first swap to fill + 61 min
TB = {}
jobs = []
for p, s in SET[100]:
    tok = meta[p["pool"]][1]; f = p["fb"]; t = p["fb"] + int((s["te"] + 3660) / SPB)
    jobs.append((p["pool"], tok, f, t))
for a in range(0, len(jobs), 20):
    g = jobs[a:a + 20]
    res = post([{"jsonrpc": "2.0", "id": k, "method": "eth_getLogs", "params": [{"address": tok, "fromBlock": hex(f), "toBlock": hex(t), "topics": [T_TR]}]} for k, (pool, tok, f, t) in enumerate(g)])
    for r in res if isinstance(res, list) else []:
        pool, tok, f, t = g[r["id"]]
        if "result" not in r: print("getLogs fail", pool, r.get("error")); continue
        seen = set(); ev = []
        fb = f
        for l in sorted(r["result"], key=lambda l: (int(l["blockNumber"], 16), int(l["logIndex"], 16))):
            to = "0x" + l["topics"][2][-40:]
            if to in seen or to == pool or to in ROUT or to == "0x" + "0" * 40: continue
            seen.add(to); ev.append((int(l["blockNumber"], 16) - fb) * SPB)
        TB[pool] = np.array(ev)
    time.sleep(0.25)
print("buyer series", len(TB), "median new buyers in first hour", st.median([len(v) for v in TB.values()]), flush=True)
def newb(pool, t1, t2):
    v = TB.get(pool)
    return None if v is None else int(((v > t1) & (v <= t2)).sum())
def run(usd, T, X, Kb):
    R = []; cut = []
    for p, s in SET[usd]:
        tc = s["te"] + T; x = s["base"]
        if tc < s["tb"]:
            pth = s["pth"]; j = state(pth, tc)
            ratio = (pth[j][1] / pth[s["i"]][1]) ** 2
            nb = newb(p["pool"], tc - 600, tc)
            if ratio < X and (Kb is None or (nb is not None and nb <= Kb)):
                x = s["exit_at"](tc); cut.append((p, s["base"], x))
        R.append((p, x, usd))
    return R, cut
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)]
def worst3(R, days):
    pn = collections.defaultdict(float)
    for p, x, u in R: pn[day(p["t"])] += (x - 1) * u
    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 S(R, days):
    R = [r for r in R if day(r[0]["t"]) in days]; v = [x for p, x, u in R]
    if not v: return dict(n=0, usd=0, w3=0, top2=0, ml=0, mean=0)
    pnl = sorted(((x - 1) * u for p, x, u in R), reverse=True); los = [x - 1 for x in v if x < 1]
    return dict(n=len(v), mean=st.mean(v), usd=sum(pnl) / len(days), w3=worst3(R, days), top2=sum(pnl[2:]) / len(days), ml=st.mean(los) if los else 0)
def fmt(d): return "n%d mean %.2fx $%+.0f/day worst3 $%+.0f no-top2 $%+.0f/day loser %.2f tkt" % (d["n"], d["mean"], d["usd"], d["w3"], d["top2"], d["ml"])
def cutstat(cut, days):
    c = [r for r in cut if day(r[0]["t"]) in days]
    lc = [r for r in c if r[1] < 1]; wk = [r for r in c if r[1] >= 1]
    return len(c), len(lc), wk
out = {}
for usd in (100, 1000):
    base = [(p, s["base"], usd) for p, s in SET[usd]]
    out[(usd, "base")] = (S(base, D1), S(base, D2))
    print("\n== $%d ==\nBASE (next-block exits) | H1 %s | H2 %s" % (usd, fmt(out[(usd, "base")][0]), fmt(out[(usd, "base")][1])), flush=True)
    for T in (600, 1200, 1800, 3600):
        for X in (0.7, 0.85, 1.0):
            for Kb in (None, 0, 3, 10):
                R, cut = run(usd, T, X, Kb); a, b = S(R, D1), S(R, D2)
                out[(usd, T, X, Kb)] = (a, b, cut)
                n1, l1, w1 = cutstat(cut, D1); n2, l2, w2 = cutstat(cut, D2)
                print("T+%dm X%.2f K%s | H1 %s cut %d (losers %d, winners %d) | H2 %s cut %d (losers %d, winners %d)" % (
                    T // 60, X, "-" if Kb is None else Kb, fmt(a), n1, l1, len(w1), fmt(b), n2, l2, len(w2)), flush=True)
keys = [k for k in out if k[0] == 100 and k[1] != "base"]
pick = max(keys, key=lambda k: out[k][0]["usd"])
b1, b2 = out[(100, "base")]
a, b, cut = out[pick]
print("\nH1 PICK ($100): T+%dm X%.2f K%s  H1 $%+.0f vs base $%+.0f" % (pick[1] // 60, pick[2], "-" if pick[3] is None else pick[3], a["usd"], b1["usd"]))
for usd in (100, 1000):
    a, b, cut = out[(usd,) + pick[1:]]; bb = out[(usd, "base")][1]
    ok = b["usd"] > bb["usd"] and b["w3"] >= bb["w3"] and b["top2"] > bb["top2"]
    print(" $%d H2 pick %s\n $%d H2 base %s\n  -> %s" % (usd, fmt(b), usd, fmt(bb), "PASS" if ok else "FAIL"))
a, b, cut = out[pick]
n2, l2, w2 = cutstat(cut, D2); n1, l1, w1 = cutstat(cut, D1)
print("pick cuts: H1 %d (losers %d), H2 %d (losers %d); winners killed:" % (n1, l1, n2, l2))
for p, xb, xd in w1 + w2: print("  %s base %.2fx -> dead exit %.2fx (%s)" % (p["pool"], xb, xd, day(p["t"])))
