"""One-shot task: close the legacy (pre-refactor) positions after market open. Sells up to LEGACY_QTY shares of each legacy symbol with market orders, waits for fill confirmation, then verifies the account is flat for them. Scheduled via cron for 2026-07-22 09:45 ET; also safe to run manually: .venv/bin/python close_legacy_positions.py """ import asyncio import logging import sys from datetime import datetime, timedelta from config import config from connection import ib_conn from orders import create_stock_contract, execute_market_order # legacy positions observed in trading_bot.log on 2026-07-21 LEGACY_QTY = {"NVDA": 1, "META": 1, "AVGO": 1, "MSFT": 1, "PLTR": 1} CONNECT_RETRY_SECONDS = 30 CONNECT_DEADLINE_MINUTES = 60 # gateway might be started a bit late ORDER_TIMEOUT = 60 logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logging.getLogger("ib_insync.wrapper").setLevel(logging.WARNING) logging.getLogger("ib_insync.client").setLevel(logging.WARNING) logger = logging.getLogger("close_legacy") async def connect_with_retries() -> bool: deadline = datetime.now() + timedelta(minutes=CONNECT_DEADLINE_MINUTES) attempt = 0 while datetime.now() < deadline: attempt += 1 if await ib_conn.ensure_connected(): logger.info("Connected on attempt %d", attempt) return True logger.warning( "Connect attempt %d failed, retrying in %ds (giving up at %s)...", attempt, CONNECT_RETRY_SECONDS, deadline.strftime("%H:%M"), ) await asyncio.sleep(CONNECT_RETRY_SECONDS) return False async def main() -> int: logger.warning("=" * 60) logger.warning("LEGACY POSITION CLEANUP - LIVE account %s", config.ib.account) logger.warning("Will market-SELL up to: %s", LEGACY_QTY) logger.warning("=" * 60) if not await connect_with_retries(): logger.error( "Could not connect to IB Gateway within %d minutes - aborting. " "Is the Gateway running and logged in?", CONNECT_DEADLINE_MINUTES, ) return 1 ib = ib_conn.ib accounts = ib.managedAccounts() logger.info("Managed accounts: %s", accounts) if config.ib.account and config.ib.account not in accounts: logger.error( "Configured account %s not in managed accounts %s - aborting", config.ib.account, accounts, ) ib_conn.disconnect() return 1 positions: dict[str, float] = {} for p in ib.positions(): if p.contract.secType == "STK": positions[p.contract.symbol] = positions.get(p.contract.symbol, 0) + p.position logger.info("Current stock positions: %s", positions) failures = 0 for symbol, expected_qty in LEGACY_QTY.items(): actual = positions.get(symbol, 0) sell_qty = min(actual, expected_qty) if sell_qty <= 0: logger.info("%s: no legacy position (actual=%g), skipping", symbol, actual) continue contract = create_stock_contract(symbol) await ib.qualifyContractsAsync(contract) trade = await execute_market_order( ib, contract, "SELL", sell_qty, f"LegacyCleanup:{symbol}", timeout=ORDER_TIMEOUT ) if trade and trade.orderStatus.filled > 0: logger.info( "%s: SOLD %g @ %.2f", symbol, trade.orderStatus.filled, trade.orderStatus.avgFillPrice, ) else: logger.error("%s: SELL FAILED", symbol) failures += 1 # verify the account is flat for the legacy symbols await asyncio.sleep(2) remaining = { p.contract.symbol: p.position for p in ib.positions() if p.contract.secType == "STK" and p.contract.symbol in LEGACY_QTY and p.position > 0 } if remaining: logger.error("Cleanup incomplete, remaining positions: %s", remaining) else: logger.info("All legacy positions closed.") ib_conn.disconnect() return 1 if (failures or remaining) else 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))