Files

188 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 {}