65ed73977e
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.
189 lines
4.8 KiB
Python
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)
|