Initial release: TradeMind crypto trading bot with paper/live modes and strategy training

This commit is contained in:
Tobias Zimmermann
2026-08-22 11:53:59 +02:00
commit 7959dd71ff
18 changed files with 1604 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
"""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)