Files
winning-wallet-finder/archive/lp_screener.py
T

185 lines
6.7 KiB
Python
Raw Normal View History

#!/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()