Files
polymarket-skills/polymarket-analyzer/scripts/momentum_scanner.py
T
Polymarket Skills Builder 068b2adc75 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>
2026-02-26 07:25:07 +00:00

230 lines
6.6 KiB
Python
Executable File

#!/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()