feat(ucits): read-only probe-ucits-symbols CLI to detect ticker vs IBKR-reported symbol divergence

The IEF UCITS line's config ticker CBU0 resolves to IBKR canonical symbol CSBGU0
(the Error-478 root). After Monday's conId fix, CBU0 fills and IBKR reports the
holding as CSBGU0 — but _expected_book_symbols keys on the display ticker CBU0, so
the legitimate IEF/Treasury book position is flagged 'stray' and dropped from NLV
(live 2026-07-20: positions_json shows CSBGU0:55).

This adds IbkrBroker.resolve_symbol_info (read-only qualify-by-ISIN -> symbol/
localSymbol/conId) + the probe-ucits-symbols CLI to enumerate every line's real
reported symbol, so the config can carry an ibkr_symbol field and the NLV
stray-check keys on the REPORTED symbol. Probe first (this commit), fix config next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 21:17:34 +00:00
parent 90fcbfde5c
commit 420cffa93c
2 changed files with 48 additions and 0 deletions

View File

@@ -185,6 +185,29 @@ class IbkrBroker:
ticker, isin, exc_info=True)
return {}
def resolve_symbol_info(self, *, ticker: str, isin: str, exchange: str = "LSEETF",
currency: str = "USD") -> dict[str, str] | None:
"""READ-ONLY: qualify a UCITS line by ISIN and report the identity IBKR actually assigns it — the
`symbol`, `localSymbol`, and `conId` that `positions()`/`accountSummary()` will report a HOLDING under.
Used by the `probe-ucits-symbols` CLI to detect where a config `ticker` (cosmetic) diverges from what
IBKR reports (e.g. IEF's `CBU0` -> canonical `CSBGU0`), so `_expected_book_symbols` can key the NLV
stray-check on the REPORTED symbol instead of the display ticker. Returns None on resolve failure
(logged), never raises — same discipline as `historical_daily_volume`."""
try:
ib = self._ensure()
contract = self._resolve_contract(ib, ticker, exchange, currency, "ISIN", isin)
if contract is None:
log.error("IBKR contract did NOT resolve for symbol probe: ticker=%r isin=%r", ticker, isin)
return None
return {"ticker": ticker, "isin": isin,
"ibkr_symbol": str(getattr(contract, "symbol", "") or ""),
"local_symbol": str(getattr(contract, "localSymbol", "") or ""),
"con_id": str(getattr(contract, "conId", "") or "")}
except Exception:
log.error("IBKR symbol probe RAISED for ticker=%r (isin=%r) — returning None.",
ticker, isin, exc_info=True)
return None
def place_order(self, order: Order) -> str:
from ib_async import MarketOrder

View File

@@ -695,6 +695,31 @@ def backfill_ucits_volume_ibkr() -> None:
f"({type(e).__name__}: {str(e)[:120]}) — skipped")
@app.command("probe-ucits-symbols")
def probe_ucits_symbols() -> None:
"""READ-ONLY: for every `settings.ucits.map` line, resolve it on IBKR by ISIN and print the SYMBOL IBKR
actually assigns it (`symbol` / `localSymbol` / `conId`) vs the config `ticker`. Detects where the cosmetic
display ticker diverges from what IBKR REPORTS a holding under — e.g. IEF's ticker `CBU0` resolves to the
canonical `CSBGU0`, which then flags a legitimate book position as a stray in the NLV view. Run it (via the
multistrat-rebalancer-labeled path that can reach ib-gateway:4004) to populate each line's `ibkr_symbol`
config field. Places NO orders; resolves + prints only."""
s = get_settings()
broker = IbkrBroker(host=s.ibkr.host, port=s.ibkr.port, client_id=s.ibkr.client_id)
typer.echo(f"{'ticker':<8}{'ibkr_symbol':<14}{'local_symbol':<14}{'conId':<12}{'DIVERGES?'}")
typer.echo("-" * 62)
with broker as brk:
for listing in s.ucits.map.values():
info = brk.resolve_symbol_info(ticker=listing.ticker, isin=listing.isin,
exchange=listing.exchange, currency=listing.currency)
if info is None:
typer.echo(f"{listing.ticker:<8}{'(unresolved)':<40}")
continue
reported = info["ibkr_symbol"] or info["local_symbol"]
diverges = "YES -> set ibkr_symbol" if reported and reported != listing.ticker else "no"
typer.echo(f"{listing.ticker:<8}{info['ibkr_symbol']:<14}{info['local_symbol']:<14}"
f"{info['con_id']:<12}{diverges}")
@app.command("ingest-forward")
def ingest_forward() -> None:
"""Evaluate + persist the forward gate for every track the engine has recorded in the cockpit DB."""