Files
DinQuant/backend_api_python/app/services/live_trading/execution.py
T
Dinger 05f07ee544 refactor deployment config and exchange integrations
Simplify runtime configuration and remove legacy database and settings surface so new installs are easier to operate.
Refresh deployment assets, docs, and order execution behavior to keep the packaged app aligned with the current backend.

Made-with: Cursor
2026-03-21 18:32:04 +08:00

381 lines
13 KiB
Python

"""
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 _normalize_symbol_for_order(symbol: str, market_type: str = "swap") -> str:
"""
规范化符号格式,确保符号符合交易所要求。
处理各种输入格式:
- BTC/USDT -> BTC/USDT
- BTCUSDT -> BTC/USDT
- BTC/USDT:USDT -> BTC/USDT
- PI, TRX -> PI/USDT, TRX/USDT (默认添加 /USDT)
Args:
symbol: 原始符号
market_type: 市场类型 (spot/swap)
Returns:
规范化后的符号
"""
if not symbol:
return symbol
sym = symbol.strip()
# 移除 swap/futures 后缀
if ':' in sym:
sym = sym.split(':', 1)[0]
sym = sym.upper()
# 如果已经有分隔符,直接返回(假设格式正确)
if '/' in sym:
return sym
# 尝试从常见报价货币中识别
common_quotes = ['USDT', 'USD', 'BTC', 'ETH', 'BUSD', 'USDC']
for quote in common_quotes:
if sym.endswith(quote) and len(sym) > len(quote):
base = sym[:-len(quote)]
if base:
return f"{base}/{quote}"
# 如果无法识别,默认使用 USDT
return f"{sym}/USDT"
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")
# 规范化符号格式(统一处理裸符号如 PI, TRX 等)
symbol = _normalize_symbol_for_order(symbol, market_type=mt)
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,
pos_side=pos_side,
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}")
# Normalize symbol before placing order (MT5 requires specific format)
from app.services.mt5_trading.symbols import normalize_symbol
normalized_symbol = normalize_symbol(symbol)
# Place market order
result = client.place_market_order(
symbol=normalized_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,
},
)