Files
DinQuant/backend_api_python/app/services/live_trading/execution.py
T

306 lines
11 KiB
Python

"""
Translate a strategy signal into a direct-exchange order call.
Supports:
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
- 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 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,
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 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,
},
)