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