""" analysis.py — Technische analyse engine Berekent: RSI, EMA, VWAP, Bollinger Bands, ATR, volume score Geeft per asset een opportunity score terug (0-100) """ import pandas as pd import pandas_ta as ta import numpy as np import logging from dataclasses import dataclass logger = logging.getLogger(__name__) @dataclass class AssetScore: symbol: str opportunity_score: float # 0-100, hoger = interessanter rsi: float rsi_signal: str # "oversold" | "neutral" | "overbought" ema9: float ema21: float ema_signal: str # "bullish" | "bearish" | "neutral" vwap: float vwap_signal: str # "above" | "below" bb_position: float # 0.0 = lower band, 1.0 = upper band atr_pct: float # ATR als % van prijs volume_score: float # 0-100, relatief volume trend_strength: str # "strong" | "moderate" | "weak" tradeable: bool # False als ATR te hoog of signalen ontbreken def calculate_indicators(df: pd.DataFrame) -> dict: """ Bereken alle indicatoren op een OHLCV dataframe. Geeft dict terug met indicator waarden. """ if df.empty or len(df) < 30: return {} try: close = df["close"] high = df["high"] low = df["low"] volume = df["volume"] # --- RSI --- rsi_series = ta.rsi(close, length=14) rsi = float(rsi_series.iloc[-1]) if rsi_series is not None else 50.0 # --- EMA --- ema9 = float(ta.ema(close, length=9).iloc[-1]) ema21 = float(ta.ema(close, length=21).iloc[-1]) ema50 = float(ta.ema(close, length=50).iloc[-1]) # EMA50 slope: positief = uptrend ema50_series = ta.ema(close, length=50) ema50_slope = float(ema50_series.iloc[-1] - ema50_series.iloc[-5]) if len(ema50_series) >= 5 else 0 # --- VWAP --- typical_price = (high + low + close) / 3 cumulative_tp_vol = (typical_price * volume).cumsum() cumulative_vol = volume.cumsum() vwap = float((cumulative_tp_vol / cumulative_vol).iloc[-1]) # --- Bollinger Bands --- bbands = ta.bbands(close, length=20, std=2) if bbands is not None and not bbands.empty: bb_upper = float(bbands.iloc[-1, 2]) # BBU bb_lower = float(bbands.iloc[-1, 0]) # BBL bb_mid = float(bbands.iloc[-1, 1]) # BBM current = float(close.iloc[-1]) bb_range = bb_upper - bb_lower bb_pos = (current - bb_lower) / bb_range if bb_range > 0 else 0.5 else: bb_upper = bb_lower = bb_mid = 0 bb_pos = 0.5 # --- ATR --- atr_series = ta.atr(high, low, close, length=14) atr = float(atr_series.iloc[-1]) if atr_series is not None else 0 atr_pct = (atr / float(close.iloc[-1])) * 100 # --- Volume score --- vol_mean = float(volume.rolling(20).mean().iloc[-1]) vol_current = float(volume.iloc[-1]) vol_ratio = vol_current / vol_mean if vol_mean > 0 else 1.0 vol_score = min(100, vol_ratio * 50) # Genormaliseerd 0-100 return { "rsi": rsi, "ema9": ema9, "ema21": ema21, "ema50": ema50, "ema50_slope": ema50_slope, "vwap": vwap, "bb_upper": bb_upper, "bb_lower": bb_lower, "bb_mid": bb_mid, "bb_position": bb_pos, "atr": atr, "atr_pct": atr_pct, "volume_score": vol_score, "close": float(close.iloc[-1]), } except Exception as e: logger.error(f"Indicator berekening mislukt: {e}") return {} def score_asset(symbol: str, indicators: dict) -> AssetScore: """ Vertaal indicator waarden naar een opportunity score (0-100). Hogere score = interessanter voor trading. Scoring breakdown: - RSI in koopzone (40-55): +25 punten - EMA bullish crossover: +20 punten - Prijs onder VWAP: +20 punten - BB positie laag (< 0.4): +15 punten - Volume boven gemiddeld: +10 punten - Uptrend (EMA50 slope +): +10 punten Totaal max: 100 """ if not indicators: return AssetScore( symbol=symbol, opportunity_score=0, rsi=0, rsi_signal="unknown", ema9=0, ema21=0, ema_signal="unknown", vwap=0, vwap_signal="unknown", bb_position=0.5, atr_pct=0, volume_score=0, trend_strength="unknown", tradeable=False ) score = 0.0 rsi = indicators["rsi"] close = indicators["close"] # RSI beoordeling if 40 <= rsi <= 55: score += 25 rsi_signal = "koopzone" elif rsi < 30: score += 15 # Oversold maar gevaarlijk — minder punten rsi_signal = "oversold" elif rsi > 70: rsi_signal = "overbought" else: score += 10 rsi_signal = "neutral" # EMA crossover ema9 = indicators["ema9"] ema21 = indicators["ema21"] if ema9 > ema21: score += 20 ema_signal = "bullish" elif ema9 < ema21 * 0.995: ema_signal = "bearish" else: score += 8 ema_signal = "neutral" # VWAP positie vwap = indicators["vwap"] if close < vwap: score += 20 vwap_signal = "onder_vwap" # Potentiële reversion kans else: score += 5 vwap_signal = "boven_vwap" # Bollinger Band positie bb_pos = indicators["bb_position"] if bb_pos < 0.25: score += 15 # Dicht bij lower band elif bb_pos < 0.4: score += 10 # Volume vol_score = indicators["volume_score"] score += (vol_score / 100) * 10 # EMA50 trend if indicators["ema50_slope"] > 0: score += 10 trend_strength = "strong" if indicators["ema50_slope"] > indicators["close"] * 0.001 else "moderate" else: trend_strength = "weak" # ATR filter — te volatiel = niet tradeable atr_pct = indicators["atr_pct"] tradeable = atr_pct < 5.0 # Max 5% ATR return AssetScore( symbol=symbol, opportunity_score=round(min(100, score), 1), rsi=round(rsi, 1), rsi_signal=rsi_signal, ema9=round(ema9, 4), ema21=round(ema21, 4), ema_signal=ema_signal, vwap=round(vwap, 4), vwap_signal=vwap_signal, bb_position=round(bb_pos, 3), atr_pct=round(atr_pct, 2), volume_score=round(vol_score, 1), trend_strength=trend_strength, tradeable=tradeable, )