121 lines
4.1 KiB
Python
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()))
|