"""Kernstrategie: Signal-Score aus EMA-Cross + RSI, gewichtet durch antrainierbare Gewichte. Die Gewichte werden beim Training (Parameter-Optimierung) so angepasst, dass die Fitness (Return/Sharpe/Drawdown) gestiegen wird. Dadurch 'lernt' der Bot aus den Simulations-Ergebnissen. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Dict, Optional import numpy as np import pandas as pd from .config import StrategyConfig from .indicators import atr, crossover, crossunder, ema, rsi @dataclass class Signal: action: int # +1 long-ein, -1 long-aus, +2 short-ein (fakultativ), 0 halten score: float = 0.0 price: float = 0.0 stop: float = 0.0 reason: str = "" def default_weights() -> Dict[str, float]: return {"ema_cross": 1.0, "rsi_long": 0.8, "rsi_exit": 0.6} class Strategy: def __init__(self, cfg: StrategyConfig, weights: Optional[Dict[str, float]] = None): self.cfg = cfg self.weights = {**default_weights(), **(weights or {})} def parameters(self) -> Dict[str, float]: """Alle anpassbaren Parameter (für den Optimierer).""" return { "fast_period": self.cfg.fast_period, "slow_period": self.cfg.slow_period, "signal_period": self.cfg.signal_period, "rsi_period": self.cfg.rsi_period, "rsi_overbought": self.cfg.rsi_overbought, "rsi_oversold": self.cfg.rsi_oversold, "atr_stop_mult": self.cfg.atr_stop_mult, **{f"w_{k}": v for k, v in self.weights.items()}, } def set_parameter(self, key: str, value: float) -> None: if key.startswith("w_"): self.weights[key[2:]] = max(0.0, value) return if hasattr(self.cfg, key): v = int(round(float(value))) if isinstance(getattr(self.cfg, key), int) and key != "rsi_overbought" and key != "rsi_oversold" and key != "atr_stop_mult" else float(value) if key.endswith("_period"): v = max(2, int(round(float(value)))) setattr(self.cfg, key, v) def prepare(self, df: pd.DataFrame) -> pd.DataFrame: """Berechnet alle Indikatoren auf dem Candles-Frame. Liefert neuen Frame.""" out = df.copy() c = self.cfg out["ema_fast"] = ema(out["close"], c.fast_period) out["ema_slow"] = ema(out["close"], c.slow_period) out["macd"] = out["ema_fast"] - out["ema_slow"] out["signal"] = ema(out["macd"], c.signal_period) out["rsi"] = rsi(out["close"], c.rsi_period) out["atr"] = atr(out, c.atr_period) out["cross_up"] = crossover(out["macd"], out["signal"]) out["cross_dn"] = crossunder(out["macd"], out["signal"]) return out def score_row(self, row: pd.Series) -> float: """Gewichteter Score: > 0 Kauf, < 0 Verkauf/Ausstieg.""" w = self.weights s = 0.0 if row["cross_up"]: s += w["ema_cross"] if row["cross_dn"]: s -= w["ema_cross"] rsi = row["rsi"] if rsi <= self.cfg.rsi_oversold: s += w["rsi_long"] if rsi >= self.cfg.rsi_overbought: s -= w["rsi_exit"] return s def decide(self, prep: pd.DataFrame) -> list[Signal]: """Erzeugt pro Candle ein Signal (für Backtest) bzw. das letzte (Live).""" signals: list[Signal] = [] for i in range(len(prep)): row = prep.iloc[i] if i < max(self.cfg.slow_period, self.cfg.signal_period): signals.append(Signal(0, 0.0, float(row["close"]), 0.0, "warmup")) continue score = self.score_row(row) price = float(row["close"]) stop = price - self.cfg.atr_stop_mult * row["atr"] if self.cfg.allow_long else price if score > 0 and self.cfg.allow_long: action = 1 elif score < 0: action = -1 else: action = 0 reasons = [] if row["cross_up"]: reasons.append("macd_cross_up") if row["rsi"] <= self.cfg.rsi_oversold: reasons.append("rsi_oversold") if score < 0: if row["cross_dn"]: reasons.append("macd_cross_down") if row["rsi"] >= self.cfg.rsi_overbought: reasons.append("rsi_overbought") signals.append( Signal( action, score, price, float(stop) if action == 1 else 0.0, ",".join(reasons) or "neutral", ) ) return signals def last_signal(self, prep: pd.DataFrame) -> Signal: return self.decide(prep)[-1] def fitness( returns: np.ndarray, final_equity: float, initial: float, max_drawdown: float, w_return: float = 0.6, w_sharpe: float = 0.3, w_dd: float = 0.1, ) -> float: """Fitness-Bewertung für den Optimierer (maximieren).""" if len(returns) == 0: return -1.0 total_return = (final_equity / initial) - 1.0 std = float(np.std(returns)) sharpe = (float(np.mean(returns)) / std * np.sqrt(len(returns))) if std > 0 else 0.0 dd_penalty = max_drawdown # 0.3 -> 0.3 Abzug score = w_return * total_return + w_sharpe * (sharpe / 10.0) - w_dd * dd_penalty return float(score)