Initial commit: TradeMind – Krypto-Trading-Bot mit Lernmodus
Per Podman deploybarer Bot, der Käufe und Verkäufe simuliert ausführt und sich aus den Ergebnissen weiter antrainiert. Aufbau - Einheitliche Bar-Verarbeitung für paper, backtest und live; ausgetauscht werden nur Datenquelle und Broker. - Börsenanbindung über ccxt: rund 100 Börsen allein über exchange.id erreichbar. Zugangsdaten kommen über ENV-Platzhalter, der Live-Modus ist doppelt abgesichert. - Paper-Broker mit Gebühren, Slippage, Börsenpräzision und Volumengrenzen. - Online trainierte logistische Regression bewertet jedes Einstiegssignal. Sie lernt aus realen Trade-Ergebnissen, aus Shadow-Labels aller Kandidaten – auch der abgelehnten – und aus Hintergrund-Stichproben; beim Kaltstart wird sie aus der Kurshistorie vorgelernt. - Risikomanagement: Positions- und Exposure-Grenzen, ATR-Stops, Cooldown sowie Tagesverlust- und Drawdown-Notbremsen. - SQLite-Persistenz, HTTP-Status mit Prometheus-Metriken und Dashboard, Webhooks. Deployment - Containerfile (zweistufig, non-root UID 10001), podman-compose, systemd-Quadlet. - Modell und Datenbank liegen im Volume /data und überleben Neustarts. 128 Tests, ruff sauber. Verifiziert gegen echte Marktdaten sowie im gebauten Container inklusive Healthcheck und Zustandswiederherstellung.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""Datenmodelle: Kerzen, Signale, Orders, Positionen, Trades."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Side(str, Enum):
|
||||
BUY = "buy"
|
||||
SELL = "sell"
|
||||
|
||||
|
||||
class Action(str, Enum):
|
||||
HOLD = "hold"
|
||||
ENTER_LONG = "enter_long"
|
||||
EXIT_LONG = "exit_long"
|
||||
|
||||
|
||||
class ExitReason(str, Enum):
|
||||
STOP_LOSS = "stop_loss"
|
||||
TAKE_PROFIT = "take_profit"
|
||||
TRAILING_STOP = "trailing_stop"
|
||||
SIGNAL = "signal"
|
||||
MAX_HOLDING = "max_holding"
|
||||
RISK_HALT = "risk_halt"
|
||||
SHUTDOWN = "shutdown"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Candles:
|
||||
"""OHLCV-Zeitreihe in Spaltenform. ``timestamp`` in Millisekunden (UTC)."""
|
||||
|
||||
symbol: str
|
||||
timeframe: str
|
||||
timestamp: np.ndarray
|
||||
open: np.ndarray
|
||||
high: np.ndarray
|
||||
low: np.ndarray
|
||||
close: np.ndarray
|
||||
volume: np.ndarray
|
||||
|
||||
def __len__(self) -> int:
|
||||
return int(self.close.size)
|
||||
|
||||
@classmethod
|
||||
def from_rows(cls, symbol: str, timeframe: str, rows: list[list[float]]) -> Candles:
|
||||
"""Erzeugt eine Serie aus ccxt-OHLCV-Zeilen ``[ts, o, h, l, c, v]``."""
|
||||
if not rows:
|
||||
empty = np.empty(0, dtype=np.float64)
|
||||
return cls(symbol, timeframe, np.empty(0, dtype=np.int64), empty, empty, empty, empty, empty)
|
||||
arr = np.asarray(rows, dtype=np.float64)
|
||||
return cls(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
timestamp=arr[:, 0].astype(np.int64),
|
||||
open=arr[:, 1].copy(),
|
||||
high=arr[:, 2].copy(),
|
||||
low=arr[:, 3].copy(),
|
||||
close=arr[:, 4].copy(),
|
||||
volume=arr[:, 5].copy(),
|
||||
)
|
||||
|
||||
def slice(self, start: int, stop: int) -> Candles:
|
||||
return Candles(
|
||||
symbol=self.symbol,
|
||||
timeframe=self.timeframe,
|
||||
timestamp=self.timestamp[start:stop],
|
||||
open=self.open[start:stop],
|
||||
high=self.high[start:stop],
|
||||
low=self.low[start:stop],
|
||||
close=self.close[start:stop],
|
||||
volume=self.volume[start:stop],
|
||||
)
|
||||
|
||||
def last_price(self) -> float:
|
||||
return float(self.close[-1])
|
||||
|
||||
def last_timestamp(self) -> int:
|
||||
return int(self.timestamp[-1])
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Signal:
|
||||
action: Action
|
||||
confidence: float = 0.0
|
||||
reason: str = ""
|
||||
exploratory: bool = False
|
||||
features: np.ndarray | None = None
|
||||
feature_names: tuple[str, ...] = ()
|
||||
|
||||
@classmethod
|
||||
def hold(cls, reason: str = "") -> Signal:
|
||||
return cls(action=Action.HOLD, reason=reason)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Fill:
|
||||
"""Ergebnis einer ausgeführten Order."""
|
||||
|
||||
symbol: str
|
||||
side: Side
|
||||
amount: float # Basiswährung, tatsächlich ausgeführt
|
||||
price: float # Durchschnittlicher Ausführungspreis inkl. Slippage
|
||||
fee_quote: float # Gebühr in Quote-Währung
|
||||
timestamp: int # Millisekunden
|
||||
order_id: str = ""
|
||||
requested_amount: float = 0.0
|
||||
|
||||
@property
|
||||
def notional(self) -> float:
|
||||
return self.amount * self.price
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Position:
|
||||
symbol: str
|
||||
amount: float
|
||||
entry_price: float
|
||||
entry_timestamp: int
|
||||
stop_loss: float | None = None
|
||||
take_profit: float | None = None
|
||||
trailing_stop: float | None = None
|
||||
highest_price: float = 0.0
|
||||
bars_held: int = 0
|
||||
entry_fee_quote: float = 0.0
|
||||
entry_features: np.ndarray | None = None
|
||||
entry_confidence: float = 0.0
|
||||
exploratory: bool = False
|
||||
id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
||||
|
||||
def unrealized_pnl(self, price: float) -> float:
|
||||
return (price - self.entry_price) * self.amount
|
||||
|
||||
def unrealized_pct(self, price: float) -> float:
|
||||
if self.entry_price <= 0:
|
||||
return 0.0
|
||||
return (price - self.entry_price) / self.entry_price
|
||||
|
||||
def notional(self, price: float) -> float:
|
||||
return self.amount * price
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Trade:
|
||||
"""Ein abgeschlossener Round-Trip."""
|
||||
|
||||
symbol: str
|
||||
amount: float
|
||||
entry_price: float
|
||||
exit_price: float
|
||||
entry_timestamp: int
|
||||
exit_timestamp: int
|
||||
fees_quote: float
|
||||
pnl_quote: float
|
||||
pnl_pct: float
|
||||
exit_reason: ExitReason
|
||||
bars_held: int
|
||||
entry_confidence: float = 0.0
|
||||
exploratory: bool = False
|
||||
mode: str = "paper"
|
||||
position_id: str = ""
|
||||
|
||||
@property
|
||||
def is_win(self) -> bool:
|
||||
return self.pnl_quote > 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data = asdict(self)
|
||||
data["exit_reason"] = self.exit_reason.value
|
||||
return data
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EquityPoint:
|
||||
timestamp: int
|
||||
equity: float
|
||||
cash: float
|
||||
exposure: float
|
||||
|
||||
@classmethod
|
||||
def now(cls, equity: float, cash: float, exposure: float) -> EquityPoint:
|
||||
return cls(timestamp=int(time.time() * 1000), equity=equity, cash=cash, exposure=exposure)
|
||||
Reference in New Issue
Block a user