Files
Trademind/src/trademind/models.py
T
Tobias Zimmermann 65ed73977e 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.
2026-08-22 08:53:04 +02:00

189 lines
4.8 KiB
Python

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