Files

1277 lines
42 KiB
Python

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "requests>=2.28.0",
# ]
# ///
"""
Polymarket prediction market data.
Enhanced with:
- Watchlist + Alerts
- Resolution Calendar
- Momentum Scanner
- Category Digests
- Paper Trading Portfolio
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
from urllib.parse import urlparse
import requests
BASE_URL = "https://gamma-api.polymarket.com"
DATA_DIR = Path.home() / ".polymarket"
def ensure_data_dir():
"""Ensure data directory exists."""
DATA_DIR.mkdir(parents=True, exist_ok=True)
def load_json(filename: str, default=None):
"""Load JSON file from data dir."""
path = DATA_DIR / filename
if path.exists():
try:
return json.loads(path.read_text())
except:
pass
return default if default is not None else {}
def save_json(filename: str, data):
"""Save JSON file to data dir."""
ensure_data_dir()
path = DATA_DIR / filename
path.write_text(json.dumps(data, indent=2, default=str))
def fetch(endpoint: str, params: dict = None) -> dict:
"""Fetch from Gamma API."""
url = f"{BASE_URL}{endpoint}"
resp = requests.get(url, params=params, timeout=30)
resp.raise_for_status()
return resp.json()
def format_price(price) -> str:
"""Format price as percentage."""
if price is None:
return "N/A"
try:
pct = float(price) * 100
return f"{pct:.1f}%"
except:
return str(price)
def format_volume(volume) -> str:
"""Format volume in human readable form."""
if volume is None:
return "N/A"
try:
v = float(volume)
if v >= 1_000_000:
return f"${v/1_000_000:.1f}M"
elif v >= 1_000:
return f"${v/1_000:.1f}K"
else:
return f"${v:.0f}"
except:
return str(volume)
def format_change(change) -> str:
"""Format price change with arrow."""
if change is None:
return ""
try:
c = float(change) * 100
if c > 0:
return f"{c:.1f}%"
elif c < 0:
return f"{abs(c):.1f}%"
else:
return "→0%"
except:
return ""
def format_time_remaining(end_date: str) -> str:
"""Format time remaining until end date."""
if not end_date:
return ""
try:
dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
now = datetime.now(timezone.utc)
delta = dt - now
if delta.days < 0:
return "Ended"
elif delta.days == 0:
hours = delta.seconds // 3600
if hours == 0:
mins = delta.seconds // 60
return f"Ends in {mins}m"
return f"Ends in {hours}h"
elif delta.days == 1:
return "Ends tomorrow"
elif delta.days < 7:
return f"Ends in {delta.days}d"
elif delta.days < 30:
weeks = delta.days // 7
return f"Ends in {weeks}w"
else:
return dt.strftime('%b %d, %Y')
except:
return ""
def extract_slug_from_url(url_or_slug: str) -> str:
"""Extract slug from Polymarket URL or return as-is if already a slug."""
if 'polymarket.com' in url_or_slug:
parsed = urlparse(url_or_slug)
path = parsed.path.strip('/')
if path.startswith('event/'):
return path.replace('event/', '')
return path
return url_or_slug
def get_market_price(market: dict) -> float:
"""Get current Yes price from market."""
prices = market.get('outcomePrices')
if prices:
if isinstance(prices, str):
try:
prices = json.loads(prices)
except:
return 0
if prices and len(prices) >= 1:
try:
return float(prices[0])
except:
pass
return 0
def format_market(market: dict, verbose: bool = False) -> str:
"""Format a single market for display."""
lines = []
question = market.get('question') or market.get('title', 'Unknown')
lines.append(f"📊 **{question}**")
prices = market.get('outcomePrices')
if prices:
if isinstance(prices, str):
try:
prices = json.loads(prices)
except:
prices = None
if prices and len(prices) >= 2:
yes_price = format_price(prices[0])
no_price = format_price(prices[1])
day_change = format_change(market.get('oneDayPriceChange'))
change_str = f" ({day_change})" if day_change else ""
lines.append(f" Yes: {yes_price}{change_str} | No: {no_price}")
bid = market.get('bestBid')
ask = market.get('bestAsk')
if bid is not None and ask is not None:
spread = float(ask) - float(bid)
if spread > 0:
lines.append(f" Spread: {spread*100:.1f}% (Bid: {format_price(bid)} / Ask: {format_price(ask)})")
volume = market.get('volume') or market.get('volumeNum')
if volume:
vol_str = f" Volume: {format_volume(volume)}"
vol_24h = market.get('volume24hr')
if vol_24h and float(vol_24h) > 0:
vol_str += f" (24h: {format_volume(vol_24h)})"
lines.append(vol_str)
end_date = market.get('endDate') or market.get('endDateIso')
time_left = format_time_remaining(end_date)
if time_left:
lines.append(f"{time_left}")
if verbose:
week_change = format_change(market.get('oneWeekPriceChange'))
month_change = format_change(market.get('oneMonthPriceChange'))
if week_change or month_change:
lines.append(f" 📈 1w: {week_change or 'N/A'} | 1m: {month_change or 'N/A'}")
liquidity = market.get('liquidityNum') or market.get('liquidity')
if liquidity:
lines.append(f" 💧 Liquidity: {format_volume(liquidity)}")
slug = market.get('slug') or market.get('market_slug')
if slug:
lines.append(f" 🔗 polymarket.com/event/{slug}")
return '\n'.join(lines)
def format_event(event: dict, show_all_markets: bool = False) -> str:
"""Format an event with its markets."""
lines = []
title = event.get('title', 'Unknown Event')
lines.append(f"🎯 **{title}**")
volume = event.get('volume')
if volume:
vol_str = f" Volume: {format_volume(volume)}"
vol_24h = event.get('volume24hr')
if vol_24h and float(vol_24h) > 0:
vol_str += f" (24h: {format_volume(vol_24h)})"
lines.append(vol_str)
end_date = event.get('endDate')
time_left = format_time_remaining(end_date)
if time_left:
lines.append(f"{time_left}")
markets = event.get('markets', [])
if markets:
market_prices = []
for m in markets:
yes_price = get_market_price(m)
if not m.get('active', True) and m.get('volumeNum', 0) == 0:
continue
market_prices.append((m, yes_price))
market_prices.sort(key=lambda x: x[1], reverse=True)
lines.append(f" Markets: {len(market_prices)}")
display_count = len(market_prices) if show_all_markets else min(10, len(market_prices))
for m, price in market_prices[:display_count]:
name = m.get('groupItemTitle') or m.get('question', '')[:40]
vol = m.get('volumeNum', 0)
day_change = format_change(m.get('oneDayPriceChange'))
change_str = f" {day_change}" if day_change else ""
if price > 0:
lines.append(f"{name}: {format_price(price)}{change_str} ({format_volume(vol)})")
else:
lines.append(f"{name}")
if len(market_prices) > display_count:
lines.append(f" ... and {len(market_prices) - display_count} more")
slug = event.get('slug')
if slug:
lines.append(f" 🔗 polymarket.com/event/{slug}")
return '\n'.join(lines)
# ==================== ORIGINAL COMMANDS ====================
def cmd_trending(args):
"""Get trending/active markets."""
params = {
'order': 'volume24hr',
'ascending': 'false',
'closed': 'false',
'limit': args.limit
}
data = fetch('/events', params)
print(f"🔥 **Trending on Polymarket**\n")
for event in data:
print(format_event(event))
print()
def cmd_featured(args):
"""Get featured markets."""
params = {
'closed': 'false',
'featured': 'true',
'limit': args.limit
}
data = fetch('/events', params)
print(f"⭐ **Featured Markets**\n")
if not data:
params = {
'order': 'volume',
'ascending': 'false',
'closed': 'false',
'limit': args.limit
}
data = fetch('/events', params)
print("(Showing highest volume markets)\n")
for event in data:
print(format_event(event))
print()
def expand_query(query: str) -> list:
"""Expand query with synonyms and variations."""
query = query.lower().strip()
expansions = set([query])
words = query.split()
# Synonym mappings
synonyms = {
'championship': ['champion', 'winner', 'tournament', 'title', 'finals'],
'trade': ['traded', 'next team', 'destination', 'move'],
'win': ['winner', 'won', 'wins', 'winning'],
'election': ['president', 'presidential', 'vote'],
'fed': ['federal reserve', 'interest rate', 'fomc'],
'bitcoin': ['btc', 'crypto'],
'btc': ['bitcoin', 'crypto'],
'ethereum': ['eth', 'crypto'],
'eth': ['ethereum', 'crypto'],
}
sport_leagues = {
'nba': ['basketball'], 'nfl': ['football'], 'mlb': ['baseball'],
'nhl': ['hockey'], 'ncaa': ['college', 'tournament'],
}
for key, values in synonyms.items():
if key in query:
for v in values:
expansions.add(query.replace(key, v))
expansions.add(v)
for league, sports in sport_leagues.items():
if league in query:
for s in sports:
expansions.add(query.replace(league, s))
if len(words) >= 2:
for word in words:
if len(word) >= 3:
expansions.add(word)
expansions.add(query.replace(' ', '-'))
return list(expansions)
def cmd_search(args):
"""Search markets with fuzzy matching."""
query = args.query.lower()
queries = expand_query(query)
slug_guess = query.replace(' ', '-')
try:
data = fetch('/events', {'slug': slug_guess, 'closed': 'false'})
if data:
print(f"🔍 **Found: '{args.query}'**\n")
for event in data[:args.limit]:
print(format_event(event, show_all_markets=args.all))
print()
return
except:
pass
try:
data = fetch('/events', {'closed': 'false', 'limit': 500})
matches = []
for event in data:
slug = event.get('slug', '').lower()
title = event.get('title', '').lower()
desc = event.get('description', '').lower()
found = False
for q in queries:
if q in slug or q in title or q in desc:
matches.append(event)
found = True
break
if found:
continue
for m in event.get('markets', []):
mq = m.get('question', '').lower()
item = m.get('groupItemTitle', '').lower()
for q in queries:
if q in mq or q in item:
matches.append(event)
found = True
break
if found:
break
print(f"🔍 **Search: '{args.query}'**\n")
if not matches:
print("No markets found.")
return
for event in matches[:args.limit]:
print(format_event(event, show_all_markets=args.all))
print()
except Exception as e:
print(f"Search error: {e}")
def cmd_event(args):
"""Get specific event by slug or URL."""
slug = extract_slug_from_url(args.slug)
try:
data = fetch('/events', {'slug': slug})
if not data:
all_events = fetch('/events', {'closed': 'false', 'limit': 200})
slug_lower = slug.lower()
matches = [e for e in all_events if slug_lower in e.get('slug', '').lower()]
if matches:
data = matches
else:
print(f"❌ Event not found: {slug}")
return
event = data[0] if isinstance(data, list) and data else data
print(format_event(event, show_all_markets=True))
except requests.HTTPError as e:
if e.response.status_code == 404:
print(f"❌ Event not found: {slug}")
else:
raise
def cmd_market(args):
"""Get specific market outcome within an event."""
slug = extract_slug_from_url(args.slug)
outcome = args.outcome.lower() if args.outcome else None
try:
data = fetch('/events', {'slug': slug})
if not data:
print(f"❌ Event not found: {slug}")
return
event = data[0] if isinstance(data, list) else data
markets = event.get('markets', [])
if not outcome:
print(f"🎯 **{event.get('title')}**\n")
for m in markets:
print(format_market(m, verbose=True))
print()
return
for m in markets:
name = m.get('groupItemTitle', '').lower()
question = m.get('question', '').lower()
if outcome in name or outcome in question:
print(format_market(m, verbose=True))
return
print(f"❌ Outcome '{args.outcome}' not found")
print(f"\nAvailable outcomes:")
for m in markets[:15]:
name = m.get('groupItemTitle') or m.get('question', '')[:40]
print(f"{name}")
except requests.HTTPError as e:
if e.response.status_code == 404:
print(f"❌ Event not found: {slug}")
else:
raise
def cmd_category(args):
"""Get markets by category."""
categories = {
'politics': ['politics', 'election', 'trump', 'biden', 'congress'],
'crypto': ['crypto', 'bitcoin', 'ethereum', 'btc', 'eth'],
'sports': ['sports', 'nba', 'nfl', 'mlb', 'soccer'],
'tech': ['tech', 'ai', 'apple', 'google', 'microsoft'],
'entertainment': ['entertainment', 'movie', 'oscar', 'grammy'],
'science': ['science', 'space', 'nasa', 'climate'],
'business': ['business', 'fed', 'interest', 'stock', 'market']
}
tags = categories.get(args.category.lower(), [args.category.lower()])
data = fetch('/events', {
'closed': 'false',
'limit': 100,
'order': 'volume24hr',
'ascending': 'false'
})
matches = []
for event in data:
title = event.get('title', '').lower()
event_tags = [t.get('label', '').lower() for t in event.get('tags', [])]
for tag in tags:
if tag in title or tag in ' '.join(event_tags):
matches.append(event)
break
print(f"📁 **Category: {args.category.title()}**\n")
if not matches:
print(f"No markets found for '{args.category}'")
return
for event in matches[:args.limit]:
print(format_event(event))
print()
# ==================== NEW: WATCHLIST ====================
def cmd_watch(args):
"""Add/remove markets from watchlist."""
watchlist = load_json('watchlist.json', {'markets': []})
if args.action == 'add':
slug = extract_slug_from_url(args.slug)
# Fetch current price
try:
data = fetch('/events', {'slug': slug})
if not data:
print(f"❌ Event not found: {slug}")
return
event = data[0] if isinstance(data, list) else data
except:
print(f"❌ Could not fetch event: {slug}")
return
# Get price from first market or specified outcome
price = 0
market_name = event.get('title', slug)
markets = event.get('markets', [])
if args.outcome and markets:
for m in markets:
name = m.get('groupItemTitle', '').lower()
if args.outcome.lower() in name:
price = get_market_price(m)
market_name = m.get('groupItemTitle', market_name)
break
elif markets:
price = get_market_price(markets[0])
if len(markets) == 1:
market_name = markets[0].get('question', market_name)
entry = {
'slug': slug,
'outcome': args.outcome,
'name': market_name,
'added_at': datetime.now(timezone.utc).isoformat(),
'added_price': price,
'alert_at': args.alert_at / 100 if args.alert_at else None,
'alert_change': args.alert_change / 100 if args.alert_change else None,
}
# Check if already watching
existing = [w for w in watchlist['markets'] if w['slug'] == slug and w.get('outcome') == args.outcome]
if existing:
watchlist['markets'] = [w for w in watchlist['markets'] if not (w['slug'] == slug and w.get('outcome') == args.outcome)]
watchlist['markets'].append(entry)
save_json('watchlist.json', watchlist)
alert_str = ""
if args.alert_at:
alert_str += f" (alert at {args.alert_at}%)"
if args.alert_change:
alert_str += f" (alert on {args.alert_change}% change)"
print(f"👁️ Now watching: **{market_name}**")
print(f" Current: {format_price(price)}{alert_str}")
print(f" Slug: {slug}")
elif args.action == 'remove':
slug = extract_slug_from_url(args.slug)
before = len(watchlist['markets'])
watchlist['markets'] = [w for w in watchlist['markets'] if w['slug'] != slug]
save_json('watchlist.json', watchlist)
if len(watchlist['markets']) < before:
print(f"✅ Removed {slug} from watchlist")
else:
print(f"{slug} not in watchlist")
elif args.action == 'list':
if not watchlist['markets']:
print("📋 Watchlist is empty")
print("\nAdd markets with: polymarket watch add <slug>")
return
print(f"👁️ **Watchlist** ({len(watchlist['markets'])} markets)\n")
for w in watchlist['markets']:
try:
data = fetch('/events', {'slug': w['slug']})
if data:
event = data[0] if isinstance(data, list) else data
markets = event.get('markets', [])
current_price = 0
if w.get('outcome') and markets:
for m in markets:
if w['outcome'].lower() in m.get('groupItemTitle', '').lower():
current_price = get_market_price(m)
break
elif markets:
current_price = get_market_price(markets[0])
added_price = w.get('added_price', 0)
change = current_price - added_price
change_str = f" ({format_change(change)})" if change != 0 else ""
print(f"• **{w['name']}**")
print(f" Current: {format_price(current_price)}{change_str}")
if w.get('alert_at'):
print(f" Alert at: {w['alert_at']*100:.0f}%")
if w.get('alert_change'):
print(f" Alert on: ±{w['alert_change']*100:.0f}% change")
print()
except Exception as e:
print(f"{w['name']} (error fetching: {e})")
print()
def cmd_alerts(args):
"""Check watchlist for alerts (for cron jobs)."""
watchlist = load_json('watchlist.json', {'markets': []})
if not watchlist['markets']:
if not args.quiet:
print("No markets in watchlist")
return
alerts = []
for w in watchlist['markets']:
try:
data = fetch('/events', {'slug': w['slug']})
if not data:
continue
event = data[0] if isinstance(data, list) else data
markets = event.get('markets', [])
current_price = 0
if w.get('outcome') and markets:
for m in markets:
if w['outcome'].lower() in m.get('groupItemTitle', '').lower():
current_price = get_market_price(m)
break
elif markets:
current_price = get_market_price(markets[0])
added_price = w.get('added_price', 0)
change = current_price - added_price
triggered = False
reason = ""
# Check alert_at threshold
if w.get('alert_at'):
if current_price >= w['alert_at']:
triggered = True
reason = f"reached {format_price(current_price)} (threshold: {w['alert_at']*100:.0f}%)"
# Check alert_change threshold
if w.get('alert_change') and added_price > 0:
pct_change = abs(change) / added_price
if pct_change >= w['alert_change']:
triggered = True
direction = "up" if change > 0 else "down"
reason = f"moved {direction} {format_change(change)} (threshold: ±{w['alert_change']*100:.0f}%)"
if triggered:
alerts.append({
'name': w['name'],
'slug': w['slug'],
'price': current_price,
'reason': reason,
})
except Exception as e:
continue
if alerts:
print(f"🚨 **Polymarket Alerts** ({len(alerts)})\n")
for a in alerts:
print(f"• **{a['name']}**")
print(f" {a['reason']}")
print(f" 🔗 polymarket.com/event/{a['slug']}")
print()
elif not args.quiet:
print("✅ No alerts triggered")
# ==================== NEW: CALENDAR ====================
def cmd_calendar(args):
"""Show markets resolving soon."""
days = args.days
data = fetch('/events', {
'closed': 'false',
'limit': 200,
'order': 'endDate',
'ascending': 'true'
})
now = datetime.now(timezone.utc)
cutoff = now + timedelta(days=days)
upcoming = []
for event in data:
end_date = event.get('endDate')
if not end_date:
continue
try:
dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
if now <= dt <= cutoff:
upcoming.append((dt, event))
except:
continue
upcoming.sort(key=lambda x: x[0])
print(f"📅 **Resolving in {days} days** ({len(upcoming)} markets)\n")
if not upcoming:
print("No markets resolving in this timeframe.")
return
current_date = None
for dt, event in upcoming[:args.limit]:
date_str = dt.strftime('%a %b %d')
if date_str != current_date:
current_date = date_str
print(f"\n**{date_str}**")
title = event.get('title', 'Unknown')[:60]
vol = format_volume(event.get('volume', 0))
time_str = dt.strftime('%I:%M %p')
# Get lead outcome
markets = event.get('markets', [])
lead = ""
if markets:
sorted_markets = sorted(markets, key=lambda m: get_market_price(m), reverse=True)
if sorted_markets:
top = sorted_markets[0]
top_name = top.get('groupItemTitle', 'Yes')[:20]
top_price = get_market_price(top)
lead = f"{top_name} {format_price(top_price)}"
print(f" {time_str} | {title}{lead} ({vol})")
# ==================== NEW: MOVERS ====================
def cmd_movers(args):
"""Find biggest price movers."""
timeframe = args.timeframe
min_volume = args.min_volume * 1000 if args.min_volume else 10000
data = fetch('/events', {
'closed': 'false',
'limit': 300,
})
movers = []
for event in data:
vol = float(event.get('volume24hr', 0) or 0)
if vol < min_volume:
continue
markets = event.get('markets', [])
for m in markets:
if timeframe == '24h':
change = m.get('oneDayPriceChange')
elif timeframe == '1w':
change = m.get('oneWeekPriceChange')
elif timeframe == '1m':
change = m.get('oneMonthPriceChange')
else:
change = m.get('oneDayPriceChange')
if change is None:
continue
try:
change_val = abs(float(change))
except:
continue
if change_val > 0.01: # At least 1% move
movers.append({
'event': event.get('title', ''),
'market': m.get('groupItemTitle') or m.get('question', ''),
'change': float(change),
'price': get_market_price(m),
'volume': vol,
'slug': event.get('slug', ''),
})
# Sort by absolute change
movers.sort(key=lambda x: abs(x['change']), reverse=True)
print(f"📈 **Biggest Movers ({timeframe})**\n")
if not movers:
print("No significant movers found.")
return
for m in movers[:args.limit]:
direction = "🟢" if m['change'] > 0 else "🔴"
change_pct = m['change'] * 100
name = m['market'] or m['event']
if len(name) > 50:
name = name[:47] + "..."
print(f"{direction} **{name}**")
print(f" {change_pct:+.1f}% → Now {format_price(m['price'])} (Vol: {format_volume(m['volume'])})")
print()
# ==================== NEW: DIGEST ====================
def cmd_digest(args):
"""Category digest with summary."""
category = args.category.lower()
categories = {
'politics': ['politics', 'election', 'trump', 'biden', 'congress', 'senate'],
'crypto': ['crypto', 'bitcoin', 'ethereum', 'btc', 'eth', 'solana'],
'sports': ['sports', 'nba', 'nfl', 'mlb', 'soccer', 'ufc', 'ncaa'],
'tech': ['tech', 'ai', 'apple', 'google', 'microsoft', 'openai'],
'business': ['business', 'fed', 'interest', 'stock', 'economy', 'recession'],
}
tags = categories.get(category, [category])
data = fetch('/events', {
'closed': 'false',
'limit': 200,
'order': 'volume24hr',
'ascending': 'false'
})
matches = []
for event in data:
title = event.get('title', '').lower()
desc = event.get('description', '').lower()
for tag in tags:
if tag in title or tag in desc:
matches.append(event)
break
if not matches:
print(f"No markets found for '{category}'")
return
# Calculate stats
total_volume = sum(float(e.get('volume', 0) or 0) for e in matches)
total_24h = sum(float(e.get('volume24hr', 0) or 0) for e in matches)
# Find biggest movers in category
movers = []
for event in matches:
for m in event.get('markets', []):
change = m.get('oneDayPriceChange')
if change:
try:
movers.append({
'name': m.get('groupItemTitle') or event.get('title', ''),
'change': float(change),
'price': get_market_price(m),
})
except:
pass
movers.sort(key=lambda x: abs(x['change']), reverse=True)
# Find upcoming resolutions
now = datetime.now(timezone.utc)
week_out = now + timedelta(days=7)
upcoming = []
for event in matches:
end = event.get('endDate')
if end:
try:
dt = datetime.fromisoformat(end.replace('Z', '+00:00'))
if now <= dt <= week_out:
upcoming.append((dt, event))
except:
pass
upcoming.sort(key=lambda x: x[0])
# Print digest
print(f"📊 **{category.title()} Digest**\n")
print(f"Markets: {len(matches)} | Volume: {format_volume(total_volume)} | 24h: {format_volume(total_24h)}")
print()
if movers:
print("**🔥 Biggest Movers (24h)**")
for m in movers[:5]:
direction = "" if m['change'] > 0 else ""
print(f" {direction} {m['name'][:40]}: {m['change']*100:+.1f}%")
print()
if upcoming:
print("**⏰ Resolving This Week**")
for dt, event in upcoming[:5]:
print(f" {dt.strftime('%a %b %d')}: {event.get('title', '')[:40]}")
print()
print("**📈 Top by Volume**")
for event in matches[:5]:
print(format_event(event))
print()
# ==================== NEW: PORTFOLIO ====================
def cmd_portfolio(args):
"""Show paper trading portfolio."""
portfolio = load_json('portfolio.json', {'positions': [], 'history': [], 'cash': 10000})
if not portfolio['positions']:
print("📈 **Paper Portfolio**\n")
print(f"Cash: ${portfolio['cash']:,.2f}")
print("\nNo positions. Start with:")
print(" polymarket buy <slug> <amount>")
return
print("📈 **Paper Portfolio**\n")
total_value = portfolio['cash']
total_cost = 0
for pos in portfolio['positions']:
try:
data = fetch('/events', {'slug': pos['slug']})
if data:
event = data[0] if isinstance(data, list) else data
markets = event.get('markets', [])
current_price = 0
if pos.get('outcome') and markets:
for m in markets:
if pos['outcome'].lower() in m.get('groupItemTitle', '').lower():
current_price = get_market_price(m)
break
elif markets:
current_price = get_market_price(markets[0])
shares = pos['shares']
cost_basis = pos['cost_basis']
current_value = shares * current_price
pnl = current_value - cost_basis
pnl_pct = (pnl / cost_basis * 100) if cost_basis > 0 else 0
total_value += current_value
total_cost += cost_basis
direction = "🟢" if pnl >= 0 else "🔴"
print(f"{direction} **{pos['name'][:40]}**")
print(f" {shares:.0f} shares @ {format_price(pos['entry_price'])}{format_price(current_price)}")
print(f" Value: ${current_value:,.2f} | P&L: ${pnl:+,.2f} ({pnl_pct:+.1f}%)")
print()
except Exception as e:
print(f"{pos['name']} (error: {e})")
print()
total_pnl = total_value - 10000 # Starting cash
print(f"**Summary**")
print(f"Cash: ${portfolio['cash']:,.2f}")
print(f"Positions: ${total_value - portfolio['cash']:,.2f}")
print(f"Total: ${total_value:,.2f} (P&L: ${total_pnl:+,.2f})")
def cmd_buy(args):
"""Paper buy a position."""
portfolio = load_json('portfolio.json', {'positions': [], 'history': [], 'cash': 10000})
slug = extract_slug_from_url(args.slug)
amount = args.amount
if amount > portfolio['cash']:
print(f"❌ Insufficient cash. Have: ${portfolio['cash']:,.2f}, Need: ${amount:,.2f}")
return
try:
data = fetch('/events', {'slug': slug})
if not data:
print(f"❌ Event not found: {slug}")
return
event = data[0] if isinstance(data, list) else data
markets = event.get('markets', [])
price = 0
market_name = event.get('title', slug)
outcome = args.outcome
if outcome and markets:
for m in markets:
name = m.get('groupItemTitle', '').lower()
if outcome.lower() in name:
price = get_market_price(m)
market_name = m.get('groupItemTitle', market_name)
break
if price == 0:
print(f"❌ Outcome '{outcome}' not found")
return
elif markets:
price = get_market_price(markets[0])
if len(markets) == 1:
market_name = markets[0].get('question', market_name)
if price <= 0:
print("❌ Could not get price")
return
shares = amount / price
# Check if already have position
existing = None
for p in portfolio['positions']:
if p['slug'] == slug and p.get('outcome') == outcome:
existing = p
break
if existing:
# Average in
total_shares = existing['shares'] + shares
total_cost = existing['cost_basis'] + amount
existing['shares'] = total_shares
existing['cost_basis'] = total_cost
existing['entry_price'] = total_cost / total_shares
else:
portfolio['positions'].append({
'slug': slug,
'outcome': outcome,
'name': market_name,
'shares': shares,
'entry_price': price,
'cost_basis': amount,
'bought_at': datetime.now(timezone.utc).isoformat(),
})
portfolio['cash'] -= amount
portfolio['history'].append({
'action': 'buy',
'slug': slug,
'outcome': outcome,
'shares': shares,
'price': price,
'amount': amount,
'at': datetime.now(timezone.utc).isoformat(),
})
save_json('portfolio.json', portfolio)
print(f"✅ Bought {shares:.1f} shares of **{market_name}**")
print(f" Price: {format_price(price)} | Cost: ${amount:,.2f}")
print(f" Cash remaining: ${portfolio['cash']:,.2f}")
except Exception as e:
print(f"❌ Error: {e}")
def cmd_sell(args):
"""Paper sell a position."""
portfolio = load_json('portfolio.json', {'positions': [], 'history': [], 'cash': 10000})
slug = extract_slug_from_url(args.slug)
# Find position
pos = None
for p in portfolio['positions']:
if p['slug'] == slug:
pos = p
break
if not pos:
print(f"❌ No position in {slug}")
return
try:
data = fetch('/events', {'slug': slug})
if not data:
print(f"❌ Event not found: {slug}")
return
event = data[0] if isinstance(data, list) else data
markets = event.get('markets', [])
price = 0
if pos.get('outcome') and markets:
for m in markets:
if pos['outcome'].lower() in m.get('groupItemTitle', '').lower():
price = get_market_price(m)
break
elif markets:
price = get_market_price(markets[0])
if price <= 0:
print("❌ Could not get price")
return
shares = pos['shares']
proceeds = shares * price
pnl = proceeds - pos['cost_basis']
portfolio['cash'] += proceeds
portfolio['positions'] = [p for p in portfolio['positions'] if p['slug'] != slug]
portfolio['history'].append({
'action': 'sell',
'slug': slug,
'shares': shares,
'price': price,
'proceeds': proceeds,
'pnl': pnl,
'at': datetime.now(timezone.utc).isoformat(),
})
save_json('portfolio.json', portfolio)
direction = "🟢" if pnl >= 0 else "🔴"
print(f"{direction} Sold {shares:.1f} shares of **{pos['name']}**")
print(f" Price: {format_price(price)} | Proceeds: ${proceeds:,.2f}")
print(f" P&L: ${pnl:+,.2f}")
print(f" Cash: ${portfolio['cash']:,.2f}")
except Exception as e:
print(f"❌ Error: {e}")
# ==================== MAIN ====================
def main():
parser = argparse.ArgumentParser(description="Polymarket prediction markets")
parser.add_argument("--limit", "-l", type=int, default=5, help="Number of results")
parser.add_argument("--json", "-j", action="store_true", help="Output raw JSON")
parser.add_argument("--all", "-a", action="store_true", help="Show all markets in event")
subparsers = parser.add_subparsers(dest="command", required=True)
# Original commands
subparsers.add_parser("trending", help="Get trending markets")
subparsers.add_parser("featured", help="Get featured markets")
search_parser = subparsers.add_parser("search", help="Search markets")
search_parser.add_argument("query", help="Search query")
search_parser.add_argument("--all", "-a", action="store_true", help="Show all outcomes")
event_parser = subparsers.add_parser("event", help="Get event by slug or URL")
event_parser.add_argument("slug", help="Event slug or polymarket.com URL")
market_parser = subparsers.add_parser("market", help="Get specific market outcome")
market_parser.add_argument("slug", help="Event slug or URL")
market_parser.add_argument("outcome", nargs="?", help="Outcome name")
cat_parser = subparsers.add_parser("category", help="Markets by category")
cat_parser.add_argument("category", help="Category: politics, crypto, sports, tech, etc.")
# NEW: Watch commands
watch_parser = subparsers.add_parser("watch", help="Manage watchlist")
watch_parser.add_argument("action", choices=['add', 'remove', 'list'], help="Action")
watch_parser.add_argument("slug", nargs="?", help="Event slug")
watch_parser.add_argument("--outcome", "-o", help="Specific outcome to watch")
watch_parser.add_argument("--alert-at", type=float, help="Alert when price reaches X%")
watch_parser.add_argument("--alert-change", type=float, help="Alert on X% change from entry")
# NEW: Alerts (for cron)
alerts_parser = subparsers.add_parser("alerts", help="Check watchlist for alerts")
alerts_parser.add_argument("--quiet", "-q", action="store_true", help="Only output if alerts triggered")
# NEW: Calendar
calendar_parser = subparsers.add_parser("calendar", help="Markets resolving soon")
calendar_parser.add_argument("--days", "-d", type=int, default=7, help="Days to look ahead")
# NEW: Movers
movers_parser = subparsers.add_parser("movers", help="Biggest price movers")
movers_parser.add_argument("--timeframe", "-t", default="24h", choices=["24h", "1w", "1m"], help="Timeframe")
movers_parser.add_argument("--min-volume", type=float, default=10, help="Min 24h volume in $K")
# NEW: Digest
digest_parser = subparsers.add_parser("digest", help="Category digest summary")
digest_parser.add_argument("category", help="Category: politics, crypto, sports, tech, business")
# NEW: Portfolio
subparsers.add_parser("portfolio", help="Show paper portfolio")
# NEW: Buy
buy_parser = subparsers.add_parser("buy", help="Paper buy position")
buy_parser.add_argument("slug", help="Event slug")
buy_parser.add_argument("amount", type=float, help="Amount in dollars")
buy_parser.add_argument("--outcome", "-o", help="Specific outcome")
# NEW: Sell
sell_parser = subparsers.add_parser("sell", help="Paper sell position")
sell_parser.add_argument("slug", help="Event slug")
args = parser.parse_args()
commands = {
"trending": cmd_trending,
"featured": cmd_featured,
"search": cmd_search,
"event": cmd_event,
"market": cmd_market,
"category": cmd_category,
"watch": cmd_watch,
"alerts": cmd_alerts,
"calendar": cmd_calendar,
"movers": cmd_movers,
"digest": cmd_digest,
"portfolio": cmd_portfolio,
"buy": cmd_buy,
"sell": cmd_sell,
}
try:
commands[args.command](args)
except requests.RequestException as e:
print(f"❌ API Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()