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