"""mc5size.py (FW Analyst 2, A2.v14): size curve of A1's mc5 rule on real v3 tick depth. Per trade: state = last Swap (sqrtPriceX96, liquidity, tick from the event) plus in-range Mints/Burns after it. Buy: (V/ETHUSD x 0.99 - gas) WETH walked through initialized ticks with the pool fee. Sell: all tokens walked back, x 0.99 - gas. Exit = A1 spec: stop when net sell value <= 0.5x on a Swap of 0.01+ WETH (fill at state t+3 s), time stop T0 + 2 h, pull = 0. Pull (spec 4c): first Burn in range taking >= 50% of the active liquidity. A1's all-range unit pull reported too.""" import json, sqlite3, bisect, math, collections, statistics as st, datetime, sys SPB = 0.101283; CUT = 1790269200 SW = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67" MI = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde" T = json.load(open("/opt/fw2/mc5/trades.json")) db = sqlite3.connect("file:/opt/fw2/mc5/logs.db?mode=ro", uri=True) def s256(h): v = int(h, 16); return v - (1 << 256) if v >= 1 << 255 else v def s24(h): v = int(h, 16) & 0xffffff; return v - (1 << 24) if v >= 1 << 23 else v def W(d): d = d[2:]; return [d[i:i + 64] for i in range(0, len(d), 64)] def tp(t): return 1.0001 ** (t / 2) class Pool: def __init__(s, t): s.t = t; s.is0 = t["is0"]; s.pf = t["fee"] / 1e6; fb = t["fb"] s.ev = [] for b, li, t0, t2, t3, data in db.execute("SELECT b, li, t0, t2, t3, data FROM ev WHERE pool=? ORDER BY b, li", (t["pool"],)): w = W(data); sec = (b - fb) * SPB if t0 == SW: a0, a1 = s256(w[0]), s256(w[1]); sp = int(w[2], 16) / 2 ** 96; L = int(w[3], 16); tk = s24(w[4]) we = abs(a1 if s.is0 else a0) / 1e18 s.ev.append((sec, "s", (sp, L, tk, we))) else: tl, tu = s24(t2), s24(t3) amt = int(w[1], 16) if t0 == MI else int(w[0], 16) s.ev.append((sec, "m" if t0 == MI else "b", (tl, tu, amt))) def states(s): """yield (sec, kind, sp, L, tick, net(dict), ticks(sorted), we) after every event; pull time (spec) and A1 pull time""" net = collections.defaultdict(int); sp = L = tk = None; ticks = []; added = 0; snap = {} s.pull = None; s.pullA1 = None; out = [] for sec, k, d in s.ev: if k == "s": sp, L, tk, we = d else: tl, tu, amt = d; sg = 1 if k == "m" else -1 net[tl] += sg * amt; net[tu] -= sg * amt ticks = sorted(x for x, v in net.items() if v != 0); snap = {x: net[x] for x in ticks} inr = tk is not None and tl <= tk < tu if k == "b": if added > 0 and amt >= 0.5 * added and s.pullA1 is None and sec > 0: s.pullA1 = sec if inr and L and amt >= 0.5 * L and s.pull is None and sec > 0: s.pull = sec else: added += amt if inr and L is not None: L = max(0, L + sg * amt) we = 0.0 if sp is not None: out.append((sec, k, sp, L, tk, snap, ticks, we)) s.S = out; s.secs = [x[0] for x in out] def at(s, sec): i = bisect.bisect_right(s.secs, sec) - 1; return i def swap(st_, amt, zf1, pf): """amt raw in (before fee); zf1 True = token0 in. returns raw out""" sec, k, sp, L, tk, net, ticks, we = st_ rem = amt * (1 - pf); out = 0.0; L = float(L) if zf1: j = bisect.bisect_right(ticks, tk) - 1 while rem > 0: if j < 0: break t = ticks[j]; spt = tp(t) if spt > sp: spt = sp if L > 0: need = L * (1 / spt - 1 / sp) if rem < need: spn = L * sp / (L + rem * sp); out += L * (sp - spn); rem = 0; break out += L * (sp - spt); rem -= need sp = spt; L -= net[t]; j -= 1 if L < 0: L = 0.0 else: j = bisect.bisect_right(ticks, tk) while rem > 0: if j >= len(ticks): break t = ticks[j]; spt = tp(t) if spt < sp: spt = sp if L > 0: need = L * (spt - sp) if rem < need: spn = sp + rem / L; out += L * (1 / sp - 1 / spn); rem = 0; break out += L * (1 / sp - 1 / spt); rem -= need sp = spt; L += net[t]; j += 1 if L < 0: L = 0.0 return out def exout(st_, want, zf1): """fee free: input needed to take `want` raw out; inf if the ticks run dry""" sec, k, sp, L, tk, net, ticks, we = st_ rem = want; need_in = 0.0; L = float(L) if rem <= 0: return 0.0 if zf1: j = bisect.bisect_right(ticks, tk) - 1 while True: if j < 0: return float("inf") t = ticks[j]; spt = min(tp(t), sp) if L > 0: av = L * (sp - spt) if rem < av: spn = sp - rem / L; return need_in + L * (1 / spn - 1 / sp) need_in += L * (1 / spt - 1 / sp); rem -= av sp = spt; L = max(0.0, L - net[t]); j -= 1 else: j = bisect.bisect_right(ticks, tk) while True: if j >= len(ticks): return float("inf") t = ticks[j]; spt = max(tp(t), sp) if L > 0: av = L * (1 / sp - 1 / spt) if rem < av: spn = 1 / (1 / sp - rem / L); return need_in + L * (spn - sp) need_in += L * (spt - sp); rem -= av sp = spt; L = max(0.0, L + net[t]); j += 1 def gross_back(P, k, xin, q): """WETH raw back for selling q tokens at state k, with OUR entry buy (xin raw WETH, after fees) sitting in the pool: our buy is replayed on state k, then q is sold back (exact reversal while it retraces our own buy).""" bd = not P.is0; st_ = P.S[k] F = swap(st_, xin, bd, 0.0) if q <= F: return xin - min(xin, exout(st_, F - q, bd)) return xin + swap(st_, q - F, not bd, 0.0) def trade(P, V, te=300, lag=0, pullkind="spec"): t = P.t; eu = t["eu"]; gas = 0.05 / eu; x = V / eu i = P.at(te + lag) if i < 0: return None xin = max(0.0, (x * 0.99 - gas) * (1 - P.pf)) * 1e18 q = swap(P.S[i], xin, not P.is0, 0.0) pull = P.pull if pullkind == "spec" else P.pullA1 def net(k): return max(0.0, gross_back(P, k, xin, q) / 1e18 * (1 - P.pf) * 0.99 - gas) / x peak = 0.0 for k in range(i + 1, len(P.S)): sec, kind, sp, L, tk, nt, tks, we = P.S[k] if sec > 7200: break if kind != "s" or we < 0.01 or sec <= te: continue m = net(k); peak = max(peak, m) if m <= 0.5: if pull is not None and pull <= sec + 3: return 0.0, "pull", peak j = max(k, P.at(sec + 3)); return net(j), "stop", peak if pull is not None and pull <= 7200: return 0.0, "pull", peak return net(P.at(7200)), "time", peak def day(ts): return datetime.datetime.utcfromtimestamp(ts + 7 * 3600).date() PS = [] for t in T: P = Pool(t); P.states(); PS.append(P) print("pools", len(PS), "spec pulls", sum(P.pull is not None and P.pull <= 7200 for P in PS), "A1-unit pulls", sum(P.pullA1 is not None and P.pullA1 <= 7200 for P in PS), flush=True) d0 = min(day(t["t"]) for t in T); d1 = max(day(t["t"]) for t in T) cal = [d0 + datetime.timedelta(k) for k in range((d1 - d0).days + 1)] tdays = sorted({day(t["t"]) for t in T}) def summ(R, V): byd = collections.defaultdict(float) for t, v, why, pk in R: byd[day(t["t"])] += (v - 1) * V roll = min(sum(byd.get(cal[k + j], 0) for j in range(3)) for k in range(len(cal) - 2)) out = {} for h, f in (("H1", lambda t: t["t"] < CUT), ("H2", lambda t: t["t"] >= CUT), ("all", lambda t: True)): Rh = [r for r in R if f(r[0])]; v = [r[1] for r in Rh] nd = len({day(r[0]["t"]) for r in Rh}) out[h] = dict(n=len(v), mean=st.mean(v), med=st.median(v), tot=sum(x - 1 for x in v) * V, perday=sum(x - 1 for x in v) * V / nd, nd=nd) out["worst_day"] = min(byd.values()); out["worst3"] = roll; out["green"] = sum(x > 0 for x in byd.values()); out["days"] = len(byd) out["why"] = dict(collections.Counter(r[2] for r in R)) return out SIZES = [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000] res = {}; rows = [] for lab, kw in (("main", {}), ("lag6", {"lag": 6}), ("a1pull", {"pullkind": "a1"})): print("==", lab, flush=True) for V in (SIZES if lab == "main" else [100, 500, 1000, 2000, 3000]): R = [] for P in PS: r = trade(P, V, **kw) if r: R.append((P.t, *r)) if lab == "main": rows.append((P.t["pool"], P.t["token"], str(day(P.t["t"])), V, round(P.t["old"][0], 4), round(r[0], 4) if r else None, r[1] if r else None, round(r[2], 3) if r else None)) o = summ(R, V); res[(lab, V)] = o print("$%5d n%d mean %.2fx (H1 %.2fx H2 %.2fx) med %.2fx tot $%+.0f /tday $%+.0f (H1 %+.0f H2 %+.0f) worst day %+.0f worst3 %+.0f green %d/%d %s" % ( V, o["all"]["n"], o["all"]["mean"], o["H1"]["mean"], o["H2"]["mean"], o["all"]["med"], o["all"]["tot"], o["all"]["perday"], o["H1"]["perday"], o["H2"]["perday"], o["worst_day"], o["worst3"], o["green"], o["days"], o["why"]), flush=True) import csv with open("/opt/fw2/mc5/mc5size_trades.csv", "w") as f: w = csv.writer(f); w.writerow(["pool", "token", "day_bkk", "usd", "x_A1_model_100", "x_tickwalk", "exit", "peak_x"]); w.writerows(rows) json.dump({"%s_%d" % k: v for k, v in res.items()}, open("/opt/fw2/mc5/mc5size.json", "w"), default=str, indent=1) print("calendar days", len(cal), "trade days", len(tdays))