Supports Interactive Brokers, US and Hong Kong stocks.
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
"""
|
||||
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/HK stocks
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,6 +25,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 IBKR
|
||||
IBKRClient = None
|
||||
|
||||
|
||||
def _signal_to_sides(signal_type: str) -> Tuple[str, str, bool]:
|
||||
"""
|
||||
@@ -144,6 +151,80 @@ 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 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,
|
||||
)
|
||||
|
||||
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/HK 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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""
|
||||
Factory for direct exchange clients.
|
||||
|
||||
Supports:
|
||||
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
|
||||
- Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from app.services.live_trading.base import BaseRestClient, LiveTradingError
|
||||
from app.services.live_trading.binance import BinanceFuturesClient
|
||||
@@ -20,6 +24,10 @@ from app.services.live_trading.kucoin import KucoinSpotClient, KucoinFuturesClie
|
||||
from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient
|
||||
from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativesClient
|
||||
|
||||
# Lazy import IBKR to avoid ImportError if ib_insync not installed
|
||||
IBKRClient = None
|
||||
IBKRConfig = None
|
||||
|
||||
|
||||
def _get(cfg: Dict[str, Any], *keys: str) -> str:
|
||||
for k in keys:
|
||||
@@ -101,6 +109,53 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap")
|
||||
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)
|
||||
|
||||
# Traditional brokers (IBKR for US/HK stocks)
|
||||
if exchange_id == "ibkr":
|
||||
return create_ibkr_client(exchange_config)
|
||||
|
||||
raise LiveTradingError(f"Unsupported exchange_id: {exchange_id}")
|
||||
|
||||
|
||||
def create_ibkr_client(exchange_config: Dict[str, Any]):
|
||||
"""
|
||||
Create IBKR client for US/HK stock trading.
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user