8958279ee8
- retired-infra/: Railway config, Mac launchd runner, GH-Actions cron (worker is Fly.io arn now; Actions cron last fired 2026-07-02) - live-research/: the June selection experiments (strategy/followability/ pnl_basket/pnl_focused/backtest_june/clean_test + outputs) - us-venue/: the scrapped Polymarket-US listability probe (+ its env-gated ONLY_CONDS replay filter stays in portfolio.py — generally useful for subset replays) - root sweeps hunt/huntwide/oos/copyback/watch.json -> archive/ - untracked logs/CSVs of dead experiments -> archive/local/ (gitignored) - READMEs updated: Fly migration, geoblock gotcha, new file map Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
6.6 KiB
Python
163 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Train/test wallet-selection study, entirely from the local cache.
|
|
|
|
TRAIN = bets resolved before --train-end (default 2026-05-30): pick wallets.
|
|
TEST = bets resolved on/after --test-start (default 2026-06-01), resolved only:
|
|
validate which picks were actually profitable to follow forward.
|
|
|
|
Lens: we have $1,000 and copy flat-size, so the key metric is COPY-ROI — the
|
|
mean per-bet return if we mirror each entry with the same stake:
|
|
win -> (1-p)/p per $1 (entry at price p pays out at 1)
|
|
loss -> -1
|
|
A wallet with positive copy-ROI would have made us money (before lag/fees).
|
|
We also compute z (beats entry prices) and the wallet's own $ ROI.
|
|
|
|
Selection gates (all on TRAIN):
|
|
* n >= MIN_N resolved bets
|
|
* z significant (Benjamini-Hochberg FDR)
|
|
* copy-ROI > 0 (copying them actually paid)
|
|
* consistent: positive copy-ROI in >= CONSISTENCY of monthly buckets
|
|
* copyable proxies: median bet size in [$5,$5000] (not dust, not whale),
|
|
not at the ~2000-bet HFT cap, not one-market-dependent
|
|
|
|
python3 strategy.py
|
|
"""
|
|
|
|
import math
|
|
import os
|
|
import sys
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
import duckdb
|
|
|
|
HERE = os.path.dirname(__file__)
|
|
DB = os.path.join(HERE, "cache.duckdb")
|
|
TRAIN_END = time.mktime(time.strptime(os.environ.get("TRAIN_END", "2026-05-30"), "%Y-%m-%d"))
|
|
TEST_START = time.mktime(time.strptime(os.environ.get("TEST_START", "2026-06-01"), "%Y-%m-%d"))
|
|
MIN_N = 30 # train bets needed to judge a wallet
|
|
MIN_N_TEST = 5 # forward bets needed to report a forward number
|
|
CAP = 1990 # >= this looks API-capped (HFT/truncated) -> deprioritize
|
|
SIZE_LO, SIZE_HI = 5.0, 5000.0 # copyable median bet size ($)
|
|
CONSISTENCY = 0.6 # fraction of monthly buckets that must be profitable
|
|
FDR_Q = 0.05
|
|
|
|
|
|
def ret(p, won):
|
|
return (1 - p) / p if won else -1.0 # copy return per $1 staked
|
|
|
|
|
|
def metrics(bets):
|
|
n = len(bets)
|
|
wins = sum(1 for b in bets if b["won"])
|
|
exp = sum(b["p"] for b in bets)
|
|
var = sum(b["p"] * (1 - b["p"]) for b in bets) or 1e-9
|
|
z = (wins - exp) / math.sqrt(var)
|
|
copy_roi = sum(ret(b["p"], b["won"]) for b in bets) / n
|
|
staked = sum(b["size"] for b in bets) or 1e-9
|
|
dollars = sum((b["size"] * (1 - b["p"]) / b["p"] if b["won"] else -b["size"]) for b in bets)
|
|
return dict(n=n, wins=wins, win_rate=100 * wins / n, z=z, copy_roi=copy_roi,
|
|
own_roi=dollars / staked, exp=exp)
|
|
|
|
|
|
def consistency(bets):
|
|
"""fraction of calendar-month buckets (by res_t) with positive copy-ROI."""
|
|
buck = defaultdict(list)
|
|
for b in bets:
|
|
buck[time.strftime("%Y-%m", time.localtime(b["res_t"] or 0))].append(b)
|
|
months = [m for m in buck.values() if len(m) >= 5]
|
|
if not months:
|
|
return 0.0, 0
|
|
good = sum(1 for m in months if sum(ret(b["p"], b["won"]) for b in m) > 0)
|
|
return good / len(months), len(months)
|
|
|
|
|
|
def concentration(bets):
|
|
"""share of total copy-return coming from the single best market."""
|
|
by = defaultdict(float)
|
|
for b in bets:
|
|
by[b["cond"]] += ret(b["p"], b["won"])
|
|
tot = sum(v for v in by.values() if v > 0) or 1e-9
|
|
return max(by.values()) / tot if by else 1.0
|
|
|
|
|
|
def norm_sf(z):
|
|
return 0.5 * math.erfc(z / math.sqrt(2))
|
|
|
|
|
|
def main():
|
|
con = duckdb.connect(DB, read_only=True)
|
|
wallets = [r[0] for r in con.execute("SELECT DISTINCT wallet FROM bets").fetchall()]
|
|
print(f"loaded {len(wallets):,} wallets; train<{time.strftime('%F', time.localtime(TRAIN_END))}"
|
|
f" test>={time.strftime('%F', time.localtime(TEST_START))}\n", flush=True)
|
|
|
|
cand = []
|
|
for w in wallets:
|
|
rows = con.execute(
|
|
"SELECT won,p,res_t,size,cond FROM bets WHERE wallet=?", [w]).fetchall()
|
|
bets = [dict(won=x[0], p=max(0.001, min(0.999, x[1] or 0)),
|
|
res_t=x[2], size=x[3] or 0, cond=x[4]) for x in rows]
|
|
train = [b for b in bets if (b["res_t"] or 0) < TRAIN_END]
|
|
if len(train) < MIN_N:
|
|
continue
|
|
m = metrics(train)
|
|
med = sorted(b["size"] for b in train)[len(train) // 2]
|
|
cons, nmonths = consistency(train)
|
|
conc = concentration(train)
|
|
test = [b for b in bets if (b["res_t"] or 0) >= TEST_START]
|
|
tm = metrics(test) if len(test) >= MIN_N_TEST else None
|
|
cand.append(dict(w=w, m=m, med=med, cons=cons, nmonths=nmonths, conc=conc,
|
|
capped=len(train) >= CAP, tm=tm, ntest=len(test)))
|
|
|
|
# FDR over the candidate edge p-values
|
|
pvals = sorted(norm_sf(c["m"]["z"]) for c in cand)
|
|
k = 0
|
|
for i, p in enumerate(pvals, 1):
|
|
if p <= FDR_Q * i / len(pvals):
|
|
k = i
|
|
thr = pvals[k - 1] if k else 0.0
|
|
|
|
# selection gates (all on TRAIN)
|
|
sel = [c for c in cand if
|
|
norm_sf(c["m"]["z"]) <= thr and thr > 0 and
|
|
c["m"]["copy_roi"] > 0 and
|
|
c["cons"] >= CONSISTENCY and
|
|
SIZE_LO <= c["med"] <= SIZE_HI and
|
|
not c["capped"] and
|
|
c["conc"] < 0.5]
|
|
sel.sort(key=lambda c: c["m"]["copy_roi"], reverse=True)
|
|
|
|
print(f"{len(cand):,} wallets with >= {MIN_N} train bets · BH thr p<= {thr:.1e}")
|
|
print(f"SELECTED (skilled + consistent + copyable on TRAIN): {len(sel)}\n")
|
|
|
|
# how did the SELECTED set do FORWARD?
|
|
fwd = [c for c in sel if c["tm"]]
|
|
if fwd:
|
|
avg_fwd = sum(c["tm"]["copy_roi"] for c in fwd) / len(fwd)
|
|
win_fwd = sum(1 for c in fwd if c["tm"]["copy_roi"] > 0)
|
|
print(f"FORWARD (June1+, resolved): {len(fwd)} selected wallets had test bets · "
|
|
f"{win_fwd}/{len(fwd)} stayed profitable · mean fwd copy-ROI {avg_fwd:+.1%}\n")
|
|
|
|
h = (f"{'train_roi':>10}{'fwd_roi':>9}{'tr_z':>6}{'tr_wr':>6}{'cons':>6}"
|
|
f"{'medSz':>7}{'tr_n':>6}{'fwd_n':>6} wallet")
|
|
print(h); print("-" * len(h))
|
|
for c in sel[:40]:
|
|
t = c["tm"]
|
|
fr = f"{t['copy_roi']:+.0%}" if t else " —"
|
|
fn = c["ntest"] if t else 0
|
|
print(f"{c['m']['copy_roi']:>+9.0%}{fr:>9}{c['m']['z']:>6.1f}{c['m']['win_rate']:>5.0f}%"
|
|
f"{c['cons']:>6.0%}{c['med']:>7.0f}{c['m']['n']:>6}{fn:>6} {c['w']}")
|
|
# persist the selection for the followability pull + watchlist
|
|
import json
|
|
json.dump([{"wallet": c["w"], "train_copy_roi": round(c["m"]["copy_roi"], 4),
|
|
"train_z": round(c["m"]["z"], 2), "train_n": c["m"]["n"],
|
|
"fwd_copy_roi": round(c["tm"]["copy_roi"], 4) if c["tm"] else None,
|
|
"fwd_n": c["ntest"], "med_size": round(c["med"], 1),
|
|
"consistency": round(c["cons"], 2)} for c in sel],
|
|
open(os.path.join(HERE, "selection.json"), "w"), indent=2)
|
|
print(f"\n-> selection.json ({len(sel)} wallets)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|