Archive dead-end strategies; add FINDINGS.md write-up

Moved the 8 tested-and-failed strategy tools into archive/ (copytrade, backtest,
edge_research, lookback, table_77, lp_screener, lp_paper, xarb) with an
archive/README explaining each. Root now holds the keepers: insider.py (made
self-sufficient — dropped the copytrade load_json dependency) and smart_money.py
(data foundation). New FINDINGS.md is the honest scorecard: six systematic
public-data edges all efficient/illusory, the win-rate survivorship-bias
finding, and the one real signal (z-score improbability + funding clustering).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-06-13 13:09:56 -04:00
parent 93b0c8c94e
commit 34d02956d8
12 changed files with 106 additions and 12 deletions
+21
View File
@@ -0,0 +1,21 @@
# Archive — strategies that didn't work
These tools were built and tested during the research in
[`../FINDINGS.md`](../FINDINGS.md). They all proved to be dead ends (the market
is efficient / the metric was biased), so they're archived here for reference
rather than deleted. Each one *works* as written — it's the *strategy* that
didn't clear. They import `smart_money`/`copytrade` from the repo root, so to
run one you'd adjust the import path.
| File | What it did | Why it's here |
|------|-------------|---------------|
| `copytrade.py` | Paper/live copy-trade engine — mirror a watchlist's entries/exits, % -of-bankroll sizing, price guard, per-position cap, Discord alerts. | Copying entries is EV; followed wallets win ~50%. Backtested 48%. |
| `backtest.py` | Replay a watchlist over a window, mark outcomes from resolution. | The tool that proved copy-trading loses. |
| `edge_research.py` | Scan ~2000 wallets for reliable weekly consistency (% green weeks, profit factor, Sharpe). | "Consistent" wallets were mostly young accounts (survivorship); no durable edge. |
| `lookback.py` | Deep-dive a wallet list over a long window, split into halves for out-of-sample reads. | Showed the "best" wallets had <90 days of history. |
| `table_77.py` | Aggregate a wallet set to CSV (ROI, total staked, consistency). | Supported the above; ROI inversely related to size. |
| `lp_screener.py` | Rank reward-eligible markets by risk-adjusted LP yield. | The high APRs were illusory — see `lp_paper`. |
| `lp_paper.py` | Paper liquidity-provision loop: simulate quoting, track net = rewards adverse selection. | Polymarket refunds unearned reward pool; thin-book "jackpots" don't pay. |
| `xarb.py` | Cross-venue scanner: match the same event on Polymarket vs Kalshi, flag price gaps. | Venues priced efficiently (~1¢); both legs cost >$1 after fees. |
The keeper that came out of all this lives at the repo root: `insider.py`.
+257
View File
@@ -0,0 +1,257 @@
#!/usr/bin/env python3
"""Backtest the copy-trade strategy over a recent window.
Replays each watched wallet's real trades through the same copy logic the live
bot uses — % -of-bankroll sizing, no-backfill, proportional adds/exits, risk
caps — but fills at the wallet's actual historical trade price. Outcomes are
marked from how each market resolved (curPrice 1/0 from closed-positions) or,
for still-open positions, the current market price.
python3 backtest.py # last 7 days, config.json watchlist
python3 backtest.py --days 7
This is an approximation. Notably the price guard is a near no-op in backtest
(we fill at their price, with no 12s real-time lag), so results are slightly
optimistic. Wallets whose history doesn't reach before the window are flagged.
"""
import argparse
import json
import time
from collections import defaultdict
import smart_money as sm
from copytrade import clob_price, DEFAULT_CONFIG, load_json
LOOKBACK_DAYS = 21 # how far before the window we try to read, for seed
MAX_TRADES = 4000 # pagination cap per wallet
def fetch_trades(wallet, since_ts):
"""Newest-first TRADE activity back to ~since_ts (capped)."""
out, off = [], 0
while off < MAX_TRADES:
page = sm.get_json("/activity",
{"user": wallet, "type": "TRADE",
"limit": 500, "offset": off})
if not page:
break
out += page
off += 500
if len(page) < 500 or page[-1].get("timestamp", 0) < since_ts:
break
return out
def mark_map(wallets):
"""asset(token) -> current/resolved price (curPrice).
Merges each wallet's open /positions (curPrice = live price, or 0/1 if it
resolved but isn't redeemed yet) and /closed-positions (resolved 1/0). This
is what lets us mark a position we still hold at its true value rather than
falling back to entry price.
"""
res = {}
for w in wallets:
for endpoint in ("/positions", "/closed-positions"):
off = 0
while off < 1000:
params = {"user": w, "limit": 50, "offset": off}
if endpoint == "/closed-positions":
params.update(sortBy="TIMESTAMP", sortDirection="DESC")
else:
params["sizeThreshold"] = 0.0
page = sm.get_json(endpoint, params)
if not page:
break
for p in page:
if p.get("asset") is not None:
# closed-positions wins ties (definitively resolved)
if endpoint == "/closed-positions" or p["asset"] not in res:
res[p["asset"]] = p.get("curPrice", 0)
off += 50
if len(page) < 50:
break
return res
def backtest(cfg, days):
wallets = cfg["watchlist"]
now = time.time()
window_start = now - days * 86400
lookback_start = window_start - LOOKBACK_DAYS * 86400
stake = cfg["bankroll_usd"] * cfg["bankroll_pct"]
risk = cfg["risk"]
print(f"Backtesting {len(wallets)} wallets over the last {days} days "
f"· ${stake:.0f}/entry · caps: ${risk['max_trade_usd']:.0f}/trade, "
f"${risk['daily_spend_cap_usd']:.0f}/day, "
f"${risk['max_total_exposure_usd']:.0f} exposure\n")
# gather every wallet's trades + per-wallet data reach
all_trades, reach = [], {}
for w in wallets:
ts = fetch_trades(w, lookback_start)
for t in ts:
t["_wallet"] = w
all_trades += ts
oldest = min((t["timestamp"] for t in ts), default=now)
reach[w] = (now - oldest) / 86400
all_trades.sort(key=lambda t: t["timestamp"])
res = mark_map(wallets)
# replay state
their_pos = defaultdict(float) # (wallet, token) -> shares
seed_tokens = set() # (wallet, token) held before window
my = {} # token -> {shares, cost, title, outcome, wallet}
daily_spend = defaultdict(float) # 'YYYY-MM-DD' -> usd
deployed = 0.0
realized = 0.0
n_open = n_add = n_exit = n_skip_guard = n_skip_cap = n_skip_backfill = 0
price_cache = {}
def cur_price(token, side):
key = (token, side)
if key not in price_cache:
price_cache[key] = clob_price(token, side)
return price_cache[key]
def exposure():
return sum(p["cost"] for p in my.values())
for t in all_trades:
w, token = t["_wallet"], t.get("asset")
side, size, price = t.get("side"), t.get("size", 0), t.get("price", 0)
key = (w, token)
prev = their_pos[key]
# pre-window trades only build their position (establish the seed)
if t["timestamp"] < window_start:
seed_tokens.add(key)
their_pos[key] = prev + size if side == "BUY" else max(0.0, prev - size)
continue
label = f"{t.get('outcome','?')} · {t.get('title','?')[:44]}"
if side == "BUY":
mine = my.get(token)
if mine is None and key in seed_tokens:
n_skip_backfill += 1
elif mine is None:
# fresh OPEN
if not (risk["min_price"] <= price <= risk["max_price"]):
n_skip_guard += 1
else:
day = time.strftime("%Y-%m-%d", time.gmtime(t["timestamp"]))
cap = min(stake, risk["max_trade_usd"],
risk.get("max_position_usd", float("inf")),
risk["daily_spend_cap_usd"] - daily_spend[day],
risk["max_total_exposure_usd"] - exposure())
if cap < risk["min_order_usd"] or len(my) >= risk["max_open_positions"]:
n_skip_cap += 1
else:
sh = cap / price
my[token] = {"shares": sh, "cost": cap,
"title": t.get("title", "?"),
"outcome": t.get("outcome", "?"), "wallet": w}
deployed += cap
daily_spend[day] += cap
n_open += 1
else:
# proportional ADD
frac = size / prev if prev > 0 else 0
add_sh = mine["shares"] * frac
add_usd = add_sh * price
day = time.strftime("%Y-%m-%d", time.gmtime(t["timestamp"]))
cap = min(add_usd, risk["max_trade_usd"],
risk.get("max_position_usd", float("inf")) - mine["cost"],
risk["daily_spend_cap_usd"] - daily_spend[day],
risk["max_total_exposure_usd"] - exposure())
if cap >= risk["min_order_usd"]:
sh = cap / price
mine["shares"] += sh
mine["cost"] += cap
deployed += cap
daily_spend[day] += cap
n_add += 1
their_pos[key] = prev + size
elif side == "SELL":
mine = my.get(token)
if mine and mine["shares"] > 0:
frac = 1.0 if prev <= 0 else min(1.0, size / prev)
sell_sh = min(mine["shares"], mine["shares"] * frac)
if sell_sh > 0:
sold_frac = sell_sh / mine["shares"]
cost_out = mine["cost"] * sold_frac
proceeds = sell_sh * price
realized += proceeds - cost_out
mine["shares"] -= sell_sh
mine["cost"] -= cost_out
n_exit += 1
if mine["shares"] <= 0.01:
del my[token]
their_pos[key] = max(0.0, prev - size)
# mark remaining open positions to resolution or current price
unrealized = 0.0
open_rows = []
for token, p in my.items():
mark = res.get(token)
if mark is None:
mark = cur_price(token, "sell")
if mark is None:
mark = p["cost"] / p["shares"] # last resort: flat
# curPrice at the extremes means the market has resolved
if mark <= 0.02:
status = "LOST"
elif mark >= 0.98:
status = "WON"
else:
status = "open"
val = p["shares"] * mark
pnl = val - p["cost"]
unrealized += pnl
open_rows.append((p, mark, pnl, status))
total_pnl = realized + unrealized
print(f"{''*74}")
print(" Per-wallet data reach (how far history extended before today):")
for w in wallets:
flag = "" if reach[w] >= days + 3 else " ⚠ short history — low confidence"
print(f" {w[:12]}{reach[w]:5.1f} days{flag}")
print(f"{''*74}")
print(f" Copies it would have made:")
print(f" {n_open} fresh entries · {n_add} adds · {n_exit} exits/trims")
print(f" skipped: {n_skip_backfill} held-before-start, "
f"{n_skip_guard} price/range, {n_skip_cap} risk-cap")
print(f"{''*74}")
print(f" Total deployed (bought): ${deployed:>12,.2f}")
print(f" Realized P&L (closed legs): ${realized:>+12,.2f}")
print(f" Unrealized P&L (still held): ${unrealized:>+12,.2f}")
print(f" ── Net P&L: ${total_pnl:>+12,.2f}"
f" ({(total_pnl/deployed*100) if deployed else 0:+.1f}% on deployed)")
print(f"{''*74}")
if open_rows:
won = sum(1 for _, _, _, s in open_rows if s == "WON")
lost = sum(1 for _, _, _, s in open_rows if s == "LOST")
opn = sum(1 for _, _, _, s in open_rows if s == "open")
print(f" Positions still on the book at window end: {len(open_rows)} "
f"({won} won, {lost} lost, {opn} open & marked-to-market)")
for p, mark, pnl, status in sorted(open_rows, key=lambda x: x[2]):
print(f" {status:>5} {pnl:>+9,.2f} {p['outcome']} · {p['title'][:40]}")
print()
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--config", default="config.json")
ap.add_argument("--days", type=int, default=7)
args = ap.parse_args()
cfg = {**DEFAULT_CONFIG, **load_json(args.config, {})}
cfg["risk"] = {**DEFAULT_CONFIG["risk"], **cfg.get("risk", {})}
backtest(cfg, args.days)
if __name__ == "__main__":
main()
+508
View File
@@ -0,0 +1,508 @@
#!/usr/bin/env python3
"""Polymarket copy-trade engine.
Watches a list of wallets and mirrors their trades onto your own account:
- sizing: a fixed % of your configured bankroll per new entry
- mirror: entries AND exits (sells are mirrored proportionally)
- guard: skip a copy if the market has moved >5% from their fill price
SAFETY
------
Runs in PAPER mode by default — it logs exactly what it would do and places
nothing. Live trading requires ALL of:
1. "mode": "live" in the config,
2. the --live command-line flag,
3. typing the confirmation phrase when prompted,
4. py-clob-client installed and valid credentials in the config.
Hard risk caps (per-trade, daily spend, total exposure, open positions, price
bounds) apply in both modes. This is real money in live mode — you are
responsible for the configuration and the outcomes.
Usage
-----
python3 copytrade.py --init # write config.example.json
python3 copytrade.py # paper mode (safe)
python3 copytrade.py --once # one polling pass, then exit
python3 copytrade.py --live # live mode (requires config + confirm)
python3 copytrade.py --config my.json # custom config path
"""
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
# reuse the scanner's hardened HTTP helper (SSL fallback, retries)
from smart_money import get_json, SSL_CTX # noqa: E402
DATA_API = "https://data-api.polymarket.com"
CLOB_API = "https://clob.polymarket.com"
POLYGON_CHAIN_ID = 137
CONFIRM_PHRASE = "TRADE LIVE"
DEFAULT_CONFIG = {
"mode": "paper", # "paper" or "live"
"poll_seconds": 12, # how often to check each wallet
"discord_webhook": "", # paste a Discord webhook URL to get pings
"watchlist": [], # ["0xwallet1", "0xwallet2", ...]
"bankroll_usd": 1000.0, # your stake pool
"bankroll_pct": 0.02, # 2% of bankroll per new entry
"price_guard_pct": 0.05, # skip if price moved >5% from their fill
"risk": {
"max_trade_usd": 50.0, # hard ceiling on any single copy
"max_position_usd": 40.0, # hard ceiling on total cost in one market
"daily_spend_cap_usd": 250.0,
"max_total_exposure_usd": 500.0,
"max_open_positions": 20,
"min_price": 0.05, # don't open longshots/near-certainties
"max_price": 0.95,
"min_order_usd": 5.0, # Polymarket min order size
},
# live credentials — only read in live mode
"live": {
"private_key": "", # EOA key that controls the funds
"funder_address": "", # proxy wallet holding USDC (sig type 1/2)
"signature_type": 1, # 0 EOA · 1 email/magic proxy · 2 browser proxy
},
}
STATE_PATH_DEFAULT = "copytrade_state.json"
def post_discord(webhook, content):
"""POST a message to a Discord webhook. Best-effort; never raises."""
if not webhook:
return False
try:
body = json.dumps({"content": content}).encode()
req = urllib.request.Request(
webhook, data=body, method="POST",
headers={"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"})
urllib.request.urlopen(req, timeout=10, context=SSL_CTX).read()
return True
except (urllib.error.URLError, TimeoutError):
return False
# ── state ─────────────────────────────────────────────────────────────────
def load_json(path, default):
if os.path.exists(path):
with open(path) as f:
return json.load(f)
return default
def save_json(path, data):
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f, indent=2)
os.replace(tmp, path)
def new_state():
return {
"started_at": time.time(),
"seen_tx": [], # transactionHashes already processed
"their_pos": {}, # wallet -> {token_id: shares}, live-tracked
"seed_tokens": {}, # wallet -> [token_id] held when we started
"my_pos": {}, # token_id -> {"shares", "cost", "title", "outcome"}
"spend": {"date": "", "usd": 0.0},
"seeded": [], # wallets whose starting positions we loaded
}
# ── market data ─────────────────────────────────────────────────────────────
def clob_price(token_id, side):
"""Best price to trade `side` ('buy'/'sell') on this token, or None."""
try:
url = f"{CLOB_API}/price?token_id={token_id}&side={side}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=10, context=SSL_CTX) as r:
return float(json.loads(r.read().decode())["price"])
except (urllib.error.URLError, KeyError, ValueError, TimeoutError):
return None
def their_positions(wallet):
"""Current open positions -> {token_id: shares}, for exit-fraction math."""
pos = {}
offset = 0
while offset < 500:
page = get_json("/positions",
{"user": wallet, "limit": 50, "offset": offset,
"sizeThreshold": 0.1})
if not page:
break
for p in page:
if p.get("asset"):
pos[p["asset"]] = pos.get(p["asset"], 0) + p.get("size", 0)
offset += 50
if len(page) < 50:
break
return pos
def recent_trades(wallet, limit=100):
"""Newest-first TRADE activity for a wallet."""
return get_json("/activity",
{"user": wallet, "type": "TRADE", "limit": limit}) or []
# ── execution ────────────────────────────────────────────────────────────────
class PaperExecutor:
"""Simulates fills at the current best price. Places nothing."""
live = False
def buy(self, token_id, shares, price, meta):
return {"ok": True, "filled_shares": shares, "price": price, "paper": True}
def sell(self, token_id, shares, price, meta):
return {"ok": True, "filled_shares": shares, "price": price, "paper": True}
class LiveExecutor:
"""Places real orders via py-clob-client. Imported lazily."""
live = True
def __init__(self, cfg):
try:
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY, SELL
except ImportError:
sys.exit("Live mode needs py-clob-client: pip install py-clob-client")
self._OrderArgs, self._OrderType = OrderArgs, OrderType
self._BUY, self._SELL = BUY, SELL
live = cfg["live"]
if not live.get("private_key"):
sys.exit("Live mode needs live.private_key in the config.")
self.client = ClobClient(
host=CLOB_API,
key=live["private_key"],
chain_id=POLYGON_CHAIN_ID,
signature_type=live.get("signature_type", 1),
funder=live.get("funder_address") or None,
)
self.client.set_api_creds(self.client.create_or_derive_api_creds())
def _order(self, token_id, shares, price, side):
args = self._OrderArgs(price=round(price, 3), size=round(shares, 2),
side=side, token_id=token_id)
signed = self.client.create_order(args)
resp = self.client.post_order(signed, self._OrderType.GTC)
ok = bool(resp and resp.get("success", True))
return {"ok": ok, "filled_shares": shares, "price": price,
"resp": resp, "paper": False}
def buy(self, token_id, shares, price, meta):
return self._order(token_id, shares, price, self._BUY)
def sell(self, token_id, shares, price, meta):
return self._order(token_id, shares, price, self._SELL)
# ── engine ────────────────────────────────────────────────────────────────
class CopyTrader:
def __init__(self, cfg, state, executor, state_path):
self.cfg = cfg
self.state = state
self.ex = executor
self.state_path = state_path
self.risk = cfg["risk"]
self.seen = set(state["seen_tx"])
self.webhook = cfg.get("discord_webhook", "")
self._discord_warned = False
# -- helpers --
def log(self, msg):
print(f"{time.strftime('%H:%M:%S')} {msg}", flush=True)
def alert(self, msg, discord_text=None):
"""Log to console AND push to Discord (used for actual placements)."""
self.log(msg)
if self.webhook:
ok = post_discord(self.webhook, discord_text or msg)
if not ok and not self._discord_warned:
self.log(" ⚠ Discord webhook post failed (check the URL)")
self._discord_warned = True
def reset_daily_if_needed(self):
today = time.strftime("%Y-%m-%d")
if self.state["spend"]["date"] != today:
self.state["spend"] = {"date": today, "usd": 0.0}
def open_exposure(self):
return sum(p["cost"] for p in self.state["my_pos"].values())
def persist(self):
self.state["seen_tx"] = list(self.seen)[-5000:]
save_json(self.state_path, self.state)
# -- risk gate: returns (allowed_usd, reason_if_blocked) --
def gate_buy(self, want_usd, price, pos_cost=0.0):
r = self.risk
if not (r["min_price"] <= price <= r["max_price"]):
return 0.0, f"price {price:.3f} outside [{r['min_price']},{r['max_price']}]"
if len(self.state["my_pos"]) >= r["max_open_positions"]:
return 0.0, f"max open positions ({r['max_open_positions']}) reached"
self.reset_daily_if_needed()
caps = [
want_usd,
r["max_trade_usd"],
r.get("max_position_usd", float("inf")) - pos_cost,
r["daily_spend_cap_usd"] - self.state["spend"]["usd"],
r["max_total_exposure_usd"] - self.open_exposure(),
]
allowed = min(caps)
if allowed < r["min_order_usd"]:
return 0.0, (f"capped to ${allowed:.2f} < min order "
f"${r['min_order_usd']:.2f} (daily/exposure caps)")
return allowed, None
# -- process one of their trades --
def handle_trade(self, wallet, t):
tx = t.get("transactionHash")
if not tx or tx in self.seen:
return
token = t.get("asset")
side = t.get("side") # BUY / SELL
their_size = t.get("size", 0)
their_price = t.get("price", 0)
title = t.get("title", "?")
outcome = t.get("outcome", "?")
label = f"{outcome} · {title[:42]}"
their_book = self.state["their_pos"].setdefault(wallet, {})
their_prev = their_book.get(token, 0)
if side == "BUY":
self._handle_their_buy(wallet, token, their_size, their_price,
label, title, outcome)
their_book[token] = their_prev + their_size
elif side == "SELL":
self._handle_their_sell(token, their_size, their_prev, label)
their_book[token] = max(0.0, their_prev - their_size)
self.seen.add(tx)
self.persist()
def _live_price(self, token, side):
p = clob_price(token, side)
if p is None:
self.log(f" ⚠ no live price for token, skipping")
return p
def _price_guard_ok(self, current, their_price):
if their_price <= 0:
return True
drift = abs(current - their_price) / their_price
return drift <= self.cfg["price_guard_pct"]
def _handle_their_buy(self, wallet, token, their_size, their_price,
label, title, outcome):
mine = self.state["my_pos"].get(token)
is_add = mine is not None
# don't backfill: never open a position they already held when we
# started watching. (A position we built during the run is an ADD;
# a brand-new position they opened after start is a fresh OPEN.)
if not is_add and token in self.state["seed_tokens"].get(wallet, []):
self.log(f"BUY {label} — skip (held before we started, no backfill)")
return
price = self._live_price(token, "buy")
if price is None:
return
if not self._price_guard_ok(price, their_price):
self.log(f"BUY {label} — skip (price {price:.3f} vs their "
f"{their_price:.3f}, >{self.cfg['price_guard_pct']:.0%})")
return
if is_add:
# proportional add: grow my position by the same fraction they did
their_prev = self.state["their_pos"].get(wallet, {}).get(token, 0)
frac = their_size / their_prev if their_prev > 0 else 0
want_shares = mine["shares"] * frac
want_usd = want_shares * price
kind = "ADD "
else:
want_usd = self.cfg["bankroll_usd"] * self.cfg["bankroll_pct"]
kind = "OPEN"
pos_cost = mine["cost"] if is_add else 0.0
allowed, reason = self.gate_buy(want_usd, price, pos_cost)
if reason:
self.log(f"{kind} {label} — skip ({reason})")
return
shares = allowed / price
res = self.ex.buy(token, shares, price, {"title": title})
if not res["ok"]:
self.log(f"{kind} {label} — ORDER FAILED: {res.get('resp')}")
return
spent = res["filled_shares"] * res["price"]
self.state["spend"]["usd"] += spent
if is_add:
mine["shares"] += res["filled_shares"]
mine["cost"] += spent
else:
self.state["my_pos"][token] = {
"shares": res["filled_shares"], "cost": spent,
"title": title, "outcome": outcome}
tag = "[PAPER]" if not self.ex.live else "[LIVE]"
self.alert(
f"{kind} {label}{tag} buy {res['filled_shares']:.1f} "
f"@ {res['price']:.3f} (${spent:.2f})",
discord_text=(f"🟢 **{kind.strip()}** {tag}\n{label}\n"
f"buy {res['filled_shares']:.0f} @ {res['price']:.3f} "
f"= **${spent:.2f}**"))
def _handle_their_sell(self, token, their_size, their_prev, label):
mine = self.state["my_pos"].get(token)
if not mine:
return # we don't hold it
frac = 1.0 if their_prev <= 0 else min(1.0, their_size / their_prev)
sell_shares = min(mine["shares"], mine["shares"] * frac)
if sell_shares <= 0:
return
price = self._live_price(token, "sell")
if price is None:
return
res = self.ex.sell(token, sell_shares, price, {})
if not res["ok"]:
self.log(f"EXIT {label} — ORDER FAILED: {res.get('resp')}")
return
proceeds = res["filled_shares"] * res["price"]
# reduce position; release cost proportionally
sold_frac = res["filled_shares"] / mine["shares"] if mine["shares"] else 1
mine["cost"] *= (1 - sold_frac)
mine["shares"] -= res["filled_shares"]
tag = "[PAPER]" if not self.ex.live else "[LIVE]"
verb = "EXIT" if frac >= 0.999 else "TRIM"
self.alert(
f"{verb} {label}{tag} sell {res['filled_shares']:.1f} "
f"@ {res['price']:.3f} (${proceeds:.2f})",
discord_text=(f"🔴 **{verb}** {tag}\n{label}\n"
f"sell {res['filled_shares']:.0f} @ {res['price']:.3f} "
f"= **${proceeds:.2f}**"))
if mine["shares"] <= 0.01:
del self.state["my_pos"][token]
# -- seed their current positions so exits mirror proportionally --
def seed_wallet(self, wallet):
if wallet in self.state["seeded"]:
return
self.state["their_pos"][wallet] = their_positions(wallet)
self.state["seed_tokens"][wallet] = list(self.state["their_pos"][wallet])
self.state["seeded"].append(wallet)
n = len(self.state["their_pos"][wallet])
self.log(f"seeded {wallet[:10]}… with {n} existing positions "
f"(won't be copied as new entries)")
# -- one polling pass over every watched wallet --
def poll_once(self, first_pass):
started = self.state["started_at"]
for wallet in self.cfg["watchlist"]:
self.seed_wallet(wallet)
trades = recent_trades(wallet)
# oldest-first so position math is causal
for t in sorted(trades, key=lambda x: x.get("timestamp", 0)):
# on the very first pass, ignore anything from before we started
if first_pass and t.get("timestamp", 0) < started:
self.seen.add(t.get("transactionHash"))
continue
self.handle_trade(wallet, t)
self.persist()
def run(self, once):
mode = "LIVE — REAL MONEY" if self.ex.live else "PAPER (no orders placed)"
self.log(f"copy-trader started · mode: {mode}")
self.log(f"watching {len(self.cfg['watchlist'])} wallets · "
f"bankroll ${self.cfg['bankroll_usd']:.0f} @ "
f"{self.cfg['bankroll_pct']:.1%}/entry · "
f"guard {self.cfg['price_guard_pct']:.0%}")
if self.webhook:
post_discord(self.webhook,
f"✅ **Copy-trade tracker connected** ({mode})\n"
f"watching {len(self.cfg['watchlist'])} wallets · "
f"${self.cfg['bankroll_usd']:.0f} bankroll @ "
f"{self.cfg['bankroll_pct']:.1%}/entry · "
f"guard {self.cfg['price_guard_pct']:.0%}\n"
f"You'll get a ping on every trade it would place.")
if not self.cfg["watchlist"]:
self.log("watchlist is empty — add wallets to the config. "
"(Run smart_money.py to find them.)")
return
first = True
try:
while True:
self.poll_once(first_pass=first)
first = False
if once:
break
time.sleep(self.cfg["poll_seconds"])
except KeyboardInterrupt:
self.log("stopped.")
# ── cli ──────────────────────────────────────────────────────────────────
def confirm_live(cfg):
print("\n" + "=" * 64)
print(" LIVE MODE — this will place REAL orders with REAL money.")
print(f" Bankroll ${cfg['bankroll_usd']:.0f} · {cfg['bankroll_pct']:.1%}/entry"
f" · max ${cfg['risk']['max_trade_usd']:.0f}/trade"
f" · daily cap ${cfg['risk']['daily_spend_cap_usd']:.0f}")
print(f" Watching {len(cfg['watchlist'])} wallets.")
print("=" * 64)
typed = input(f'Type "{CONFIRM_PHRASE}" to proceed (anything else aborts): ')
if typed.strip() != CONFIRM_PHRASE:
sys.exit("Aborted — not confirmed.")
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--config", default="config.json")
ap.add_argument("--state", default=STATE_PATH_DEFAULT)
ap.add_argument("--live", action="store_true",
help="enable live trading (also needs mode:live in config)")
ap.add_argument("--once", action="store_true", help="one pass, then exit")
ap.add_argument("--init", action="store_true",
help="write config.example.json and exit")
args = ap.parse_args()
if args.init:
save_json("config.example.json", DEFAULT_CONFIG)
print("Wrote config.example.json — copy to config.json and edit.")
return
if not os.path.exists(args.config):
sys.exit(f"No config at {args.config}. Run --init to create a template.")
cfg = {**DEFAULT_CONFIG, **load_json(args.config, {})}
cfg["risk"] = {**DEFAULT_CONFIG["risk"], **cfg.get("risk", {})}
cfg["live"] = {**DEFAULT_CONFIG["live"], **cfg.get("live", {})}
want_live = args.live and cfg.get("mode") == "live"
if args.live and cfg.get("mode") != "live":
sys.exit('--live given but config "mode" is not "live". Refusing to trade.')
state = load_json(args.state, new_state())
if want_live:
confirm_live(cfg)
executor = LiveExecutor(cfg)
else:
executor = PaperExecutor()
CopyTrader(cfg, state, executor, args.state).run(once=args.once)
if __name__ == "__main__":
main()
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Scan many wallets for a RELIABLE, COPYABLE weekly edge.
Two passes:
1. metrics — for every candidate, bucket resolved-bet PnL by week over the
window and compute consistency (% green weeks, profit factor, Sharpe, ROI).
Results stream to a JSONL file so a long run is crash-safe.
2. copyability — for the wallets that look profitable, pull /activity and
measure how much they hold to resolution (mirrorable) vs trade around
(not mirrorable by copying entries).
python3 edge_research.py --pool 1500 --days 120
Outputs: edge_metrics.jsonl (raw, all wallets)
edge_profitable.json (filtered + copyability, ranked)
"""
import argparse
import json
import os
import statistics
import sys
import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
import smart_money as sm
WEEK = 7 * 86400
MAX_PAGES = 40 # per endpoint, bounds runtime on hyperactive wallets
def _parse_end(end):
if not end:
return 0
end = end.replace("Z", "")
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
try:
return time.mktime(time.strptime(end, fmt))
except ValueError:
continue
return 0
def candidates(pool):
seen = {}
for window in ("7d", "30d", "all"):
offset = 0
while offset < pool and offset < 2000:
page = sm.get_json("/v1/leaderboard",
{"window": window, "limit": 50, "offset": offset})
if not page:
break
for u in page:
w = u.get("proxyWallet")
if w and w not in seen:
seen[w] = {"wallet": w,
"username": u.get("userName") or w[:10] + "...",
"lb_pnl": u.get("pnl", 0)}
offset += 50
if len(page) < 50:
break
if len(seen) >= pool:
break
return list(seen.values())[:pool]
def resolved_with_stake(wallet, cutoff):
now = time.time()
out = []
off = 0
while off < MAX_PAGES * 50:
page = sm.get_json("/closed-positions",
{"user": wallet, "limit": 50, "offset": off,
"sortBy": "TIMESTAMP", "sortDirection": "DESC"})
if not page:
break
for p in page:
if p.get("timestamp", 0) >= cutoff:
out.append({"ts": p["timestamp"], "pnl": p.get("realizedPnl", 0),
"stake": p.get("avgPrice", 0) * p.get("totalBought", 0)})
off += 50
if len(page) < 50 or page[-1].get("timestamp", 0) < cutoff:
break
off = 0
while off < MAX_PAGES * 50:
page = sm.get_json("/positions",
{"user": wallet, "limit": 50, "offset": off,
"sizeThreshold": 0.0})
if not page:
break
for p in page:
end = _parse_end(p.get("endDate"))
if cutoff <= end < now:
out.append({"ts": end, "pnl": p.get("cashPnl", 0),
"stake": p.get("initialValue", 0)})
off += 50
if len(page) < 50:
break
return out
def metrics(cand, cutoff):
bets = resolved_with_stake(cand["wallet"], cutoff)
if len(bets) < 20:
return None
by_week = defaultdict(lambda: [0.0, 0.0])
for b in bets:
wk = int(b["ts"] // WEEK)
by_week[wk][0] += b["pnl"]
by_week[wk][1] += b["stake"]
weeks = sorted(by_week)
wpnl = [by_week[w][0] for w in weeks]
wroi = [by_week[w][0] / by_week[w][1] if by_week[w][1] else 0 for w in weeks]
total_pnl = sum(wpnl)
total_stake = sum(by_week[w][1] for w in weeks)
gw = sum(p for p in wpnl if p > 0)
gl = abs(sum(p for p in wpnl if p < 0))
mean_roi = statistics.mean(wroi)
std_roi = statistics.pstdev(wroi) if len(wroi) > 1 else 0
return {
"wallet": cand["wallet"], "username": cand["username"],
"lb_pnl": round(cand["lb_pnl"]),
"n_weeks": len(weeks), "n_bets": len(bets),
"pct_weeks_pos": round(sum(1 for p in wpnl if p > 0) / len(weeks) * 100),
"mean_weekly_roi": round(mean_roi * 100, 1),
"weekly_sharpe": round(mean_roi / std_roi, 2) if std_roi else 0,
"profit_factor": round(gw / gl, 2) if gl else 999,
"total_pnl": round(total_pnl),
"total_roi": round(total_pnl / total_stake * 100, 1) if total_stake else 0,
}
def copyability(wallet):
trades, off = [], 0
while off < 2000: # cap fills for speed
p = sm.get_json("/activity",
{"user": wallet, "type": "TRADE", "limit": 500, "offset": off})
if not p:
break
trades += p
off += 500
if len(p) < 500:
break
by_mkt = defaultdict(lambda: {"buy_usd": 0.0, "sell_usd": 0.0, "sold": False})
for t in trades:
m = by_mkt[t.get("conditionId")]
if t.get("side") == "BUY":
m["buy_usd"] += t.get("usdcSize", 0)
else:
m["sell_usd"] += t.get("usdcSize", 0)
m["sold"] = True
n = len(by_mkt) or 1
hold = sum(1 for m in by_mkt.values() if not m["sold"])
return {"markets": len(by_mkt), "hold_pct": round(hold / n * 100),
"fills": len(trades)}
def run(pool, days, workers):
cutoff = time.time() - days * 86400
out_path, prof_path = "edge_metrics.jsonl", "edge_profitable.json"
print(f"[{time.strftime('%H:%M:%S')}] pulling up to {pool} candidates...", flush=True)
cands = candidates(pool)
print(f"[{time.strftime('%H:%M:%S')}] {len(cands)} candidates · "
f"window {days}d · analyzing (workers={workers})", flush=True)
done = kept = 0
with open(out_path, "w") as fout, ThreadPoolExecutor(max_workers=workers) as ex:
futs = {ex.submit(metrics, c, cutoff): c for c in cands}
for f in as_completed(futs):
done += 1
try:
r = f.result()
except Exception:
r = None
if r:
kept += 1
fout.write(json.dumps(r) + "\n")
fout.flush()
if done % 50 == 0 or done == len(cands):
print(f"[{time.strftime('%H:%M:%S')}] {done}/{len(cands)} analyzed "
f"· {kept} with enough history", flush=True)
rows = [json.loads(l) for l in open(out_path)]
# "looks profitable" screen
prof = [r for r in rows if r["n_weeks"] >= max(4, days // 7 * 0.4)
and r["n_bets"] >= 30 and r["total_pnl"] > 0 and r["total_roi"] > 0
and r["pct_weeks_pos"] >= 60 and r["profit_factor"] >= 1.3]
print(f"\n[{time.strftime('%H:%M:%S')}] {len(prof)} wallets pass the profitable "
f"screen · checking copyability...", flush=True)
with ThreadPoolExecutor(max_workers=workers) as ex:
futs = {ex.submit(copyability, r["wallet"]): r for r in prof}
for f in as_completed(futs):
r = futs[f]
try:
r["copy"] = f.result()
except Exception:
r["copy"] = {"markets": 0, "hold_pct": 0, "fills": 0}
for r in prof:
r["copyable"] = r["copy"]["hold_pct"] >= 70
# composite: reward consistency, profit factor, and ROI
r["score"] = round(r["pct_weeks_pos"] / 100 * r["profit_factor"]
* (1 + r["total_roi"] / 100), 2)
prof.sort(key=lambda r: (r["copyable"], r["score"]), reverse=True)
json.dump(prof, open(prof_path, "w"), indent=2)
print(f"\n{'='*94}")
print(f" PROFITABLE & COPYABLE wallets (window {days}d, pool {len(cands)})")
print(f"{'='*94}")
h = (f"{'Trader':<20}{'wks':>4}{'bets':>6}{'%wk+':>6}{'PF':>6}"
f"{'Sharpe':>7}{'totROI':>8}{'hold%':>7}{'copy':>6}{'90d PnL':>13}")
print(h)
print("-" * len(h))
for r in prof:
print(f"{r['username'][:20]:<20}{r['n_weeks']:>4}{r['n_bets']:>6}"
f"{r['pct_weeks_pos']:>5}%{r['profit_factor']:>6.2f}"
f"{r['weekly_sharpe']:>7.2f}{r['total_roi']:>7}%"
f"{r['copy']['hold_pct']:>6}%{'yes' if r['copyable'] else 'no':>6}"
f"{'$'+format(r['total_pnl'], ','):>13}")
print("-" * len(h))
cop = sum(1 for r in prof if r["copyable"])
print(f"{len(prof)} profitable · {cop} of them copyable (hold-to-resolution ≥70%)")
print(f"Full detail: {prof_path}\n")
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--pool", type=int, default=1500)
ap.add_argument("--days", type=int, default=120)
ap.add_argument("--workers", type=int, default=12)
args = ap.parse_args()
run(args.pool, args.days, args.workers)
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""240-day lookback on a short list of wallets, split into halves.
We selected these wallets on their last 120 days. The *older* half (240->120
days ago) is data that played no part in selection — so consistency there is
backward out-of-sample evidence the edge is real, not a lucky recent stretch.
"""
import statistics
import sys
import time
from collections import defaultdict
import smart_money as sm
WEEK = 7 * 86400
PAGES = 160 # generous for this focused 5-wallet run
def parse_end(end):
if not end:
return 0
end = end.replace("Z", "")
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
try:
return time.mktime(time.strptime(end, fmt))
except ValueError:
continue
return 0
def resolved(wallet, cutoff):
now = time.time()
out = []
off = 0
while off < PAGES * 50:
page = sm.get_json("/closed-positions",
{"user": wallet, "limit": 50, "offset": off,
"sortBy": "TIMESTAMP", "sortDirection": "DESC"})
if not page:
break
for p in page:
if p.get("timestamp", 0) >= cutoff:
out.append({"ts": p["timestamp"], "pnl": p.get("realizedPnl", 0),
"stake": p.get("avgPrice", 0) * p.get("totalBought", 0)})
off += 50
if len(page) < 50 or page[-1].get("timestamp", 0) < cutoff:
break
off = 0
while off < PAGES * 50:
page = sm.get_json("/positions",
{"user": wallet, "limit": 50, "offset": off,
"sizeThreshold": 0.0})
if not page:
break
for p in page:
end = parse_end(p.get("endDate"))
if cutoff <= end < now:
out.append({"ts": end, "pnl": p.get("cashPnl", 0),
"stake": p.get("initialValue", 0)})
off += 50
if len(page) < 50:
break
return out
def stats(bets):
if not bets:
return None
by_week = defaultdict(lambda: [0.0, 0.0])
for b in bets:
wk = int(b["ts"] // WEEK)
by_week[wk][0] += b["pnl"]
by_week[wk][1] += b["stake"]
weeks = sorted(by_week)
wpnl = [by_week[w][0] for w in weeks]
wroi = [by_week[w][0] / by_week[w][1] if by_week[w][1] else 0 for w in weeks]
tot_pnl = sum(wpnl)
tot_stake = sum(by_week[w][1] for w in weeks)
gw = sum(p for p in wpnl if p > 0)
gl = abs(sum(p for p in wpnl if p < 0))
mean = statistics.mean(wroi)
std = statistics.pstdev(wroi) if len(wroi) > 1 else 0
return {
"weeks": len(weeks), "bets": len(bets),
"green": round(sum(1 for p in wpnl if p > 0) / len(weeks) * 100),
"pf": round(gw / gl, 2) if gl else 999,
"sharpe": round(mean / std, 2) if std else 0,
"roi": round(tot_pnl / tot_stake * 100, 1) if tot_stake else 0,
"pnl": round(tot_pnl),
}
def line(label, s):
if not s:
print(f" {label:<8} (no resolved bets in this period)")
return
print(f" {label:<8} {s['weeks']:>2}wk {s['bets']:>5}bets "
f"{s['green']:>3}%grn PF {s['pf']:>6} Sharpe {s['sharpe']:>5} "
f"ROI {s['roi']:>6}% ${s['pnl']:>12,}")
def main(wallets):
now = time.time()
mid = now - 120 * 86400
for name, w in wallets:
bets = resolved(w, now - 240 * 86400)
older = [b for b in bets if b["ts"] < mid] # 240->120d (not used to select)
recent = [b for b in bets if b["ts"] >= mid] # 120->0d (selection window)
print(f"\n{name} ({w[:16]}…)")
line("240d all", stats(bets))
line("older½", stats(older)) # out-of-sample
line("recent½", stats(recent)) # in-sample
if __name__ == "__main__":
# name, wallet — passed as alternating argv or hardcoded by caller
pairs = [(sys.argv[i], sys.argv[i + 1]) for i in range(1, len(sys.argv), 2)]
main(pairs)
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""Paper liquidity-provision loop — measures NET reward yield without real money.
It simulates posting two-sided limit orders near the midpoint on the screener's
top markets, against the LIVE order book, and tracks:
net P&L = rewards accrued - adverse-selection / inventory P&L
Reward model (per poll, per market):
your_share = your_notional / (your_notional + competing_notional_near_mid)
rewards += daily_pool * your_share * (seconds_elapsed / 86400)
(matches Polymarket's score-share mechanic; assumes ~equal price-quality, which
is conservative since we quote tight to mid.)
Fill / adverse-selection model:
We rest a bid at mid-tick and an ask at mid+tick. Between polls, if the
midpoint crosses a quote, that quote is assumed FILLED at its price, and we
take on inventory marked at the NEW mid — so a price that runs through us
books an immediate loss. This is the bleed that must stay below rewards.
Each poll we "cancel and re-quote" around the new mid (a bot requoting every
poll interval). Shorter --poll = less bleed, fewer missed requotes.
This is an APPROXIMATION (ignores queue position, partial fills, requote
latency), deliberately a bit pessimistic on fills. Good enough to answer the
one question that gates real money: is net positive, and how big?
python3 lp_paper.py --capital 1000 --markets 6 --poll 20
"""
import argparse
import json
import os
import time
from concurrent.futures import ThreadPoolExecutor
from lp_screener import get, reward_markets, daily_rate, hours_to_end, realized_vol_cents, CLOB
from copytrade import post_discord, load_json
STATE_PATH = "lp_paper_state.json"
def screen_targets(n, min_rate, per_market, max_vol):
"""Pick the top-N low-vol reward markets we can actually qualify in.
Filters out markets where our per-side size would fall below the market's
min_size (we'd earn nothing) and markets priced outside 0.10-0.90 (the
double-sided-only regime, easy to get adversely filled at the extremes).
"""
mkts = [m for m in reward_markets()
if m.get("active") and not m.get("closed") and daily_rate(m) >= min_rate]
scored = []
def assess(m):
r = m.get("rewards") or {}
ms = r.get("max_spread", 0) / 100.0
min_size = r.get("min_size", 0)
toks = m.get("tokens") or []
if not toks or ms <= 0:
return None
tok = toks[0]["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
mid = (max(p for p, _ in bids) + min(p for p, _ in asks)) / 2
if not (0.10 <= mid <= 0.90): # extreme regime: skip
return None
if mid <= 0 or (per_market / 2) / mid < min_size: # can't meet min_size
return None
comp = min(sum(p * s for p, s in bids if p >= mid - ms),
sum((1 - p) * s for p, s in asks if p <= mid + ms))
vol = realized_vol_cents(tok)
hrs = hours_to_end(m)
if vol is None or vol > max_vol: # skip toxic / unknown-vol
return None
if hrs is not None and hrs < 24: # skip imminent/live
return None
return {
"token": tok, "question": m.get("question", "?")[:50],
"pool": daily_rate(m), "max_spread": ms, "min_size": min_size,
"tick": float(bk.get("tick_size", 0.01)),
"comp": comp, "mid": mid, "vol": vol,
}
with ThreadPoolExecutor(max_workers=16) as ex:
for res in ex.map(assess, mkts):
if res:
scored.append(res)
# rank by reward-yield / competition, low vol
scored.sort(key=lambda x: x["pool"] / (x["comp"] + x["pool"]) / (1 + x["vol"]),
reverse=True)
return scored[:n]
def fresh_market_state(t, per_market):
# cash = cumulative cash flow from simulated trades (buys negative, sells
# positive); inv = signed share position. Net trading P&L = cash + inv*mid.
return {**t, "notional": per_market / 2, "bid": None, "ask": None,
"inv": 0.0, "cash": 0.0, "rewards": 0.0, "fills": 0,
"last_t": time.time()}
def poll_market(s, max_inv_mult, max_dt):
"""One observe-fill-accrue-requote step against the live book."""
try:
bk = get(f"{CLOB}/book?token_id={s['token']}")
except Exception:
return
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
mid = (max(p for p, _ in bids) + min(p for p, _ in asks)) / 2
if mid <= 0:
return
now = time.time()
dt = min(now - s["last_t"], max_dt) # cap dt so a stall/sleep can't over-credit
s["last_t"] = now
size = s["notional"] / mid # intended per-side size (shares)
cap = max_inv_mult * size # price-aware inventory cap
# 1) fills: did the mid cross our resting quotes? cap the fill to remaining
# inventory room so one fill can't overshoot the intended position.
if s["bid"] is not None and mid <= s["bid"]: # bought at our bid
f = min(size, max(0.0, cap - s["inv"]))
if f > 0:
s["inv"] += f
s["cash"] -= f * s["bid"]
s["fills"] += 1
if s["ask"] is not None and mid >= s["ask"]: # sold at our ask
f = min(size, max(0.0, cap + s["inv"]))
if f > 0:
s["inv"] -= f
s["cash"] += f * s["ask"]
s["fills"] += 1
# 2) accrue rewards — only if we'd actually qualify (min_size, price regime),
# and at 1/3 share when only one side is live (Polymarket's Q_min penalty).
comp = s["comp"]
base = s["notional"] / (s["notional"] + comp) if (s["notional"] + comp) > 0 else 0
both_live = (s["inv"] < cap) and (s["inv"] > -cap)
qualifies = size >= s["min_size"] and 0.10 <= mid <= 0.90
eff = 0.0 if not qualifies else (base if both_live else base / 3.0)
s["rewards"] += s["pool"] * eff * (dt / 86400.0)
# 3) re-quote around the new mid (within max_spread), respecting inventory cap
s["mid"] = mid
s["bid"] = mid - s["tick"] if s["inv"] < cap else None
s["ask"] = mid + s["tick"] if s["inv"] > -cap else None
ms = s["max_spread"]
s["comp"] = min(sum(p * sz for p, sz in bids if p >= mid - ms),
sum((1 - p) * sz for p, sz in asks if p <= mid + ms))
def net_pnl(s):
return s["rewards"] + s["cash"] + s["inv"] * s["mid"]
def summary(states, started, capital, retired):
rew = retired["rewards"] + sum(s["rewards"] for s in states)
trading = retired["trading"] + sum(s["cash"] + s["inv"] * s["mid"] for s in states)
net = rew + trading
hrs = (time.time() - started) / 3600 or 1e-9
apr = net / capital / (hrs / 24) * 365 * 100 if capital else 0
lines = [
f"{hrs:.1f}h · capital ${capital:,.0f}",
f" rewards accrued : +${rew:,.2f}",
f" trading/inventory: {trading:+,.2f} (adverse-selection bleed)",
f" ── NET : {net:+,.2f} (~{apr:,.0f}% APR if it holds)",
]
return net, "\n".join(lines)
def run(args):
cfg = load_json("config.json", {})
webhook = cfg.get("discord_webhook", "")
per_market = args.capital / args.markets
print(f"[{time.strftime('%H:%M:%S')}] screening for {args.markets} low-vol markets...",
flush=True)
targets = screen_targets(args.markets, args.min_rate, per_market, args.max_vol)
if not targets:
print("No suitable markets we can qualify in at this capital/market split.")
return
states = [fresh_market_state(t, per_market) for t in targets]
started = time.time()
print(f"[{time.strftime('%H:%M:%S')}] making markets on {len(states)} markets "
f"(${per_market:,.0f} each, ${args.capital:,.0f} total):", flush=True)
for s in states:
print(f" ${s['pool']:>4.0f}/day vol {s['vol']:.1f}c comp ${s['comp']:,.0f}"
f" {s['question']}", flush=True)
if webhook:
post_discord(webhook, f"📊 **Paper LP started** · {len(states)} markets · "
f"${args.capital:,.0f} capital. Tracking net = rewards bleed.")
max_dt = max(120, args.poll * 5) # cap reward accrual gap (sleep/stall guard)
# P&L from markets that have rotated out (resolved/expired) is banked here
# so cumulative net survives rotation.
retired = {"rewards": 0.0, "trading": 0.0}
def retire(s):
retired["rewards"] += s["rewards"]
retired["trading"] += s["cash"] + s["inv"] * s["mid"]
last_report = started
next_rescreen = started + args.refresh
try:
while True:
for s in states:
poll_market(s, args.max_inv, max_dt)
now = time.time()
# rotate: drop markets that fell out of the fresh screen (resolved /
# vol spiked / out-competed), bank their P&L, add fresh ones.
if now >= next_rescreen:
next_rescreen = now + args.refresh
try:
fresh = screen_targets(args.markets, args.min_rate, per_market, args.max_vol)
except Exception as e:
fresh = None
print(f"[{time.strftime('%H:%M:%S')}] re-screen failed ({e}); "
f"keeping current markets", flush=True)
if fresh:
fresh_toks = {t["token"] for t in fresh}
kept = []
for s in states:
if s["token"] in fresh_toks:
kept.append(s)
else:
retire(s)
states = kept
held = {s["token"] for s in states}
for t in fresh:
if len(states) >= args.markets:
break
if t["token"] not in held:
states.append(fresh_market_state(t, per_market))
print(f"[{time.strftime('%H:%M:%S')}] re-screened · {len(states)} active "
f"· banked net so far ${retired['rewards'] + retired['trading']:,.2f}",
flush=True)
save_state(states, started, args.capital, retired)
if now - last_report >= args.report:
net, txt = summary(states, started, args.capital, retired)
print(f"\n[{time.strftime('%H:%M:%S')}]\n{txt}", flush=True)
if webhook:
post_discord(webhook, "📊 **Paper LP update**\n" + txt)
last_report = now
if args.duration and (now - started) >= args.duration * 3600:
break
time.sleep(args.poll)
except KeyboardInterrupt:
pass
net, txt = summary(states, started, args.capital, retired)
print(f"\n=== FINAL ===\n{txt}")
print("\nPer-market:")
for s in sorted(states, key=net_pnl, reverse=True):
print(f" net {net_pnl(s):+8.2f} | rew +{s['rewards']:6.2f} | "
f"fills {s['fills']:3d} | inv {s['inv']:+8.1f} | {s['question']}")
def save_state(states, started, capital, retired):
slim = [{"question": s["question"], "pool": s["pool"],
"rewards": round(s["rewards"], 2), "trading": round(s["cash"] + s["inv"] * s["mid"], 2),
"inv": round(s["inv"], 1), "fills": s["fills"],
"net": round(net_pnl(s), 2)} for s in states]
net, _ = summary(states, started, capital, retired)
tmp = STATE_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump({"started": started, "capital": capital, "net": round(net, 2),
"retired": {k: round(v, 2) for k, v in retired.items()},
"markets": slim}, f, indent=2)
os.replace(tmp, STATE_PATH)
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--capital", type=float, default=1000)
ap.add_argument("--markets", type=int, default=6)
ap.add_argument("--poll", type=int, default=20, help="seconds between requotes")
ap.add_argument("--report", type=int, default=900, help="seconds between summaries")
ap.add_argument("--refresh", type=int, default=3600, help="seconds between re-screens")
ap.add_argument("--min-rate", type=float, default=50)
ap.add_argument("--max-vol", type=float, default=1.5, help="max 24h vol (cents) to qualify")
ap.add_argument("--max-inv", type=float, default=1.0, help="inventory cap multiple")
ap.add_argument("--duration", type=float, default=0, help="hours to run (0 = until killed)")
run(ap.parse_args())
if __name__ == "__main__":
main()
+184
View File
@@ -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()
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Aggregate the 77 copyable wallets into one table: total staked, PnL, ROI,
consistency. Prints sorted by ROI and writes copyable_77.csv."""
import csv
import json
import statistics
import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
import smart_money as sm
from lookback import resolved # reuse the 120d+ resolved-bet puller
WEEK = 7 * 86400
def compute(r):
cutoff = time.time() - 120 * 86400
bets = resolved(r["wallet"], cutoff)
if not bets:
return None
by_week = defaultdict(lambda: [0.0, 0.0])
for b in bets:
wk = int(b["ts"] // WEEK)
by_week[wk][0] += b["pnl"]
by_week[wk][1] += b["stake"]
weeks = sorted(by_week)
wpnl = [by_week[w][0] for w in weeks]
wroi = [by_week[w][0] / by_week[w][1] if by_week[w][1] else 0 for w in weeks]
total_bet = sum(b["stake"] for b in bets)
total_pnl = sum(b["pnl"] for b in bets)
gw = sum(p for p in wpnl if p > 0)
gl = abs(sum(p for p in wpnl if p < 0))
mean = statistics.mean(wroi)
std = statistics.pstdev(wroi) if len(wroi) > 1 else 0
oldest_days = round((time.time() - min(b["ts"] for b in bets)) / 86400)
return {
"username": r["username"], "wallet": r["wallet"],
"weeks": len(weeks), "bets": len(bets),
"total_bet": round(total_bet), "total_pnl": round(total_pnl),
"roi_pct": round(total_pnl / total_bet * 100, 1) if total_bet else 0,
"pct_weeks_green": round(sum(1 for p in wpnl if p > 0) / len(weeks) * 100),
"profit_factor": round(gw / gl, 2) if gl else 999,
"weekly_sharpe": round(mean / std, 2) if std else 0,
"hold_pct": r["copy"]["hold_pct"],
"history_days": oldest_days,
"avg_bet": round(total_bet / len(bets)) if bets else 0,
}
def main():
cop = [r for r in json.load(open("edge_profitable.json")) if r.get("copyable")]
out = []
with ThreadPoolExecutor(max_workers=12) as ex:
futs = {ex.submit(compute, r): r for r in cop}
for f in as_completed(futs):
r = f.result()
if r:
out.append(r)
out.sort(key=lambda r: r["roi_pct"], reverse=True)
cols = ["username", "roi_pct", "total_bet", "total_pnl", "avg_bet",
"pct_weeks_green", "profit_factor", "weekly_sharpe", "weeks",
"bets", "hold_pct", "history_days", "wallet"]
with open("copyable_77.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=cols)
w.writeheader()
w.writerows(out)
print(f"{'#':>3} {'Trader':<20}{'ROI%':>7}{'TotalBet':>13}{'TotalPnL':>13}"
f"{'AvgBet':>9}{'%grn':>6}{'PF':>6}{'Shrp':>6}{'wks':>4}{'hist_d':>7}")
print("-" * 100)
for i, r in enumerate(out, 1):
print(f"{i:>3} {r['username'][:20]:<20}{r['roi_pct']:>6}%"
f"{'$'+format(r['total_bet'], ','):>13}{'$'+format(r['total_pnl'], ','):>13}"
f"{'$'+format(r['avg_bet'], ','):>9}{r['pct_weeks_green']:>5}%"
f"{r['profit_factor']:>6.1f}{r['weekly_sharpe']:>6.2f}{r['weeks']:>4}"
f"{r['history_days']:>7}")
print("-" * 100)
print(f"{len(out)} copyable wallets · saved to copyable_77.csv")
print(f" total staked across all: ${sum(r['total_bet'] for r in out):,}")
print(f" median history: {statistics.median([r['history_days'] for r in out])} days")
if __name__ == "__main__":
main()
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Polymarket <-> Kalshi cross-venue arbitrage scanner.
Pulls live prices from both venues, matches the same event across them, and
flags executable spreads: buy YES on one + NO on the other for < $1 (net of
fees) = locked profit regardless of resolution.
python3 xarb.py # one-shot scan -> xarb_hits.csv
python3 xarb.py --min-vol 5000 # only liquid markets
Matching is conservative (token overlap + same resolution month) to limit
false matches — a wrong match isn't an arb, it's two different bets
(resolution risk). Treat flagged hits as candidates to eyeball, not gospel.
"""
import argparse
import csv
import json
import re
import ssl
import urllib.request
from collections import defaultdict
ctx = ssl._create_unverified_context()
K = "https://api.elections.kalshi.com/trade-api/v2"
GAMMA = "https://gamma-api.polymarket.com"
STOP = {"will", "the", "a", "an", "to", "of", "in", "by", "be", "win", "wins",
"winner", "2026", "2025", "at", "on", "for", "and", "vs", "game", "match",
"who", "what", "during", "this", "next", "before", "after", "his", "her"}
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=30, context=ctx) as r:
return json.loads(r.read().decode())
def get_safe(url):
try:
return get(url)
except Exception:
return None
def fnum(v, d=0.0):
try:
return float(v)
except (TypeError, ValueError):
return d
def norm(s):
s = re.sub(r"[^a-z0-9 ]", " ", (s or "").lower())
return {t for t in s.split() if t not in STOP and len(t) > 1}
def nums(s):
"""Numeric tokens (thresholds, scores, dates) that must match exactly for
two markets to be the *same* contract, not just the same event."""
return set(re.findall(r"\d+", (s or "").lower()))
def kalshi_fee(price):
"""Kalshi taker fee per $1 contract ≈ 0.07 * P * (1-P)."""
return 0.07 * price * (1 - price)
# ── data pulls ──────────────────────────────────────────────────────────────
def pull_kalshi(min_vol):
evs, cur = [], ""
for _ in range(60):
d = get(K + "/events?limit=200&status=open&with_nested_markets=true"
+ (f"&cursor={cur}" if cur else ""))
evs += d.get("events", [])
cur = d.get("cursor")
if not cur:
break
out = []
for e in evs:
for m in e.get("markets", []):
ya, na = fnum(m.get("yes_ask_dollars")), fnum(m.get("no_ask_dollars"))
if not (0 < ya < 1 and 0 < na < 1):
continue
vol = fnum(m.get("volume_24h_fp"))
if vol < min_vol:
continue
text = f"{e.get('title','')} {m.get('yes_sub_title','')}"
out.append({
"venue": "kalshi", "ticker": m["ticker"], "text": text,
"tokens": norm(text), "end": (m.get("close_time") or "")[:7],
"yes_ask": ya, "no_ask": na,
"yes_bid": fnum(m.get("yes_bid_dollars")),
"no_bid": fnum(m.get("no_bid_dollars")), "vol": vol,
})
return out
def pull_polymarket(min_vol):
out, offset = [], 0
for _ in range(200):
g = get_safe(f"{GAMMA}/markets?limit=100&offset={offset}&active=true"
f"&closed=false&order=volumeNum&ascending=false")
if not g:
break
for m in g:
try:
outcomes = json.loads(m.get("outcomes", "[]"))
except Exception:
outcomes = []
if [o.lower() for o in outcomes] != ["yes", "no"]:
continue
ask = fnum(m.get("bestAsk"))
bid = fnum(m.get("bestBid"))
if not (0 < ask < 1 and 0 < bid < 1):
continue
vol = fnum(m.get("volumeNum"))
if vol < min_vol:
continue
q = m.get("question", "")
out.append({
"venue": "poly", "text": q, "tokens": norm(q),
"end": (m.get("endDateIso") or m.get("endDate") or "")[:7],
"yes_ask": ask, "no_ask": round(1 - bid, 4), # NO ask ≈ 1 - YES bid
"vol": vol,
})
if len(g) < 100:
break
offset += 100
return out
# ── matching + arb ────────────────────────────────────────────────────────
def match_and_scan(poly, kalshi, min_sim):
# inverted index: token -> kalshi markets containing it
idx = defaultdict(list)
for k in kalshi:
for t in k["tokens"]:
idx[t].append(k)
hits = []
for p in poly:
if len(p["tokens"]) < 2:
continue
p_nums = nums(p["text"])
cand = {id(k): k for t in p["tokens"] for k in idx.get(t, [])}
best, best_sim = None, 0
for k in cand.values():
if p["end"] and k["end"] and p["end"] != k["end"]:
continue # different resolution month → skip
if nums(k["text"]) != p_nums:
continue # different thresholds/scores/dates → not the same contract
inter = len(p["tokens"] & k["tokens"])
sim = inter / len(p["tokens"] | k["tokens"])
if sim > best_sim:
best, best_sim = k, sim
if not best or best_sim < min_sim:
continue
# two arb directions
# A: poly YES + kalshi NO
a_cost = p["yes_ask"] + best["no_ask"]
a_edge = 1 - a_cost - kalshi_fee(best["no_ask"])
# B: kalshi YES + poly NO
b_cost = best["yes_ask"] + p["no_ask"]
b_edge = 1 - b_cost - kalshi_fee(best["yes_ask"])
if a_edge >= b_edge:
edge, leg = a_edge, "poly YES + kalshi NO"
else:
edge, leg = b_edge, "kalshi YES + poly NO"
hits.append({
"edge_c": round(edge * 100, 2), "sim": round(best_sim, 2),
"leg": leg, "poly": p["text"][:46], "kalshi": best["text"][:46],
"p_yes": p["yes_ask"], "p_no": p["no_ask"],
"k_yes": best["yes_ask"], "k_no": best["no_ask"],
"min_vol": round(min(p["vol"], best["vol"])),
})
hits.sort(key=lambda h: h["edge_c"], reverse=True)
return hits
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--min-vol", type=float, default=2000)
ap.add_argument("--min-sim", type=float, default=0.5, help="token-overlap threshold")
ap.add_argument("--top", type=int, default=30)
args = ap.parse_args()
print("pulling Kalshi...", flush=True)
kalshi = pull_kalshi(args.min_vol)
print(f" {len(kalshi)} liquid Kalshi markets", flush=True)
print("pulling Polymarket...", flush=True)
poly = pull_polymarket(args.min_vol)
print(f" {len(poly)} liquid Polymarket markets", flush=True)
hits = match_and_scan(poly, kalshi, args.min_sim)
with open("xarb_hits.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(hits[0].keys()) if hits else
["edge_c", "sim", "leg", "poly", "kalshi"])
w.writeheader()
w.writerows(hits)
arbs = [h for h in hits if h["edge_c"] > 0]
print(f"\nmatched pairs: {len(hits)} · positive-edge (after fees): {len(arbs)}")
print(f"\n{'edge¢':>6}{'sim':>5}{'minVol':>9} match (poly ↔ kalshi)")
print("-" * 92)
for h in hits[:args.top]:
print(f"{h['edge_c']:>6.1f}{h['sim']:>5.2f}{h['min_vol']:>9} "
f"{h['poly'][:34]:34}{h['kalshi'][:30]}")
print("-" * 92)
print(f"saved {len(hits)} matched pairs → xarb_hits.csv (edge>0 = arb after fees)")
return arbs, hits
if __name__ == "__main__":
main()