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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user