Some checks failed
CI / build (push) Failing after 3s
PHASE 2.3 (AUDIT-2026-Q3.md):
Task 1 — Wire-in Wave 3 (1 router mounted, 2 deferred):
- app.routers.unified_scanner_router mounted at /api/v2/scanner/* (2 routes:
POST /api/v2/scanner/token/scan, POST /api/v2/scanner/wallet/scan).
Refactored prefix from /api/v2 -> /api/v2/scanner to avoid future conflicts
with the v1 /api/v1/scanner/ stub.
- app.routers.unified_wallet_scanner DEFERRED (no router APIRouter attribute;
library module consumed by unified_scanner_router via get_wallet_scanner()).
- app.routers.admin_extensions DEFERRED (DORMANT per audit; 25 routes at
/api/v1/admin/* would shadow /api/v1/admin/alerts_webhook).
Task 2 — Archive 136 dead-code files to app/_archive/legacy_2026_07/:
- 73 routers in app/routers/ (reach graph showed zero reach into mount.py).
- 63 flat app/*.py (domain modules never imported by live code).
- 1 file RESTORED post-archive: app/routers/x402_bridge_health.py (caught by
tests/unit/test_bridge_health.py which directly imports it; reach graph
considered tests/ only as transitive reach — to be patched in next cycle).
Forced-LIVE (NOT archived per user directive):
- app/ai_pipeline_v3.py (3 importers in audit window, importers themselves DEAD)
- app/splade_bm25.py (LIVE via app.rag_service)
- app/wallet_manager_v2.py (LIVE via x402_enforcement, x402_tools, sweep_all, sweep_now)
- app/crypto_embeddings.py (NOT in audit ARCHIVE list; heavy import graph)
Verification (forward-import closure from mount.py + main.py + factory.py + lifespan.py):
- imports = 348 app.* modules
- reached = 194 files reachable from roots
- archive set = audit_dead (186) - reached - forced_live (4) - test_live (1) = 136
- Net delta: 136 files moved, 44,932 LOC reduction, 293->295 active routes (+2 from Wave 3)
pyproject.toml updates:
- setuptools.packages.find: added exclude for app._archive*
- ruff.extend-exclude: added "app/_archive/"
- mypy.exclude: added "app/_archive/"
Smoke test: pytest tests/ — 817 passed, 3 pre-existing failures unchanged
(0 new failures; 0 routes lost; all 4 forced-LIVE files still importable).
Restoration: git mv app/_archive/legacy_2026_07/<name>.py <original-path>
and add the import to app/mount.py ROUTER_MODULES.
Refs: AUDIT-2026-Q3.md /home/dev/pry/rmi-final-deadcode-2026-07-06.md
255 lines
9.9 KiB
Python
255 lines
9.9 KiB
Python
"""Prediction Market Monitor - Polymarket + Kalshi + Manifold tracker.
|
|
Premium feature: wallet-level tracking, insider pattern detection, P&L analytics."""
|
|
|
|
import json
|
|
import os
|
|
from datetime import UTC, datetime
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Query
|
|
|
|
router = APIRouter(prefix="/api/v1/prediction-markets", tags=["prediction-markets"])
|
|
|
|
POLYMARKET = "https://clob.polymarket.com"
|
|
BACKEND = os.getenv("BACKEND_URL", "http://localhost:8000")
|
|
RMI_KEY = os.getenv("RMI_INTERNAL_KEY", "rmi-internal-2026")
|
|
|
|
# Tracked wallet registry (Redis in prod, in-memory for now)
|
|
_tracked_wallets: dict[str, dict] = {}
|
|
_insider_flags: list[dict] = []
|
|
|
|
# ═══════════════════════════════════════════
|
|
# MARKET DATA
|
|
# ═══════════════════════════════════════════
|
|
|
|
|
|
@router.get("/markets")
|
|
async def get_markets(
|
|
category: str = Query("crypto", description="crypto|politics|sports|all"), limit: int = Query(10, le=50)
|
|
):
|
|
"""Get active prediction markets from Polymarket."""
|
|
markets = []
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"{POLYMARKET}/markets", params={"limit": limit, "closed": "false"})
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
for m in data if isinstance(data, list) else data.get("data", []):
|
|
if category == "all" or category in (m.get("category", "") or "").lower():
|
|
markets.append(
|
|
{
|
|
"id": m.get("id"),
|
|
"question": m.get("question"),
|
|
"volume_usd": m.get("volumeNum", 0),
|
|
"liquidity": m.get("liquidityNum", 0),
|
|
"end_date": m.get("endDateIso"),
|
|
"outcomes": m.get("outcomes", []),
|
|
"outcome_prices": json.loads(m.get("outcomePrices", "[]")),
|
|
"category": m.get("category"),
|
|
}
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
# Also get from DataBus if available
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"{BACKEND}/api/v1/databus/fetch/prediction_markets", headers={"X-RMI-Key": RMI_KEY})
|
|
if r.status_code == 200:
|
|
db_markets = r.json().get("data", r.json()).get("markets", [])
|
|
markets.extend(db_markets)
|
|
except Exception:
|
|
pass
|
|
|
|
markets.sort(key=lambda m: m.get("volume_usd", 0), reverse=True)
|
|
return {"markets": markets[:limit], "source": "polymarket+databus", "count": len(markets[:limit])}
|
|
|
|
|
|
@router.get("/market/{market_id}")
|
|
async def get_market_detail(market_id: str):
|
|
"""Get detailed market info + recent trades."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"{POLYMARKET}/markets/{market_id}")
|
|
if r.status_code == 200:
|
|
market = r.json()
|
|
return {
|
|
"id": market.get("id"),
|
|
"question": market.get("question"),
|
|
"description": market.get("description"),
|
|
"volume_usd": market.get("volumeNum", 0),
|
|
"liquidity": market.get("liquidityNum", 0),
|
|
"end_date": market.get("endDateIso"),
|
|
"outcomes": market.get("outcomes", []),
|
|
"outcome_prices": json.loads(market.get("outcomePrices", "[]")),
|
|
}
|
|
except Exception:
|
|
pass
|
|
return {"error": "Market not found"}
|
|
|
|
|
|
# ═══════════════════════════════════════════
|
|
# WALLET TRACKING (PREMIUM)
|
|
# ═══════════════════════════════════════════
|
|
|
|
|
|
@router.post("/track-wallet")
|
|
async def track_wallet(wallet_address: str, label: str = "", user_id: str = ""):
|
|
"""Start tracking a wallet's prediction market activity. Premium feature."""
|
|
wid = f"{user_id}:{wallet_address}" if user_id else wallet_address
|
|
_tracked_wallets[wid] = {
|
|
"wallet": wallet_address,
|
|
"label": label,
|
|
"user_id": user_id,
|
|
"added_at": datetime.now(UTC).isoformat(),
|
|
"positions": [],
|
|
"pnl_usd": 0,
|
|
"win_rate": 0,
|
|
"total_trades": 0,
|
|
}
|
|
return {"tracked": wid, "wallet": wallet_address, "status": "monitoring"}
|
|
|
|
|
|
@router.get("/wallet/{wallet_address}")
|
|
async def get_wallet_profile(wallet_address: str):
|
|
"""Get a tracked wallet's prediction market profile."""
|
|
# Find wallet in tracked registry
|
|
wallet_data = None
|
|
for wid, data in _tracked_wallets.items():
|
|
if wallet_address in wid:
|
|
wallet_data = data
|
|
break
|
|
|
|
if not wallet_data:
|
|
# Try to fetch from DataBus
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(
|
|
f"{BACKEND}/api/v1/databus/fetch/entity_intel?address={wallet_address}",
|
|
headers={"X-RMI-Key": RMI_KEY},
|
|
)
|
|
if r.status_code == 200:
|
|
wallet_data = r.json().get("data", r.json())
|
|
except Exception:
|
|
pass
|
|
|
|
if not wallet_data:
|
|
return {
|
|
"wallet": wallet_address,
|
|
"tracked": False,
|
|
"note": "Not tracked. Use POST /api/v1/prediction-markets/track-wallet",
|
|
}
|
|
|
|
# Check for insider patterns
|
|
insider_risk = _analyze_insider_patterns(wallet_address, wallet_data)
|
|
|
|
return {
|
|
"wallet": wallet_address,
|
|
"tracked": True,
|
|
"label": wallet_data.get("label", ""),
|
|
"positions": wallet_data.get("positions", []),
|
|
"pnl_usd": wallet_data.get("pnl_usd", 0),
|
|
"win_rate": wallet_data.get("win_rate", 0),
|
|
"total_trades": wallet_data.get("total_trades", 0),
|
|
"insider_risk": insider_risk,
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════
|
|
# INSIDER DETECTION (PREMIUM)
|
|
# ═══════════════════════════════════════════
|
|
|
|
|
|
def _analyze_insider_patterns(wallet: str, data: dict) -> dict:
|
|
"""Analyze wallet for insider trading patterns."""
|
|
risk = 0
|
|
flags = []
|
|
|
|
trades = data.get("positions", [])
|
|
if not trades:
|
|
return {"risk": "unknown", "score": 0, "flags": []}
|
|
|
|
# Pattern 1: Trades right before major price moves
|
|
pre_move_trades = 0
|
|
for trade in trades:
|
|
price_before = trade.get("price_before_move", trade.get("avg_price", 0))
|
|
price_after = trade.get("price_after_move", trade.get("current_price", 0))
|
|
if price_before > 0 and abs(price_after - price_before) / price_before > 0.3:
|
|
pre_move_trades += 1
|
|
|
|
if pre_move_trades > 3:
|
|
risk += 30
|
|
flags.append(f"{pre_move_trades} trades before >30% price moves")
|
|
|
|
# Pattern 2: High win rate (>80%) with significant volume
|
|
win_rate = data.get("win_rate", 0)
|
|
total_trades = data.get("total_trades", 0)
|
|
volume = data.get("pnl_usd", 0)
|
|
|
|
if win_rate > 80 and total_trades > 20 and volume > 10000:
|
|
risk += 25
|
|
flags.append(f"{win_rate}% win rate across {total_trades} trades - statistically anomalous")
|
|
|
|
# Pattern 3: Trades on markets with low public interest
|
|
low_interest_trades = sum(1 for t in trades if t.get("market_volume", 0) < 1000)
|
|
if low_interest_trades > 5 and win_rate > 70:
|
|
risk += 20
|
|
flags.append(f"{low_interest_trades} trades on low-volume markets with {win_rate}% win rate")
|
|
|
|
# Pattern 4: Consistent last-minute entries before resolution
|
|
last_minute = sum(1 for t in trades if t.get("hours_before_resolution", 999) < 1)
|
|
if last_minute > 3:
|
|
risk += 15
|
|
flags.append(f"{last_minute} trades within 1 hour of resolution - possible info leak")
|
|
|
|
if risk >= 60:
|
|
level = "CRITICAL"
|
|
elif risk >= 35:
|
|
level = "HIGH"
|
|
elif risk >= 15:
|
|
level = "MEDIUM"
|
|
else:
|
|
level = "LOW"
|
|
|
|
return {"risk": level, "score": min(100, risk), "flags": flags}
|
|
|
|
|
|
@router.get("/insider-leaderboard")
|
|
async def insider_leaderboard(min_risk: int = Query(35, le=100), limit: int = Query(10, le=25)):
|
|
"""Get wallets flagged for potential insider trading. Premium feature."""
|
|
flagged = []
|
|
for wid, data in list(_tracked_wallets.items())[:50]:
|
|
analysis = _analyze_insider_patterns(wid, data)
|
|
if analysis["score"] >= min_risk:
|
|
flagged.append(
|
|
{
|
|
"wallet": data["wallet"],
|
|
"label": data.get("label", "Unlabeled"),
|
|
"insider_risk": analysis["risk"],
|
|
"risk_score": analysis["score"],
|
|
"flags": analysis["flags"],
|
|
"pnl_usd": data.get("pnl_usd", 0),
|
|
"win_rate": data.get("win_rate", 0),
|
|
}
|
|
)
|
|
|
|
flagged.sort(key=lambda w: w["risk_score"], reverse=True)
|
|
return {"flagged_wallets": flagged[:limit], "total_flagged": len(flagged)}
|
|
|
|
|
|
@router.get("/market-snapshot")
|
|
async def market_snapshot():
|
|
"""Quick overview of prediction markets + insider activity."""
|
|
markets = (await get_markets("all", 5))["markets"]
|
|
top_markets = [{"question": m["question"][:80], "volume_usd": m["volume_usd"]} for m in markets[:5]]
|
|
|
|
insider_count = len([w for w in _tracked_wallets.values() if w.get("insider_flag", False)])
|
|
|
|
return {
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
"active_markets": len(markets),
|
|
"top_markets": top_markets,
|
|
"tracked_wallets": len(_tracked_wallets),
|
|
"insider_flags": insider_count,
|
|
"source": "Polymarket + DataBus",
|
|
}
|