fix(tui): stream real-time updates to Orders, Positions and Portfolio tabs#10
Merged
Conversation
Beta testers reported that Orders showed stale data until manually refreshed, and Positions only updated mark price / P&L live while Size stayed frozen until refresh. Root cause: user.order and user.positions WebSocket events were reaching App::on_data but not the tabs — events were forwarded only to the active tab, and OrdersTab/PositionsTab had no match arm for DataEvent::OrdersUpdate / DataEvent::PositionsSnapshot. On top of that, on_activate wiped loaded, forcing a REST round-trip every tab-switch. - Route user-channel deltas (OrdersUpdate, PositionsSnapshot, BalanceSnapshot) to their owning tabs regardless of which tab is active, so state stays fresh cross-tab. - OrdersTab: upsert by order_id, remove on terminal statuses (FILLED / CANCELED / CANCELLED / REJECTED / EXPIRED). Share one parser between the REST snapshot and WS delta paths. - PositionsTab: replace the positions vector on every user.positions snapshot so Size / Entry / Liq price follow the exchange live; the existing ticker-driven mark + P&L recompute is preserved. - Drop on_activate resets for both tabs — WS now keeps state current, so wiping the cache on every tab-switch just produced a blank frame.
…riters The previous commit left two writers competing for mark_price and pnl on Position rows: parse_position_record seeded them from the WS/REST payload (mark = last-known ask from state.tickers, pnl = session_pnl) while the tick fallthrough recomputed both from state.tickers. Every snapshot reset the fields to stale/zero values that the next tick repaired, producing a visible 0.00 → 1.23 → 0.00 → 1.24 flicker whenever user.positions deltas arrived. Make the ticker-driven recompute the single source of truth: - Extract apply_live_mark_and_pnl(positions, state) and call it from both rebuild_positions (WS snapshot / REST response) and the tick fallthrough so mark_price / pnl are only ever written in one place. - parse_position_record now populates only static fields (instrument, side, qty, entry, liq) and leaves mark_price / pnl as 0.0 sentinels overwritten immediately by the recompute. - Drop session_pnl parsing — it's exchange-session cumulative P&L and was semantically wrong for the per-position row anyway.
PortfolioTab was receiving DataEvent::BalanceSnapshot from the prior commit's broadcast routing but had no match arm for it, so user.balance WS deltas were silently dropped. Non-stable holdings also stored a value computed once at REST-response time (total * ticker.ask) and never recomputed between refreshes, so BTC/ETH rows stayed stale until the next 30s REST tick even while the Market tab showed live prices. Mirror the Positions fix: - Handle DataEvent::BalanceSnapshot → rebuild_from_records, sharing one parser (parse_balance_record) between REST and WS paths. - Split parsing from valuation: parse_balance_record populates only payload-sourced fields (name / amount / available); a single recompute_holding_values pass owns `value`, sourced from state.tickers. Called from REST, WS, and tick paths so holdings track the market between user.balance deltas (which fire on trades and deposits, not on price moves). - Drop on_activate reset — WS + tick recompute keep state current, so wiping the cache on tab-switch just produced a blank frame.
test_environment_urls and test_env_var_url_overrides both mutate the
same process-global CDCX_* env vars. Rust's default test harness runs
tests on multiple threads in the same binary, so set_var calls in one
test race reads in the other. The override test's mid-flight
`set_var("CDCX_WS_USER_URL", ...)` sporadically leaked into the
defaults test's `ws_user_url()` read, producing the Windows CI failure:
assertion `left == right` failed
left: "wss://custom-ws.example.com/user"
right: "wss://stream.crypto.com/exchange/v1/user"
Windows is hit hardest because its env syscalls are slower, widening
the window. Merge the two tests into a single sequential test so all
set/assert/remove cycles run on one thread — no new deps, no behavior
change, race is closed by construction.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Beta testers reported that Orders required a manual refresh to see fresh data, and Positions only updated mark price / P&L live while Size stayed frozen until
rwas pressed. Investigation surfaced the same class of bug on Portfolio (holdings stale between 30s REST refreshes).Root cause: the
UserStreamManageralready subscribed touser.order,user.positions, anduser.balance, andlib.rsalready dispatched them asDataEvents, but:App::on_dataforwarded events only to the active tab, so user-channel deltas to Orders/Positions/Portfolio were dropped whenever another tab was visible.OrdersTab,PositionsTab, andPortfolioTabhad no match arms for their respectiveDataEventvariants, so even when active they silently discarded the WS updates.on_activateresetloadedon every tab switch, forcing a REST round-trip and a "loading..." flash.Commits
df136d0— Orders and Positions wiring. BroadcastOrdersUpdate/PositionsSnapshot/BalanceSnapshotto their owning tabs viaTabKindlookup regardless of active tab.OrdersTabupserts byorder_idand removes on terminal statuses (FILLED/CANCELED/CANCELLED/REJECTED/EXPIRED), sharing one parser between REST and WS paths.PositionsTabreplaces the positions vec on eachuser.positionssnapshot so Size / Entry / Liq track live.404d4f3— Positions flicker fix. The previous commit left two writers racing formark_price/pnl(payload-sourced on WS snapshot, ticker-recomputed on tick), producing a visible0.00 → 1.23 → 0.00 → 1.24flash. Extracted a singleapply_live_mark_and_pnlhelper owning those fields;parse_position_recordnow only sets static fields.14b50c7— Portfolio tab. Same pattern applied: handleBalanceSnapshot, split parsing (parse_balance_record) from valuation (recompute_holding_values), call recompute from REST + WS + tick paths so non-stable holdings follow the market betweenuser.balancedeltas (which only fire on trades/deposits).05985a7— Windows CI race fix. Collapsetest_environment_urlsandtest_env_var_url_overridesinto one sequential test. Both mutated process-globalCDCX_*env vars and Rust's default parallel test harness raced them on Windows (slower env syscalls amplify the window). One test, one thread, race closed by construction. No new deps.All three TUI tabs drop their
on_activateresets — WS + tick recompute keep state current.Test plan
cdcx tui→ tab 3 Orders: open a limit order via CLI/another client, confirm it appears in the table without pressingr; cancel it and confirm the row disappears.0.00flash onuser.positionsdeltas.cargo test --workspacepasses locally; Windows CI race fix verified via 10-run loop.