Initial release: TradeMind crypto trading bot with paper/live modes and strategy training
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user