""" Factory for direct exchange clients. Supports: - Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex - Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks - Forex brokers: MetaTrader 5 (MT5) """ from __future__ import annotations from typing import Any, Dict, Union from app.services.live_trading.base import BaseRestClient, LiveTradingError from app.services.live_trading.binance import BinanceFuturesClient from app.services.live_trading.binance_spot import BinanceSpotClient from app.services.live_trading.okx import OkxClient from app.services.live_trading.bitget import BitgetMixClient from app.services.live_trading.bitget_spot import BitgetSpotClient from app.services.live_trading.bybit import BybitClient from app.services.live_trading.coinbase_exchange import CoinbaseExchangeClient from app.services.live_trading.kraken import KrakenClient from app.services.live_trading.kraken_futures import KrakenFuturesClient from app.services.live_trading.kucoin import KucoinSpotClient, KucoinFuturesClient from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativesClient from app.services.live_trading.deepcoin import DeepcoinClient # Lazy import IBKR to avoid ImportError if ib_insync not installed IBKRClient = None IBKRConfig = None # Lazy import MT5 to avoid ImportError if MetaTrader5 not installed MT5Client = None MT5Config = None def _get(cfg: Dict[str, Any], *keys: str) -> str: for k in keys: v = cfg.get(k) if v is None: continue s = str(v).strip() if s: return s return "" def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap") -> BaseRestClient: if not isinstance(exchange_config, dict): raise LiveTradingError("Invalid exchange_config") exchange_id = _get(exchange_config, "exchange_id", "exchangeId").lower() api_key = _get(exchange_config, "api_key", "apiKey") secret_key = _get(exchange_config, "secret_key", "secret") passphrase = _get(exchange_config, "passphrase", "password") mt = (market_type or exchange_config.get("market_type") or exchange_config.get("defaultType") or "swap").strip().lower() if mt in ("futures", "future", "perp", "perpetual"): mt = "swap" if exchange_id == "binance": # 检查是否启用模拟交易,支持布尔值和字符串 enable_demo = exchange_config.get("enable_demo_trading") or exchange_config.get("enableDemoTrading") is_demo = bool(enable_demo) if isinstance(enable_demo, bool) else str(enable_demo).lower() in ("true", "1", "yes") if mt == "spot": default_url = "https://demo-api.binance.com" if is_demo else "https://api.binance.com" base_url = _get(exchange_config, "base_url", "baseUrl") or default_url return BinanceSpotClient(api_key=api_key, secret_key=secret_key, base_url=base_url, enable_demo_trading=is_demo) # Default to USDT-M futures default_url = "https://demo-fapi.binance.com" if is_demo else "https://fapi.binance.com" base_url = _get(exchange_config, "base_url", "baseUrl") or default_url return BinanceFuturesClient(api_key=api_key, secret_key=secret_key, base_url=base_url, enable_demo_trading=is_demo) if exchange_id == "okx": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://www.okx.com" return OkxClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url) if exchange_id == "bitget": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.bitget.com" if mt == "spot": channel_api_code = _get(exchange_config, "channel_api_code", "channelApiCode") or "bntva" return BitgetSpotClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url, channel_api_code=channel_api_code) return BitgetMixClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url) if exchange_id == "bybit": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.bybit.com" category = "spot" if mt == "spot" else "linear" recv_window_ms = int(exchange_config.get("recv_window_ms") or exchange_config.get("recvWindow") or 5000) return BybitClient(api_key=api_key, secret_key=secret_key, base_url=base_url, category=category, recv_window_ms=recv_window_ms) if exchange_id in ("coinbaseexchange", "coinbase_exchange"): base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.exchange.coinbase.com" if mt != "spot": raise LiveTradingError("CoinbaseExchange only supports spot market_type in this project") return CoinbaseExchangeClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url) if exchange_id == "kraken": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.kraken.com" if mt == "spot": return KrakenClient(api_key=api_key, secret_key=secret_key, base_url=base_url) # Futures/perp fut_url = _get(exchange_config, "futures_base_url", "futuresBaseUrl") or "https://futures.kraken.com" return KrakenFuturesClient(api_key=api_key, secret_key=secret_key, base_url=fut_url) if exchange_id == "kucoin": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.kucoin.com" if mt == "spot": return KucoinSpotClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url) fut_url = _get(exchange_config, "futures_base_url", "futuresBaseUrl") or "https://api-futures.kucoin.com" return KucoinFuturesClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=fut_url) if exchange_id == "gate": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.gateio.ws" if mt == "spot": return GateSpotClient(api_key=api_key, secret_key=secret_key, base_url=base_url) # Default to USDT futures for swap return GateUsdtFuturesClient(api_key=api_key, secret_key=secret_key, base_url=base_url) if exchange_id == "bitfinex": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.bitfinex.com" if mt == "spot": return BitfinexClient(api_key=api_key, secret_key=secret_key, base_url=base_url) return BitfinexDerivativesClient(api_key=api_key, secret_key=secret_key, base_url=base_url) if exchange_id == "deepcoin": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.deepcoin.com" return DeepcoinClient( api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url, market_type=mt, ) # Traditional brokers (IBKR for US/HK stocks only) if exchange_id == "ibkr": # Note: Market category validation should be done at the caller level # This factory only creates clients based on exchange_id return create_ibkr_client(exchange_config) # Forex brokers (MT5 for Forex only) if exchange_id == "mt5": # Note: Market category validation should be done at the caller level # This factory only creates clients based on exchange_id return create_mt5_client(exchange_config) raise LiveTradingError(f"Unsupported exchange_id: {exchange_id}") def create_ibkr_client(exchange_config: Dict[str, Any]): """ Create IBKR client for US/HK stock trading. exchange_config should contain: - ibkr_host: TWS/Gateway host (default: 127.0.0.1) - ibkr_port: TWS/Gateway port (default: 7497) - ibkr_client_id: Client ID (default: 1) - ibkr_account: Account ID (optional, auto-select if empty) """ global IBKRClient, IBKRConfig # Lazy import to avoid ImportError if ib_insync not installed if IBKRClient is None or IBKRConfig is None: try: from app.services.ibkr_trading import IBKRClient as _IBKRClient, IBKRConfig as _IBKRConfig IBKRClient = _IBKRClient IBKRConfig = _IBKRConfig except ImportError: raise LiveTradingError("IBKR trading requires ib_insync. Run: pip install ib_insync") host = str(exchange_config.get("ibkr_host") or "127.0.0.1").strip() port = int(exchange_config.get("ibkr_port") or 7497) client_id = int(exchange_config.get("ibkr_client_id") or 1) account = str(exchange_config.get("ibkr_account") or "").strip() config = IBKRConfig( host=host, port=port, client_id=client_id, account=account, readonly=False, ) client = IBKRClient(config) # Connect immediately (IBKR requires active connection) if not client.connect(): raise LiveTradingError("Failed to connect to IBKR TWS/Gateway. Please check if it's running.") return client def create_mt5_client(exchange_config: Dict[str, Any]): """ Create MT5 client for forex trading. exchange_config should contain: - mt5_login: MT5 account number - mt5_password: MT5 password - mt5_server: Broker server name (e.g., "ICMarkets-Demo") - mt5_terminal_path: Optional path to terminal64.exe """ global MT5Client, MT5Config # Lazy import to avoid ImportError if MetaTrader5 not installed if MT5Client is None or MT5Config is None: try: from app.services.mt5_trading import MT5Client as _MT5Client, MT5Config as _MT5Config MT5Client = _MT5Client MT5Config = _MT5Config except ImportError: raise LiveTradingError( "MT5 trading requires MetaTrader5 library. Run: pip install MetaTrader5\n" "Note: This library only works on Windows." ) login = int(exchange_config.get("mt5_login") or 0) password = str(exchange_config.get("mt5_password") or "").strip() server = str(exchange_config.get("mt5_server") or "").strip() terminal_path = str(exchange_config.get("mt5_terminal_path") or "").strip() if not login or not password or not server: raise LiveTradingError("MT5 requires login, password, and server") config = MT5Config( login=login, password=password, server=server, terminal_path=terminal_path, ) client = MT5Client(config) # Connect immediately if not client.connect(): raise LiveTradingError( "Failed to connect to MT5 terminal. Please check:\n" "1. MT5 terminal is running\n" "2. Credentials are correct\n" "3. You are on Windows" ) return client