mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-08-01 02:07:44 +00:00
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>
This commit is contained in:
@@ -12,3 +12,5 @@ copytrade_state.json
|
||||
edge_metrics.jsonl
|
||||
edge_profitable.json
|
||||
copyable_77.csv
|
||||
lp_markets.csv
|
||||
follow_10.json
|
||||
|
||||
@@ -19,6 +19,7 @@ live), and backtest the strategy. Zero dependencies — Python 3 stdlib only
|
||||
| `table_77.py` | Aggregate a filtered wallet set into one CSV (ROI, total staked, consistency). |
|
||||
| `copytrade.py` | Copy-trade engine — mirror a watchlist (paper by default, live gated). |
|
||||
| `backtest.py` | Replay a watchlist over a recent window and mark outcomes. |
|
||||
| `lp_screener.py` | Rank reward-eligible markets by risk-adjusted LP yield (pool ÷ competition, penalized by volatility). |
|
||||
|
||||
## Run the dashboard
|
||||
|
||||
@@ -200,3 +201,37 @@ story. At 72 days old, we can't yet tell.
|
||||
- **Copying entries ≠ copying edge.** A working strategy likely needs to model
|
||||
sizing/pricing, or pivot to a consensus signal (bet where many vetted wallets
|
||||
agree) rather than blind mirroring.
|
||||
|
||||
## Liquidity rewards (the market-making pivot)
|
||||
|
||||
After copy-trading proved unreliable, the research pointed to **liquidity
|
||||
rewards** as the lowest-risk edge. Polymarket pays makers a daily USDC pool for
|
||||
resting limit orders near a market's midpoint — your share = your score ÷ total
|
||||
score, where score rewards size and closeness to mid (quadratic:
|
||||
`((max_spread − your_spread) / max_spread)²`). ~$200K/day is distributed across
|
||||
~8,000 eligible markets (queryable via the CLOB `sampling-markets` endpoint;
|
||||
each market exposes `rewards.rates[].rewards_daily_rate`, `min_size`,
|
||||
`max_spread`).
|
||||
|
||||
`lp_screener.py` ranks those markets by **risk-adjusted** yield — reward pool ÷
|
||||
order-book competition near mid (gross APR for a $1000 two-sided position),
|
||||
penalized by 24h midpoint volatility (the adverse-selection proxy) and by
|
||||
time-to-resolution (imminent = live = toxic).
|
||||
|
||||
```bash
|
||||
python3 lp_screener.py --min-rate 50 --capital 1000 # one-shot snapshot → lp_markets.csv
|
||||
```
|
||||
|
||||
**It's a one-shot snapshot, not a daemon** — reward pools, books, and the
|
||||
markets themselves churn daily, so re-run before each session.
|
||||
|
||||
**What it found:** the sweet spot is **long-dated, low-volatility prop markets**
|
||||
(World Cup player props, eliminations) — thin books, decent pools, vol ~0, days
|
||||
to resolution. Live esports markets show astronomical gross APR but get
|
||||
correctly de-ranked: that's where you get picked off.
|
||||
|
||||
**Caveats that still gate real money:** headline APRs are a *snapshot* — thin
|
||||
pools attract competitors and yield mean-reverts down; they're *gross*, ignoring
|
||||
inventory losses when you get filled; and we have not yet confirmed near-empty
|
||||
books actually pay the full pool. The paper LP loop (post near mid, requote on
|
||||
moves, track **net** = rewards − pick-off) is the next and decisive test.
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user