363 lines
12 KiB
Python
363 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Polymarket Autopilot — Paper trading with TAIL/BONDING/SPREAD strategies.
|
|
|
|
Fetches live Polymarket data, runs strategy signals, manages a paper portfolio,
|
|
and generates daily summaries. Inspired by MoonDev's approach.
|
|
|
|
Usage:
|
|
python3 polymarket-autopilot.py scan # Scan markets for opportunities
|
|
python3 polymarket-autopilot.py trade # Run strategies & execute paper trades
|
|
python3 polymarket-autopilot.py portfolio # Show current paper portfolio
|
|
python3 polymarket-autopilot.py summary # Daily performance summary
|
|
python3 polymarket-autopilot.py cron # Full cycle (scan + trade + log)
|
|
|
|
Strategies:
|
|
TAIL — Buy extreme tail events (< 10% probability) for asymmetric upside
|
|
SPREAD — Exploit price gaps between related markets
|
|
MOMENTUM — Ride probability momentum (big moves in last 24h)
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from urllib.request import urlopen, Request
|
|
from urllib.error import URLError, HTTPError
|
|
|
|
DATA_DIR = Path(__file__).parent.parent / "data" / "polymarket"
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
PORTFOLIO_FILE = DATA_DIR / "portfolio.json"
|
|
TRADES_FILE = DATA_DIR / "trades.jsonl"
|
|
CONFIG_FILE = DATA_DIR / "config.json"
|
|
|
|
POLYMARKET_API = "https://gamma-api.polymarket.com"
|
|
|
|
DEFAULT_CONFIG = {
|
|
"starting_balance": 10000,
|
|
"max_position_pct": 5, # Max 5% of portfolio per trade
|
|
"tail_threshold": 0.10, # Buy events under 10% probability
|
|
"momentum_threshold": 0.15, # 15% probability change = momentum signal
|
|
"min_volume": 10000, # Minimum market volume
|
|
"strategies_enabled": ["tail", "momentum"],
|
|
}
|
|
|
|
|
|
def load_config():
|
|
if CONFIG_FILE.exists():
|
|
return json.loads(CONFIG_FILE.read_text())
|
|
CONFIG_FILE.write_text(json.dumps(DEFAULT_CONFIG, indent=2))
|
|
return DEFAULT_CONFIG
|
|
|
|
|
|
def load_portfolio():
|
|
if PORTFOLIO_FILE.exists():
|
|
return json.loads(PORTFOLIO_FILE.read_text())
|
|
portfolio = {
|
|
"balance": load_config()["starting_balance"],
|
|
"positions": [],
|
|
"total_trades": 0,
|
|
"pnl": 0,
|
|
"created": datetime.now().isoformat(),
|
|
}
|
|
save_portfolio(portfolio)
|
|
return portfolio
|
|
|
|
|
|
def save_portfolio(portfolio):
|
|
portfolio["updated"] = datetime.now().isoformat()
|
|
PORTFOLIO_FILE.write_text(json.dumps(portfolio, indent=2))
|
|
|
|
|
|
def log_trade(trade):
|
|
with open(TRADES_FILE, "a") as f:
|
|
f.write(json.dumps(trade) + "\n")
|
|
|
|
|
|
def fetch_json(url):
|
|
req = Request(url, headers={'User-Agent': 'polymarket-autopilot/1.0'})
|
|
try:
|
|
with urlopen(req, timeout=15) as resp:
|
|
return json.loads(resp.read().decode('utf-8'))
|
|
except (HTTPError, URLError) as e:
|
|
print(f" ❌ API error: {e}")
|
|
return None
|
|
|
|
|
|
def fetch_markets(limit=50, active=True):
|
|
"""Fetch markets from Polymarket CLOB API."""
|
|
url = f"{POLYMARKET_API}/markets?limit={limit}&active={str(active).lower()}&order=volume&ascending=false"
|
|
data = fetch_json(url)
|
|
if not data:
|
|
return []
|
|
|
|
markets = []
|
|
for m in (data if isinstance(data, list) else data.get("data", data.get("markets", []))):
|
|
if isinstance(m, dict):
|
|
markets.append({
|
|
"id": m.get("id", m.get("condition_id", "")),
|
|
"question": m.get("question", m.get("title", "")),
|
|
"volume": float(m.get("volume", m.get("volumeNum", 0)) or 0),
|
|
"liquidity": float(m.get("liquidity", 0) or 0),
|
|
"outcomes": m.get("outcomes", []),
|
|
"outcomePrices": m.get("outcomePrices", m.get("outcome_prices", [])),
|
|
"end_date": m.get("end_date_iso", m.get("endDate", "")),
|
|
"active": m.get("active", True),
|
|
"slug": m.get("slug", ""),
|
|
})
|
|
return markets
|
|
|
|
|
|
def parse_prices(market):
|
|
"""Parse outcome prices into float list."""
|
|
prices = market.get("outcomePrices", [])
|
|
if isinstance(prices, str):
|
|
try:
|
|
prices = json.loads(prices)
|
|
except:
|
|
prices = []
|
|
return [float(p) for p in prices if p]
|
|
|
|
|
|
def strategy_tail(markets, config):
|
|
"""TAIL: Find extreme low-probability events for asymmetric bets."""
|
|
signals = []
|
|
threshold = config.get("tail_threshold", 0.10)
|
|
|
|
for m in markets:
|
|
prices = parse_prices(m)
|
|
outcomes = m.get("outcomes", [])
|
|
if isinstance(outcomes, str):
|
|
try:
|
|
outcomes = json.loads(outcomes)
|
|
except:
|
|
outcomes = []
|
|
|
|
for i, price in enumerate(prices):
|
|
if 0.01 < price < threshold and m["volume"] > config.get("min_volume", 10000):
|
|
outcome_name = outcomes[i] if i < len(outcomes) else f"Outcome {i}"
|
|
signals.append({
|
|
"strategy": "TAIL",
|
|
"market": m["question"][:80],
|
|
"outcome": outcome_name,
|
|
"price": price,
|
|
"potential_return": f"{(1/price - 1)*100:.0f}%",
|
|
"volume": m["volume"],
|
|
"market_id": m["id"],
|
|
"action": "BUY",
|
|
})
|
|
|
|
return sorted(signals, key=lambda x: x["price"])[:10]
|
|
|
|
|
|
def strategy_momentum(markets, config):
|
|
"""MOMENTUM: Find markets with big recent probability moves."""
|
|
# Note: Would need historical price data for true momentum.
|
|
# For now, flag high-volume active markets near decision points.
|
|
signals = []
|
|
for m in markets:
|
|
prices = parse_prices(m)
|
|
outcomes = m.get("outcomes", [])
|
|
if isinstance(outcomes, str):
|
|
try:
|
|
outcomes = json.loads(outcomes)
|
|
except:
|
|
outcomes = []
|
|
|
|
for i, price in enumerate(prices):
|
|
# Markets near tipping points (40-60%) with high volume
|
|
if 0.40 < price < 0.60 and m["volume"] > config.get("min_volume", 10000) * 5:
|
|
outcome_name = outcomes[i] if i < len(outcomes) else f"Outcome {i}"
|
|
signals.append({
|
|
"strategy": "MOMENTUM",
|
|
"market": m["question"][:80],
|
|
"outcome": outcome_name,
|
|
"price": price,
|
|
"volume": m["volume"],
|
|
"market_id": m["id"],
|
|
"action": "WATCH",
|
|
})
|
|
|
|
return sorted(signals, key=lambda x: x["volume"], reverse=True)[:10]
|
|
|
|
|
|
def scan_markets():
|
|
config = load_config()
|
|
print(f"\n🔍 Polymarket Scanner — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print("=" * 60)
|
|
|
|
print(" Fetching markets...")
|
|
markets = fetch_markets(limit=100)
|
|
if not markets:
|
|
print(" ❌ Could not fetch markets")
|
|
return []
|
|
|
|
print(f" 📊 Found {len(markets)} active markets")
|
|
|
|
all_signals = []
|
|
|
|
if "tail" in config.get("strategies_enabled", []):
|
|
print("\n🎯 TAIL Strategy (low-probability events):")
|
|
tail_signals = strategy_tail(markets, config)
|
|
for s in tail_signals:
|
|
print(f" 💰 [{s['price']:.1%}] {s['market']}")
|
|
print(f" {s['outcome']} → Potential: {s['potential_return']}")
|
|
all_signals.extend(tail_signals)
|
|
if not tail_signals:
|
|
print(" No tail signals found.")
|
|
|
|
if "momentum" in config.get("strategies_enabled", []):
|
|
print("\n📈 MOMENTUM Strategy (high-volume tipping points):")
|
|
mom_signals = strategy_momentum(markets, config)
|
|
for s in mom_signals:
|
|
print(f" 👀 [{s['price']:.1%}] {s['market']}")
|
|
print(f" Vol: ${s['volume']:,.0f}")
|
|
all_signals.extend(mom_signals)
|
|
if not mom_signals:
|
|
print(" No momentum signals found.")
|
|
|
|
# Save signals
|
|
(DATA_DIR / "latest-signals.json").write_text(json.dumps({
|
|
"timestamp": datetime.now().isoformat(),
|
|
"signals": all_signals,
|
|
"markets_scanned": len(markets),
|
|
}, indent=2))
|
|
|
|
return all_signals
|
|
|
|
|
|
def execute_paper_trades(signals):
|
|
"""Execute paper trades based on signals."""
|
|
config = load_config()
|
|
portfolio = load_portfolio()
|
|
max_position = portfolio["balance"] * (config.get("max_position_pct", 5) / 100)
|
|
|
|
trades_made = 0
|
|
for signal in signals:
|
|
if signal["action"] != "BUY":
|
|
continue
|
|
if portfolio["balance"] < 100:
|
|
break
|
|
|
|
# Check if we already have a position in this market
|
|
existing = [p for p in portfolio["positions"] if p["market_id"] == signal["market_id"]]
|
|
if existing:
|
|
continue
|
|
|
|
size = min(max_position, portfolio["balance"] * 0.03) # 3% per trade
|
|
shares = size / signal["price"]
|
|
|
|
trade = {
|
|
"ts": datetime.now().isoformat(),
|
|
"strategy": signal["strategy"],
|
|
"market": signal["market"],
|
|
"outcome": signal["outcome"],
|
|
"action": "BUY",
|
|
"price": signal["price"],
|
|
"size": round(size, 2),
|
|
"shares": round(shares, 2),
|
|
"market_id": signal["market_id"],
|
|
}
|
|
|
|
portfolio["balance"] -= size
|
|
portfolio["positions"].append({
|
|
"market_id": signal["market_id"],
|
|
"market": signal["market"],
|
|
"outcome": signal["outcome"],
|
|
"entry_price": signal["price"],
|
|
"shares": round(shares, 2),
|
|
"cost": round(size, 2),
|
|
"strategy": signal["strategy"],
|
|
"opened": datetime.now().isoformat(),
|
|
})
|
|
portfolio["total_trades"] += 1
|
|
|
|
log_trade(trade)
|
|
trades_made += 1
|
|
print(f" 📝 PAPER BUY: {signal['outcome']} @ {signal['price']:.1%} (${size:.0f})")
|
|
|
|
save_portfolio(portfolio)
|
|
return trades_made
|
|
|
|
|
|
def show_portfolio():
|
|
portfolio = load_portfolio()
|
|
print(f"\n💼 Paper Portfolio — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
|
print("=" * 60)
|
|
print(f" 💰 Cash Balance: ${portfolio['balance']:,.2f}")
|
|
print(f" 📊 Total Trades: {portfolio['total_trades']}")
|
|
print(f" 📈 P&L: ${portfolio['pnl']:,.2f}")
|
|
|
|
if portfolio["positions"]:
|
|
print(f"\n Open Positions ({len(portfolio['positions'])}):")
|
|
for p in portfolio["positions"]:
|
|
print(f" [{p['strategy']}] {p['market'][:60]}")
|
|
print(f" {p['outcome']} | Entry: {p['entry_price']:.1%} | Cost: ${p['cost']:.0f} | Shares: {p['shares']:.0f}")
|
|
else:
|
|
print("\n No open positions.")
|
|
|
|
|
|
def daily_summary():
|
|
portfolio = load_portfolio()
|
|
trades_today = []
|
|
if TRADES_FILE.exists():
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
for line in TRADES_FILE.read_text().strip().split('\n'):
|
|
if line:
|
|
t = json.loads(line)
|
|
if t["ts"].startswith(today):
|
|
trades_today.append(t)
|
|
|
|
total_invested = sum(p["cost"] for p in portfolio["positions"])
|
|
|
|
summary = {
|
|
"date": datetime.now().strftime('%Y-%m-%d'),
|
|
"balance": portfolio["balance"],
|
|
"positions": len(portfolio["positions"]),
|
|
"total_invested": total_invested,
|
|
"portfolio_value": portfolio["balance"] + total_invested,
|
|
"trades_today": len(trades_today),
|
|
"total_trades": portfolio["total_trades"],
|
|
}
|
|
|
|
print(f"\n📊 Daily Summary — {summary['date']}")
|
|
print("=" * 40)
|
|
print(f" Portfolio Value: ${summary['portfolio_value']:,.2f}")
|
|
print(f" Cash: ${summary['balance']:,.2f}")
|
|
print(f" Invested: ${summary['total_invested']:,.2f}")
|
|
print(f" Positions: {summary['positions']}")
|
|
print(f" Trades Today: {summary['trades_today']}")
|
|
|
|
(DATA_DIR / "daily-summary.json").write_text(json.dumps(summary, indent=2))
|
|
return summary
|
|
|
|
|
|
def run_cron():
|
|
"""Full cron cycle: scan + trade + log."""
|
|
print("🤖 Polymarket Autopilot — Cron Cycle")
|
|
signals = scan_markets()
|
|
if signals:
|
|
buy_signals = [s for s in signals if s["action"] == "BUY"]
|
|
if buy_signals:
|
|
print(f"\n💸 Executing {len(buy_signals)} paper trades...")
|
|
execute_paper_trades(buy_signals)
|
|
daily_summary()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "scan"
|
|
if cmd == "scan":
|
|
scan_markets()
|
|
elif cmd == "trade":
|
|
signals = scan_markets()
|
|
execute_paper_trades([s for s in signals if s["action"] == "BUY"])
|
|
elif cmd == "portfolio":
|
|
show_portfolio()
|
|
elif cmd == "summary":
|
|
daily_summary()
|
|
elif cmd == "cron":
|
|
run_cron()
|
|
else:
|
|
print(__doc__)
|