Skip to content

req_pnl: pnl() callback never fires for intraday-opened positions #166

@deepentropy

Description

@deepentropy

Summary

After the v0.4.5 fix for #142, req_pnl() no longer leaks notional values into daily_pnl, but introduces a silent regression: the pnl() callback never fires for positions opened intraday.

Repro

Probe script: connect to paper account (flat at start), req_account_updates(True) + req_pnl(), place a 1-share marketable BUY, observe callbacks for 180s, place SELL to flatten.

Probe results (paper, US pre-market 2026-05-08, account/IDs redacted):

[ 11.633s] req_account_updates(True) + req_pnl(...)
[ 11.655s] update_account_value DailyPnL=0.00       (one push, never updates again)
[ 11.984s] pnl()  daily=+0.00  unrealized=+0.00  realized=+~$X
[ 41.967s] BUY 1 SPY @ 735.00  filled
            -- 180s observation window --
            -- ZERO pnl() callbacks fired during this window --
[222.199s] SELL @ 735.07  filled (flat)

Key data points:

  • pnl() fires once at startup (computed from prior-session state via the midnight-seed reconstruction)
  • During 180s post-fill on a fresh intraday position, pnl() never fires
  • update_account_value("DailyPnL", ...) also pushes only once with stale 0.00, despite NetLiquidation updating multiple times

Diagnosis

src/client_core.rs:898-956 (poll_pnl) iterates only over midnight seeds:

let seeds = shared.portfolio.midnight_seeds();
if seeds.is_empty() { return None; }
for seed in &seeds { ... }

Positions opened intraday have no entry in the midnight-seed map, so they're excluded from the computation entirely. For a flat-at-midnight account, the loop produces stale totals that never change → cached last_pnl suppresses any callback.

Bug class progression

Version Symptom for intraday-opened position
v0.4.4 daily_pnl accumulates trade notional per fill (the original report)
v0.4.5+ pnl() callback never fires at all

The v0.4.5 fix swapped a loud bug for a quiet one. Both stem from the same root cause: client-side reconstruction from midnight seeds cannot represent positions opened today.

Open architectural question

Whether the upstream client achieves this via:

  1. A different subscription that streams server-computed daily P&L live, or
  2. The same midnight-seed message but with intraday updates after fills

is unknown without protocol capture. A capture request has been filed at deepentropy/ib-agent.

A purely-local fix (extend poll_pnl to handle no-seed positions by synthesizing money_traded = qty_now × avg_cost, which collapses to unrealized P&L for fresh positions) is mathematically correct but patches the architecture rather than aligning with upstream behavior. Holding the PR pending capture data.

Probe script

scripts/.tmp/probe_pnl_142.py (not committed; uses .venv\Scripts\python.exe directly).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions