refactor(V2): IBKR final review fixes — WS shutdown, conid match, clock note

Final code-review fixes:
- __main__: lifespan stops IBKRWebSocket singletons before registry close
- close_position: resolve symbol→conid first, match positions on conid
  (was matching contractDesc which is a long display string, not ticker)
- close_all_positions: prefer ticker field, fallback to contractDesc
- get_clock: explicit approximate=true + note about US holidays/half-days

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-03 21:46:11 +00:00
parent cddf88afb4
commit 880faa7fd4
3 changed files with 22 additions and 3 deletions
+6
View File
@@ -9,6 +9,7 @@ Boot:
"""
from __future__ import annotations
import contextlib
from contextlib import asynccontextmanager
from typing import Literal, cast
@@ -54,6 +55,11 @@ def _make_app(settings: Settings) -> FastAPI:
try:
yield
finally:
# Stop any IBKR WebSocket singletons before closing client registry
ibkr_ws_dict = getattr(app.state, "ibkr_ws", {}) or {}
for ws in ibkr_ws_dict.values():
with contextlib.suppress(Exception):
await ws.stop()
await app.state.registry.aclose()
app.router.lifespan_context = lifespan
+10 -3
View File
@@ -392,10 +392,17 @@ class IBKRClient:
async def close_position(
self, symbol: str, qty: float | None = None
) -> dict:
# Resolve symbol → conid, then match positions on conid (positions
# return `contractDesc` as a long display string, not ticker).
sec_type = _SEC_TYPE_MAP.get("stocks", "STK")
conid = await self.resolve_conid(symbol, sec_type)
positions = await self.get_positions()
target = next((p for p in positions if p.get("contractDesc") == symbol), None)
target = next(
(p for p in positions if int(p.get("conid", 0)) == conid),
None,
)
if not target:
raise IBKRError(f"IBKR_NO_POSITION: {symbol}")
raise IBKRError(f"IBKR_NO_POSITION: {symbol} (conid={conid})")
position_qty = float(target.get("position", 0))
close_qty = abs(qty if qty is not None else position_qty)
side = "SELL" if position_qty > 0 else "BUY"
@@ -407,7 +414,7 @@ class IBKRClient:
positions = await self.get_positions()
results = []
for p in positions:
sym = p.get("contractDesc")
sym = p.get("ticker") or p.get("contractDesc")
if not sym:
continue
try:
+6
View File
@@ -192,6 +192,12 @@ async def get_clock(client: IBKRClient, params: GetClockReq) -> dict:
"timestamp": now.isoformat(),
"is_open": _dt.time(13, 30) <= now.time() <= _dt.time(20, 0)
and now.weekday() < 5,
"approximate": True,
"note": (
"is_open is a UTC-based approximation; does not account for "
"US market holidays or half-days. Use IBKR /trsrv/marketdata/calendar "
"for authoritative schedule."
),
}