rmi-backend/app/domains/databus/news_provider.py

295 lines
10 KiB
Python

"""
RugCharts News & Market Data Provider
======================================
Wires ALL free data sources into DataBus:
- CoinGecko (prices, trending)
- Alternative.me (Fear & Greed Index)
- Polymarket (prediction markets)
- CryptoPanic (news sentiment - if key)
- CoinMarketCap (free tier)
- Our 200+ RSS feeds from news_service
- Existing MCP servers (feargreed, prediction-market, jupiter)
Every endpoint accessible via DataBus. Every response enriched.
"""
import logging
import os
import time
from datetime import UTC, datetime
import httpx
logger = logging.getLogger("news_provider")
# ── Free API endpoints (no keys needed) ────────────────────────────
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
FEAR_GREED_URL = "https://api.alternative.me/fng/?limit=1"
POLYMARKET_URL = "https://gamma-api.polymarket.com/events"
COINGECKO_KEY = os.getenv("COINGECKO_API_KEY", "")
CACHE = {} # Simple in-memory cache with TTL
CACHE_TTL = 60
def _cached(key: str, ttl: int = 60) -> dict | None:
if key in CACHE:
data, ts = CACHE[key]
if time.time() - ts < ttl:
return data
return None
def _cache_set(key: str, data: dict):
CACHE[key] = (data, time.time())
# ── 1. COINGECKO - Prices, trending, market data ───────────────────
async def get_market_prices(coins: str = "bitcoin,ethereum,solana", **kw) -> dict | None:
"""Live crypto prices from CoinGecko. Free tier, no key needed."""
cache_key = f"prices:{coins}"
cached = _cached(cache_key, 30)
if cached:
return cached
try:
async with httpx.AsyncClient(timeout=10) as c:
headers = {}
if COINGECKO_KEY:
headers["x-cg-demo-api-key"] = COINGECKO_KEY
r = await c.get(
f"{COINGECKO_BASE}/simple/price",
params={
"ids": coins,
"vs_currencies": "usd",
"include_24hr_change": "true",
"include_market_cap": "true",
},
headers=headers,
)
if r.status_code == 200:
data = r.json()
result = {
"prices": data,
"timestamp": datetime.now(UTC).isoformat(),
"source": "coingecko",
"free": True,
}
_cache_set(cache_key, result)
return result
except Exception as e:
logger.warning(f"CoinGecko failed: {e}")
return None
async def get_trending_coins(**kw) -> dict | None:
"""Trending coins from CoinGecko search."""
cache_key = "trending"
cached = _cached(cache_key, 120)
if cached:
return cached
try:
async with httpx.AsyncClient(timeout=10) as c:
headers = {}
if COINGECKO_KEY:
headers["x-cg-demo-api-key"] = COINGECKO_KEY
r = await c.get(f"{COINGECKO_BASE}/search/trending", headers=headers)
if r.status_code == 200:
data = r.json()
coins = data.get("coins", [])[:10]
result = {
"trending": [
{
"name": c["item"]["name"],
"symbol": c["item"]["symbol"],
"market_cap_rank": c["item"].get("market_cap_rank"),
"score": c["item"].get("score"),
}
for c in coins
],
"source": "coingecko",
"free": True,
}
_cache_set(cache_key, result)
return result
except Exception as e:
logger.warning(f"Trending failed: {e}")
return None
# ── 2. FEAR & GREED INDEX - Alternative.me ─────────────────────────
async def get_fear_greed(**kw) -> dict | None:
"""Crypto Fear & Greed Index. Completely free, no key."""
cache_key = "fear_greed"
cached = _cached(cache_key, 300)
if cached:
return cached
try:
async with httpx.AsyncClient(timeout=10) as c:
r = await c.get(FEAR_GREED_URL)
if r.status_code == 200:
data = r.json().get("data", [{}])[0]
value = int(data.get("value", 50))
classification = data.get("value_classification", "Neutral")
# Map to color and sentiment
if value <= 25:
color, sentiment = "#ff0044", "Extreme Fear"
elif value <= 45:
color, sentiment = "#ff8800", "Fear"
elif value <= 55:
color, sentiment = "#ffd700", "Neutral"
elif value <= 75:
color, sentiment = "#88ff00", "Greed"
else:
color, sentiment = "#00ff88", "Extreme Greed"
result = {
"value": value,
"classification": classification,
"sentiment": sentiment,
"color": color,
"timestamp": data.get("timestamp"),
"source": "alternative.me",
"free": True,
}
_cache_set(cache_key, result)
return result
except Exception as e:
logger.warning(f"Fear & Greed failed: {e}")
return None
# ── 3. POLYMARKET - Prediction markets ─────────────────────────────
async def get_prediction_markets(limit: int = 5, tag: str = "crypto", **kw) -> dict | None:
"""Prediction market events from Polymarket. Free, no key."""
cache_key = f"polymarket:{tag}:{limit}"
cached = _cached(cache_key, 120)
if cached:
return cached
try:
async with httpx.AsyncClient(timeout=10) as c:
params = {"limit": limit, "active": "true", "closed": "false"}
if tag:
params["tag"] = tag
r = await c.get(POLYMARKET_URL, params=params)
if r.status_code == 200:
events = r.json()
result = {
"markets": [
{
"title": e.get("title", ""),
"volume": e.get("volume", 0),
"liquidity": e.get("liquidity", 0),
"end_date": e.get("endDate", ""),
"slug": e.get("slug", ""),
}
for e in events[:limit]
],
"total": len(events),
"source": "polymarket",
"free": True,
}
_cache_set(cache_key, result)
return result
except Exception as e:
logger.warning(f"Polymarket failed: {e}")
return None
# ── 4. COMBINED MARKET BRIEF - All sources in one call ─────────────
async def get_market_brief(**kw) -> dict | None:
"""One-call market overview: prices + fear/greed + trending + prediction markets."""
prices_task = get_market_prices()
fear_task = get_fear_greed()
trending_task = get_trending_coins()
poly_task = get_prediction_markets(limit=3)
prices = await prices_task
fear = await fear_task
trending = await trending_task
polymarket = await poly_task
# Build natural-language brief
brief_parts = []
if fear:
brief_parts.append(f"Market sentiment: {fear['classification']} ({fear['value']}/100)")
if prices:
for coin, data in prices.get("prices", {}).items():
change = data.get("usd_24h_change", 0) or 0
emoji = "🔴" if change < -3 else "🟠" if change < 0 else "🟢"
brief_parts.append(f"{emoji} {coin.title()}: ${data.get('usd', 0):,.0f} ({change:+.1f}%)")
return {
"brief": " | ".join(brief_parts),
"prices": prices,
"fear_greed": fear,
"trending": trending,
"prediction_markets": polymarket,
"generated_at": datetime.now(UTC).isoformat(),
"sources": ["coingecko", "alternative.me", "polymarket"],
"source": "market_brief",
"free": True,
}
# ── 5. NEWS AGGREGATION - from our existing 200+ feeds ─────────────
async def get_aggregated_news(limit: int = 20, category: str = "", **kw) -> dict | None:
"""Pull news from our existing news_service.py aggregator."""
try:
from app.news_service import fetch_all_news
articles = await fetch_all_news()
if articles:
if category:
articles = [
a for a in articles if category.lower() in (a.get("category", "") + a.get("source", "")).lower()
]
result = {
"articles": articles[:limit],
"total": len(articles),
"filtered": len(articles[:limit]),
"source": "rmi_news_aggregator",
"free": True,
}
return result
except Exception as e:
logger.warning(f"News aggregation failed: {e}")
return None
# ── 6. COMBINED NEWS + MARKET - The full picture ───────────────────
async def get_full_news_feed(limit: int = 15, **kw) -> dict | None:
"""Complete news page data: headlines + prices + fear/greed + trending + polymarket."""
brief = await get_market_brief()
news = await get_aggregated_news(limit=limit)
return {
"market_brief": brief,
"headlines": news,
"generated_at": datetime.now(UTC).isoformat(),
"data_sources": [
"CoinGecko (prices, trending)",
"Alternative.me (Fear & Greed Index)",
"Polymarket (prediction markets)",
"RMI News Aggregator (200+ RSS feeds, 15 tiers)",
],
"source": "full_news_feed",
"free": True,
}