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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Literal, cast
|
from typing import Literal, cast
|
||||||
|
|
||||||
@@ -54,6 +55,11 @@ def _make_app(settings: Settings) -> FastAPI:
|
|||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
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()
|
await app.state.registry.aclose()
|
||||||
|
|
||||||
app.router.lifespan_context = lifespan
|
app.router.lifespan_context = lifespan
|
||||||
|
|||||||
@@ -392,10 +392,17 @@ class IBKRClient:
|
|||||||
async def close_position(
|
async def close_position(
|
||||||
self, symbol: str, qty: float | None = None
|
self, symbol: str, qty: float | None = None
|
||||||
) -> dict:
|
) -> 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()
|
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:
|
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))
|
position_qty = float(target.get("position", 0))
|
||||||
close_qty = abs(qty if qty is not None else position_qty)
|
close_qty = abs(qty if qty is not None else position_qty)
|
||||||
side = "SELL" if position_qty > 0 else "BUY"
|
side = "SELL" if position_qty > 0 else "BUY"
|
||||||
@@ -407,7 +414,7 @@ class IBKRClient:
|
|||||||
positions = await self.get_positions()
|
positions = await self.get_positions()
|
||||||
results = []
|
results = []
|
||||||
for p in positions:
|
for p in positions:
|
||||||
sym = p.get("contractDesc")
|
sym = p.get("ticker") or p.get("contractDesc")
|
||||||
if not sym:
|
if not sym:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -192,6 +192,12 @@ async def get_clock(client: IBKRClient, params: GetClockReq) -> dict:
|
|||||||
"timestamp": now.isoformat(),
|
"timestamp": now.isoformat(),
|
||||||
"is_open": _dt.time(13, 30) <= now.time() <= _dt.time(20, 0)
|
"is_open": _dt.time(13, 30) <= now.time() <= _dt.time(20, 0)
|
||||||
and now.weekday() < 5,
|
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