""" main.py — FastAPI backend Endpoints: GET /api/assets/ranked — Top 6 ranked assets GET /api/assets/{symbol} — Detail data voor één asset GET /api/news/{coin} — Nieuws + sentiment voor één coin GET /api/health — Health check """ import json import logging import asyncio from datetime import datetime from contextlib import asynccontextmanager import redis.asyncio as redis from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from ranker import rank_assets, RankedAsset from exchange import fetch_ohlcv, fetch_tickers, TRADING_UNIVERSE from sentiment import fetch_sentiment from analysis import calculate_indicators, score_asset logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Cache TTL in seconden CACHE_TTL_RANKED = 60 # Ranking elke minuut ververst CACHE_TTL_DETAIL = 30 # Detail data elke 30s CACHE_TTL_NEWS = 120 # Nieuws elke 2 minuten @asynccontextmanager async def lifespan(app: FastAPI): """Start Redis connectie bij opstart.""" app.state.redis = redis.from_url("redis://redis:6379", decode_responses=True) logger.info("Redis verbonden") yield await app.state.redis.close() app = FastAPI( title="Crypto Bot API", version="1.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"], ) async def get_cached(redis_client, key: str): """Haal gecachte data op, of None als niet aanwezig.""" try: data = await redis_client.get(key) return json.loads(data) if data else None except Exception: return None async def set_cached(redis_client, key: str, data, ttl: int): """Sla data op in cache.""" try: await redis_client.setex(key, ttl, json.dumps(data, default=str)) except Exception as e: logger.warning(f"Cache write mislukt: {e}") @app.get("/api/health") async def health(): return {"status": "ok", "timestamp": datetime.utcnow().isoformat()} @app.get("/api/assets/ranked") async def get_ranked_assets(): """ Geeft top 6 ranked assets terug op basis van TA + sentiment. Gecached voor 60 seconden. """ cache_key = "ranked_assets" cached = await get_cached(app.state.redis, cache_key) if cached: return {"data": cached, "cached": True, "timestamp": datetime.utcnow().isoformat()} try: ranked = await rank_assets(top_n=6) data = [_serialize_ranked(a) for a in ranked] await set_cached(app.state.redis, cache_key, data, CACHE_TTL_RANKED) return {"data": data, "cached": False, "timestamp": datetime.utcnow().isoformat()} except Exception as e: logger.error(f"Ranking mislukt: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/assets/{symbol:path}") async def get_asset_detail(symbol: str): """ Detail data voor één asset inclusief OHLCV candles. symbol: bijv. BTC%2FEUR of BTC/EUR """ symbol = symbol.replace("%2F", "/").upper() cache_key = f"detail_{symbol}" cached = await get_cached(app.state.redis, cache_key) if cached: return {"data": cached, "cached": True} try: df = fetch_ohlcv(symbol, timeframe="5m", limit=100) indicators = calculate_indicators(df) ta_score = score_asset(symbol, indicators) coin_code = symbol.split("/")[0] sentiment = await fetch_sentiment(coin_code) tickers = fetch_tickers([symbol]) ticker = tickers.get(symbol, {}) # Candles voor grafiek candles = [] if not df.empty: for ts, row in df.iterrows(): candles.append({ "time": ts.isoformat(), "open": row["open"], "high": row["high"], "low": row["low"], "close": row["close"], "volume": row["volume"], }) data = { "symbol": symbol, "price": ticker.get("price", 0), "change_24h": ticker.get("change_24h", 0), "volume_24h": ticker.get("volume_24h", 0), "indicators": indicators, "ta_score": ta_score.opportunity_score, "tradeable": ta_score.tradeable, "sentiment": { "score": sentiment.score, "label": sentiment.label, "confidence": sentiment.confidence, "news_count": sentiment.news_count, "news": [ { "title": n.title, "source": n.source, "url": n.url, "sentiment": n.sentiment, "score": n.score, "published": n.published.isoformat(), } for n in sentiment.recent_news ], }, "candles": candles[-50:], # Laatste 50 candles } await set_cached(app.state.redis, cache_key, data, CACHE_TTL_DETAIL) return {"data": data, "cached": False} except Exception as e: logger.error(f"Detail fetch mislukt voor {symbol}: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/news/{coin}") async def get_news(coin: str): """Nieuws + sentiment voor één coin. bijv. /api/news/BTC""" coin = coin.upper() cache_key = f"news_{coin}" cached = await get_cached(app.state.redis, cache_key) if cached: return {"data": cached, "cached": True} try: sentiment = await fetch_sentiment(coin) data = { "coin": coin, "score": sentiment.score, "label": sentiment.label, "confidence": sentiment.confidence, "news_count": sentiment.news_count, "news": [ { "title": n.title, "source": n.source, "url": n.url, "sentiment": n.sentiment, "score": n.score, "published": n.published.isoformat(), } for n in sentiment.recent_news ], } await set_cached(app.state.redis, cache_key, data, CACHE_TTL_NEWS) return {"data": data, "cached": False} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) def _serialize_ranked(asset: RankedAsset) -> dict: return { "symbol": asset.symbol, "rank": asset.rank, "combined_score": asset.combined_score, "ta_score": asset.ta_score, "sentiment_score": asset.sentiment_score, "sentiment_label": asset.sentiment_label, "sentiment_news": asset.sentiment_news, "rsi": asset.rsi, "rsi_signal": asset.rsi_signal, "ema_signal": asset.ema_signal, "vwap_signal": asset.vwap_signal, "trend_strength": asset.trend_strength, "atr_pct": asset.atr_pct, "volume_score": asset.volume_score, "price": asset.price, "change_24h": asset.change_24h, "volume_24h": asset.volume_24h, "tradeable": asset.tradeable, "reason": asset.reason, }