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