Initial release: TradeMind crypto trading bot with paper/live modes and strategy training

This commit is contained in:
Tobias Zimmermann
2026-08-22 11:53:59 +02:00
commit 7959dd71ff
18 changed files with 1604 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
"""Austauschschicht: abstrakter Broker + ccxt-Implementierung + Mock.
Der Broker versorgt die Engine mit Kursen (für Simulation) und platziert echte
Orders (nur Live-Modus). Für Backtesting/Simulation ohne Netzwerk steht ein
OfflineMock bereit.
"""
from __future__ import annotations
import abc
import logging
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
import pandas as pd
from .config import ExchangeConfig
log = logging.getLogger("trademind.exchange")
@dataclass
class Quote:
bid: float
ask: float
ts: str = ""
class Broker(abc.ABC):
name: str = "abstract"
@abc.abstractmethod
def fetch_ohlcv(
self, symbol: str, timeframe: str, limit: int, since: Optional[int] = None
) -> pd.DataFrame:
"""Liefert OHLCV-Candles als DataFrame mit open/high/low/close/volume."""
@abc.abstractmethod
def fetch_ticker(self, symbol: str) -> Quote:
"""Letztes Bid/Ask."""
@abc.abstractmethod
def create_market_order(
self, symbol: str, side: str, amount: float
) -> Dict[str, Any]:
"""Platziert eine Markerorder. side = 'buy' | 'sell'."""
@abc.abstractmethod
def fetch_balance(self) -> Dict[str, float]:
"""Verfügbare Balancen (free)."""
def close(self) -> None: # pragma: no cover - optional
pass
def build_exchange(cfg: ExchangeConfig) -> Broker:
"""Erzeugt aus der Konfiguration einen konkreten Broker (via ccxt)."""
import ccxt # lazy import für schnellere Tests ohne ccxt
if cfg.name not in ccxt.exchanges:
raise ValueError(f"ccxt kennt Exchange '{cfg.name}' nicht")
klass = getattr(ccxt, cfg.name)
params: Dict[str, Any] = {
"apiKey": cfg.api_key,
"secret": cfg.api_secret,
"password": cfg.password,
"enableRateLimit": True,
}
broker = klass(params)
if cfg.sandbox:
broker.set_sandbox_mode(True)
broker.name = cfg.name
return broker
def build_data_broker(name: str) -> Broker:
"""Erzeugt einen Broker nur für öffentliche Kursdaten (ohne Keys, ohne Sandbox).
`fetch_ohlcv`/`fetch_ticker` sind öffentliche Endpunkte ideal für den
Paper-/Simulationsmodus, der echte Marktkurse nutzt, aber keine Orders platziert.
"""
import ccxt
if name not in ccxt.exchanges:
raise ValueError(f"ccxt kennt Exchange '{name}' nicht")
exchange = getattr(ccxt, name)({"enableRateLimit": True})
return CcxtBroker(exchange, name)
class CcxtBroker(Broker):
"""Wrapper rund um eine ccxt-Exchange-Instanz."""
def __init__(self, exchange: Any, name: str = "ccxt"):
self._ex = exchange
self.name = name
def _symbol(self, symbol: str) -> str:
return symbol if "/" in symbol else symbol
def fetch_ohlcv(
self, symbol: str, timeframe: str, limit: int, since: Optional[int] = None
) -> pd.DataFrame:
raw = self._ex.fetch_ohlcv(self._symbol(symbol), timeframe, since=since, limit=limit)
df = pd.DataFrame(raw, columns=["ts", "open", "high", "low", "close", "volume"])
df["time"] = pd.to_datetime(df["ts"], unit="ms")
return df[["time", "open", "high", "low", "close", "volume"]]
def fetch_ticker(self, symbol: str) -> Quote:
t = self._ex.fetch_ticker(self._symbol(symbol))
return Quote(bid=float(t.get("bid") or t.get("last")),
ask=float(t.get("ask") or t.get("last")), ts=str(t.get("timestamp", "")))
def create_market_order(self, symbol: str, side: str, amount: float) -> Dict[str, Any]:
log.info("LIVE order: %s %s %.8f", side, symbol, amount)
order = self._ex.create_order(self._symbol(symbol), "market", side, amount)
return {"id": order.get("id"), "side": side, "amount": amount, "price": order.get("average")}
def fetch_balance(self) -> Dict[str, float]:
bal = self._ex.fetch_balance()
return {k: float(v.get("free") or 0.0) for k, v in bal.items() if isinstance(v, dict)}
def close(self) -> None:
try:
self._ex.close()
except Exception: # pragma: no cover
pass
class MockBroker(Broker):
"""Erzeugt deterministische OHLCV-Daten, damit Simulation & Backtest offline laufen."""
def __init__(
self,
name: str = "mock",
seed: int = 7,
start_price: float = 50_000.0,
drift: float = 0.0002,
vol: float = 0.02,
quote: Optional[Quote] = None,
):
import numpy as np
self._seed = seed
self._start = start_price
self._drift = drift
self._vol = vol
self._quote = quote
self.name = name
def fetch_ohlcv(
self, symbol: str, timeframe: str, limit: int, since: Optional[int] = None
) -> pd.DataFrame:
import numpy as np
rng = np.random.default_rng(self._seed * 1000 + limit)
n = limit
drift = self._drift
vol = self._vol
steps = drift + vol * rng.standard_normal(n)
close = self._start * np.exp(np.cumsum(steps))
open_ = np.roll(close, 1)
open_[0] = self._start
spread = np.abs(rng.standard_normal(n)) * self._vol * close * 0.5
high = np.maximum(open_, close) + spread
low = np.minimum(open_, close) - spread
volume = np.abs(rng.standard_normal(n)).sum() * 10 + rng.uniform(1, 100, n)
idx = pd.date_range(end=pd.Timestamp.utcnow().floor("h"), periods=n, freq="h")
return pd.DataFrame(
{"time": idx, "open": open_, "high": high, "low": low, "close": close, "volume": volume}
)
def fetch_ticker(self, symbol: str) -> Quote:
if self._quote:
return self._quote
df = self.fetch_ohlcv(symbol, "1h", 1)
last = float(df["close"].iloc[-1])
return Quote(bid=last * 0.99999, ask=last * 1.00001)
def create_market_order(self, symbol: str, side: str, amount: float) -> Dict[str, Any]:
q = self.fetch_ticker(symbol)
price = q.ask if side == "buy" else q.bid
log.info("MOCK order: %s %s %.8f @ %.4f", side, symbol, amount, price)
return {"id": "mock", "side": side, "amount": amount, "price": price}
def fetch_balance(self) -> Dict[str, float]:
return {}