import asyncio import logging from typing import Optional from ib_insync import IB, Contract, MarketOrder, Trade from config import config logger = logging.getLogger(__name__) def create_stock_contract(symbol: str, exchange: str = "SMART", currency: str = "USD") -> Contract: return Contract(symbol=symbol, secType="STK", exchange=exchange, currency=currency) def has_open_order(ib: IB, order_ref: str) -> bool: """True if an unfinished order with this orderRef already exists.""" for trade in ib.openTrades(): if (trade.order.orderRef == order_ref and not trade.isDone() and trade.orderStatus.remaining > 0): return True return False async def wait_trade_done(trade: Trade, timeout: float) -> bool: """Event-driven wait for a trade to reach a terminal state. Waits on trade.statusEvent (which fires on every status update) instead of polling, returning as soon as isDone() becomes true or the timeout elapses. """ done = asyncio.Event() def _on_status(_): if trade.isDone(): done.set() trade.statusEvent.connect(_on_status) try: if trade.isDone(): return True try: await asyncio.wait_for(done.wait(), timeout) except asyncio.TimeoutError: return False return trade.isDone() finally: trade.statusEvent.disconnect(_on_status) async def execute_market_order( ib: IB, contract: Contract, action: str, quantity: float, order_ref: str, timeout: Optional[float] = None, ) -> Optional[Trade]: """Place a market order and wait for the fill. Returns the Trade if (partially) filled, None on rejection/timeout. On timeout the order is cancelled before giving up. """ if has_open_order(ib, order_ref): logger.warning( "Skip %s %s x%g: an open order already exists (ref=%s)", action, contract.symbol, quantity, order_ref, ) return None order = MarketOrder(action, quantity) if config.ib.account: order.account = config.ib.account order.orderRef = order_ref order.tif = "DAY" order.outsideRth = False trade = ib.placeOrder(contract, order) logger.info( "Placed %s market order: %s x%g (ref=%s, orderId=%s)", action, contract.symbol, quantity, order_ref, trade.order.orderId, ) timeout = timeout or config.order_timeout if not await wait_trade_done(trade, timeout): logger.warning( "%s %s x%g not filled within %.0fs, cancelling...", action, contract.symbol, quantity, timeout, ) ib.cancelOrder(order) await wait_trade_done(trade, 2.0) # wait for the cancel to confirm if trade.isDone(): status = trade.orderStatus.status if status == "Filled": logger.info( "%s %s x%g FILLED @ %.4f", action, contract.symbol, trade.orderStatus.filled, trade.orderStatus.avgFillPrice, ) return trade logger.error( "%s %s x%g finished with status=%s: %s", action, contract.symbol, quantity, status, [m.message for m in trade.log] if trade.log else "", ) return None if trade.orderStatus.filled > 0: logger.info( "%s %s partially filled x%g @ %.4f before cancel", action, contract.symbol, trade.orderStatus.filled, trade.orderStatus.avgFillPrice, ) return trade logger.error( "%s %s x%g cancelled/unfilled (status=%s): %s", action, contract.symbol, quantity, trade.orderStatus.status, [m.message for m in trade.log] if trade.log else "", ) return None