Files
Trading_bot/backend/main.py
T
2026-10-30 09:20:09 +01:00

231 lines
7.4 KiB
Python

"""
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,
}