@@ -2,7 +2,7 @@
|
||||
Translate a strategy signal into a direct-exchange order call.
|
||||
|
||||
Supports:
|
||||
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
|
||||
- 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)
|
||||
"""
|
||||
@@ -26,6 +26,9 @@ 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
|
||||
|
||||
@@ -94,6 +97,7 @@ def place_order_from_signal(
|
||||
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,
|
||||
@@ -155,6 +159,25 @@ def place_order_from_signal(
|
||||
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:
|
||||
|
||||
@@ -294,21 +294,79 @@ class OkxClient(BaseRestClient):
|
||||
if params:
|
||||
# OKX expects the query string in the signed request path. Keep key order stable.
|
||||
# Convert all values to string to avoid "True"/"False" surprises.
|
||||
norm = {str(k): "" if v is None else str(v) for k, v in dict(params).items()}
|
||||
qs = urlencode(sorted(norm.items()), doseq=True)
|
||||
# Filter out empty strings and None values (OKX doesn't like empty params)
|
||||
norm = {str(k): str(v) for k, v in dict(params).items() if v is not None and str(v).strip() != ""}
|
||||
if norm:
|
||||
# Sort by key to ensure consistent ordering (OKX requirement)
|
||||
qs = urlencode(sorted(norm.items()), doseq=True)
|
||||
|
||||
signed_path = f"{path}?{qs}" if qs else path
|
||||
sign = self._sign(ts, method, signed_path, body_str)
|
||||
|
||||
# For GET requests with query params, we need to ensure the actual request URL matches the signed path
|
||||
# OKX requires exact match between signed path and actual request path
|
||||
if method.upper() == "GET" and qs:
|
||||
# Append query string directly to path to match signature exactly
|
||||
# Don't use params parameter to avoid double encoding
|
||||
request_path = f"{path}?{qs}"
|
||||
request_params = None
|
||||
else:
|
||||
request_path = path
|
||||
request_params = params
|
||||
|
||||
code, data, text = self._request(
|
||||
method,
|
||||
path,
|
||||
params=params,
|
||||
request_path,
|
||||
params=request_params,
|
||||
data=body_str if body_str else None,
|
||||
headers=self._headers(ts, sign),
|
||||
)
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"OKX HTTP {code}: {text[:500]}")
|
||||
# Provide more helpful error messages for common permission issues
|
||||
error_msg = text[:500] if text else f"HTTP {code}"
|
||||
if code == 401:
|
||||
error_code = ""
|
||||
if isinstance(data, dict):
|
||||
error_code = str(data.get("code") or "")
|
||||
if error_code == "50120" or "permission" in error_msg.lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX API permission error (HTTP {code}, code {error_code}): {error_msg}\n"
|
||||
f"Solution: Please enable 'Trade' permission for your API key in OKX account.\n"
|
||||
f"Path: OKX website -> API Management -> Edit API Key -> Enable 'Trade' permission"
|
||||
)
|
||||
raise LiveTradingError(f"OKX HTTP {code}: {error_msg}")
|
||||
if isinstance(data, dict) and str(data.get("code") or "") not in ("0", ""):
|
||||
error_code = str(data.get("code") or "")
|
||||
error_msg = str(data.get("msg") or data)
|
||||
|
||||
# Check for specific error codes in data array
|
||||
data_array = data.get("data", [])
|
||||
if isinstance(data_array, list) and data_array:
|
||||
first_item = data_array[0] if isinstance(data_array[0], dict) else {}
|
||||
s_code = str(first_item.get("sCode") or "")
|
||||
s_msg = str(first_item.get("sMsg") or "")
|
||||
|
||||
# Error code 51008: Insufficient margin
|
||||
if s_code == "51008" or "insufficient" in s_msg.lower() or "margin" in s_msg.lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX insufficient margin error (code {s_code}): {s_msg}\n"
|
||||
f"Solution: Please ensure you have sufficient USDT margin in your account to place this order."
|
||||
)
|
||||
# Error code 50120: Permission error
|
||||
if s_code == "50120" or error_code == "50120" or "permission" in str(error_msg).lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX API permission error (code {s_code or error_code}): {s_msg or error_msg}\n"
|
||||
f"Solution: Please enable 'Trade' permission for your API key in OKX account.\n"
|
||||
f"Path: OKX website -> API Management -> Edit API Key -> Enable 'Trade' permission"
|
||||
)
|
||||
|
||||
# Fallback for permission errors
|
||||
if error_code == "50120" or "permission" in str(error_msg).lower():
|
||||
raise LiveTradingError(
|
||||
f"OKX API permission error (code {error_code}): {error_msg}\n"
|
||||
f"Solution: Please enable 'Trade' permission for your API key in OKX account.\n"
|
||||
f"Path: OKX website -> API Management -> Edit API Key -> Enable 'Trade' permission"
|
||||
)
|
||||
raise LiveTradingError(f"OKX error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
|
||||
@@ -316,21 +374,44 @@ class OkxClient(BaseRestClient):
|
||||
code, data, _ = self._request("GET", "/api/v5/public/time")
|
||||
return code == 200 and isinstance(data, dict)
|
||||
|
||||
def get_ticker(self, *, inst_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get ticker price for an instrument.
|
||||
|
||||
Endpoint: GET /api/v5/market/ticker?instId=...
|
||||
"""
|
||||
if not inst_id:
|
||||
return {}
|
||||
raw = self._public_request("GET", "/api/v5/market/ticker", params={"instId": inst_id})
|
||||
data = (raw.get("data") or []) if isinstance(raw, dict) else []
|
||||
first: Dict[str, Any] = data[0] if isinstance(data, list) and data else {}
|
||||
return first if isinstance(first, dict) else {}
|
||||
|
||||
def get_balance(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Private endpoint to validate credentials (best-effort).
|
||||
"""
|
||||
return self._signed_request("GET", "/api/v5/account/balance")
|
||||
|
||||
def get_positions(self, *, inst_id: str = "") -> Dict[str, Any]:
|
||||
def get_positions(self, *, inst_id: str = "", inst_type: str = "SWAP") -> Dict[str, Any]:
|
||||
"""
|
||||
Get swap positions (best-effort).
|
||||
Get positions (best-effort).
|
||||
|
||||
Args:
|
||||
inst_id: Instrument ID (optional, for filtering)
|
||||
inst_type: Instrument type - "SPOT" or "SWAP" (default: "SWAP")
|
||||
|
||||
Endpoint: GET /api/v5/account/positions
|
||||
"""
|
||||
params: Dict[str, Any] = {"instType": "SWAP"}
|
||||
if inst_id:
|
||||
params["instId"] = str(inst_id)
|
||||
# Validate inst_type
|
||||
it = str(inst_type or "SWAP").strip().upper()
|
||||
if it not in ("SPOT", "SWAP", "FUTURES", "OPTION"):
|
||||
it = "SWAP"
|
||||
|
||||
params: Dict[str, Any] = {"instType": it}
|
||||
# Only add instId if it's not empty
|
||||
if inst_id and str(inst_id).strip():
|
||||
params["instId"] = str(inst_id).strip()
|
||||
return self._signed_request("GET", "/api/v5/account/positions", params=params)
|
||||
|
||||
def set_leverage(self, *, inst_id: str, lever: float, mgn_mode: str = "cross", pos_side: str = "") -> bool:
|
||||
|
||||
Reference in New Issue
Block a user