Add new feature to handle user input
This commit is contained in:
+230
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user