- 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>
276 lines
9 KiB
Python
276 lines
9 KiB
Python
"""
|
|
Intelligence Panel Router
|
|
=========================
|
|
Endpoints for the intelligence feed panel on rugmunch.io/intelligence
|
|
|
|
Data sources:
|
|
- News service (200+ crypto news sources, 15 tiers)
|
|
- Security alerts (rug detection, whale tracking)
|
|
- Prediction market signals (Polymarket, Kalshi, Limitless, Manifold)
|
|
- Crypto Fear Index (aggregated risk from prediction markets)
|
|
- Bulletin posts (community intelligence)
|
|
"""
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from fastapi import APIRouter, Query
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/v1/intelligence", tags=["intelligence-panel"])
|
|
|
|
# Import prediction market intelligence (LAZY - deferred to first request)
|
|
pm_intel = None
|
|
_pm_intel_loaded = False
|
|
|
|
|
|
def _get_pm_intel():
|
|
global pm_intel, _pm_intel_loaded
|
|
if _pm_intel_loaded:
|
|
return pm_intel
|
|
_pm_intel_loaded = True
|
|
try:
|
|
from app.services.prediction_market_intel import (
|
|
get_prediction_market_intel,
|
|
)
|
|
|
|
pm_intel = get_prediction_market_intel()
|
|
except ImportError:
|
|
pm_intel = None
|
|
return pm_intel
|
|
|
|
|
|
class IntelSignalResponse(BaseModel):
|
|
signal_type: str
|
|
severity: str
|
|
headline: str
|
|
insight: str
|
|
action: str
|
|
confidence: float
|
|
generated_at: str
|
|
|
|
|
|
class FearIndexResponse(BaseModel):
|
|
overall_risk: float
|
|
exploit_risk: float
|
|
regulatory_risk: float
|
|
stablecoin_risk: float
|
|
exchange_risk: float
|
|
generated_at: str
|
|
|
|
|
|
class IntelligenceItem(BaseModel):
|
|
id: str
|
|
title: str
|
|
content: str | None = None
|
|
url: str | None = None
|
|
source: str
|
|
published_at: str
|
|
category: str
|
|
kind: str
|
|
highlight: bool | None = False
|
|
severity: str | None = None
|
|
|
|
|
|
class IntelligenceFeedResponse(BaseModel):
|
|
items: list[IntelligenceItem]
|
|
count: int
|
|
timestamp: str
|
|
|
|
|
|
# ── Prediction Market Intelligence ─────────────────────────
|
|
|
|
|
|
@router.get("/prediction-signals", response_model=list[IntelSignalResponse])
|
|
async def get_prediction_signals(limit: int = Query(10, ge=1, le=20)):
|
|
"""Get actionable intelligence signals from prediction markets.
|
|
|
|
Signal types:
|
|
- HIGH_CONSENSUS_THREAT: Markets strongly expect an adverse event
|
|
- LOW_CONSENSUS_RISK: Markets dismiss a feared risk
|
|
- TOKEN_RISK_SIGNAL: Specific token flagged by prediction markets
|
|
- ECOSYSTEM_RISK: Broad ecosystem-level threat indicator
|
|
|
|
Each signal includes: severity, plain-English insight, recommended action.
|
|
"""
|
|
_get_pm_intel()
|
|
if not pm_intel:
|
|
return []
|
|
|
|
signals = await pm_intel.generate_signals(max_signals=limit)
|
|
return [
|
|
IntelSignalResponse(
|
|
signal_type=s.signal_type,
|
|
severity=s.severity,
|
|
headline=s.headline,
|
|
insight=s.insight,
|
|
action=s.action,
|
|
confidence=round(s.confidence, 2),
|
|
generated_at=s.generated_at,
|
|
)
|
|
for s in signals
|
|
]
|
|
|
|
|
|
@router.get("/crypto-fear-index", response_model=FearIndexResponse)
|
|
async def get_crypto_fear_index():
|
|
"""Get the Crypto Fear Index - risk aggregated from prediction markets.
|
|
|
|
Components:
|
|
- overall_risk: Weighted aggregate of all categories
|
|
- exploit_risk: DeFi hack/exploit/vulnerability probability
|
|
- regulatory_risk: SEC/CFTC/enforcement action probability
|
|
- stablecoin_risk: Depeg/collapse probability
|
|
- exchange_risk: Insolvency/breach probability
|
|
|
|
Derived from live prediction market data across Polymarket, Kalshi, Limitless, Manifold.
|
|
Higher = more risk priced in by traders with real money at stake.
|
|
"""
|
|
_get_pm_intel()
|
|
if not pm_intel:
|
|
return FearIndexResponse(
|
|
overall_risk=0,
|
|
exploit_risk=0,
|
|
regulatory_risk=0,
|
|
stablecoin_risk=0,
|
|
exchange_risk=0,
|
|
generated_at=datetime.now(UTC).isoformat(),
|
|
)
|
|
|
|
fear = await pm_intel.generate_crypto_fear_index()
|
|
return FearIndexResponse(
|
|
overall_risk=fear.overall_risk,
|
|
exploit_risk=fear.exploit_risk,
|
|
regulatory_risk=fear.regulatory_risk,
|
|
stablecoin_risk=fear.stablecoin_risk,
|
|
exchange_risk=fear.exchange_risk,
|
|
generated_at=fear.generated_at,
|
|
)
|
|
|
|
|
|
# ── Aggregated Feed ────────────────────────────────────────
|
|
|
|
|
|
@router.get("/feed", response_model=IntelligenceFeedResponse)
|
|
async def get_intelligence_feed(limit: int = Query(20, ge=1, le=50)):
|
|
"""Get aggregated intelligence feed combining all sources."""
|
|
_get_pm_intel()
|
|
items = []
|
|
now_ts = datetime.now(UTC).isoformat()
|
|
|
|
# 1. Prediction market signals (highest priority - forward-looking)
|
|
if pm_intel:
|
|
try:
|
|
signals = await pm_intel.generate_signals(max_signals=5)
|
|
for s in signals:
|
|
severity = s.severity
|
|
items.append(
|
|
IntelligenceItem(
|
|
id=f"pm-{s.signal_type}-{now_ts}",
|
|
title=s.headline,
|
|
content=s.insight,
|
|
url="/intelligence",
|
|
source=f"Prediction Markets ({s.signal_type.replace('_', ' ').title()})",
|
|
published_at=now_ts,
|
|
category="prediction-market",
|
|
kind="signal",
|
|
highlight=(severity in ("critical", "high")),
|
|
severity=severity,
|
|
)
|
|
)
|
|
except Exception as e:
|
|
logger.info(f"Prediction market signals unavailable: {e}")
|
|
# 2. Crypto Fear Index summary
|
|
if pm_intel:
|
|
try:
|
|
fear = await pm_intel.generate_crypto_fear_index()
|
|
risk_label = (
|
|
"EXTREME FEAR"
|
|
if fear.overall_risk >= 75
|
|
else "FEAR"
|
|
if fear.overall_risk >= 50
|
|
else "NEUTRAL"
|
|
if fear.overall_risk >= 25
|
|
else "GREED"
|
|
if fear.overall_risk >= 10
|
|
else "EXTREME GREED"
|
|
)
|
|
items.append(
|
|
IntelligenceItem(
|
|
id=f"fear-index-{now_ts}",
|
|
title=f"Crypto Fear Index: {fear.overall_risk:.0f}/100 - {risk_label}",
|
|
content=(
|
|
f"Exploit risk: {fear.exploit_risk:.0f}/100 | "
|
|
f"Regulatory: {fear.regulatory_risk:.0f}/100 | "
|
|
f"Stablecoin: {fear.stablecoin_risk:.0f}/100 | "
|
|
f"Exchange: {fear.exchange_risk:.0f}/100. "
|
|
f"Derived from live Polymarket + Kalshi + Manifold prediction markets."
|
|
),
|
|
url="/intelligence",
|
|
source="Crypto Fear Index",
|
|
published_at=now_ts,
|
|
category="risk-index",
|
|
kind="index",
|
|
highlight=(fear.overall_risk >= 50),
|
|
severity="high" if fear.overall_risk >= 50 else "medium",
|
|
)
|
|
)
|
|
except Exception as e:
|
|
logger.info(f"Crypto Fear Index unavailable: {e}")
|
|
# 3. Prediction market data sources status
|
|
items.append(
|
|
IntelligenceItem(
|
|
id=f"sources-{now_ts}",
|
|
title="Prediction Market Intelligence Active - 4 Sources Live",
|
|
content="RMI now monitors Polymarket, Kalshi, Limitless, and Manifold for crypto security signals. Live risk indicators, token threat detection, and ecosystem monitoring are online.",
|
|
url="/intelligence",
|
|
source="RMI System",
|
|
published_at=now_ts,
|
|
category="system",
|
|
kind="status",
|
|
highlight=False,
|
|
)
|
|
)
|
|
|
|
return IntelligenceFeedResponse(
|
|
items=items[:limit],
|
|
count=min(len(items), limit),
|
|
timestamp=now_ts,
|
|
)
|
|
|
|
|
|
@router.get("/latest")
|
|
async def get_latest_intelligence():
|
|
"""Get latest intelligence snapshot for quick updates."""
|
|
_get_pm_intel()
|
|
latest = {
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
|
|
if pm_intel:
|
|
try:
|
|
signals = await pm_intel.generate_signals(max_signals=3)
|
|
if signals:
|
|
top = signals[0]
|
|
latest["top_signal"] = {
|
|
"headline": top.headline,
|
|
"severity": top.severity,
|
|
"insight": top.insight,
|
|
"action": top.action,
|
|
}
|
|
|
|
fear = await pm_intel.generate_crypto_fear_index()
|
|
latest["fear_index"] = {
|
|
"overall": fear.overall_risk,
|
|
"exploit": fear.exploit_risk,
|
|
"regulatory": fear.regulatory_risk,
|
|
"stablecoin": fear.stablecoin_risk,
|
|
"exchange": fear.exchange_risk,
|
|
}
|
|
except Exception as e:
|
|
latest["error"] = str(e)
|
|
|
|
return latest
|