2025-12-29 03:06:49 +08:00
"""
Factory for direct exchange clients.
2026-01-13 02:35:48 +08:00
Supports:
2026-04-06 01:39:25 +08:00
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex, Deepcoin, HTX
2026-02-27 01:57:04 +08:00
- Traditional brokers: Interactive Brokers (IBKR) for US stocks
2026-01-13 04:01:42 +08:00
- Forex brokers: MetaTrader 5 (MT5)
2025-12-29 03:06:49 +08:00
"""
from __future__ import annotations
2026-01-13 02:35:48 +08:00
from typing import Any , Dict , Union
2025-12-29 03:06:49 +08:00
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
2025-12-29 04:45:59 +08:00
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
2026-01-26 20:32:34 +08:00
from app.services.live_trading.deepcoin import DeepcoinClient
2026-04-06 01:39:25 +08:00
from app.services.live_trading.htx import HtxClient
2025-12-29 03:06:49 +08:00
2026-01-13 02:35:48 +08:00
# Lazy import IBKR to avoid ImportError if ib_insync not installed
IBKRClient = None
IBKRConfig = None
2026-01-13 04:01:42 +08:00
# Lazy import MT5 to avoid ImportError if MetaTrader5 not installed
MT5Client = None
MT5Config = None
2025-12-29 03:06:49 +08:00
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 ""
2026-03-24 19:10:18 +08:00
def _demo_enabled ( cfg : Dict [ str , Any ]) -> bool :
v = cfg . get ( "enable_demo_trading" ) or cfg . get ( "enableDemoTrading" )
if isinstance ( v , bool ):
return v
return str ( v or "" ) . strip () . lower () in ( "true" , "1" , "yes" )
2025-12-29 03:06:49 +08:00
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"
2026-03-24 19:10:18 +08:00
is_demo = _demo_enabled ( exchange_config )
2025-12-29 03:06:49 +08:00
if exchange_id == "binance" :
2026-04-06 01:39:25 +08:00
spot_broker_id = _get ( exchange_config , "spot_broker_id" , "spotBrokerId" , "broker_id" , "brokerId" ) or "A2NAPZAC"
futures_broker_id = _get ( exchange_config , "futures_broker_id" , "futuresBrokerId" , "broker_id" , "brokerId" ) or "HBpUbQjT"
2025-12-29 03:06:49 +08:00
if mt == "spot" :
2026-01-27 19:18:12 +08:00
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
2026-04-06 01:39:25 +08:00
return BinanceSpotClient ( api_key = api_key , secret_key = secret_key , base_url = base_url , enable_demo_trading = is_demo , broker_id = spot_broker_id )
2026-03-24 19:10:18 +08:00
# Default to USDT-M futures
2026-01-27 19:18:12 +08:00
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
2026-04-06 01:39:25 +08:00
return BinanceFuturesClient ( api_key = api_key , secret_key = secret_key , base_url = base_url , enable_demo_trading = is_demo , broker_id = futures_broker_id )
2025-12-29 03:06:49 +08:00
if exchange_id == "okx" :
base_url = _get ( exchange_config , "base_url" , "baseUrl" ) or "https://www.okx.com"
2026-03-12 00:02:53 +08:00
broker_code = "56fa80b0ce8cBCDE"
return OkxClient (
api_key = api_key ,
secret_key = secret_key ,
passphrase = passphrase ,
base_url = base_url ,
2026-03-24 19:10:18 +08:00
broker_code = broker_code ,
simulated_trading = is_demo ,
2026-03-12 00:02:53 +08:00
)
2025-12-29 03:06:49 +08:00
if exchange_id == "bitget" :
2026-03-24 19:10:18 +08:00
# Bitget simulated trading uses the same REST host; keys must be created in Bitget demo trading.
2025-12-29 03:06:49 +08:00
base_url = _get ( exchange_config , "base_url" , "baseUrl" ) or "https://api.bitget.com"
if mt == "spot" :
2026-03-21 18:32:04 +08:00
channel_api_code = _get ( exchange_config , "channel_api_code" , "channelApiCode" ) or "qvz9x"
2026-04-06 01:39:25 +08:00
return BitgetSpotClient (
api_key = api_key ,
secret_key = secret_key ,
passphrase = passphrase ,
base_url = base_url ,
channel_api_code = channel_api_code ,
simulated_trading = is_demo ,
)
2026-03-21 18:32:04 +08:00
channel_api_code = _get ( exchange_config , "channel_api_code" , "channelApiCode" ) or "qvz9x"
2026-04-06 01:39:25 +08:00
return BitgetMixClient (
api_key = api_key ,
secret_key = secret_key ,
passphrase = passphrase ,
base_url = base_url ,
channel_api_code = channel_api_code ,
simulated_trading = is_demo ,
)
2025-12-29 03:06:49 +08:00
2025-12-29 04:45:59 +08:00
if exchange_id == "bybit" :
2026-03-24 19:10:18 +08:00
default_bybit = "https://api-testnet.bybit.com" if is_demo else "https://api.bybit.com"
base_url = _get ( exchange_config , "base_url" , "baseUrl" ) or default_bybit
2025-12-29 04:45:59 +08:00
category = "spot" if mt == "spot" else "linear"
recv_window_ms = int ( exchange_config . get ( "recv_window_ms" ) or exchange_config . get ( "recvWindow" ) or 5000 )
2026-04-06 01:39:25 +08:00
broker_referer = _get ( exchange_config , "bybit_referer" , "broker_referer" , "brokerReferer" ) or "Ri001020"
hedge_mode_raw = exchange_config . get ( "hedge_mode" )
if hedge_mode_raw is None :
hedge_mode_raw = exchange_config . get ( "hedgeMode" )
if hedge_mode_raw is None :
hedge_mode_raw = exchange_config . get ( "position_mode" ) or exchange_config . get ( "positionMode" )
hedge_mode = False
if isinstance ( hedge_mode_raw , bool ):
hedge_mode = hedge_mode_raw
else :
hedge_mode = str ( hedge_mode_raw or "" ) . strip () . lower () in ( "true" , "1" , "yes" , "hedge" , "both_side" )
2026-03-21 18:32:04 +08:00
return BybitClient (
api_key = api_key ,
secret_key = secret_key ,
base_url = base_url ,
category = category ,
recv_window_ms = recv_window_ms ,
2026-04-06 01:39:25 +08:00
broker_referer = broker_referer ,
hedge_mode = hedge_mode ,
2026-03-21 18:32:04 +08:00
)
2025-12-29 04:45:59 +08:00
if exchange_id in ( "coinbaseexchange" , "coinbase_exchange" ):
2026-03-24 19:10:18 +08:00
default_cb = "https://api-public.sandbox.exchange.coinbase.com" if is_demo else "https://api.exchange.coinbase.com"
base_url = _get ( exchange_config , "base_url" , "baseUrl" ) or default_cb
2025-12-29 04:45:59 +08:00
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" :
2026-03-24 19:10:18 +08:00
# Kraken spot REST has no separate public sandbox URL; use demo keys on production API if offered by Kraken.
2025-12-29 04:45:59 +08:00
return KrakenClient ( api_key = api_key , secret_key = secret_key , base_url = base_url )
2026-03-24 19:10:18 +08:00
fut_default = "https://demo-futures.kraken.com" if is_demo else "https://futures.kraken.com"
fut_url = _get ( exchange_config , "futures_base_url" , "futuresBaseUrl" ) or fut_default
2025-12-29 04:45:59 +08:00
return KrakenFuturesClient ( api_key = api_key , secret_key = secret_key , base_url = fut_url )
if exchange_id == "kucoin" :
2026-03-24 19:10:18 +08:00
default_spot = "https://openapi-sandbox.kucoin.com" if is_demo else "https://api.kucoin.com"
base_url = _get ( exchange_config , "base_url" , "baseUrl" ) or default_spot
2025-12-29 04:45:59 +08:00
if mt == "spot" :
return KucoinSpotClient ( api_key = api_key , secret_key = secret_key , passphrase = passphrase , base_url = base_url )
2026-03-24 19:10:18 +08:00
fut_default = "https://api-sandbox-futures.kucoin.com" if is_demo else "https://api-futures.kucoin.com"
fut_url = _get ( exchange_config , "futures_base_url" , "futuresBaseUrl" ) or fut_default
2025-12-29 04:45:59 +08:00
return KucoinFuturesClient ( api_key = api_key , secret_key = secret_key , passphrase = passphrase , base_url = fut_url )
if exchange_id == "gate" :
2026-04-06 01:39:25 +08:00
gate_channel_id = _get ( exchange_config , "gate_channel_id" , "gateChannelId" ) or "dinger"
2025-12-29 04:45:59 +08:00
if mt == "spot" :
2026-03-24 19:10:18 +08:00
default_gate = "https://api-testnet.gateio.ws" if is_demo else "https://api.gateio.ws"
base_url = _get ( exchange_config , "base_url" , "baseUrl" ) or default_gate
2026-04-06 01:39:25 +08:00
return GateSpotClient ( api_key = api_key , secret_key = secret_key , base_url = base_url , channel_id = gate_channel_id )
default_fut = "https://fx-api-testnet.gateio.ws" if is_demo else "https://fx-api.gateio.ws"
2026-03-24 19:10:18 +08:00
base_url = _get ( exchange_config , "base_url" , "baseUrl" ) or default_fut
2026-04-06 01:39:25 +08:00
return GateUsdtFuturesClient ( api_key = api_key , secret_key = secret_key , base_url = base_url , channel_id = gate_channel_id )
2025-12-29 04:45:59 +08:00
if exchange_id == "bitfinex" :
2026-03-24 19:10:18 +08:00
# Same REST host; use keys from Bitfinex paper/sub-account where applicable.
2025-12-29 04:45:59 +08:00
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 )
2026-01-26 20:32:34 +08:00
if exchange_id == "deepcoin" :
2026-04-06 01:39:25 +08:00
if is_demo and not ( _get ( exchange_config , "base_url" , "baseUrl" )):
raise LiveTradingError ( "Deepcoin demo/testnet is not configured in this project yet. Please disable demo mode or provide an explicit testnet base_url." )
2026-01-26 20:32:34 +08:00
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 ,
)
2026-04-06 01:39:25 +08:00
if exchange_id == "htx" :
if is_demo and not ( _get ( exchange_config , "base_url" , "baseUrl" ) or _get ( exchange_config , "futures_base_url" , "futuresBaseUrl" )):
raise LiveTradingError ( "HTX demo/testnet is not configured in this project yet. Please disable demo mode or provide explicit testnet base_url/futures_base_url." )
spot_url = _get ( exchange_config , "base_url" , "baseUrl" ) or "https://api.huobi.pro"
futures_url = _get ( exchange_config , "futures_base_url" , "futuresBaseUrl" ) or "https://api.hbdm.com"
broker_id = _get ( exchange_config , "broker_id" , "brokerId" ) or "AA7b890547"
return HtxClient (
api_key = api_key ,
secret_key = secret_key ,
base_url = spot_url ,
futures_base_url = futures_url ,
market_type = mt ,
broker_id = broker_id ,
)
2026-02-27 01:57:04 +08:00
# Traditional brokers (IBKR for US stocks only)
2026-01-13 02:35:48 +08:00
if exchange_id == "ibkr" :
2026-01-13 04:01:42 +08:00
# Note: Market category validation should be done at the caller level
# This factory only creates clients based on exchange_id
2026-01-13 02:35:48 +08:00
return create_ibkr_client ( exchange_config )
2026-01-13 04:01:42 +08:00
# 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 )
2025-12-29 03:06:49 +08:00
raise LiveTradingError ( f "Unsupported exchange_id: { exchange_id } " )
2026-01-13 02:35:48 +08:00
def create_ibkr_client ( exchange_config : Dict [ str , Any ]):
"""
2026-02-27 01:57:04 +08:00
Create IBKR client for US stock trading.
2026-01-13 02:35:48 +08:00
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
2026-01-13 04:01:42 +08:00
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
2026-03-12 00:02:53 +08:00
- market_category: Must be "Forex" (validated)
Note: MT5 is ONLY for Forex trading, not for Crypto or Stocks.
2026-01-13 04:01:42 +08:00
"""
global MT5Client , MT5Config
2026-03-12 00:02:53 +08:00
# Validate market category - MT5 is ONLY for Forex
market_category = str ( exchange_config . get ( "market_category" ) or "" ) . strip ()
if market_category and market_category != "Forex" :
raise LiveTradingError (
f "MT5 can only be used for Forex trading, but market_category is ' { market_category } '. "
f "MT5 does not support Crypto or Stock trading. Please use MT5 only with Forex market."
)
2026-01-13 04:01:42 +08:00
# 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."
)
2026-03-12 00:02:53 +08:00
# Handle login as int (may come as string from JSON)
login_raw = exchange_config . get ( "mt5_login" ) or 0
try :
login = int ( login_raw ) if login_raw else 0
except ( ValueError , TypeError ):
# Try converting string to int
try :
login = int ( str ( login_raw ) . strip ())
except ( ValueError , TypeError ):
login = 0
2026-01-13 04:01:42 +08:00
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