Initial release: TradeMind crypto trading bot with paper/live modes and strategy training
This commit is contained in:
@@ -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 {}
|
||||
Reference in New Issue
Block a user