Refactor code for improved readability and consistency
- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts. - Consolidated import statements and removed unnecessary blank lines. - Updated logging configuration for better clarity. - Enhanced the safe execution code with improved error handling and logging. - Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts. - Added a pyproject.toml for Ruff and Vulture configuration. - Introduced requirements-dev.txt for development dependencies. - Removed commented-out stock entries in init.sql for cleaner migration scripts.
This commit is contained in:
@@ -125,7 +125,7 @@ Then remove the related import and registration code in `app/routes/__init__.py`
|
||||
|
||||
## Docker Note
|
||||
|
||||
When running in Docker, IBKR trading requires TWS/IB Gateway to be accessible from the container.
|
||||
When running in Docker, IBKR trading requires TWS/IB Gateway to be accessible from the container.
|
||||
For local deployment, you can:
|
||||
|
||||
1. Run TWS/Gateway on host machine
|
||||
|
||||
@@ -11,4 +11,4 @@ Port Reference:
|
||||
from app.services.ibkr_trading.client import IBKRClient, IBKRConfig
|
||||
from app.services.ibkr_trading.symbols import normalize_symbol, parse_symbol
|
||||
|
||||
__all__ = ['IBKRClient', 'IBKRConfig', 'normalize_symbol', 'parse_symbol']
|
||||
__all__ = ["IBKRClient", "IBKRConfig", "normalize_symbol", "parse_symbol"]
|
||||
|
||||
@@ -4,14 +4,13 @@ Interactive Brokers Trading Client
|
||||
Uses ib_insync library to connect to TWS or IB Gateway for trading.
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
import asyncio
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.services.ibkr_trading.symbols import format_display_symbol, normalize_symbol
|
||||
from app.utils.logger import get_logger
|
||||
from app.services.ibkr_trading.symbols import normalize_symbol, format_display_symbol
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -19,7 +18,7 @@ logger = get_logger(__name__)
|
||||
def _ensure_event_loop():
|
||||
"""
|
||||
Ensure there is an event loop in the current thread.
|
||||
|
||||
|
||||
ib_insync requires an asyncio event loop to function.
|
||||
When called from Flask request threads, there may not be one.
|
||||
"""
|
||||
@@ -34,6 +33,7 @@ def _ensure_event_loop():
|
||||
logger.debug("Created new event loop for IBKR client")
|
||||
return loop
|
||||
|
||||
|
||||
# Lazy import ib_insync to allow other features to work without it installed
|
||||
ib_insync = None
|
||||
|
||||
@@ -44,17 +44,17 @@ def _ensure_ib_insync():
|
||||
if ib_insync is None:
|
||||
try:
|
||||
import ib_insync as _ib
|
||||
|
||||
ib_insync = _ib
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"ib_insync is not installed. Run: pip install ib_insync"
|
||||
)
|
||||
raise ImportError("ib_insync is not installed. Run: pip install ib_insync")
|
||||
return ib_insync
|
||||
|
||||
|
||||
@dataclass
|
||||
class IBKRConfig:
|
||||
"""IBKR connection configuration."""
|
||||
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 7497 # TWS Live:7497, TWS Paper:7496, Gateway Live:4001, Gateway Paper:4002
|
||||
client_id: int = 1
|
||||
@@ -66,6 +66,7 @@ class IBKRConfig:
|
||||
@dataclass
|
||||
class OrderResult:
|
||||
"""Order execution result."""
|
||||
|
||||
success: bool
|
||||
order_id: int = 0
|
||||
filled: float = 0.0
|
||||
@@ -78,67 +79,69 @@ class OrderResult:
|
||||
class IBKRClient:
|
||||
"""
|
||||
Interactive Brokers Trading Client
|
||||
|
||||
|
||||
Usage:
|
||||
config = IBKRConfig(port=7497) # TWS Live
|
||||
client = IBKRClient(config)
|
||||
|
||||
|
||||
if client.connect():
|
||||
# Place order
|
||||
result = client.place_market_order("AAPL", "buy", 10, "USStock")
|
||||
|
||||
|
||||
# Get positions
|
||||
positions = client.get_positions()
|
||||
|
||||
|
||||
client.disconnect()
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, config: Optional[IBKRConfig] = None):
|
||||
self.config = config or IBKRConfig()
|
||||
self._ib = None
|
||||
self._connected = False
|
||||
self._lock = threading.Lock()
|
||||
self._account = ""
|
||||
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""Check if connected."""
|
||||
if self._ib is None:
|
||||
return False
|
||||
return self._ib.isConnected()
|
||||
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
Connect to TWS or IB Gateway.
|
||||
|
||||
|
||||
Returns:
|
||||
True if connected successfully
|
||||
"""
|
||||
with self._lock:
|
||||
if self.connected:
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
# Ensure event loop exists in this thread (required by ib_insync)
|
||||
_ensure_event_loop()
|
||||
|
||||
|
||||
_ensure_ib_insync()
|
||||
|
||||
|
||||
if self._ib is None:
|
||||
self._ib = ib_insync.IB()
|
||||
|
||||
logger.info(f"Connecting to IBKR: {self.config.host}:{self.config.port} (clientId={self.config.client_id})")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Connecting to IBKR: {self.config.host}:{self.config.port} (clientId={self.config.client_id})"
|
||||
)
|
||||
|
||||
self._ib.connect(
|
||||
host=self.config.host,
|
||||
port=self.config.port,
|
||||
clientId=self.config.client_id,
|
||||
readonly=self.config.readonly,
|
||||
timeout=self.config.timeout
|
||||
timeout=self.config.timeout,
|
||||
)
|
||||
|
||||
|
||||
self._connected = True
|
||||
|
||||
|
||||
# Get account
|
||||
accounts = self._ib.managedAccounts()
|
||||
if accounts:
|
||||
@@ -146,14 +149,14 @@ class IBKRClient:
|
||||
logger.info(f"IBKR connected, account: {self._account}")
|
||||
else:
|
||||
logger.warning("IBKR connected but no account info retrieved")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"IBKR connection failed: {e}")
|
||||
self._connected = False
|
||||
return False
|
||||
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from IBKR."""
|
||||
with self._lock:
|
||||
@@ -165,7 +168,7 @@ class IBKRClient:
|
||||
finally:
|
||||
self._connected = False
|
||||
logger.info("IBKR disconnected")
|
||||
|
||||
|
||||
def _ensure_connected(self):
|
||||
"""Ensure connection is established."""
|
||||
# Ensure event loop exists (may be called from different threads)
|
||||
@@ -173,27 +176,23 @@ class IBKRClient:
|
||||
if not self.connected:
|
||||
if not self.connect():
|
||||
raise ConnectionError("Cannot connect to IBKR")
|
||||
|
||||
|
||||
def _create_contract(self, symbol: str, market_type: str):
|
||||
"""
|
||||
Create IB contract object.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code
|
||||
market_type: Market type (USStock)
|
||||
"""
|
||||
_ensure_ib_insync()
|
||||
|
||||
|
||||
ib_symbol, exchange, currency = normalize_symbol(symbol, market_type)
|
||||
|
||||
contract = ib_insync.Stock(
|
||||
symbol=ib_symbol,
|
||||
exchange=exchange,
|
||||
currency=currency
|
||||
)
|
||||
|
||||
|
||||
contract = ib_insync.Stock(symbol=ib_symbol, exchange=exchange, currency=currency)
|
||||
|
||||
return contract
|
||||
|
||||
|
||||
def _qualify_contract(self, contract) -> bool:
|
||||
"""Validate contract."""
|
||||
try:
|
||||
@@ -202,9 +201,9 @@ class IBKRClient:
|
||||
except Exception as e:
|
||||
logger.warning(f"Contract qualification failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ==================== Order Methods ====================
|
||||
|
||||
|
||||
def place_market_order(
|
||||
self,
|
||||
symbol: str,
|
||||
@@ -214,38 +213,33 @@ class IBKRClient:
|
||||
) -> OrderResult:
|
||||
"""
|
||||
Place a market order.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code (e.g., AAPL, 0700.HK)
|
||||
side: Direction ("buy" or "sell")
|
||||
quantity: Number of shares
|
||||
market_type: Market type ("USStock")
|
||||
|
||||
|
||||
Returns:
|
||||
OrderResult
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_ib_insync()
|
||||
|
||||
|
||||
contract = self._create_contract(symbol, market_type)
|
||||
if not self._qualify_contract(contract):
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Invalid contract: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Invalid contract: {symbol}")
|
||||
|
||||
order = ib_insync.MarketOrder(
|
||||
action="BUY" if side.lower() == "buy" else "SELL",
|
||||
totalQuantity=quantity,
|
||||
account=self._account
|
||||
action="BUY" if side.lower() == "buy" else "SELL", totalQuantity=quantity, account=self._account
|
||||
)
|
||||
|
||||
|
||||
trade = self._ib.placeOrder(contract, order)
|
||||
|
||||
|
||||
# Wait for order status update
|
||||
self._ib.sleep(2)
|
||||
|
||||
|
||||
return OrderResult(
|
||||
success=True,
|
||||
order_id=trade.order.orderId,
|
||||
@@ -258,16 +252,13 @@ class IBKRClient:
|
||||
"status": trade.orderStatus.status,
|
||||
"filled": float(trade.orderStatus.filled or 0),
|
||||
"remaining": float(trade.orderStatus.remaining or 0),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Order failed: {e}")
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=str(e)
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=str(e))
|
||||
|
||||
def place_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
@@ -278,38 +269,35 @@ class IBKRClient:
|
||||
) -> OrderResult:
|
||||
"""
|
||||
Place a limit order.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code
|
||||
side: Direction ("buy" or "sell")
|
||||
quantity: Number of shares
|
||||
price: Limit price
|
||||
market_type: Market type
|
||||
|
||||
|
||||
Returns:
|
||||
OrderResult
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_ib_insync()
|
||||
|
||||
|
||||
contract = self._create_contract(symbol, market_type)
|
||||
if not self._qualify_contract(contract):
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Invalid contract: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Invalid contract: {symbol}")
|
||||
|
||||
order = ib_insync.LimitOrder(
|
||||
action="BUY" if side.lower() == "buy" else "SELL",
|
||||
totalQuantity=quantity,
|
||||
lmtPrice=price,
|
||||
account=self._account
|
||||
account=self._account,
|
||||
)
|
||||
|
||||
|
||||
trade = self._ib.placeOrder(contract, order)
|
||||
self._ib.sleep(1)
|
||||
|
||||
|
||||
return OrderResult(
|
||||
success=True,
|
||||
order_id=trade.order.orderId,
|
||||
@@ -321,167 +309,161 @@ class IBKRClient:
|
||||
"orderId": trade.order.orderId,
|
||||
"status": trade.orderStatus.status,
|
||||
"limitPrice": price,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Limit order failed: {e}")
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=str(e)
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=str(e))
|
||||
|
||||
def cancel_order(self, order_id: int) -> bool:
|
||||
"""
|
||||
Cancel an order.
|
||||
|
||||
|
||||
Args:
|
||||
order_id: Order ID
|
||||
|
||||
|
||||
Returns:
|
||||
True if cancelled successfully
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
|
||||
|
||||
for trade in self._ib.openTrades():
|
||||
if trade.order.orderId == order_id:
|
||||
self._ib.cancelOrder(trade.order)
|
||||
logger.info(f"Order {order_id} cancelled")
|
||||
return True
|
||||
|
||||
|
||||
logger.warning(f"Order not found: {order_id}")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cancel order failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ==================== Query Methods ====================
|
||||
|
||||
|
||||
def get_account_summary(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get account summary.
|
||||
|
||||
|
||||
Returns:
|
||||
Account info dictionary
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
|
||||
|
||||
summary = self._ib.accountSummary(self._account)
|
||||
result = {}
|
||||
for item in summary:
|
||||
result[item.tag] = {
|
||||
"value": item.value,
|
||||
"currency": item.currency
|
||||
}
|
||||
|
||||
return {
|
||||
"account": self._account,
|
||||
"summary": result,
|
||||
"success": True
|
||||
}
|
||||
|
||||
result[item.tag] = {"value": item.value, "currency": item.currency}
|
||||
|
||||
return {"account": self._account, "summary": result, "success": True}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get account summary failed: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def get_positions(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get current positions.
|
||||
|
||||
|
||||
Returns:
|
||||
List of positions
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
|
||||
|
||||
positions = self._ib.positions(self._account)
|
||||
result = []
|
||||
|
||||
|
||||
for pos in positions:
|
||||
contract = pos.contract
|
||||
exchange = contract.exchange or contract.primaryExchange or "SMART"
|
||||
|
||||
result.append({
|
||||
"symbol": format_display_symbol(contract.symbol, exchange),
|
||||
"ib_symbol": contract.symbol,
|
||||
"secType": contract.secType,
|
||||
"exchange": exchange,
|
||||
"currency": contract.currency,
|
||||
"quantity": float(pos.position),
|
||||
"avgCost": float(pos.avgCost),
|
||||
"marketValue": float(pos.position) * float(pos.avgCost),
|
||||
})
|
||||
|
||||
|
||||
result.append(
|
||||
{
|
||||
"symbol": format_display_symbol(contract.symbol, exchange),
|
||||
"ib_symbol": contract.symbol,
|
||||
"secType": contract.secType,
|
||||
"exchange": exchange,
|
||||
"currency": contract.currency,
|
||||
"quantity": float(pos.position),
|
||||
"avgCost": float(pos.avgCost),
|
||||
"marketValue": float(pos.position) * float(pos.avgCost),
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get positions failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_open_orders(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get open orders.
|
||||
|
||||
|
||||
Returns:
|
||||
List of orders
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
|
||||
|
||||
trades = self._ib.openTrades()
|
||||
result = []
|
||||
|
||||
|
||||
for trade in trades:
|
||||
order = trade.order
|
||||
contract = trade.contract
|
||||
status = trade.orderStatus
|
||||
|
||||
result.append({
|
||||
"orderId": order.orderId,
|
||||
"symbol": contract.symbol,
|
||||
"action": order.action,
|
||||
"quantity": float(order.totalQuantity),
|
||||
"orderType": order.orderType,
|
||||
"limitPrice": getattr(order, 'lmtPrice', None),
|
||||
"status": status.status,
|
||||
"filled": float(status.filled or 0),
|
||||
"remaining": float(status.remaining or 0),
|
||||
"avgFillPrice": float(status.avgFillPrice or 0),
|
||||
})
|
||||
|
||||
|
||||
result.append(
|
||||
{
|
||||
"orderId": order.orderId,
|
||||
"symbol": contract.symbol,
|
||||
"action": order.action,
|
||||
"quantity": float(order.totalQuantity),
|
||||
"orderType": order.orderType,
|
||||
"limitPrice": getattr(order, "lmtPrice", None),
|
||||
"status": status.status,
|
||||
"filled": float(status.filled or 0),
|
||||
"remaining": float(status.remaining or 0),
|
||||
"avgFillPrice": float(status.avgFillPrice or 0),
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get orders failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_quote(self, symbol: str, market_type: str = "USStock") -> Dict[str, Any]:
|
||||
"""
|
||||
Get real-time quote.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code
|
||||
market_type: Market type
|
||||
|
||||
|
||||
Returns:
|
||||
Quote data
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
|
||||
|
||||
contract = self._create_contract(symbol, market_type)
|
||||
if not self._qualify_contract(contract):
|
||||
return {"success": False, "error": f"Invalid contract: {symbol}"}
|
||||
|
||||
|
||||
# Request market data
|
||||
ticker = self._ib.reqMktData(contract, '', False, False)
|
||||
|
||||
ticker = self._ib.reqMktData(contract, "", False, False)
|
||||
|
||||
# Wait for data
|
||||
self._ib.sleep(2)
|
||||
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"symbol": symbol,
|
||||
@@ -493,16 +475,16 @@ class IBKRClient:
|
||||
"volume": ticker.volume if ticker.volume and ticker.volume > 0 else None,
|
||||
"close": ticker.close if ticker.close and ticker.close > 0 else None,
|
||||
}
|
||||
|
||||
|
||||
# Cancel subscription
|
||||
self._ib.cancelMktData(contract)
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get quote failed: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def get_connection_status(self) -> Dict[str, Any]:
|
||||
"""Get connection status."""
|
||||
return {
|
||||
@@ -523,15 +505,15 @@ _global_lock = threading.Lock()
|
||||
def get_ibkr_client(config: Optional[IBKRConfig] = None) -> IBKRClient:
|
||||
"""
|
||||
Get global IBKR client singleton.
|
||||
|
||||
|
||||
Args:
|
||||
config: Configuration (only effective on first call)
|
||||
|
||||
|
||||
Returns:
|
||||
IBKRClient instance
|
||||
"""
|
||||
global _global_client
|
||||
|
||||
|
||||
with _global_lock:
|
||||
if _global_client is None:
|
||||
_global_client = IBKRClient(config)
|
||||
@@ -541,7 +523,7 @@ def get_ibkr_client(config: Optional[IBKRConfig] = None) -> IBKRClient:
|
||||
def reset_ibkr_client():
|
||||
"""Reset global client (disconnect and clear instance)."""
|
||||
global _global_client
|
||||
|
||||
|
||||
with _global_lock:
|
||||
if _global_client is not None:
|
||||
_global_client.disconnect()
|
||||
|
||||
@@ -4,28 +4,28 @@ Symbol Mapping and Conversion
|
||||
Converts QuantDinger system symbols to IB contract format.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Optional
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def normalize_symbol(symbol: str, market_type: str) -> Tuple[str, str, str]:
|
||||
"""
|
||||
Convert system symbol to IB contract parameters.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code in the system
|
||||
market_type: Market type (USStock)
|
||||
|
||||
|
||||
Returns:
|
||||
(ib_symbol, exchange, currency)
|
||||
"""
|
||||
symbol = (symbol or "").strip().upper()
|
||||
market_type = (market_type or "").strip()
|
||||
|
||||
|
||||
if market_type == "USStock":
|
||||
# US stocks: AAPL, TSLA, GOOGL
|
||||
# Use SMART routing for best execution
|
||||
return symbol, "SMART", "USD"
|
||||
|
||||
|
||||
else:
|
||||
# Default to US stock
|
||||
return symbol, "SMART", "USD"
|
||||
@@ -34,15 +34,15 @@ def normalize_symbol(symbol: str, market_type: str) -> Tuple[str, str, str]:
|
||||
def parse_symbol(symbol: str) -> Tuple[str, Optional[str]]:
|
||||
"""
|
||||
Parse symbol and auto-detect market type.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code
|
||||
|
||||
|
||||
Returns:
|
||||
(clean_symbol, market_type)
|
||||
"""
|
||||
symbol = (symbol or "").strip().upper()
|
||||
|
||||
|
||||
# Default to US stock
|
||||
return symbol, "USStock"
|
||||
|
||||
@@ -50,11 +50,11 @@ def parse_symbol(symbol: str) -> Tuple[str, Optional[str]]:
|
||||
def format_display_symbol(ib_symbol: str, exchange: str) -> str:
|
||||
"""
|
||||
Convert IB contract format back to display format.
|
||||
|
||||
|
||||
Args:
|
||||
ib_symbol: IB symbol
|
||||
exchange: Exchange code
|
||||
|
||||
|
||||
Returns:
|
||||
Display symbol
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user