Add 6 Polymarket trading skills with paper trading engine

Composable Agent Skills (SKILL.md format) for Polymarket prediction market
trading. Includes scanner, analyzer, monitor, paper trader, strategy advisor,
and live executor. All tested against live Polymarket APIs. Security audited
with all HIGH/MEDIUM findings resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Polymarket Skills Builder
2026-02-26 07:25:07 +00:00
co-authored by Claude Opus 4.6
parent 6a03bbe2e5
commit 068b2adc75
35 changed files with 7842 additions and 1 deletions
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""Analyze a Polymarket order book for a given token ID.
Calculates spread, depth, bid-ask imbalance, and classifies book thickness.
Requires: py-clob-client (pip install py-clob-client)
"""
import argparse
import json
import sys
from py_clob_client.client import ClobClient
CLOB_HOST = "https://clob.polymarket.com"
def fetch_orderbook(token_id: str) -> object:
"""Fetch order book from CLOB API."""
client = ClobClient(CLOB_HOST)
return client.get_order_book(token_id)
def analyze(book, depth: int = 5) -> dict:
"""Analyze an order book and return metrics."""
bids = [(float(b.price), float(b.size)) for b in (book.bids or [])]
asks = [(float(a.price), float(a.size)) for a in (book.asks or [])]
# Sort: bids descending by price, asks ascending by price
bids.sort(key=lambda x: x[0], reverse=True)
asks.sort(key=lambda x: x[0])
result = {
"token_id": book.asset_id,
"total_bid_levels": len(bids),
"total_ask_levels": len(asks),
}
if not bids and not asks:
result["status"] = "EMPTY_BOOK"
return result
# Best bid / best ask
best_bid = bids[0][0] if bids else 0.0
best_ask = asks[0][0] if asks else 1.0
spread = best_ask - best_bid
mid_price = (best_bid + best_ask) / 2.0 if (bids and asks) else None
result["best_bid"] = best_bid
result["best_ask"] = best_ask
result["spread"] = round(spread, 6)
result["spread_pct"] = round((spread / mid_price * 100) if mid_price else 0, 4)
result["mid_price"] = round(mid_price, 6) if mid_price else None
# Depth at top N levels
top_bids = bids[:depth]
top_asks = asks[:depth]
bid_depth = sum(size for _, size in top_bids)
ask_depth = sum(size for _, size in top_asks)
total_depth = bid_depth + ask_depth
result["bid_depth"] = round(bid_depth, 2)
result["ask_depth"] = round(ask_depth, 2)
result["total_depth"] = round(total_depth, 2)
result["depth_levels_used"] = depth
# Bid-ask imbalance ratio: positive = more bids (buying pressure)
if total_depth > 0:
imbalance = (bid_depth - ask_depth) / total_depth
else:
imbalance = 0.0
result["imbalance_ratio"] = round(imbalance, 4)
# Classify the imbalance
if imbalance > 0.3:
result["imbalance_signal"] = "STRONG_BUY_PRESSURE"
elif imbalance > 0.1:
result["imbalance_signal"] = "MODERATE_BUY_PRESSURE"
elif imbalance < -0.3:
result["imbalance_signal"] = "STRONG_SELL_PRESSURE"
elif imbalance < -0.1:
result["imbalance_signal"] = "MODERATE_SELL_PRESSURE"
else:
result["imbalance_signal"] = "BALANCED"
# Book thickness classification
if total_depth < 500:
result["book_class"] = "THIN"
result["book_note"] = "Easy to move price; high slippage risk"
elif total_depth < 5000:
result["book_class"] = "MODERATE"
result["book_note"] = "Normal depth; moderate slippage on large orders"
else:
result["book_class"] = "THICK"
result["book_note"] = "Stable book; low slippage for most order sizes"
# Bid levels detail
result["bid_levels"] = [
{"price": p, "size": round(s, 2), "cumulative": round(sum(sz for _, sz in top_bids[:i+1]), 2)}
for i, (p, s) in enumerate(top_bids)
]
result["ask_levels"] = [
{"price": p, "size": round(s, 2), "cumulative": round(sum(sz for _, sz in top_asks[:i+1]), 2)}
for i, (p, s) in enumerate(top_asks)
]
# Slippage estimate: cost to buy/sell $100 worth
slippage_size = 100.0
result["buy_slippage"] = _estimate_slippage(asks, slippage_size)
result["sell_slippage"] = _estimate_slippage(
[(p, s) for p, s in bids], slippage_size, selling=True
)
return result
def _estimate_slippage(
levels: list[tuple[float, float]], target_size: float, selling: bool = False
) -> dict | None:
"""Estimate average fill price and slippage for a target size."""
if not levels:
return None
filled = 0.0
cost = 0.0
for price, size in levels:
remaining = target_size - filled
fill_qty = min(size, remaining)
cost += fill_qty * price
filled += fill_qty
if filled >= target_size:
break
if filled == 0:
return None
avg_price = cost / filled
best_price = levels[0][0]
slippage = abs(avg_price - best_price)
return {
"target_size": target_size,
"filled": round(filled, 2),
"avg_price": round(avg_price, 6),
"best_price": best_price,
"slippage": round(slippage, 6),
"slippage_pct": round(slippage / best_price * 100 if best_price else 0, 4),
"fully_filled": filled >= target_size,
}
def format_output(result: dict) -> str:
"""Format analysis result for display."""
lines = []
lines.append(f"Order Book Analysis for {result['token_id'][:30]}...")
lines.append("=" * 70)
if result.get("status") == "EMPTY_BOOK":
lines.append("Order book is empty -- no bids or asks.")
return "\n".join(lines)
lines.append(f" Best Bid: ${result['best_bid']:.4f}")
lines.append(f" Best Ask: ${result['best_ask']:.4f}")
lines.append(f" Mid Price: ${result['mid_price']:.4f}" if result['mid_price'] else " Mid Price: N/A")
lines.append(f" Spread: ${result['spread']:.4f} ({result['spread_pct']:.2f}%)")
lines.append("")
lines.append(f"Depth (top {result['depth_levels_used']} levels):")
lines.append(f" Bid Depth: {result['bid_depth']:,.2f} shares")
lines.append(f" Ask Depth: {result['ask_depth']:,.2f} shares")
lines.append(f" Total: {result['total_depth']:,.2f} shares")
lines.append(f" Imbalance: {result['imbalance_ratio']:+.4f} ({result['imbalance_signal']})")
lines.append(f" Book Class: {result['book_class']} -- {result['book_note']}")
lines.append("")
lines.append("Bid Levels:")
lines.append(f" {'Price':>8} {'Size':>10} {'Cumulative':>12}")
for lvl in result.get("bid_levels", []):
lines.append(f" ${lvl['price']:<7.4f} {lvl['size']:>10,.2f} {lvl['cumulative']:>12,.2f}")
lines.append("")
lines.append("Ask Levels:")
lines.append(f" {'Price':>8} {'Size':>10} {'Cumulative':>12}")
for lvl in result.get("ask_levels", []):
lines.append(f" ${lvl['price']:<7.4f} {lvl['size']:>10,.2f} {lvl['cumulative']:>12,.2f}")
for label, key in [("Buy", "buy_slippage"), ("Sell", "sell_slippage")]:
slip = result.get(key)
lines.append("")
if slip:
status = "YES" if slip["fully_filled"] else "PARTIAL"
lines.append(
f"{label} Slippage ({slip['target_size']:.0f} shares): "
f"avg ${slip['avg_price']:.4f}, "
f"slippage ${slip['slippage']:.4f} ({slip['slippage_pct']:.2f}%), "
f"filled: {status}"
)
else:
lines.append(f"{label} Slippage: No liquidity on this side")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze Polymarket order book"
)
parser.add_argument(
"--token-id",
required=True,
help="CLOB token ID to analyze",
)
parser.add_argument(
"--depth",
type=int,
default=5,
help="Number of price levels to analyze (default: 5)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON",
)
args = parser.parse_args()
try:
book = fetch_orderbook(args.token_id)
except Exception as e:
print(f"Error fetching order book: {e}", file=sys.stderr)
sys.exit(1)
result = analyze(book, depth=args.depth)
if args.json:
print(json.dumps(result, indent=2))
else:
print(format_output(result))
if __name__ == "__main__":
main()
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""Scan active Polymarket markets for arbitrage edges.
Detects:
- Underpriced markets: best-ask YES + best-ask NO < $1.00 (buy both for profit)
- Overpriced markets: best-bid YES + best-bid NO > $1.00 (sell both for profit)
- Wide spreads: markets where bid-ask spread creates opportunity
Uses Gamma API for market discovery and CLOB API for real order book prices.
Gamma mid-prices always sum to $1.00 by construction, so order book prices are
needed to find real executable edges.
"""
import argparse
import json
import sys
import time
import requests
from py_clob_client.client import ClobClient
GAMMA_API = "https://gamma-api.polymarket.com"
CLOB_HOST = "https://clob.polymarket.com"
def fetch_markets(limit: int = 100, offset: int = 0) -> list[dict]:
"""Fetch active markets from Gamma API."""
url = (
f"{GAMMA_API}/markets"
f"?limit={limit}&offset={offset}&active=true&closed=false"
)
resp = requests.get(url, timeout=15)
resp.raise_for_status()
return resp.json()
def parse_token_ids(market: dict) -> tuple[str, str] | None:
"""Extract YES and NO token IDs from a market dict."""
raw = market.get("clobTokenIds")
if not raw:
return None
try:
ids = json.loads(raw)
if len(ids) < 2:
return None
return ids[0], ids[1]
except (json.JSONDecodeError, ValueError, IndexError):
return None
def parse_mid_prices(market: dict) -> tuple[float, float] | None:
"""Extract mid-prices from Gamma API (for display context)."""
raw = market.get("outcomePrices")
if not raw:
return None
try:
prices = json.loads(raw)
if len(prices) < 2:
return None
return float(prices[0]), float(prices[1])
except (json.JSONDecodeError, ValueError, IndexError):
return None
def calculate_fee(price: float, base_rate: float = 0.063) -> float:
"""Calculate dynamic taker fee rate for fee-bearing markets."""
return base_rate * min(price, 1.0 - price)
def get_book_prices(client: ClobClient, token_id: str) -> tuple[float, float] | None:
"""Get best bid and best ask for a token. Returns (best_bid, best_ask) or None."""
try:
book = client.get_order_book(token_id)
except Exception:
return None
bids = [(float(b.price), float(b.size)) for b in (book.bids or [])]
asks = [(float(a.price), float(a.size)) for a in (book.asks or [])]
bids.sort(key=lambda x: x[0], reverse=True)
asks.sort(key=lambda x: x[0])
best_bid = bids[0][0] if bids else None
best_ask = asks[0][0] if asks else None
if best_bid is None or best_ask is None:
return None
return best_bid, best_ask
def scan_edges(
max_markets: int = 200,
min_edge: float = 0.005,
check_orderbooks: bool = True,
) -> list[dict]:
"""Scan markets for pricing edges.
Two modes:
1. Fast scan (check_orderbooks=False): Uses Gamma mid-prices (always sum to 1.0,
so only finds spread-based opportunities via CLOB spot check)
2. Deep scan (check_orderbooks=True): Fetches actual order book for each market
to find real executable edges (slower, rate-limited)
"""
client = ClobClient(CLOB_HOST) if check_orderbooks else None
edges = []
offset = 0
batch_size = 100
fetched = 0
checked_books = 0
while fetched < max_markets:
batch = fetch_markets(limit=batch_size, offset=offset)
if not batch:
break
for market in batch:
token_ids = parse_token_ids(market)
mid_prices = parse_mid_prices(market)
if token_ids is None or mid_prices is None:
continue
yes_token_id, no_token_id = token_ids
yes_mid, no_mid = mid_prices
# Skip very low-liquidity markets
liquidity = float(market.get("liquidityNum", 0) or 0)
if liquidity < 100:
continue
if not check_orderbooks:
continue
# Fetch real order book prices
yes_book = get_book_prices(client, yes_token_id)
no_book = get_book_prices(client, no_token_id)
checked_books += 1
if yes_book is None or no_book is None:
continue
yes_bid, yes_ask = yes_book
no_bid, no_ask = no_book
# Check underpriced: buy YES at ask + buy NO at ask < $1.00
buy_both_cost = yes_ask + no_ask
if buy_both_cost < (1.0 - min_edge):
raw_edge = 1.0 - buy_both_cost
yes_fee = calculate_fee(yes_ask)
no_fee = calculate_fee(no_ask)
total_fee = yes_fee + no_fee
net = raw_edge - total_fee
edges.append({
"question": market.get("question", "Unknown"),
"slug": market.get("slug", ""),
"type": "UNDERPRICED",
"yes_ask": yes_ask,
"no_ask": no_ask,
"cost_sum": round(buy_both_cost, 6),
"raw_edge": round(raw_edge, 6),
"fee_impact": round(total_fee, 6),
"net_profit_per_share": round(net, 6),
"profitable_after_fees": net > 0,
"yes_mid": yes_mid,
"no_mid": no_mid,
"volume_24h": market.get("volume24hr", 0) or 0,
"liquidity": liquidity,
})
# Check overpriced: sell YES at bid + sell NO at bid > $1.00
sell_both_value = yes_bid + no_bid
if sell_both_value > (1.0 + max(min_edge, 0.005)):
raw_edge = sell_both_value - 1.0
yes_fee = calculate_fee(yes_bid)
no_fee = calculate_fee(no_bid)
total_fee = yes_fee + no_fee
net = raw_edge - total_fee
edges.append({
"question": market.get("question", "Unknown"),
"slug": market.get("slug", ""),
"type": "OVERPRICED",
"yes_bid": yes_bid,
"no_bid": no_bid,
"cost_sum": round(sell_both_value, 6),
"raw_edge": round(raw_edge, 6),
"fee_impact": round(total_fee, 6),
"net_profit_per_share": round(net, 6),
"profitable_after_fees": net > 0,
"yes_mid": yes_mid,
"no_mid": no_mid,
"volume_24h": market.get("volume24hr", 0) or 0,
"liquidity": liquidity,
})
# Also report wide spreads (opportunity for market making)
yes_spread = yes_ask - yes_bid
no_spread = no_ask - no_bid
max_spread = max(yes_spread, no_spread)
if max_spread >= 0.03: # 3 cent spread or wider
edges.append({
"question": market.get("question", "Unknown"),
"slug": market.get("slug", ""),
"type": "WIDE_SPREAD",
"yes_bid": yes_bid,
"yes_ask": yes_ask,
"yes_spread": round(yes_spread, 6),
"no_bid": no_bid,
"no_ask": no_ask,
"no_spread": round(no_spread, 6),
"max_spread": round(max_spread, 6),
"raw_edge": round(max_spread, 6),
"fee_impact": 0.0,
"net_profit_per_share": round(max_spread, 6),
"profitable_after_fees": True,
"yes_mid": yes_mid,
"no_mid": no_mid,
"volume_24h": market.get("volume24hr", 0) or 0,
"liquidity": liquidity,
})
# Rate limit: avoid hammering the CLOB API
if checked_books % 5 == 0:
time.sleep(0.2)
fetched += len(batch)
offset += batch_size
if len(batch) < batch_size:
break
# Sort by raw edge descending
edges.sort(key=lambda x: x["raw_edge"], reverse=True)
return edges
def format_output(edges: list[dict]) -> str:
"""Format edges for display."""
if not edges:
return (
"No arbitrage edges found in current markets.\n"
"This is normal -- Polymarket is well-arbitraged, with most\n"
"opportunities lasting only ~2.7 seconds (median) in 2026."
)
lines = []
# Group by type
underpriced = [e for e in edges if e["type"] == "UNDERPRICED"]
overpriced = [e for e in edges if e["type"] == "OVERPRICED"]
wide_spread = [e for e in edges if e["type"] == "WIDE_SPREAD"]
if underpriced:
lines.append(f"\n=== UNDERPRICED ({len(underpriced)}) - Buy both sides for guaranteed profit ===\n")
lines.append(f" {'YES ask':>8} {'NO ask':>8} {'Sum':>8} {'Edge':>7} {'Net':>7} {'Vol24h':>10} Question")
lines.append(" " + "-" * 100)
for e in underpriced:
marker = " *" if e["profitable_after_fees"] else ""
lines.append(
f" ${e['yes_ask']:<7.4f} ${e['no_ask']:<7.4f} "
f"${e['cost_sum']:<7.4f} ${e['raw_edge']:<6.4f} "
f"${e['net_profit_per_share']:<+6.4f}{marker} "
f"${e['volume_24h']:>9,.0f} {e['question'][:55]}"
)
if overpriced:
lines.append(f"\n=== OVERPRICED ({len(overpriced)}) - Sell both sides ===\n")
lines.append(f" {'YES bid':>8} {'NO bid':>8} {'Sum':>8} {'Edge':>7} {'Net':>7} {'Vol24h':>10} Question")
lines.append(" " + "-" * 100)
for e in overpriced:
marker = " *" if e["profitable_after_fees"] else ""
lines.append(
f" ${e['yes_bid']:<7.4f} ${e['no_bid']:<7.4f} "
f"${e['cost_sum']:<7.4f} ${e['raw_edge']:<6.4f} "
f"${e['net_profit_per_share']:<+6.4f}{marker} "
f"${e['volume_24h']:>9,.0f} {e['question'][:55]}"
)
if wide_spread:
lines.append(f"\n=== WIDE SPREADS ({len(wide_spread)}) - Market-making opportunities ===\n")
lines.append(f" {'Y Spread':>8} {'N Spread':>8} {'Max':>7} {'Vol24h':>10} {'Liq':>10} Question")
lines.append(" " + "-" * 100)
for e in wide_spread:
lines.append(
f" ${e.get('yes_spread', 0):<7.4f} ${e.get('no_spread', 0):<7.4f} "
f"${e.get('max_spread', 0):<6.4f} "
f"${e['volume_24h']:>9,.0f} "
f"${e['liquidity']:>9,.0f} "
f"{e['question'][:55]}"
)
lines.append("")
lines.append("* = profitable even on fee-bearing markets (most markets are fee-free)")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Scan Polymarket for arbitrage edges using real order book data"
)
parser.add_argument(
"--min-edge",
type=float,
default=0.005,
help="Minimum edge to report (default: 0.005 = $0.005/share)",
)
parser.add_argument(
"--limit",
type=int,
default=200,
help="Maximum markets to scan (default: 200, each requires 2 API calls)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON",
)
args = parser.parse_args()
print(f"Scanning up to {args.limit} markets (2 order book lookups each)...",
file=sys.stderr)
try:
edges = scan_edges(
max_markets=args.limit,
min_edge=args.min_edge,
)
except requests.RequestException as e:
print(f"Error fetching data: {e}", file=sys.stderr)
sys.exit(1)
if args.json:
print(json.dumps(edges, indent=2))
else:
print(format_output(edges))
if __name__ == "__main__":
main()
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Scan Polymarket for momentum signals: volume surges and price trends.
Detects:
- Volume surges: 24h volume significantly exceeds 7-day daily average
- Price momentum: markets with strong directional price movement
- Liquidity anomalies: unusually high or low liquidity relative to volume
Uses Gamma API (no auth required).
"""
import argparse
import json
import sys
import requests
GAMMA_API = "https://gamma-api.polymarket.com"
def fetch_markets(limit: int = 100, offset: int = 0) -> list[dict]:
"""Fetch active markets from Gamma API."""
url = (
f"{GAMMA_API}/markets"
f"?limit={limit}&offset={offset}&active=true&closed=false"
)
resp = requests.get(url, timeout=15)
resp.raise_for_status()
return resp.json()
def compute_signals(market: dict) -> dict | None:
"""Compute momentum signals for a single market."""
vol_24h = float(market.get("volume24hr", 0) or 0)
vol_1wk = float(market.get("volume1wk", 0) or 0)
liquidity = float(market.get("liquidityNum", 0) or 0)
# Need at least some volume data
if vol_24h <= 0 and vol_1wk <= 0:
return None
# Parse prices
raw_prices = market.get("outcomePrices")
if not raw_prices:
return None
try:
prices = json.loads(raw_prices)
yes_price = float(prices[0])
except (json.JSONDecodeError, ValueError, IndexError):
return None
# Volume surge: compare 24h volume to 7-day daily average
daily_avg_7d = vol_1wk / 7.0 if vol_1wk > 0 else 0
if daily_avg_7d > 0:
volume_ratio = vol_24h / daily_avg_7d
else:
volume_ratio = 0.0
# Price extremity: how far from 0.50 (max uncertainty)
# Prices near 0 or 1 suggest strong directional conviction
price_extremity = abs(yes_price - 0.5) * 2.0 # 0 at 0.50, 1 at 0 or 1
# Volume-to-liquidity ratio: high ratio suggests heavy activity relative to depth
vol_liq_ratio = vol_24h / liquidity if liquidity > 0 else 0
# Composite momentum score
# volume_ratio contributes most -- a surge is the primary signal
score = 0.0
if volume_ratio > 1.0:
score += min((volume_ratio - 1.0) * 0.4, 2.0) # Cap contribution at 2.0
if vol_liq_ratio > 1.0:
score += min((vol_liq_ratio - 1.0) * 0.3, 1.5)
# Extreme prices amplify the signal (market is moving toward resolution)
if price_extremity > 0.6:
score += (price_extremity - 0.6) * 0.3
if score <= 0:
return None
# Classify the signal
if volume_ratio >= 3.0:
volume_signal = "VOLUME_SURGE"
elif volume_ratio >= 1.5:
volume_signal = "ELEVATED_VOLUME"
else:
volume_signal = "NORMAL_VOLUME"
if yes_price >= 0.85:
direction = "STRONG_YES"
elif yes_price >= 0.65:
direction = "LEANING_YES"
elif yes_price <= 0.15:
direction = "STRONG_NO"
elif yes_price <= 0.35:
direction = "LEANING_NO"
else:
direction = "NEUTRAL"
return {
"question": market.get("question", "Unknown"),
"slug": market.get("slug", ""),
"yes_price": yes_price,
"direction": direction,
"volume_24h": round(vol_24h, 2),
"daily_avg_7d": round(daily_avg_7d, 2),
"volume_ratio": round(volume_ratio, 2),
"volume_signal": volume_signal,
"liquidity": round(liquidity, 2),
"vol_liq_ratio": round(vol_liq_ratio, 2),
"momentum_score": round(score, 4),
}
def scan_momentum(
max_markets: int = 300,
min_volume: float = 1000.0,
min_score: float = 0.1,
) -> list[dict]:
"""Scan markets and rank by momentum score."""
signals = []
offset = 0
batch_size = 100
fetched = 0
while fetched < max_markets:
batch = fetch_markets(limit=batch_size, offset=offset)
if not batch:
break
for market in batch:
vol_24h = float(market.get("volume24hr", 0) or 0)
if vol_24h < min_volume:
continue
sig = compute_signals(market)
if sig and sig["momentum_score"] >= min_score:
signals.append(sig)
fetched += len(batch)
offset += batch_size
if len(batch) < batch_size:
break
# Rank by momentum score descending
signals.sort(key=lambda x: x["momentum_score"], reverse=True)
return signals
def format_output(signals: list[dict]) -> str:
"""Format momentum signals for display."""
if not signals:
return "No momentum signals found matching criteria."
lines = []
lines.append(f"Found {len(signals)} market(s) with momentum signals:\n")
lines.append(
f"{'Score':>6} {'YES':>5} {'Direction':<12} "
f"{'VolRatio':>8} {'Signal':<16} "
f"{'Vol24h':>12} {'Avg7d':>10} Question"
)
lines.append("-" * 120)
for s in signals:
lines.append(
f"{s['momentum_score']:>6.2f} "
f"${s['yes_price']:<4.2f} "
f"{s['direction']:<12} "
f"{s['volume_ratio']:>7.1f}x "
f"{s['volume_signal']:<16} "
f"${s['volume_24h']:>11,.0f} "
f"${s['daily_avg_7d']:>9,.0f} "
f"{s['question'][:55]}"
)
lines.append("")
lines.append("Score = composite of volume surge, vol/liquidity ratio, and price extremity.")
lines.append("Volume Ratio = 24h volume / 7-day daily average (>3x = VOLUME_SURGE).")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Scan Polymarket for momentum signals"
)
parser.add_argument(
"--min-volume",
type=float,
default=1000,
help="Minimum 24h volume to consider (default: $1,000)",
)
parser.add_argument(
"--min-score",
type=float,
default=0.1,
help="Minimum momentum score to report (default: 0.1)",
)
parser.add_argument(
"--limit",
type=int,
default=300,
help="Maximum number of markets to scan (default: 300)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON",
)
args = parser.parse_args()
try:
signals = scan_momentum(
max_markets=args.limit,
min_volume=args.min_volume,
min_score=args.min_score,
)
except requests.RequestException as e:
print(f"Error fetching data from Gamma API: {e}", file=sys.stderr)
sys.exit(1)
if args.json:
print(json.dumps(signals, indent=2))
else:
print(format_output(signals))
if __name__ == "__main__":
main()