feat(V2): IBKR client read methods + conid LRU cache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-03 20:29:11 +00:00
parent f4f4e4efd7
commit 611a2695a9
2 changed files with 176 additions and 0 deletions
+132
View File
@@ -134,3 +134,135 @@ class IBKRClient:
async def get_account(self) -> dict:
return await self._request("GET", f"/portfolio/{self.account_id}/summary")
# ── Conid resolution ────────────────────────────────────────
async def resolve_conid(self, symbol: str, sec_type: str = "STK") -> int:
key = f"{sec_type}:{symbol}"
if key in self._conid_cache:
self._conid_cache.move_to_end(key)
return self._conid_cache[key]
result = await self._request(
"GET", "/trsrv/secdef/search",
params={"symbol": symbol, "secType": sec_type},
)
if not result or not isinstance(result, list):
raise IBKRError(f"IBKR_CONID_NOT_FOUND: {symbol}/{sec_type}")
conid = int(result[0]["conid"])
self._conid_cache[key] = conid
if len(self._conid_cache) > self._CONID_CACHE_MAX:
self._conid_cache.popitem(last=False)
return conid
# ── Positions / orders / activities ─────────────────────────
async def get_positions(self, page: int = 0) -> list[dict]:
data = await self._request(
"GET", f"/portfolio/{self.account_id}/positions/{page}"
)
return list(data) if isinstance(data, list) else []
async def get_open_orders(self) -> list[dict]:
data = await self._request(
"GET", "/iserver/account/orders",
params={"filters": "Submitted,PreSubmitted"},
)
if isinstance(data, dict):
return list(data.get("orders") or [])
return list(data) if isinstance(data, list) else []
async def get_activities(self, days: int = 7) -> list[dict]:
days = max(1, min(days, 90))
data = await self._request(
"GET", "/iserver/account/trades", params={"days": days},
)
return list(data) if isinstance(data, list) else []
# ── Market data ─────────────────────────────────────────────
_SNAPSHOT_FIELDS = "31,84,86,7295,7296" # last,bid,ask,bid_size,ask_size
async def get_ticker(self, symbol: str, asset_class: str = "stocks") -> dict:
sec_type = {"stocks": "STK", "options": "OPT", "futures": "FUT", "forex": "CASH"}.get(
asset_class.lower(), "STK"
)
conid = await self.resolve_conid(symbol, sec_type)
data = await self._request(
"GET", "/iserver/marketdata/snapshot",
params={"conids": str(conid), "fields": self._SNAPSHOT_FIELDS},
)
if not data or not isinstance(data, list):
raise IBKRError("IBKR_NO_MARKET_DATA_SUBSCRIPTION")
row = data[0]
def _f(k: str) -> float | None:
v = row.get(k)
try:
return float(v) if v not in (None, "") else None
except (TypeError, ValueError):
return None
return {
"symbol": symbol,
"asset_class": asset_class,
"last_price": _f("31"),
"bid": _f("84"),
"ask": _f("86"),
"bid_size": _f("7295"),
"ask_size": _f("7296"),
}
async def get_bars(
self, symbol: str, asset_class: str = "stocks",
period: str = "1d", bar: str = "5min",
) -> dict:
sec_type = {"stocks": "STK", "options": "OPT", "futures": "FUT"}.get(
asset_class.lower(), "STK"
)
conid = await self.resolve_conid(symbol, sec_type)
data = await self._request(
"GET", "/iserver/marketdata/history",
params={"conid": str(conid), "period": period, "bar": bar},
)
rows = (data or {}).get("data") or []
return {
"symbol": symbol,
"asset_class": asset_class,
"interval": bar,
"bars": [
{
"timestamp": r.get("t"),
"open": r.get("o"),
"high": r.get("h"),
"low": r.get("l"),
"close": r.get("c"),
"volume": r.get("v"),
}
for r in rows
],
}
async def get_option_chain(
self, underlying: str, expiry: str | None = None
) -> dict:
conid = await self.resolve_conid(underlying, "STK")
params: dict[str, Any] = {"conid": str(conid), "secType": "OPT"}
if expiry:
params["month"] = expiry # IBKR format: "JAN26"
strikes = await self._request(
"GET", "/iserver/secdef/strikes", params=params,
)
return {
"underlying": underlying,
"expiry": expiry,
"strikes": strikes,
}
async def search_contracts(
self, symbol: str, sec_type: str = "STK"
) -> list[dict]:
data = await self._request(
"GET", "/trsrv/secdef/search",
params={"symbol": symbol, "secType": sec_type},
)
return list(data) if isinstance(data, list) else []