"""Q-FLOAT (DC Analyst 1, 7 Oct 2026). Prereg: dc-prereg.md line '7 Oct 11:01 | A1 | Q-FLOAT'. Read only on PP data.
Insider float share (F1) and outside net buy (F2) at D = cross($500k) + 1 h, vs DC0 v2 outcome (3x or 48 h, pull = 0)."""
import sqlite3, pickle, json, bisect, datetime, collections, random, statistics, csv, gzip
OUT = "/root/dca1"; BKK = datetime.timezone(datetime.timedelta(hours=7))
def bk(t): return datetime.datetime.fromtimestamp(t, BKK).strftime("%Y-%m-%d %H:%M")
T_START = int(datetime.datetime(2026, 9, 15, tzinfo=BKK).timestamp()); T_B = int(datetime.datetime(2026, 9, 29, tzinfo=BKK).timestamp())
T_END = int(datetime.datetime(2026, 10, 6, tzinfo=BKK).timestamp()); H48 = 48 * 3600
PM = "0x8366a39cc670b4001a1121b8f6a443a643e40951"; WETH = "0x0bd7d308f8e1639fab988df18a8011f41eacad73"; ZERO = "0x" + "0" * 40
HUB = {PM, ZERO, "0x8876789976decbfcbbbe364623c63652db8c0904", "0x204faca1764b154221e35c0d20abb3c525710498",
       "0x6aa80dbbed9ae5ab45fbf61f9644fada3b29326e", "0x36dc95f1f088e11c0066dc19173f745655002bc2", "0x65050a9b7e5075a2ba5ced7b1b64ee66262c40dc",
       "0x039ec98a76f111092d4751365ff09dd2aec301e8", "0x0da5899dbb36bd741885d4228f04cd9c6304fc56", "0x000000000022d473030f116ddee9f6b43ac78ba3",
       "0x000000000000000000000000000000000000dead"}
T_TR = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
T_V4SW = "0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f"; T_V3SW = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"
LQ = sqlite3.connect("file:/opt/pricepredict-data/liqlock.db?mode=ro", uri=True)
BT = LQ.execute("SELECT k, t, spb FROM btime ORDER BY k").fetchall(); BTT = [r[1] for r in BT]
def blk(t):
    i = max(0, bisect.bisect_right(BTT, t) - 1); k, t0, spb = BT[i]; return int(k * 1000 + (t - t0) / (spb or 0.1013))
EUL = json.load(open("/opt/sw/ethusd_hourly.json")); EUT = [a for a, _ in EUL]
def eu(t): return EUL[min(len(EUL) - 1, max(0, bisect.bisect_left(EUT, t)))][1]
def word(d, k): return d[2 + 64 * k: 2 + 64 * (k + 1)]
def i256(h): v = int(h, 16); return v - (1 << 256) if v >= 1 << 255 else v
O = sqlite3.connect("file:/opt/sw/pp3.db?mode=ro", uri=True); RAW = sqlite3.connect("file:/opt/sw/pp3_raw.db?mode=ro", uri=True, timeout=120)
CR = sqlite3.connect("file:/opt/pricepredict-data/crew.db?mode=ro", uri=True)
P = {}
for f in ("ev_v4.pkl", "ev_v3.pkl"): P.update(pickle.load(open(OUT + "/" + f, "rb")))
DEND = {"v4": min(LQ.execute("SELECT MAX(t) FROM btime").fetchone()[0], max(x[0] for d in P.values() if d["venue"] == "v4" for x in d["ev"][-1:])),
        "v3": max(x[0] for d in P.values() if d["venue"] != "v4" for x in d["ev"][-1:])}
def Rfix(R, cap):
    if not R or R <= 0: R = 0.1 * cap
    return min(R, 0.5 * cap) if cap > 0 else R
def trade(d, i, ticket=100):
    ev = d["ev"]; pull = d["pull"] or 9e18; t_e, cap_e = ev[i][0], ev[i][1]
    if cap_e <= 0: return None
    R = Rfix(ev[i][7], cap_e); units = ticket * 0.99 * R / (R + ticket) / cap_e
    def out(cap, R):
        if cap <= 0: return 0.0
        v = units * cap; R = Rfix(R, cap); return v * R / (R + v) * 0.99 / ticket
    lastS = None; last = ev[i]
    for j in range(i + 1, len(ev)):
        x = ev[j]
        if x[0] >= pull: return (0.0, "pull", 0)
        if x[0] > t_e + H48: break
        if x[6] >= 3 * cap_e: return (out(x[6], x[7]), "3x", 1)
        if x[6] > 0: lastS = x
        last = x
    if pull <= t_e + H48: return (0.0, "pull", 0)
    if lastS: return (out(lastS[6], lastS[7]), "48h", 0)
    return (out(min(last[1], cap_e), last[7]), "48h", 0)
rows = []; skip = collections.Counter()
for pool, venue, tok, sym, cr, tc in O.execute("SELECT pool, venue, token, symbol, creator, t_cross FROM cross WHERE level=500000"):
    pool = pool.lower(); tok = (tok or "").lower(); cr = (cr or "").lower(); D = tc + 3600
    if not (T_START <= tc < T_END): skip["window"] += 1; continue
    s6 = O.execute("SELECT lp_removed_share FROM series WHERE level=500000 AND pool=? AND slot=6", (pool,)).fetchone()
    if s6 and s6[0] is not None and s6[0] >= 0.5: skip["pulled by D"] += 1; continue
    d = P.get(pool)
    if not d: skip["no price path"] += 1; continue
    if D + H48 > DEND["v4" if d["venue"] == "v4" else "v3"]: skip["48h not covered"] += 1; continue
    if d["pull"] and d["pull"] <= D: skip["pulled by D"] += 1; continue
    bD = blk(D); bc = blk(tc)
    tr = RAW.execute("SELECT t1, t2, data, b, li, tx FROM lg WHERE addr=? AND t0=? AND b<=? ORDER BY b, li", (tok, T_TR, bD)).fetchall()
    if not tr: skip["no transfers"] += 1; continue
    is_v4 = len(pool) == 66; pside = {PM} if is_v4 else {pool}; MK = HUB | pside
    T = [("0x" + t1[-40:], "0x" + t2[-40:], int(dd, 16) if dd not in ("0x", "") else 0, b, tx) for t1, t2, dd, b, li, tx in tr]
    bfirst = next((b for f, to, v, b, tx in T if f in pside and to not in MK), None)
    if bfirst is None: skip["no first buy"] += 1; continue
    ins = {cr} if cr else set(); bal = collections.defaultdict(int)
    for f, to, v, b, tx in T:
        if b <= bfirst + 30 and to not in MK: ins.add(to)
        elif f in ins and to not in MK and f not in MK: ins.add(to)
        bal[f] -= v; bal[to] += v
    hold = {w: v for w, v in bal.items() if w not in MK and v > 0}
    tot = sum(hold.values())
    if tot <= 0: skip["no holders"] += 1; continue
    F1 = sum(v for w, v in hold.items() if w in ins) / tot
    # outside net buy ETH between cross and D, from the pool's swaps
    if is_v4: sw = RAW.execute("SELECT data, b, tx FROM lg WHERE addr=? AND b BETWEEN ? AND ? AND t0=? AND t1=?", (PM, bc, bD, T_V4SW, pool)).fetchall()
    else: sw = RAW.execute("SELECT data, b, tx FROM lg WHERE addr=? AND t0=? AND b BETWEEN ? AND ?", (pool, T_V3SW, bc, bD)).fetchall()
    txtr = collections.defaultdict(list)
    for f, to, v, b, tx in T:
        if b >= bc: txtr[tx].append((f, to))
    tok0 = int(tok, 16) < int(WETH, 16) if tok else False
    outside = 0.0; inside = 0.0; nb = 0
    for dd, b, tx in sw:
        a0, a1 = i256(word(dd, 0)), i256(word(dd, 1))
        if is_v4: a0, a1 = -a0, -a1
        ae = a1 if tok0 else a0
        trs = txtr.get(tx, []); who = None
        if ae > 0:
            frm = {f for f, _ in trs}; fin = [to for f, to in trs if to not in frm and to not in MK]; who = fin[-1] if fin else None
        else:
            src = [f for f, to in trs if to in MK and f not in MK]; who = src[0] if src else None
        eth = ae / 1e18
        if who is None: continue
        if who in ins: inside += eth
        else: outside += eth; nb += 1
    F2 = outside * eu(D)
    TS = [x[0] for x in d["ev"]]; j = bisect.bisect_left(TS, D)
    if j >= len(d["ev"]): skip["no trade after D"] += 1; continue
    r = trade(d, j)
    if not r: continue
    root = (CR.execute("SELECT root FROM launch WHERE pool=?", (pool,)).fetchone() or [cr])[0] or cr
    rows.append(dict(token=tok, pool=pool, venue=venue, sym=sym or "", creator=cr, root=(root or "").lower(), cross=bk(tc), entry_t=int(d["ev"][j][0]),
                     day=bk(d["ev"][j][0])[:10], half="A" if tc < T_B else "B", insiders=len(ins), F1=round(F1, 4), F2=round(F2, 1),
                     inside_net_usd=round(inside * eu(D), 1), entry_cap=round(d["ev"][j][1]), mult=round(r[0], 4), reason=r[1], win3x=r[2]))
# one per cluster per day
keep = []; seen = collections.defaultdict(set)
for r in sorted(rows, key=lambda r: r["entry_t"]):
    keys = [k for k in (("s", r["sym"].upper()) if r["sym"] else None, ("c", r["creator"]) if r["creator"] else None, ("r", r["root"]) if r["root"] else None) if k]
    if any(k in seen[r["day"]] for k in keys): continue
    seen[r["day"]].update(keys); keep.append(r)
with gzip.open(OUT + "/dc-a1-3-qfloat.csv.gz", "wt") as f:
    w = csv.DictWriter(f, fieldnames=list(keep[0])); w.writeheader(); w.writerows(keep)
DA = (T_B - T_START) / 86400; DB = (min(T_END, DEND["v3"] - H48) - T_B) / 86400
A = [r for r in keep if r["half"] == "A"]; B = [r for r in keep if r["half"] == "B"]
def usd(g, D_): return sum((r["mult"] - 1) * 100 for r in g) / D_
mA = statistics.median([r["F1"] for r in A]); qA = sorted(r["F1"] for r in A)[len(A) // 4]; tA = statistics.median([r["F2"] for r in A])
RULES = {"F1<med": lambda r: r["F1"] < mA, "F1<q25": lambda r: r["F1"] < qA, "F2>0": lambda r: r["F2"] > 0, "F2>=medA": lambda r: r["F2"] >= tA,
         "F1<med&F2>0": lambda r: r["F1"] < mA and r["F2"] > 0}
res = {"n": len(keep), "nA": len(A), "nB": len(B), "days": [DA, DB], "cuts_from_A": {"F1_med": mA, "F1_q25": qA, "F2_med": tA}, "skip": dict(skip),
       "base": {"usdA": round(usd(A, DA), 1), "usdB": round(usd(B, DB), 1), "winA": sum(r["win3x"] for r in A), "winB": sum(r["win3x"] for r in B)}, "rules": {}}
for name, f in RULES.items():
    a = [r for r in A if f(r)]; b = [r for r in B if f(r)]
    rnd = 0
    if b:
        mb = statistics.mean([r["mult"] for r in b]); random.seed(7)
        for _ in range(10000):
            s = random.sample(B, len(b)); rnd += mb > statistics.mean([r["mult"] for r in s])
    res["rules"][name] = dict(nA=len(a), nB=len(b), winA=sum(r["win3x"] for r in a), winB=sum(r["win3x"] for r in b), usdA=round(usd(a, DA), 1), usdB=round(usd(b, DB), 1),
                              xA=round(statistics.mean([r["mult"] for r in a]), 3) if a else None, xB=round(statistics.mean([r["mult"] for r in b]), 3) if b else None,
                              beats_random_B=round(rnd / 10000, 3))
# AUC of F1 (low better) and F2 (high better) for win3x, per half
def auc(g, key, sign):
    pos = [sign * r[key] for r in g if r["win3x"]]; neg = [sign * r[key] for r in g if not r["win3x"]]
    if not pos or not neg: return None
    return round(sum((p > n) + 0.5 * (p == n) for p in pos for n in neg) / (len(pos) * len(neg)), 3)
res["auc"] = {"F1_low_A": auc(A, "F1", -1), "F1_low_B": auc(B, "F1", -1), "F2_high_A": auc(A, "F2", 1), "F2_high_B": auc(B, "F2", 1)}
choose = max(RULES, key=lambda k: (res["rules"][k]["usdA"] - res["base"]["usdA"]))
res["chosen_on_A"] = choose
json.dump(res, open(OUT + "/qfloat.json", "w"), indent=1); print(json.dumps(res, indent=1))
