Files
2026-10-30 09:20:09 +01:00

210 lines
6.6 KiB
Python

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