"""mc5health.py (FW Analyst 1, A1.20): nightly edge-health alarm for the mc5 rule.
Rebuilds the last 7 days of mc5 trades from chain: v3 WETH pools (from /opt/sw/v3live.db), entry at first swap +300 s if cap $250k-$1M,
$100 real-depth fill (L from swaps), pool fee + GMGN 1% each way + $0.05 gas, 3 s delay, stop 0.5x on 0.01+ ETH swaps, time stop 4 h,
pull (Burn of 50%+ of minted liquidity units) = 0, sell capped at 90% of pool WETH.
Writes /root/fwa1/health/mc5-health.txt (main pulls it to fw/mc5-health.txt). Paid dRPC key read in code only, never printed.
Usage: python3 mc5health.py [days=7]"""
import json, sqlite3, time, urllib.request, math, statistics as st, sys, os, datetime, bisect
DAYS = int(sys.argv[1]) if len(sys.argv) > 1 else 7
SPB = 0.101283; B0, T0 = 76678020, 1790787651.0
WETH = "0x0bd7d308f8e1639fab988df18a8011f41eacad73"
T_SWAP = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"
T_MINT = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde"
T_BURN = "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c"
CH = 50000; HOLD = 4 * 3600; NEED = int((300 + HOLD + 600) / SPB)
K = json.load(open("/opt/common/drpc_paid.json"))["robinhood"]
src = open("/root/fwa1/v3pull.py").read().split("\n"); exec(next(l for l in src if l.startswith("U =")))
os.makedirs("/root/fwa1/health", exist_ok=True)
D = sqlite3.connect("/root/fwa1/health/h.db", timeout=120)
D.execute("CREATE TABLE IF NOT EXISTS ev(pool TEXT, b INT, li INT, kind TEXT, data TEXT, PRIMARY KEY(pool, b, li))")
D.execute("CREATE TABLE IF NOT EXISTS done(pool TEXT, f INT, PRIMARY KEY(pool, f))")
D.execute("CREATE TABLE IF NOT EXISTS meta(pool TEXT PRIMARY KEY, token TEXT, is0 INT, fee INT, cb INT, supply REAL, dec INT)")
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
head = int(post({"jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": []})["result"], 16)
# real block clock: block time drifts (0.1009 to 0.1014 s), so map block -> time by interpolating real block timestamps (A1.20 fix)
_ab = sorted({max(1, head - int(k * 86400 / SPB)) for k in range(0, DAYS + 3)} | {head})
_r = post([{"jsonrpc": "2.0", "id": k, "method": "eth_getBlockByNumber", "params": [hex(b), False]} for k, b in enumerate(_ab)])
_at = sorted((_ab[x["id"]], int(x["result"]["timestamp"], 16)) for x in _r if x.get("result"))
def btime(b):
    for (b1, t1), (b2, t2) in zip(_at, _at[1:]):
        if b <= b2: return t1 + (b - b1) * (t2 - t1) / (b2 - b1)
    b1, t1 = _at[-2]; b2, t2 = _at[-1]; return t1 + (b - b1) * (t2 - t1) / (b2 - b1)
now = btime(head)
V = sqlite3.connect("file:/opt/sw/v3live.db?mode=ro", uri=True)
lo_b = head - int((DAYS * 86400 + 6 * 3600) / SPB)
pools = V.execute("SELECT pool, token, token_is0, fee, created_b FROM pools WHERE venue='v3' AND created_b >= ? AND created_b + ? <= ?", (lo_b, NEED, head)).fetchall()
have = {r[0] for r in D.execute("SELECT pool FROM meta")}
new = [p for p in pools if p[0] not in have]
for i in range(0, len(new), 50):                      # supply + decimals
    g = new[i:i + 50]; body = []
    for j, p in enumerate(g):
        body += [{"jsonrpc": "2.0", "id": 2 * j, "method": "eth_call", "params": [{"to": p[1], "data": "0x18160ddd"}, "latest"]},
                 {"jsonrpc": "2.0", "id": 2 * j + 1, "method": "eth_call", "params": [{"to": p[1], "data": "0x313ce567"}, "latest"]}]
    r = {x["id"]: x.get("result") for x in post(body)}
    for j, p in enumerate(g):
        try: dec = int(r.get(2 * j + 1) or "0x12", 16)
        except Exception: dec = 18
        try: sup = int(r.get(2 * j) or "0x0", 16) / 10 ** dec
        except Exception: sup = 0
        D.execute("INSERT OR REPLACE INTO meta VALUES(?,?,?,?,?,?,?)", (p[0], p[1].lower(), p[2], p[3], p[4], sup, dec))
    D.commit(); time.sleep(0.2)
def pull(jobs):
    for i in range(0, len(jobs), 20):
        g = jobs[i:i + 20]
        body = [{"jsonrpc": "2.0", "id": k, "method": "eth_getLogs", "params": [{"address": p, "fromBlock": hex(f), "toBlock": hex(t), "topics": [[T_SWAP, T_MINT, T_BURN]]}]} for k, (p, f, t) in enumerate(g)]
        res = post(body); res = res if isinstance(res, list) else []
        for r in res:
            p, f, t = g[r["id"]]
            if "result" not in r: continue
            for l in r["result"]:
                k = {T_SWAP: "s", T_MINT: "m", T_BURN: "b"}[l["topics"][0]]
                D.execute("INSERT OR IGNORE INTO ev VALUES(?,?,?,?,?)", (p, int(l["blockNumber"], 16), int(l["logIndex"], 16), k, l["data"]))
            D.execute("INSERT OR IGNORE INTO done VALUES(?,?)", (p, f))
        D.commit(); time.sleep(0.25)
done = {tuple(r) for r in D.execute("SELECT pool, f FROM done")}
meta = {r[0]: r for r in D.execute("SELECT pool, token, is0, fee, cb, supply, dec FROM meta")}
P = [p for p in meta.values() if p[4] >= lo_b and p[4] + NEED <= head and p[5]]
pull([(p[0], p[4], p[4] + CH - 1) for p in P if (p[0], p[4]) not in done])          # phase A: first 50k blocks (~84 min)
r = json.loads(urllib.request.urlopen(urllib.request.Request("https://api.coingecko.com/api/v3/coins/ethereum/market_chart?vs_currency=usd&days=%d" % (DAYS + 2),
    headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["prices"]
EUt = [x[0] / 1000 for x in r]
def ethusd(ts): return r[max(0, min(len(r) - 1, bisect.bisect_left(EUt, ts)))][1]
def s256(h):
    v = int(h, 16); return v - (1 << 256) if v >= 1 << 255 else v
def words(d): d = d[2:]; return [int(d[i:i + 64], 16) for i in range(0, len(d), 64)]
def path(pool):
    _, tok, is0, fee, cb, sup, dec = meta[pool]
    ev = D.execute("SELECT b, li, kind, data FROM ev WHERE pool=? ORDER BY b, li", (pool,)).fetchall()
    sw = [e for e in ev if e[2] == "s"]
    if not sw: return None
    fb = sw[0][0]; out = []; prev = None; res = 0.0; added = 0; pull_t = None
    for b, li, kind, d in ev:
        w = words(d)
        if kind != "s":
            amt = (w[3] if is0 else w[2]) / 1e18 if kind == "m" else (w[2] if is0 else w[1]) / 1e18
            liq = w[1] if kind == "m" else w[0]
            if kind == "m": added += liq; res += amt
            else:
                res -= amt
                if added > 0 and liq >= 0.5 * added and pull_t is None and b >= fb: pull_t = (b - fb) * SPB
            continue
        a0 = w[0] - (1 << 256) if w[0] >= 1 << 255 else w[0]; a1 = w[1] - (1 << 256) if w[1] >= 1 << 255 else w[1]
        Pr = (w[2] / 2 ** 96) ** 2
        sw_we = (a1 if is0 else a0) / 1e18; res += sw_we
        if Pr <= 0: continue
        px = (Pr if is0 else 1 / Pr) * 10 ** dec / 1e18
        we = abs(sw_we); rr = math.sqrt(px); L = None
        if prev is not None and we >= 0.001 and abs(rr - prev) / prev > 1e-5: L = we / abs(rr - prev)
        out.append(((b - fb) * SPB, rr, L, we, max(res, 0.0))); prev = rr
    return out, pull_t, fee / 1e6, sup, fb
def Lat(pth, i):
    Ls = [x[2] for x in pth[max(0, i - 40):i + 1] if x[2]][-8:]
    return st.median(Ls) if Ls else None
def state(pth, t): return max((k for k, x in enumerate(pth) if x[0] <= t), default=None)
def cpm(r, L, x, sell=False):
    Wv, Tv = L * r, L / r
    return (Wv - Wv * Tv / (Tv + x)) if sell else (Tv - Wv * Tv / (Wv + x))
def entry(pool):
    pp = path(pool)
    if not pp: return None
    pth, pull_t, pf, sup, fb = pp
    i = state(pth, 300)
    if i is None: return None
    eu = ethusd(btime(fb)); mc = pth[i][1] ** 2 * sup * eu
    return pp, i, eu, mc
cand = []
for p in P:
    e = entry(p[0])
    if e and 2.5e5 <= e[3] < 1e6 and e[0][0][e[1]][4] >= 0.1 and any(x_[2] for x_ in e[0][0][:e[1] + 1]): cand.append(p)   # dust guard A1.20
pull([(p[0], f, min(f + CH - 1, p[4] + NEED)) for p in cand for f in range(p[4] + CH, p[4] + NEED + 1, CH) if (p[0], f) not in done])   # phase B
T = []
for p in cand:
    (pth, pull_t, pf, sup, fb), i, eu, mc = entry(p[0])
    L = Lat(pth, i)
    if not L: continue
    x = 100 / eu; gas = 0.05 / eu
    xin = x * (1 - pf) * 0.99 - gas; q = cpm(pth[i][1], L, xin)
    Tm = []; M = []
    for k in range(i + 1, len(pth)):
        if pth[k][0] > 300 + HOLD: break
        Lk = Lat(pth, k)
        if not Lk or pth[k][3] < 0.01: continue
        out = min(cpm(pth[k][1], Lk, q, True), 0.9 * (pth[k][4] + xin))   # pool WETH includes our own buy
        Tm.append(pth[k][0]); M.append(max(0, out * (1 - pf) * 0.99 - gas) / x)
    v, why = None, "time"
    for k in range(len(M)):
        if pull_t is not None and pull_t <= Tm[k]: v, why = 0.0, "pull"; break
        if M[k] <= 0.5:
            j = max(k, bisect.bisect_right(Tm, Tm[k] + 3) - 1); v, why = M[j], "stop"; break
    if v is None:
        if pull_t is not None and pull_t <= 300 + HOLD: v, why = 0.0, "pull"
        else: v = M[-1] if M else max(0, min(cpm(pth[i][1], L, q, True), 0.9 * (pth[i][4] + xin)) * (1 - pf) * 0.99 - gas) / x   # no 0.01+ swap after entry: sell at entry state
    T.append((btime(fb), p[0], p[1], mc, v, why))
def bkk(ts): return datetime.datetime.utcfromtimestamp(ts + 7 * 3600)
def window(nd):
    cut = now - NEED * SPB - nd * 86400; S = [t for t in T if t[0] >= cut]
    v = [t[4] for t in S]
    if not v: return nd, 0, 0, 0, 0, 0
    return nd, len(v), len(v) / nd, st.mean(v), st.median(v), sum(x - 1 for x in v) * 100 / nd
w3, w7 = window(3), window(min(7, DAYS))
word = "DEAD" if w7[1] and w7[3] < 1.0 else "WEAK" if w7[1] and w7[3] < 1.2 else "HEALTHY" if w7[1] else "NO TRADES"
L = ["mc5 edge health (FW Analyst 1). Made %s Bangkok. Data to %s Bangkok (coins need 4 h 15 min to finish)." % (bkk(time.time()).strftime("%d %b %H:%M"), bkk(now - NEED * SPB).strftime("%d %b %H:%M")),
     "", "STATUS: %s   (rule: 7-day mean under 1.0x = DEAD, under 1.2x = WEAK)" % word, ""]
for nd, n, pd, mn, md, usd in (w3, w7):
    L.append("last %d days: %d trades (%.1f a day), mean %.2fx, median %.2fx, $%+.0f a day at $100" % (nd, n, pd, mn, md, usd))
L += ["", "Rule: v3 WETH pool, first swap +5 min, cap $250k to $1M, $100 real-depth fill, fees 1% GMGN each way + pool fee + gas,", "stop 0.5x, time stop 4 h, pulled = 0. Code: http://159.223.88.168/hx-c9ois/fw/a1code/mc5health.py", "",
      "Trades (newest first): first swap (Bangkok) | pool | token | cap at +5 min | x | exit"]
for t in sorted(T, reverse=True):
    L.append("  %s | %s | %s | $%.0fk | %.2fx | %s" % (bkk(t[0]).strftime("%d %b %H:%M"), t[1], t[2], t[3] / 1e3, t[4], t[5]))
open("/root/fwa1/health/mc5-health.txt.tmp", "w").write("\n".join(L) + "\n"); os.replace("/root/fwa1/health/mc5-health.txt.tmp", "/root/fwa1/health/mc5-health.txt")
print(word, w3, w7, "pools", len(P), "cand", len(cand), "trades", len(T))
