153 lines
4.8 KiB
Python
153 lines
4.8 KiB
Python
"""
|
|
ranker.py — Asset ranker
|
|
Combineert: technische analyse score + sentiment score
|
|
Geeft top 6 tradeable assets terug uit het universe
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
from dataclasses import dataclass
|
|
|
|
from exchange import fetch_ohlcv, fetch_tickers, TRADING_UNIVERSE
|
|
from analysis import calculate_indicators, score_asset, AssetScore
|
|
from sentiment import fetch_all_sentiments, SentimentResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Gewichten voor gecombineerde score
|
|
TA_WEIGHT = 0.70 # Technische analyse weegt zwaarder
|
|
SENTIMENT_WEIGHT = 0.30 # Sentiment is bevestigend, niet leidend
|
|
|
|
|
|
@dataclass
|
|
class RankedAsset:
|
|
symbol: str
|
|
rank: int
|
|
combined_score: float # 0-100
|
|
ta_score: float # 0-100 (AssetScore.opportunity_score)
|
|
sentiment_score: float # -1.0 tot +1.0
|
|
sentiment_label: str
|
|
sentiment_news: int
|
|
rsi: float
|
|
rsi_signal: str
|
|
ema_signal: str
|
|
vwap_signal: str
|
|
trend_strength: str
|
|
atr_pct: float
|
|
volume_score: float
|
|
price: float
|
|
change_24h: float
|
|
volume_24h: float
|
|
tradeable: bool
|
|
reason: str # Mensleesbare reden voor ranking
|
|
|
|
|
|
async def rank_assets(top_n: int = 6) -> list[RankedAsset]:
|
|
"""
|
|
Rangschik alle assets in het universe op combined score.
|
|
Geeft top_n tradeable assets terug.
|
|
"""
|
|
logger.info(f"Start ranking van {len(TRADING_UNIVERSE)} assets")
|
|
|
|
# 1. Ticker data ophalen (prijs, volume, 24h change)
|
|
tickers = fetch_tickers(TRADING_UNIVERSE)
|
|
|
|
# 2. OHLCV + TA berekeningen — sequentieel om rate limits te respecteren
|
|
ta_scores: dict[str, AssetScore] = {}
|
|
for symbol in TRADING_UNIVERSE:
|
|
try:
|
|
df = fetch_ohlcv(symbol, timeframe="5m", limit=100)
|
|
indicators = calculate_indicators(df)
|
|
ta_scores[symbol] = score_asset(symbol, indicators)
|
|
except Exception as e:
|
|
logger.warning(f"TA berekening mislukt voor {symbol}: {e}")
|
|
|
|
# 3. Sentiment — parallel ophalen
|
|
coin_codes = [s.split("/")[0] for s in TRADING_UNIVERSE]
|
|
sentiments = await fetch_all_sentiments(coin_codes)
|
|
|
|
# 4. Combineer scores
|
|
ranked = []
|
|
for symbol in TRADING_UNIVERSE:
|
|
ta = ta_scores.get(symbol)
|
|
coin_code = symbol.split("/")[0]
|
|
sent: SentimentResult = sentiments.get(coin_code)
|
|
ticker = tickers.get(symbol, {})
|
|
|
|
if not ta or not sent:
|
|
continue
|
|
|
|
# Sentiment score omzetten naar 0-100 schaal
|
|
sent_normalized = (sent.score + 1) * 50 # -1→0, 0→50, +1→100
|
|
|
|
combined = (
|
|
ta.opportunity_score * TA_WEIGHT +
|
|
sent_normalized * SENTIMENT_WEIGHT
|
|
)
|
|
|
|
# Reden genereren
|
|
reason = _build_reason(ta, sent)
|
|
|
|
ranked.append(RankedAsset(
|
|
symbol=symbol,
|
|
rank=0, # Wordt hieronder ingevuld
|
|
combined_score=round(combined, 1),
|
|
ta_score=ta.opportunity_score,
|
|
sentiment_score=sent.score,
|
|
sentiment_label=sent.label,
|
|
sentiment_news=sent.news_count,
|
|
rsi=ta.rsi,
|
|
rsi_signal=ta.rsi_signal,
|
|
ema_signal=ta.ema_signal,
|
|
vwap_signal=ta.vwap_signal,
|
|
trend_strength=ta.trend_strength,
|
|
atr_pct=ta.atr_pct,
|
|
volume_score=ta.volume_score,
|
|
price=ticker.get("price", 0),
|
|
change_24h=ticker.get("change_24h", 0),
|
|
volume_24h=ticker.get("volume_24h", 0),
|
|
tradeable=ta.tradeable and sent.score >= 0,
|
|
reason=reason,
|
|
))
|
|
|
|
# Sorteer op combined score
|
|
ranked.sort(key=lambda x: x.combined_score, reverse=True)
|
|
|
|
# Ken rang toe
|
|
for i, asset in enumerate(ranked):
|
|
asset.rank = i + 1
|
|
|
|
# Geef top_n terug
|
|
logger.info(f"Ranking klaar. Top asset: {ranked[0].symbol if ranked else 'geen'}")
|
|
return ranked[:top_n]
|
|
|
|
|
|
def _build_reason(ta: AssetScore, sent: SentimentResult) -> str:
|
|
"""Genereer een mensleesbare reden voor de ranking."""
|
|
parts = []
|
|
|
|
if ta.rsi_signal == "koopzone":
|
|
parts.append("RSI in koopzone")
|
|
elif ta.rsi_signal == "oversold":
|
|
parts.append("RSI oversold")
|
|
|
|
if ta.ema_signal == "bullish":
|
|
parts.append("EMA bullish")
|
|
|
|
if ta.vwap_signal == "onder_vwap":
|
|
parts.append("prijs onder VWAP")
|
|
|
|
if ta.bb_position < 0.3:
|
|
parts.append("dicht bij BB lower band")
|
|
|
|
if sent.score > 0.2:
|
|
parts.append(f"sterk positief nieuws ({sent.news_count} items)")
|
|
elif sent.score > 0:
|
|
parts.append("neutraal-positief nieuws")
|
|
elif sent.score < -0.1:
|
|
parts.append("negatief sentiment")
|
|
|
|
if not parts:
|
|
return "Geen duidelijk signaal"
|
|
|
|
return " · ".join(parts)
|