""" 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)