rmi-backend/app/routers/prediction_market_router.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

294 lines
11 KiB
Python

"""
RMI Prediction Market Router
==============================
FastAPI router exposing prediction market data for crypto security intelligence.
Endpoints:
GET /api/v1/prediction-markets/search - Search all 4 sources
GET /api/v1/prediction-markets/trending - Top markets by volume
GET /api/v1/prediction-markets/token/{symbol} - Token-specific markets
GET /api/v1/prediction-markets/sentinel - Daily security digest
GET /api/v1/prediction-markets/detail/{source}/{id} - Market detail
x402 Tool Registration:
prediction_market_search - Search prediction markets (basic tier, $0.01)
prediction_market_token - Token-specific odds (standard tier, $0.03)
prediction_market_sentinel - Daily threat digest (advanced tier, $0.05)
Caching: Redis-backed, 30s-1hr TTL by endpoint.
Rate limiting: Inherited from app-wide slowapi configuration.
"""
import logging
from fastapi import APIRouter, HTTPException, Query
from app.prediction_market_service import (
PredictionDigest,
PredictionMarket,
get_prediction_market_service,
)
logger = logging.getLogger("prediction_market.router")
router = APIRouter(prefix="/api/v1/prediction-markets", tags=["prediction-markets"])
@router.get("/search")
async def search_markets(
q: str = Query(..., description="Search query (token name, event, protocol, etc.)"),
security_only: bool = Query(False, description="Only return security-relevant markets"),
min_volume: float = Query(0.0, description="Minimum USD volume to include"),
limit: int = Query(20, ge=1, le=100, description="Max results to return"),
) -> dict:
"""Search all prediction market sources for crypto security intelligence.
Queries Polymarket, Kalshi, Limitless, and Manifold in parallel.
Results show probability, volume, and auto-classified relevance.
"""
svc = get_prediction_market_service()
results = await svc.search(q, min_volume=min_volume, security_only=security_only)
return {
"query": q,
"total": len(results),
"sources_searched": ["polymarket", "kalshi", "limitless", "manifold"],
"security_relevant": sum(1 for m in results if m.is_security_relevant),
"crypto_relevant": sum(1 for m in results if m.is_crypto_relevant),
"results": [_market_response(m) for m in results[:limit]],
}
@router.get("/trending")
async def trending_markets(
category: str | None = Query(None, description="Filter: crypto, security, regulation, all"),
limit: int = Query(20, ge=1, le=100, description="Max results"),
) -> dict:
"""Get top trending prediction markets by volume across all sources."""
svc = get_prediction_market_service()
results = await svc.trending(limit=limit)
if category and category != "all":
if category == "security":
results = [m for m in results if m.is_security_relevant]
elif category == "crypto":
results = [m for m in results if m.is_crypto_relevant]
elif category == "regulation":
results = [
m
for m in results
if m.is_security_relevant
and any(kw in m.question.lower() for kw in ["sec", "cftc", "doj", "regulation", "sanction", "ban"])
]
return {
"category": category or "all",
"total": len(results),
"sources": ["polymarket", "kalshi", "limitless"],
"results": [_market_response(m) for m in results[:limit]],
}
@router.get("/token/{symbol}")
async def token_markets(
symbol: str,
limit: int = Query(20, ge=1, le=50, description="Max results"),
) -> dict:
"""Find all prediction markets mentioning a specific token symbol.
Useful for: gauging market sentiment on specific tokens,
finding rug/exploit probability markets for tokens in scanner results.
"""
svc = get_prediction_market_service()
results = await svc.token_markets(symbol)
return {
"token": symbol.upper(),
"total": len(results),
"avg_probability": (sum(m.probability_yes for m in results) / len(results) if results else 0),
"security_relevant": sum(1 for m in results if m.is_security_relevant),
"results": [_market_response(m) for m in results[:limit]],
}
@router.get("/sentinel")
async def sentinel_digest(
refresh: bool = Query(False, description="Force refresh (bypass cache)"),
) -> dict:
"""Daily intelligence digest of security-relevant prediction markets.
Categorizes markets into:
- Top threats: High-volume security-relevant markets
- Token-specific: Markets about specific tokens with security signals
- Ecosystem risk: Broad crypto risk markets
- Regulatory: SEC, CFTC, DOJ, sanction-related markets
Used by: SENTINEL scanner for cross-referencing with on-chain risk scores.
"""
svc = get_prediction_market_service()
digest: PredictionDigest = await svc.security_digest()
return {
"generated_at": digest.generated_at,
"summary": {
"total_markets": digest.total_markets_searched,
"security_relevant": digest.security_relevant_count,
"crypto_relevant": digest.crypto_relevant_count,
},
"top_threats": [_market_response(m) for m in digest.top_threats],
"token_specific": [_market_response(m) for m in digest.token_specific_markets],
"ecosystem_risk": [_market_response(m) for m in digest.ecosystem_risk_markets],
"regulatory": [_market_response(m) for m in digest.regulatory_markets],
}
@router.get("/detail/{source}/{market_id}")
async def market_detail(
source: str,
market_id: str,
) -> dict:
"""Get detailed market data including live orderbook prices.
Sources: polymarket, kalshi
"""
valid_sources = {"polymarket", "kalshi"}
if source not in valid_sources:
raise HTTPException(400, f"Invalid source. Use: {', '.join(valid_sources)}")
svc = get_prediction_market_service()
market = await svc.market_detail(source, market_id)
if not market:
raise HTTPException(404, f"Market not found: {source}/{market_id}")
return {
"market": _market_response(market),
"interpretation": _interpret_market(market),
}
# ── Sources Info ─────────────────────────────────────────────────
@router.get("/sources")
async def list_sources() -> dict:
"""List all integrated prediction market data sources."""
return {
"sources": [
{
"name": "Polymarket",
"type": "Real-money prediction market (Polygon)",
"api_count": 3,
"endpoints": [
"Gamma (search/discovery)",
"CLOB (prices/orderbooks)",
"Data (trades/OI)",
],
"auth": "None required for read-only",
"rate_limits": "4K-9K req/10s",
"url": "https://polymarket.com",
},
{
"name": "Kalshi",
"type": "Regulated prediction market (US)",
"api_count": 1,
"endpoints": ["REST API (series, markets, events, orderbooks)"],
"auth": "None for market data",
"rate_limits": "Generous",
"url": "https://kalshi.com",
},
{
"name": "Limitless Exchange",
"type": "Daily prediction market (Base L2)",
"api_count": 1,
"endpoints": ["REST API (markets, orderbooks)", "WebSocket (live updates)"],
"auth": "None for public endpoints",
"rate_limits": "Standard",
"url": "https://limitless.exchange",
},
{
"name": "Manifold Markets",
"type": "Play-money prediction market (sentiment signals)",
"api_count": 1,
"endpoints": ["REST API (search, markets, users)"],
"auth": "None for read-only",
"rate_limits": "Generous",
"url": "https://manifold.markets",
},
],
"open_source_references": [
{
"name": "homerun",
"repo": "braedonsaunders/homerun",
"description": "Open-source prediction market platform for Polymarket + Kalshi. Python strategies, backtesting, live trading.",
},
{
"name": "prediction-market-edge-bot",
"description": "SX Bet + Polymarket aggregator with smart order routing.",
},
{
"name": "Awesome-Prediction-Market-Tools",
"repo": "aarora4/Awesome-Prediction-Market-Tools",
"description": "Curated directory of 50+ tools including Oddpool, analytics, trading bots.",
},
],
}
# ── Helpers ──────────────────────────────────────────────────────
def _market_response(m: PredictionMarket) -> dict:
"""Format a PredictionMarket into API response."""
return {
"source": m.source,
"question": m.question,
"slug": m.slug,
"probability": {
"yes": round(m.probability_yes, 4),
"no": round(m.probability_no, 4),
"yes_pct": f"{m.probability_yes * 100:.1f}%",
"no_pct": f"{m.probability_no * 100:.1f}%",
},
"volume_usd": round(m.volume_usd, 2),
"category": m.category,
"tokens_mentioned": m.tokens_mentioned,
"is_security_relevant": m.is_security_relevant,
"is_crypto_relevant": m.is_crypto_relevant,
"url": m.url,
"ends_at": m.ends_at,
"updated_at": m.updated_at,
}
def _interpret_market(m: PredictionMarket) -> dict:
"""Generate a human-readable interpretation of market probabilities."""
prob_pct = m.probability_yes * 100
if prob_pct >= 80:
sentiment = "strong consensus"
elif prob_pct >= 60:
sentiment = "moderate consensus"
elif prob_pct >= 40:
sentiment = "divided / uncertain"
elif prob_pct >= 20:
sentiment = "leaning against"
else:
sentiment = "strong consensus against"
signals = []
if m.is_security_relevant:
signals.append("security_threat_indicator")
if m.is_crypto_relevant:
signals.append("crypto_ecosystem_relevant")
if m.volume_usd > 100000:
signals.append("high_volume_confidence")
if m.tokens_mentioned:
signals.append(f"token_specific: {', '.join(m.tokens_mentioned[:5])}")
return {
"market_sentiment": sentiment,
"probability": f"{prob_pct:.1f}%",
"confidence": "high" if m.volume_usd > 100000 else "moderate" if m.volume_usd > 10000 else "low",
"intelligence_signals": signals,
}