""" Translate a strategy signal into a direct-exchange order call. Supports: - Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex, Deepcoin - Traditional brokers: Interactive Brokers (IBKR) for US stocks - Forex brokers: MetaTrader 5 (MT5) """ from __future__ import annotations from typing import Any, Dict, Optional, Tuple from app.services.live_trading.base import BaseRestClient, LiveOrderResult, 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 from app.services.live_trading.kucoin import KucoinFuturesClient from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativesClient # Lazy import Deepcoin DeepcoinClient = None # Lazy import IBKR IBKRClient = None # Lazy import MT5 MT5Client = None def _signal_to_sides(signal_type: str) -> Tuple[str, str, bool]: """ Returns (side, pos_side, reduce_only) - side: buy/sell - pos_side: long/short (for OKX) """ sig = (signal_type or "").strip().lower() if sig in ("open_long", "add_long"): return "buy", "long", False if sig in ("open_short", "add_short"): return "sell", "short", False if sig in ("close_long", "reduce_long"): return "sell", "long", True if sig in ("close_short", "reduce_short"): return "buy", "short", True raise LiveTradingError(f"Unsupported signal_type: {signal_type}") def place_order_from_signal( client: BaseRestClient, *, signal_type: str, symbol: str, amount: float, market_type: str = "swap", exchange_config: Optional[Dict[str, Any]] = None, client_order_id: Optional[str] = None, ) -> LiveOrderResult: if amount is None: amount = 0.0 qty = float(amount or 0.0) if qty <= 0: raise LiveTradingError("Invalid amount") side, pos_side, reduce_only = _signal_to_sides(signal_type) cfg = exchange_config if isinstance(exchange_config, dict) else {} mt = (market_type or cfg.get("market_type") or "swap").strip().lower() if mt in ("futures", "future", "perp", "perpetual"): mt = "swap" # Spot does not support short signals in this system. if mt == "spot" and ("short" in (signal_type or "").lower()): raise LiveTradingError("spot market does not support short signals") if isinstance(client, BinanceFuturesClient): return client.place_market_order( symbol=symbol, side="BUY" if side == "buy" else "SELL", quantity=qty, reduce_only=reduce_only, position_side=pos_side, client_order_id=client_order_id, ) if isinstance(client, OkxClient): td_mode = (cfg.get("margin_mode") or cfg.get("td_mode") or "cross") return client.place_market_order( symbol=symbol, side=side, pos_side=pos_side, size=qty, market_type=mt, td_mode=str(td_mode), reduce_only=reduce_only, client_order_id=client_order_id, ) if isinstance(client, BitgetMixClient): margin_coin = str(cfg.get("margin_coin") or cfg.get("marginCoin") or "USDT") product_type = str(cfg.get("product_type") or cfg.get("productType") or "USDT-FUTURES") margin_mode = str(cfg.get("margin_mode") or cfg.get("marginMode") or cfg.get("td_mode") or "cross") return client.place_market_order( symbol=symbol, side=side, size=qty, margin_coin=margin_coin, product_type=product_type, margin_mode=margin_mode, reduce_only=reduce_only, client_order_id=client_order_id, ) if isinstance(client, BinanceSpotClient): return client.place_market_order( symbol=symbol, side="BUY" if side == "buy" else "SELL", quantity=qty, client_order_id=client_order_id, ) if isinstance(client, BitgetSpotClient): # For spot market BUY, Bitget may expect quote size; we pass base size here and let caller override if needed. return client.place_market_order( symbol=symbol, side=side, size=qty, client_order_id=client_order_id, ) if isinstance(client, BybitClient): return client.place_market_order( symbol=symbol, side=side, qty=qty, reduce_only=reduce_only, client_order_id=client_order_id, ) if isinstance(client, CoinbaseExchangeClient): return client.place_market_order(symbol=symbol, side=side, size=qty, client_order_id=client_order_id) if isinstance(client, KrakenClient): return client.place_market_order(symbol=symbol, side=side, size=qty, client_order_id=client_order_id) if isinstance(client, KucoinSpotClient): # KuCoin market BUY often requires quote funds; this simplified path does not convert. return client.place_market_order(symbol=symbol, side=side, size=qty, client_order_id=client_order_id, quote_size=False) if isinstance(client, KucoinFuturesClient): return client.place_market_order(symbol=symbol, side=side, size=qty, reduce_only=reduce_only, client_order_id=client_order_id) if isinstance(client, GateSpotClient): return client.place_market_order(symbol=symbol, side=side, size=qty, client_order_id=client_order_id) if isinstance(client, GateUsdtFuturesClient): return client.place_market_order(symbol=symbol, side=side, size=qty, reduce_only=reduce_only, client_order_id=client_order_id) if isinstance(client, BitfinexClient): return client.place_market_order(symbol=symbol, side=side, size=qty, client_order_id=client_order_id) if isinstance(client, BitfinexDerivativesClient): return client.place_market_order(symbol=symbol, side=side, size=qty, client_order_id=client_order_id) if isinstance(client, KrakenFuturesClient): return client.place_market_order(symbol=symbol, side=side, size=qty, reduce_only=reduce_only, client_order_id=client_order_id) # Check for Deepcoin client (lazy import to avoid circular dependency) global DeepcoinClient if DeepcoinClient is None: try: from app.services.live_trading.deepcoin import DeepcoinClient as _DeepcoinClient DeepcoinClient = _DeepcoinClient except ImportError: pass if DeepcoinClient is not None and isinstance(client, DeepcoinClient): return client.place_market_order( symbol=symbol, side=side, qty=qty, reduce_only=reduce_only, pos_side=pos_side, client_order_id=client_order_id, ) # Check for IBKR client (lazy import to avoid circular dependency) global IBKRClient if IBKRClient is None: try: from app.services.ibkr_trading import IBKRClient as _IBKRClient IBKRClient = _IBKRClient except ImportError: pass if IBKRClient is not None and isinstance(client, IBKRClient): return _place_ibkr_order( client=client, signal_type=signal_type, symbol=symbol, amount=qty, exchange_config=exchange_config, ) # Check for MT5 client (lazy import to avoid circular dependency) global MT5Client if MT5Client is None: try: from app.services.mt5_trading import MT5Client as _MT5Client MT5Client = _MT5Client except ImportError: pass if MT5Client is not None and isinstance(client, MT5Client): return _place_mt5_order( client=client, signal_type=signal_type, symbol=symbol, amount=qty, exchange_config=exchange_config, ) raise LiveTradingError(f"Unsupported client type: {type(client)}") def _place_ibkr_order( client, *, signal_type: str, symbol: str, amount: float, exchange_config: Optional[Dict[str, Any]] = None, ) -> LiveOrderResult: """ Place order via IBKR for US stocks. Signal mapping for stocks (no short selling in this implementation): - open_long / add_long -> BUY - close_long / reduce_long -> SELL - open_short / close_short -> Not supported (raises error) """ sig = (signal_type or "").strip().lower() # Stock trading: no short selling support in basic implementation if "short" in sig: raise LiveTradingError("IBKR stock trading does not support short signals in this implementation") # Determine action if sig in ("open_long", "add_long"): action = "buy" elif sig in ("close_long", "reduce_long"): action = "sell" else: raise LiveTradingError(f"Unsupported signal_type for IBKR: {signal_type}") # Get market type from config cfg = exchange_config if isinstance(exchange_config, dict) else {} market_type = str(cfg.get("market_type") or cfg.get("market_category") or "USStock").strip() # Place market order result = client.place_market_order( symbol=symbol, action=action, quantity=amount, market_type=market_type, ) # Convert IBKRClient result to LiveOrderResult format return LiveOrderResult( success=result.success, exchange_order_id=str(result.order_id) if result.order_id else "", filled=result.filled, avg_price=result.avg_price, raw={ "status": result.status, "message": result.message, "raw": result.raw, }, ) def _place_mt5_order( client, *, signal_type: str, symbol: str, amount: float, exchange_config: Optional[Dict[str, Any]] = None, ) -> LiveOrderResult: """ Place order via MT5 for forex trading. Signal mapping for forex: - open_long / add_long -> BUY - close_long / reduce_long -> SELL - open_short / add_short -> SELL - close_short / reduce_short -> BUY """ sig = (signal_type or "").strip().lower() # Determine action based on signal if sig in ("open_long", "add_long"): action = "buy" elif sig in ("close_long", "reduce_long"): action = "sell" elif sig in ("open_short", "add_short"): action = "sell" elif sig in ("close_short", "reduce_short"): action = "buy" else: raise LiveTradingError(f"Unsupported signal_type for MT5: {signal_type}") # Place market order result = client.place_market_order( symbol=symbol, side=action, volume=amount, comment="QuantDinger", ) # Convert MT5Client result to LiveOrderResult format return LiveOrderResult( success=result.success, exchange_order_id=str(result.order_id) if result.order_id else "", filled=result.filled, avg_price=result.price, raw={ "status": result.status, "message": result.message, "deal_id": result.deal_id, "raw": result.raw, }, )