Skip to content

[IBKR] Use broker NetLiquidation and refresh option position marks from snapshots #314

Open

Description

@randomradio

Symptom

IBKR account and position reporting can drift from TWS/IB Gateway values:

  • IbkrBroker.getAccount() reconstructs netLiquidation from cash plus cached position market values whenever positions exist, even when IBKR account summary exposes an authoritative NetLiquidation value.
  • If requestAccountSummary() is unavailable, the fallback should be the cached account NetLiquidation value before reconstructing from positions.
  • IbkrBroker.getPositions() currently returns cached updatePortfolio() marks directly. Those marks can freeze after market hours; for option positions we should refresh from snapshot bid/ask when available and recompute market value / unrealized PnL.
  • RequestBridge.updatePortfolio() should deduplicate repeated callbacks for the same account + contract instead of appending duplicate positions during a batch.

Evidence / repro

A local regression spec has been drafted at:

services/uta/src/domain/trading/brokers/ibkr/IbkrBroker.spec.ts

The spec covers:

  1. getAccount() prefers account-summary NetLiquidation over reconstructed position values.
  2. getAccount() falls back to cached account values when account summary is unavailable.
  3. Net liquidation is reconstructed only when no broker NetLiquidation is available.
  4. getPositions() refreshes option marks using snapshot quote bid/ask and recalculates market value / unrealized PnL.
  5. getPositions() keeps cached updatePortfolio() values if the snapshot quote fails.
  6. RequestBridge deduplicates repeated updatePortfolio callbacks by account and contract, preserving the newest values.

Suspected locations

  • services/uta/src/domain/trading/brokers/ibkr/IbkrBroker.ts around getAccount() and getPositions()
  • services/uta/src/domain/trading/brokers/ibkr/request-bridge.ts around updatePortfolio()

Expected behavior

  • Account-level NetLiquidation from IBKR wins whenever available.
  • Cached account-level NetLiquidation is the fallback if summary fetch fails.
  • Position-derived reconstruction is only a last resort.
  • Option position marks use snapshot midpoint (bid + ask) / 2 when snapshot data is available, with cached values retained on quote failure.
  • Repeated portfolio callbacks update the existing cache entry instead of duplicating the position.

Related context

This is adjacent to #295, but distinct: #295 is about account-value keys overwriting across currencies; this issue is about account valuation precedence, stale option marks, and duplicate portfolio rows.

Activity

  1. luokerenx4 commented on Jun 13, 2026

    @luokerenx4
    Contributor

    2 of 3 addressed in #335 (merged to master):

    • ✅ Duplicate updatePortfolio rows: the account cache now upserts by conId (both the live cache and the pending rebuild buffer). Zero-quantity updates REMOVE the position — previously a fully-closed position lingered until the next full download. Deltas also apply to the live cache immediately instead of waiting for the next accountDownloadEnd swap (a filled sell used to show as still-held for minutes).
    • ✅ NetLiquidation precedence: mixed-currency books now prefer TWS's consolidated NetLiquidation tag (it's the FX-correct value); position-derived reconstruction is rate-converted via TWS ExchangeRate tags and only used as fallback. Same-currency books intentionally KEEP the position-derived reconstruction: the account-level tag is server-cached and refreshes slower than position marks, so freshness wins there. Happy to revisit if you have a live case where the reconstruction drifts worse than the staleness.
    • ⏳ Option-mark refresh from snapshot bid/ask: not done. Our paper setup has no market-data entitlement, so a snapshot-refresh path can't be live-verified here yet; it also interacts with delayed-data availability (snapshot + delayed don't combine on TWS). Leaving this issue open, scoped to this remaining item.
  2. luokerenx4 commented on Jul 17, 2026

    @luokerenx4
    Contributor

    Implemented the remaining valuation/option-mark work in #658 (merged to dev, merge commit fa4c2795).

    Semantics fixed

    NetLiquidation is now one stable account-level concept:

    • the finite broker-delivered NetLiquidation value wins for both same-currency and mixed-currency books;
    • local cash + signed, FX-converted position value reconstruction is used only when the broker value is missing or non-finite;
    • adding a foreign-currency position can no longer silently change which definition the field uses.

    The implementation uses the broker value already delivered through the persistent account-update channel. We did not add a fresh reqAccountSummary subscription to every account read: IBKR documents that account-summary updates occur on a three-minute cadence, so repeatedly opening that subscription would not make the value semantically fresher. In live paper acceptance, the UTA value matched a separate reqAccountSummary(NetLiquidation) read exactly. See IBKR account-summary behavior.

    The duplicate updatePortfolio upsert/removal behavior was already fixed by #335 and remains covered; this PR does not replace that work.

    Option position marks

    getPositions() now refreshes only OPT/FOP rows:

    • resolves the position's canonical IBKR contract;
    • requests an ordinary snapshot with snapshot=true and regulatorySnapshot=false;
    • when both bid and ask are positive, returns midpoint (bid + ask) / 2;
    • recomputes marketValue and unrealizedPnL through the shared multiplier-aware position math;
    • overlays those values on a returned copy—the broker-owned updatePortfolio cache is not mutated;
    • retains the cached row if entitlement is missing, bid/ask is incomplete, the market is closed, or the request times out.

    Successful marks are cached for 15s; failed-entitlement/incomplete attempts are negatively cached for 60s. Refresh concurrency is capped at 8 and concurrent reads for the same conId share one request.

    IBKR documents tickSnapshotEnd at roughly 11 seconds, so ordinary snapshot timeout is now 12.5s. Option marks may resolve earlier as soon as a positive bid+ask pair arrives. regulatorySnapshot=false means this path does not opt the account into paid per-request US regulatory snapshots. References: market-data snapshots, live/delayed market-data types.

    Verification performed

    Deterministic specs cover:

    • broker NetLiquidation precedence for same/mixed currency;
    • missing and non-finite broker-value fallback;
    • midpoint, multiplier, side, market value, and PnL math;
    • source-cache immutability;
    • missing-entitlement fallback and negative caching;
    • same-conId request coalescing and non-option no-snapshot behavior;
    • ordinary quotes waiting for tickSnapshotEnd while option marks can resolve on bid+ask.

    Live read-only paper acceptance on IB Gateway 10.37.1r / serverVersion 203 confirmed:

    • account read succeeded and UTA NetLiquidation equaled a separate live account-summary value;
    • a real option contract resolved successfully;
    • this account has no usable option market-data permission, so the option mark took the intended cached fallback and left the source cache unchanged;
    • no order was submitted.

    Full repository tests passed (3161 tests); the targeted UTA/IBKR suite passed 151 tests.

    Reproduction invitation

    The remaining evidence we cannot produce locally is a positive entitled option bid/ask overlay. If you have a paper account with an OPT or FOP position and live or delayed API market data, please retry against dev or a build containing fa4c2795:

    1. Read the position once and note the TWS cached portfolio mark plus current snapshot bid/ask.
    2. Expect the returned UTA mark to equal the positive bid/ask midpoint.
    3. Expect marketValue = quantity × midpoint × multiplier; PnL should use the same multiplier and long/short sign.
    4. Poll again inside 15s; it should reuse the mark rather than create a snapshot loop.
    5. If the account lacks permission, the whole positions read should still succeed with the cached updatePortfolio row.

    Useful sanitized details are Gateway/TWS version, API serverVersion, OPT vs FOP, multiplier, entitlement/delayed-data mode, bid/ask, cached mark, and returned mark. Please do not post account IDs or a full position/account payload.

    I am leaving this issue open until someone with positive option market-data entitlement confirms the overlay. If nobody can, the implementation, acceptance boundary, and next debugging steps are preserved here.

  3. RainMona commented on Aug 17, 2026

    @RainMona
    Contributor

    Community triage (current dev, Aug 2026)

    Status: likely fixed in code — still waiting on entitled option confirmation

    #658 / related IBKR work on dev includes:

    • broker NetLiquidation precedence
    • OPT/FOP snapshot midpoint overlay (regulatorySnapshot=false) with entitlement fallback + caching

    Maintainer paper acceptance could not confirm a positive entitled bid/ask overlay.

    Ask: If anyone has paper/live API access with usable OPT/FOP market data, please retest on current dev:

    1. note cached portfolio mark vs bid/ask
    2. expect midpoint mark + multiplier-aware PnL
    3. expect 15s reuse / cached fallback when entitlement missing

    Sanitized Gateway version, OPT vs FOP, bid/ask, returned mark is enough to close. Please do not post account IDs.

    — triage note

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions