Files
winning-wallet-finder_github/lp_screener.py
T
jaxperro d1ca52a33f Add liquidity-rewards market screener (lp_screener.py)
Ranks Polymarket's ~8000 reward-eligible markets by risk-adjusted LP yield:
reward pool / order-book competition near mid (gross APR for a $1000
two-sided position), penalized by 24h midpoint volatility (adverse-selection
proxy) and time-to-resolution. One-shot snapshot -> lp_markets.csv. README
documents the rewards mechanics, the screener, and the open caveats before
the paper LP loop. Generated data files gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 11:01:26 -04:00

185 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Liquidity-rewards market screener.
Ranks Polymarket's reward-eligible markets by *risk-adjusted* yield, so we can
find where providing liquidity actually pays — high reward pool, thin enough
book to capture share, but stable enough not to get picked off.
For each market it pulls the order book and a 24h price series and computes:
- gross APR : reward pool / competition, for a $1000 two-sided position
- vol_24h : stdev of 15-min midpoint moves, in cents = adverse-selection proxy
- hrs_to_end : time to resolution (imminent = toxic/live)
- score : gross APR penalized by volatility and imminence
GROSS APR ignores pick-off losses — that's exactly what vol_24h flags. A high
APR with high vol is a trap; the sweet spot is decent APR with low vol and
days (not hours) to resolution.
python3 lp_screener.py --min-rate 50 --capital 1000
"""
import argparse
import csv
import json
import ssl
import statistics
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
CLOB = "https://clob.polymarket.com"
SSL_CTX = ssl._create_unverified_context()
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=20, context=SSL_CTX) as r:
return json.loads(r.read().decode())
def reward_markets():
out, cursor = [], ""
for _ in range(20):
try:
url = CLOB + "/sampling-markets" + (f"?next_cursor={cursor}" if cursor else "")
d = get(url)
except Exception:
break
out += d.get("data", [])
cursor = d.get("next_cursor")
if not cursor or not d.get("data"):
break
return out
def daily_rate(m):
rts = (m.get("rewards") or {}).get("rates") or []
return sum(x.get("rewards_daily_rate", 0) for x in rts)
def hours_to_end(m):
iso = m.get("end_date_iso")
if not iso:
return None
try:
end = time.mktime(time.strptime(iso.replace("Z", ""), "%Y-%m-%dT%H:%M:%S"))
return (end - time.time()) / 3600
except ValueError:
return None
def realized_vol_cents(token_id):
"""Stdev of 15-min midpoint moves over the last 24h, in cents."""
now = int(time.time())
try:
h = get(f"{CLOB}/prices-history?market={token_id}"
f"&startTs={now - 86400}&endTs={now}&fidelity=15").get("history", [])
except Exception:
return None
prices = [p["p"] for p in h if "p" in p]
if len(prices) < 4:
return None
diffs = [abs(prices[i] - prices[i - 1]) * 100 for i in range(1, len(prices))]
return round(statistics.pstdev(diffs), 2)
def analyze(m, capital):
pool = daily_rate(m)
r = m.get("rewards") or {}
ms = r.get("max_spread", 0) / 100.0
toks = m.get("tokens") or []
if not toks or ms <= 0:
return None
tok = toks[0].get("token_id")
try:
bk = get(f"{CLOB}/book?token_id={tok}")
except Exception:
return None
bids = [(float(o["price"]), float(o["size"])) for o in bk.get("bids", [])]
asks = [(float(o["price"]), float(o["size"])) for o in bk.get("asks", [])]
if not bids or not asks:
return None
bb = max(p for p, _ in bids)
ba = min(p for p, _ in asks)
mid = (bb + ba) / 2
comp_bid = sum(p * s for p, s in bids if p >= mid - ms)
comp_ask = sum((1 - p) * s for p, s in asks if p <= mid + ms)
myside = capital / 2
share = min(myside / (myside + comp_bid), myside / (myside + comp_ask))
apr = pool * share / capital * 365 * 100
vol = realized_vol_cents(tok)
hrs = hours_to_end(m)
# risk-adjusted score: reward yield, penalized by adverse selection (vol)
# and by imminence (markets resolving within a day are live/toxic).
vol_pen = 1 + (vol if vol is not None else 5) # unknown vol treated as risky
time_pen = 1.0 if (hrs is None or hrs >= 48) else max(0.15, hrs / 48)
score = round(apr * time_pen / vol_pen, 1)
return {
"question": m.get("question", "?")[:50],
"daily_usd": round(pool),
"max_spread_c": r.get("max_spread", 0),
"min_size": r.get("min_size", 0),
"mid": round(mid, 3),
"comp_usd": round(min(comp_bid, comp_ask)),
"gross_apr": round(apr),
"vol_24h_c": vol if vol is not None else -1,
"hrs_to_end": round(hrs) if hrs is not None else -1,
"score": score,
"token_id": tok,
}
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--min-rate", type=float, default=50, help="min $/day pool")
ap.add_argument("--capital", type=float, default=1000)
ap.add_argument("--workers", type=int, default=16)
ap.add_argument("--top", type=int, default=30)
args = ap.parse_args()
print(f"[{time.strftime('%H:%M:%S')}] pulling reward markets...", flush=True)
mkts = [m for m in reward_markets()
if m.get("active") and not m.get("closed") and daily_rate(m) >= args.min_rate]
print(f"[{time.strftime('%H:%M:%S')}] {len(mkts)} markets with >=${args.min_rate}/day "
f"· analyzing books + volatility...", flush=True)
rows = []
with ThreadPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(analyze, m, args.capital): m for m in mkts}
done = 0
for f in as_completed(futs):
done += 1
try:
r = f.result()
except Exception:
r = None
if r:
rows.append(r)
if done % 100 == 0:
print(f" {done}/{len(mkts)}", flush=True)
rows.sort(key=lambda x: x["score"], reverse=True)
cols = ["question", "score", "gross_apr", "vol_24h_c", "hrs_to_end", "daily_usd",
"comp_usd", "max_spread_c", "min_size", "mid", "token_id"]
with open("lp_markets.csv", "w", newline="") as fp:
w = csv.DictWriter(fp, fieldnames=cols)
w.writeheader()
w.writerows(rows)
print(f"\n{'score':>6}{'grossAPR':>9}{'vol_c':>7}{'hrs':>6}{'$/day':>7}"
f"{'comp$':>9}{'spr':>5} market")
print("-" * 92)
for r in rows[:args.top]:
vol = "n/a" if r["vol_24h_c"] < 0 else f"{r['vol_24h_c']:.1f}"
hrs = "?" if r["hrs_to_end"] < 0 else r["hrs_to_end"]
print(f"{r['score']:>6.0f}{r['gross_apr']:>8}%{vol:>7}{hrs:>6}{r['daily_usd']:>7}"
f"{r['comp_usd']:>9}{r['max_spread_c']:>5} {r['question'][:42]}")
print("-" * 92)
print(f"{len(rows)} markets ranked → lp_markets.csv")
print("score = gross APR × time-factor ÷ (1+vol). High APR + low vol + days-to-end = real.")
if __name__ == "__main__":
main()