"""mc5top.py (FW Analyst 1, A1.24, sup-to-a1 v20): FORWARD PAPER log for the three mc5 top cells, from 6 Oct 2026 17:46 Bangkok.
Cells (signal at first swap + checkpoint, cap band): +5 min $1M-$2M, +10 min $500k-$2M, +10 min $1M-$3M.
Same as mc5: v3 WETH pools, dust guard (0.001+ ETH price-moving swap before the checkpoint, pool WETH 0.1+), $100 real-depth fill
(L from swaps), honest fill = pool state 6 s after the signal, pool fee + GMGN 1% each way + $0.05 gas, stop 0.5x on 0.01+ ETH swaps
(sold 3 s later), time stop 4 h from the fill, pull (Burn of 50%+ minted liquidity units) = 0, sell capped at 90% of pool WETH incl. our buy.
One position per token per cell; base wins (a coin the base rule buys, +5 min $250k-$1M, is not in any cell). A coin can sit in two cells.
Runs every 30 min (cron). Writes /root/fwa1/top/a1-mc5-top-paper.txt. Paid dRPC key read in code only, never printed."""
import json, sqlite3, time, urllib.request, math, statistics as st, os, datetime, bisect
from lpclass import lp_class   # A1.26 tag (sup-to-a1 v23)
START = int(os.environ.get("TOPSTART", 1791283560))   # 6 Oct 2026 17:46 Bangkok (env only for tests)
TOP = os.environ.get("TOPDIR", "/root/fwa1/top")
SPB = 0.101283; FD = 6; HOLD = 4 * 3600; CH = 50000
WIN = int(6 * 3600 / SPB)     # pull each pool up to 6 h after creation (first swap + 10 min + 4 h fits)
CELLS = (("+5 min $1M to $2M", 300, 1e6, 2e6), ("+10 min $500k to $2M", 600, 5e5, 2e6), ("+10 min $1M to $3M", 600, 1e6, 3e6))
T_SWAP = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"
T_MINT = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde"
T_BURN = "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c"
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(TOP, exist_ok=True)
D = sqlite3.connect(TOP + "/t.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 upto(pool TEXT PRIMARY KEY, b INT)")
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)
_ab = sorted({max(1, head - int(k * 3600 / SPB)) for k in range(0, 60, 2)} | {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)
sb = next(b for b, t in _at if t >= START - 2 * 3600)     # pools created from 2 h before start (first swap can lag creation)
V = sqlite3.connect("file:/opt/sw/v3live.db?mode=ro", uri=True)
pools = V.execute("SELECT pool, token, token_is0, fee, created_b FROM pools WHERE venue='v3' AND created_b >= ?", (sb,)).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):
    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)
meta = {r[0]: r for r in D.execute("SELECT pool, token, is0, fee, cb, supply, dec FROM meta")}
upto = dict(D.execute("SELECT pool, b FROM upto").fetchall())
jobs = []
for p in meta.values():
    if not p[5]: continue
    a = upto.get(p[0], p[4] - 1) + 1; z = min(head - 5, p[4] + WIN)
    for f in range(a, z + 1, CH): jobs.append((p[0], f, min(f + CH - 1, z)))
for i in range(0, len(jobs), 20):
    g = jobs[i:i + 20]
    res = post([{"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 = res if isinstance(res, list) else []
    ok = {r["id"] for r in res if "result" in r}
    for r in res:
        if "result" not in r: continue
        p = g[r["id"]][0]
        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"]))
    for k, (p, f, t) in enumerate(g):            # advance only through an unbroken run of good chunks
        if k in ok and upto.get(p, meta[p][4] - 1) == f - 1: upto[p] = t; D.execute("INSERT OR REPLACE INTO upto VALUES(?,?)", (p, t))
    D.commit(); time.sleep(0.25)
r = json.loads(urllib.request.urlopen(urllib.request.Request("https://api.coingecko.com/api/v3/coins/ethereum/market_chart?vs_currency=usd&days=3",
    headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["prices"]      # no ETH price = abort (no silent fallback)
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 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 signal(pp, ts):          # cap at checkpoint ts with dust guard, or None
    pth, pull_t, pf, sup, fb = pp
    i = state(pth, ts)
    if i is None or pth[i][4] < 0.1 or not any(x[2] for x in pth[:i + 1]) or not Lat(pth, i): return None
    return pth[i][1] ** 2 * sup * ethusd(btime(fb))
def trade(pp, ts, ft):        # $100 bought at state ts + 6 s; marked up to ft (seconds after first swap)
    pth, pull_t, pf, sup, fb = pp
    i = state(pth, ts + FD); L = Lat(pth, i); eu = ethusd(btime(fb))
    if pull_t is not None and pull_t <= ts + FD: return 0.0, "pull", True
    x = 100 / eu; gas = 0.05 / eu; xin = x * (1 - pf) * 0.99 - gas; q = cpm(pth[i][1], L, xin)
    te = ts + FD; Tm = []; M = []
    for k in range(i + 1, len(pth)):
        if pth[k][0] > min(te + HOLD, ft): break
        if pth[k][0] <= te: continue
        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))
        Tm.append(pth[k][0]); M.append(max(0, out * (1 - pf) * 0.99 - gas) / x)
    for k in range(len(M)):
        if pull_t is not None and pull_t <= Tm[k]: return 0.0, "pull", True
        if M[k] <= 0.5:
            j = max(k, bisect.bisect_right(Tm, Tm[k] + 3) - 1); return M[j], "stop", True
    done = ft >= te + HOLD
    if pull_t is not None and pull_t <= min(te + HOLD, ft): return 0.0, "pull", True
    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
    return v, ("time 4 h" if done else "open"), done
BOOK = {c[0]: [] for c in CELLS}
for p in meta.values():
    if not p[5]: continue
    pp = path(p[0])
    if not pp: continue
    fb = pp[4]; t0 = btime(fb)
    if t0 < START: continue
    ft = (min(upto.get(p[0], fb), head - 5) - fb) * SPB          # data covered so far, seconds after first swap
    b = signal(pp, 300) if ft >= 300 else None
    if b is not None and 2.5e5 <= b < 1e6: continue                # base wins
    for lab, ts, lo, hi in CELLS:
        if ft < ts + FD: continue
        mc = signal(pp, ts)
        if mc is None or not (lo <= mc < hi): continue
        v, why, done = trade(pp, ts, ft)
        BOOK[lab].append((t0, p[0], p[1], mc, v, why, done))
def bkk(ts): return datetime.datetime.utcfromtimestamp(ts + 7 * 3600)
days = max((now - START) / 86400, 1 / 24)
L = ["mc5 TOP CELLS forward paper log (FW Analyst 1, A1.24). PAPER ONLY, never live until the pass rule says so.",
     "Made %s Bangkok. Forward from 06 Oct 17:46 Bangkok (%.1f h so far). Refreshed every 30 min." % (bkk(time.time()).strftime("%d %b %H:%M"), max(0, now - START) / 3600),
     "Rule: v3 WETH pool, signal at first swap + checkpoint with cap in band, dust guard, $100 real-depth fill 6 s after the signal,",
     "pool fee + GMGN 1% each way + gas, stop 0.5x, time stop 4 h from the fill, pulled = 0. One position per token per cell; base (+5 min $250k to $1M) wins.",
     "Pass rule after 48 h (08 Oct 17:46 Bangkok): a cell joins live only if its closed trades are positive AND positive without its top 2 coins.",
     "Code: http://159.223.88.168/hx-c9ois/fw/a1code/mc5top.py", ""]
for lab, ts, lo, hi in CELLS:
    R = BOOK[lab]; C = [t for t in R if t[6]]; O = [t for t in R if not t[6]]
    pnl = sorted(((t[4] - 1) * 100 for t in C), reverse=True)
    L.append("%s: closed %d (%.1f a day), mean %s, $%+.0f total, $%+.0f a day at $100, without top 2 $%+.0f | open %d (marked $%+.0f)" % (
        lab, len(C), len(C) / days, ("%.2fx" % st.mean(t[4] for t in C)) if C else "-", sum(pnl), sum(pnl) / days, sum(pnl[2:]), len(O), sum((t[4] - 1) * 100 for t in O)))
L += ["", "Trades (newest first): cell | first swap (Bangkok) | entry (Bangkok) | pool | token | cap at signal | x | exit | lp_class"]
for lab, ts, lo, hi in CELLS:
    for t in sorted(BOOK[lab], reverse=True):
        L.append("  %s | %s | %s | %s | %s | $%.0fk | %.2fx | %s | %s" % (lab, bkk(t[0]).strftime("%d %b %H:%M:%S"), bkk(t[0] + ts + FD).strftime("%H:%M:%S"), t[1], t[2], t[3] / 1e3, t[4], t[5], lp_class(t[1])))
if not any(BOOK.values()): L.append("  none yet")
open(TOP + "/a1-mc5-top-paper.txt.tmp", "w").write("\n".join(L) + "\n"); os.replace(TOP + "/a1-mc5-top-paper.txt.tmp", TOP + "/a1-mc5-top-paper.txt")
print("pools", len(meta), "jobs", len(jobs), {k: len(v) for k, v in BOOK.items()})
