BotDeepSeek/close_legacy_positions.py

121 lines
4.1 KiB
Python

"""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()))