mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
T12 + T15 scripts (runs in flight; verdict headers follow with results)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""T12 EXPLORATORY (2026-07-23) — maker-sharp UNWIND leans: the sell side
|
||||
of T6/Study C. When a screened maker-sharp builds a directional inventory
|
||||
(>= $150 one-sided) and then takes back >= half of it, is the unwind an
|
||||
exit signal (the market should be faded / a Study-C hold should exit) or
|
||||
bankroll ops like taker-sharp exits (#21: exits are noise)?
|
||||
|
||||
Walk-forward, no self-selection: per tape day D the maker-sharp set is
|
||||
screened on tape < D only (maker_lean.screen_asof — same discipline as
|
||||
T6). During D, per (wallet, asset): running net/gross from orders_matched
|
||||
MAKER fills; an unwind fires ONCE when peak |net|*px >= $150 (a T6-grade
|
||||
lean existed) AND |net| has dropped to <= 50% of peak with the same sign.
|
||||
|
||||
Scored at the unwind print, chain-true (payouts_for — scorer law):
|
||||
STAY $100 on the ORIGINAL lean side from the unwind print — what a
|
||||
no-exit-rule Study C book experiences from this moment on.
|
||||
STAY >= 0 => unwinds are noise, hold through (mirrors #21).
|
||||
FADE $100 against the original lean — is the unwind actively
|
||||
informative in reverse?
|
||||
Comparator for STAY is literally 0 (exiting at the unwind print).
|
||||
|
||||
FROZEN v0 params (declared before the run, not tuned after):
|
||||
build |net|*px >= $150 AND |net|/gross >= 0.6 at peak (T6 trigger)
|
||||
unwind |net| <= 0.5 * peak|net|, same sign, first per (w,a,day)
|
||||
price lean-side last print in [0.05, 0.95] at unwind
|
||||
Kill bar for the idea: STAY EV/unwind >= 0 at n >= 100 (exit rule adds
|
||||
nothing); informative if STAY <= -$3/unwind at n >= 100."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, "/Users/jaxmakielski/polymarket-smart-money/research")
|
||||
import tape # noqa: E402
|
||||
import forward as fwd # noqa: E402
|
||||
import maker_lean as ml # noqa: E402
|
||||
|
||||
LEAN_USD = 150.0 # FROZEN — must equal maker_lean.py
|
||||
NET_GROSS = 0.6 # FROZEN — must equal maker_lean.py
|
||||
UNWIND_FRAC = 0.5
|
||||
BAND = (0.05, 0.95)
|
||||
|
||||
|
||||
def day_unwinds(db, lo, hi, sharps):
|
||||
rows = db.execute("""
|
||||
SELECT lower(json_extract_string(payload,'$.proxyWallet')) w,
|
||||
json_extract_string(payload,'$.asset') a,
|
||||
json_extract_string(payload,'$.side') s,
|
||||
cast(json_extract(payload,'$.price') AS DOUBLE) p,
|
||||
cast(json_extract(payload,'$.size') AS DOUBLE) z, ts
|
||||
FROM aux WHERE type='orders_matched' AND ts >= ? AND ts < ?
|
||||
ORDER BY ts""", [lo, hi]).fetchall()
|
||||
book, fired, out = {}, set(), []
|
||||
for w, a, s_, p, z, ts in rows:
|
||||
if w not in sharps or (w, a) in fired:
|
||||
continue
|
||||
st = book.setdefault((w, a), [0.0, 0.0, 0.0, False])
|
||||
# [net, gross, peak_net_abs_at_qualifying_lean, lean_armed]
|
||||
st[0] += z if s_ == "BUY" else -z
|
||||
st[1] += z
|
||||
net, gross = st[0], st[1]
|
||||
if gross < 1e-9:
|
||||
continue
|
||||
# arm (or re-peak) the lean state at each new extreme
|
||||
if abs(net) > st[2]:
|
||||
px_row = db.execute("""SELECT price FROM trades WHERE asset=?
|
||||
AND ts<=? ORDER BY ts DESC LIMIT 1""", [a, ts]).fetchone()
|
||||
if px_row is not None:
|
||||
px_now = float(px_row[0])
|
||||
if (abs(net) * px_now >= LEAN_USD
|
||||
and abs(net) / gross >= NET_GROSS):
|
||||
st[2] = abs(net)
|
||||
st[3] = net > 0 # sign of the armed lean
|
||||
# unwind: armed lean and net back to <= half the peak, same sign
|
||||
if st[2] > 0 and (net > 0) == st[3] \
|
||||
and abs(net) <= UNWIND_FRAC * st[2]:
|
||||
px_row = db.execute("""SELECT price FROM trades WHERE asset=?
|
||||
AND ts<=? ORDER BY ts DESC LIMIT 1""", [a, ts]).fetchone()
|
||||
if px_row is None:
|
||||
continue
|
||||
px = float(px_row[0])
|
||||
lean_px = px if st[3] else 1 - px
|
||||
if not (BAND[0] <= lean_px <= BAND[1]):
|
||||
continue
|
||||
fired.add((w, a))
|
||||
out.append({"w": w, "a": a, "ts": ts,
|
||||
"side": 1 if st[3] else -1,
|
||||
"peak_usd": st[2] * px, "px": px,
|
||||
"lean_px": lean_px})
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
dump = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
".maker_unwind_triggers.json")
|
||||
db = tape.connect()
|
||||
if "--grade-only" in sys.argv:
|
||||
# walk already done; grade the dumped triggers (lets a re-grade
|
||||
# queue behind another process's cache.duckdb write lock)
|
||||
triggers = json.load(open(dump))
|
||||
print(f"grade-only: {len(triggers)} dumped triggers", flush=True)
|
||||
else:
|
||||
t_lo, t_hi = db.execute(
|
||||
"SELECT min(ts), max(ts) FROM aux WHERE type='orders_matched'"
|
||||
).fetchone()
|
||||
day0 = int(t_lo // 86400 + 2)
|
||||
days = [d * 86400 for d in range(day0, int(t_hi // 86400) + 1)]
|
||||
print(f"walk-forward days: {len(days)}", flush=True)
|
||||
triggers = []
|
||||
for lo in days:
|
||||
hi = min(lo + 86400, t_hi)
|
||||
sharps = ml.screen_asof(db, lo)
|
||||
d_str = time.strftime("%m-%d", time.gmtime(lo))
|
||||
if not sharps:
|
||||
print(f"{d_str}: 0 screened wallets", flush=True)
|
||||
continue
|
||||
found = day_unwinds(db, lo, hi, sharps)
|
||||
for t in found:
|
||||
t["day"] = d_str
|
||||
triggers.extend(found)
|
||||
print(f"{d_str}: {len(sharps)} screened · {len(found)} unwinds",
|
||||
flush=True)
|
||||
print(f"total unwinds: {len(triggers)}", flush=True)
|
||||
json.dump(triggers, open(dump, "w"))
|
||||
pays = fwd.payouts_for(db, [t["a"] for t in triggers])
|
||||
graded = [(t, pays.get(t["a"])) for t in triggers]
|
||||
graded = [(t, p) for t, p in graded if p is not None and p != 0.5]
|
||||
|
||||
def report(tag, rs):
|
||||
if not rs:
|
||||
print(f"{tag}: 0 graded")
|
||||
return
|
||||
n = len(rs)
|
||||
stay = fade = 0.0
|
||||
holds = 0
|
||||
for t, p in rs:
|
||||
lean_pay = p if t["side"] > 0 else 1 - p
|
||||
sh = 100.0 / t["lean_px"]
|
||||
stay += sh * (lean_pay - t["lean_px"])
|
||||
shf = 100.0 / (1 - t["lean_px"])
|
||||
fade += shf * ((1 - lean_pay) - (1 - t["lean_px"]))
|
||||
holds += lean_pay == 1.0
|
||||
print(f"{tag}: n={n} · lean-side hit {holds/n:.2f} · avg lean px "
|
||||
f"{sum(t['lean_px'] for t, _ in rs)/n:.2f} · "
|
||||
f"STAY EV/unwind {stay/n:+.2f} · FADE EV/unwind {fade/n:+.2f}")
|
||||
|
||||
print(f"chain-graded: {len(graded)}/{len(triggers)}")
|
||||
report("ALL", graded)
|
||||
for lo_, hi_, tag in [(150, 500, "$150-500"), (500, 2000, "$500-2k"),
|
||||
(2000, 1e9, "$2k+")]:
|
||||
report(f"peak {tag}",
|
||||
[(t, p) for t, p in graded if lo_ <= t["peak_usd"] < hi_])
|
||||
for d in sorted({t["day"] for t, _ in graded}):
|
||||
report(f"day {d}", [(t, p) for t, p in graded if t["day"] == d])
|
||||
# event concentration (the #22 fade-arm lesson): top-asset share
|
||||
by_a = {}
|
||||
for t, p in graded:
|
||||
lean_pay = p if t["side"] > 0 else 1 - p
|
||||
sh = 100.0 / t["lean_px"]
|
||||
by_a[t["a"]] = by_a.get(t["a"], 0.0) + sh * (lean_pay - t["lean_px"])
|
||||
if by_a:
|
||||
tot = sum(by_a.values())
|
||||
top = sorted(by_a.items(), key=lambda kv: -abs(kv[1]))[:5]
|
||||
print(f"STAY concentration: total {tot:+.0f} · top-5 assets "
|
||||
f"{[round(v) for _, v in top]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""T15 EXPLORATORY (2026-07-23) — SHARP HALF-LIFE: how long does a wallet
|
||||
stay sharp after we detect it? The live follow set has principled entry
|
||||
criteria and no exit criteria; this measures whether detection-day edge
|
||||
decays with age-in-set and how fast wallets churn out of the published
|
||||
screen.
|
||||
|
||||
Detection dates are REAL as-of history, not reconstruction: the git
|
||||
commit series of live/watch_sharps.json (published daily by the pipeline
|
||||
since 2026-06-18) — a wallet's detection date is the first commit that
|
||||
contains it. Wallets already present in the FIRST commit are
|
||||
left-censored (true detection unknown) and excluded from the age curves.
|
||||
|
||||
Performance is the wallet's OWN bets from the cache (live/cache.duckdb
|
||||
bets, read-only snapshot taken first so the payouts writer never
|
||||
contends), graded to CHAIN TRUTH per bet via payouts.ensure/truth —
|
||||
the cache's won/res_t columns are never trusted (the res_t=ts poison).
|
||||
EV is per $100 at the wallet's own entry price, feeless: this measures
|
||||
SIGNAL decay, not our execution (T3/T11 own execution).
|
||||
|
||||
Outputs: EV/bet by age-in-set bucket (0-2d / 3-6d / 7-13d / 14d+),
|
||||
pooled AND per-wallet-day mean-of-means (concentration guard), plus the
|
||||
survival curve (fraction of detected wallets still published at age k).
|
||||
NOT pre-registered — informs a rotation policy for the follow set."""
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
sys.path.insert(0, os.path.join(ROOT, "live"))
|
||||
BAND = (0.05, 0.95)
|
||||
BUCKETS = [(0, 3, "age 0-2d"), (3, 7, "age 3-6d"),
|
||||
(7, 14, "age 7-13d"), (14, 10**6, "age 14d+")]
|
||||
|
||||
|
||||
def set_history():
|
||||
"""[(date_str, {wallets})] oldest-first from git history."""
|
||||
log = subprocess.run(
|
||||
["git", "log", "--reverse", "--format=%ad %H", "--date=short",
|
||||
"--", "live/watch_sharps.json"],
|
||||
cwd=ROOT, capture_output=True, text=True).stdout.split()
|
||||
pairs = list(zip(log[0::2], log[1::2]))
|
||||
out = []
|
||||
for date, sha in pairs:
|
||||
try:
|
||||
blob = subprocess.run(
|
||||
["git", "show", f"{sha}:live/watch_sharps.json"],
|
||||
cwd=ROOT, capture_output=True, text=True).stdout
|
||||
rows = json.loads(blob)
|
||||
ws = {r["wallet"].lower() for r in rows if r.get("wallet")}
|
||||
if ws:
|
||||
out.append((date, ws))
|
||||
except Exception:
|
||||
continue
|
||||
# one snapshot per date (last commit of the day wins)
|
||||
byday = {}
|
||||
for date, ws in out:
|
||||
byday[date] = ws
|
||||
return sorted(byday.items())
|
||||
|
||||
|
||||
def main():
|
||||
hist = set_history()
|
||||
print(f"set snapshots: {len(hist)} days "
|
||||
f"({hist[0][0]} .. {hist[-1][0]})", flush=True)
|
||||
first_day, censored = {}, set()
|
||||
for i, (date, ws) in enumerate(hist):
|
||||
for w in ws:
|
||||
if w not in first_day:
|
||||
first_day[w] = date
|
||||
if i == 0:
|
||||
censored.add(w)
|
||||
pool = set(first_day) - censored
|
||||
print(f"wallets ever published: {len(first_day)} · "
|
||||
f"left-censored (first snapshot): {len(censored)} · "
|
||||
f"age-eligible: {len(pool)}", flush=True)
|
||||
|
||||
day_n = {d: time.mktime(time.strptime(d, "%Y-%m-%d")) // 86400
|
||||
for d, _ in hist}
|
||||
det_n = {w: day_n[first_day[w]] for w in pool}
|
||||
|
||||
# survival: still-published at age k (right-censored by last snapshot)
|
||||
last_n = day_n[hist[-1][0]]
|
||||
print("\nSURVIVAL (still in the published set at age k):", flush=True)
|
||||
for k in (1, 3, 7, 14, 21, 28):
|
||||
elig = [w for w in pool if det_n[w] + k <= last_n]
|
||||
if not elig:
|
||||
continue
|
||||
alive = 0
|
||||
for w in elig:
|
||||
# snapshot on-or-after detection+k (carry-forward between days)
|
||||
snap = None
|
||||
for d, ws in hist:
|
||||
if day_n[d] <= det_n[w] + k:
|
||||
snap = ws
|
||||
else:
|
||||
break
|
||||
alive += snap is not None and w in snap
|
||||
print(f" age {k:>2}d: {alive}/{len(elig)} = "
|
||||
f"{alive/len(elig):.0%}", flush=True)
|
||||
|
||||
# bets snapshot (read-only, close before payouts writes the same file)
|
||||
import duckdb
|
||||
con = duckdb.connect(os.path.join(ROOT, "live", "cache.duckdb"),
|
||||
read_only=True)
|
||||
wl = ",".join(f"'{w}'" for w in pool)
|
||||
bets = con.execute(f"""
|
||||
SELECT lower(wallet) w, cond, asset,
|
||||
any_value(p::DOUBLE) p, min(ts) ts
|
||||
FROM bets
|
||||
WHERE lower(wallet) IN ({wl}) AND p BETWEEN {BAND[0]} AND {BAND[1]}
|
||||
AND resolved
|
||||
GROUP BY lower(wallet), cond, asset""").fetchall()
|
||||
con.close()
|
||||
print(f"\nresolved cache bets for age-eligible wallets: {len(bets)}",
|
||||
flush=True)
|
||||
|
||||
import payouts
|
||||
conds = sorted({b[1] for b in bets if b[1]})
|
||||
print(f"ensuring {len(conds)} conds against chain…", flush=True)
|
||||
payouts.ensure(conds)
|
||||
|
||||
per_bucket = {tag: [] for _, _, tag in BUCKETS}
|
||||
per_wd = {tag: collections.defaultdict(list) for _, _, tag in BUCKETS}
|
||||
graded = skipped = 0
|
||||
for w, cond, asset, p, ts in bets:
|
||||
if ts // 86400 < det_n[w]:
|
||||
continue # pre-detection bet
|
||||
pay = payouts.truth(cond, asset)
|
||||
if pay is None or pay == 0.5:
|
||||
skipped += 1
|
||||
continue
|
||||
graded += 1
|
||||
age = int(ts // 86400 - det_n[w])
|
||||
ev = (100.0 / p) * (pay - p)
|
||||
for lo, hi, tag in BUCKETS:
|
||||
if lo <= age < hi:
|
||||
per_bucket[tag].append(ev)
|
||||
per_wd[tag][(w, int(ts // 86400))].append(ev)
|
||||
break
|
||||
print(f"chain-graded post-detection bets: {graded} "
|
||||
f"(ungraded/refund skipped: {skipped})\n", flush=True)
|
||||
|
||||
print("EV BY AGE-IN-SET (per $100 at the wallet's own entry, feeless):",
|
||||
flush=True)
|
||||
for _, _, tag in BUCKETS:
|
||||
evs = per_bucket[tag]
|
||||
if not evs:
|
||||
print(f" {tag:>10}: n=0")
|
||||
continue
|
||||
pooled = sum(evs) / len(evs)
|
||||
wd = [sum(v) / len(v) for v in per_wd[tag].values()]
|
||||
mom = sum(wd) / len(wd)
|
||||
nw = len({k[0] for k in per_wd[tag]})
|
||||
print(f" {tag:>10}: n={len(evs):>5} · pooled EV/bet {pooled:+6.2f}"
|
||||
f" · wallet-day mean {mom:+6.2f} · {nw} wallets", flush=True)
|
||||
# concentration guard: top-5 wallet share of |pnl| overall
|
||||
by_w = collections.defaultdict(float)
|
||||
for _, _, tag in BUCKETS:
|
||||
for (w, _), v in per_wd[tag].items():
|
||||
by_w[w] += sum(v)
|
||||
if by_w:
|
||||
tot = sum(by_w.values())
|
||||
top = sorted(by_w.items(), key=lambda kv: -abs(kv[1]))[:5]
|
||||
print(f"\nconcentration: total {tot:+.0f} · top-5 wallets "
|
||||
f"{[(w[:8], round(v)) for w, v in top]}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user