Skip to content

feat(ts-sdk): account value on the PnL history - #311

Open
majesticwizardcat wants to merge 5 commits into
mainfrom
giannis/account-value
Open

majesticwizardcat wants to merge 5 commits into
mainfrom
giannis/account-value

Conversation

@majesticwizardcat

@majesticwizardcat majesticwizardcat commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What

Every point of pnlHistory and pnlHistoryStream now carries the absolute accountValue, next to pnl, when the node serves the activity feed (ADR 0057 on the node). Builds on the merged activity resource (#310) and PnL history.

How

Account value is PnL plus every movement that is not PnL:

accountValue(t) = accountValue(ref) + [PnL(t) − PnL(ref)] − Σ_{flows in (t, ref]} value(flow)

The backward walk keeps one more running sum over money events, fetched per window next to the fills and cached with them:

Row Event Value that moved
bridge_transfer with a positive amount, native token cash the amount
bridge_transfer with a positive amount, spot token deposit, a size event quantity × that tick's clearing price, which is also the cost the engine marks it at
transfer, either sign cash or a size event by token as above; a token leaving goes at the average cost, like a withdrawal
backstop without orderbook_id cash of −cash the balance moved to the backstop
native withdrawals from the bridge feed already read cash the amount
any row with error skipped

Negative bridge_transfer rows and backstop legs are skipped because withdrawals and sweep legs already come from the existing feeds. Within a tick, events are undone as withdrawals, cash, fills, deposits, then sweeps.

On a node without /clob/activity the points omit accountValue and a warning says so; PnL is unchanged.

Also on this branch: two loading fixes

  • Empty tick lookups are remembered. A lookup for the newest solutions row at or before t that comes back empty proves the market has no rows before t, so the cache keeps that bound per market and skips every older point. A yearly window on a canary account went from about 4,800 lookups cold and 4,300 again warm to about 600 cold and none warm: 6.7 s to 3.4 s cold, 5.7 s to 1.6 s warm.
  • Smaller first chunks, shorter preamble. Chunks start at 10 points and grow 20, 40, 50; backstop and withdrawals ride with the snapshot round; the first windows are fetched while the watermark is checked, with a refetch of the newest window in the rare case the indexer was behind. Staging maker, 30 min: first chunk 1.7 s to 1.0 s. Canary account, 1 year: warm 1.3 s, which is now two round trips to Singapore plus the newest window.

The frontend's chart branch calls the one-shot pnlHistory and waits for every point; it needs pnlHistoryStream to see the chunks arrive.

Also on this branch: fills and solutions over REST

The node's /clob/fills/{account} and /clob/solutions routes (backend #129, same JSON as the ob_* reads, microsecond _us params) replace ob_getFills and ob_getSolutions in the walk. pod_getSolverState is the one JSON-RPC read left. Nodes that predate the routes answer 404 and get the ob_* twins behind the same calls; that fallback goes once every network has the routes.

What REST changes in cost: JSON-RPC let the walk batch 200 tick lookups into one request, REST has no batch, so each lookup is its own request, at most 24 in flight. Quiet accounts hardly notice; a yearly window is a few hundred requests. For a market maker, where the funding index of every fill batch is a lookup, the request count is in the thousands per hour of history. Two things on the node would remove that: a times= or until_us= list on /clob/solutions to answer several lookups in one response, and Cache-Control: immutable on past windows instead of today's no-store, which would let the browser cache do what the SDK cache does across reloads.

No deployed network has the routes yet, so the REST path is exercised by the unit tests and the fallback by the live checks until one does.

Verification

Unit tests only so far. The forward engine simulation tracks cash and account value through a cash deposit, a token deposit in the same tick as a fill, and a cash withdrawal, and the fold matches it at every tick; a stub node pushes a deposit, an outgoing transfer, a refused deposit and a cash sweep through the stream. No network serves the activity feed yet, so there is no live comparison against the positions endpoint's account value.

🤖 Generated with Claude Code

@poszu
poszu added this pull request to stack #312 October 2, 2026 06:16
Base automatically changed from giannis/activity-feed to main October 5, 2026 12:06
Giannis Gkoulioumis and others added 4 commits October 5, 2026 15:08
Every point of `pnlHistory` and `pnlHistoryStream` carries the absolute
`accountValue` when the node serves the activity feed. Account value is
PnL plus every movement that is not PnL, so the backward walk keeps one
more running sum over money events:

- native deposits, transfers in and out, and backstop cash sweeps from
  `/clob/activity` (types backstop, bridge_transfer, transfer), fetched per
  window next to the fills and cached with them;
- native withdrawals from the bridge feed already read;
- spot token movements as size events: a withdrawal leaves at the average
  cost, a deposit arrives marked at the tick's clearing price, and the value
  that moved is what the event is priced at.

Refused rows are skipped. On a node without the feed the points omit
`accountValue` and a warning says so; PnL is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A lookup for the newest solutions row at or before t that comes back empty
proves the market has no rows at any earlier time. The cache now keeps that
bound per market and skips every older point for it, and the range path
skips a tick before every known market's first row. Before this, a window
reaching back past a market's birth asked the node for the same empty rows
on every run: a yearly window on a canary account issued about 4,800
lookups cold and 4,300 again warm. It is now about 600 cold and none warm,
6.7 s to 3.4 s cold and 5.7 s to 1.6 s warm.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Chunks now start at 10 points and grow 20, 40, 50, so the newest points
draw after one small window instead of fifty points' worth. Backstop and
withdrawal reads ride with the snapshot round, and the first windows are
fetched while the indexer watermark is checked; if the indexer was behind
the engine tick the newest window is forgotten and fetched again.

Staging market maker, 30 minutes: first chunk 1.7 s to 1.0 s, warm run
0.36 s to 0.31 s. Canary account, one year: first chunk 2.3 s to 2.0 s,
warm 1.6 s to 1.3 s, the rest being round trips to Singapore.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The node now serves `/clob/fills/{account}` and `/clob/solutions` with the
same JSON as the `ob_*` reads, so the walk uses them: one request per fills
page, per grid tick, and per fill-batch lookup, at most 24 in flight. The
JSON-RPC batch helper goes; `pod_getSolverState` stays the one JSON-RPC
read. Nodes that predate the routes answer 404 and get the `ob_*` twins
behind the same calls, to be removed once every network has the routes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@majesticwizardcat
majesticwizardcat marked this pull request as ready for review October 5, 2026 12:09

@0zzy-o 0zzy-o left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a couple of things raised by the agent that seems worth checking on.

Comment thread ts-sdk/src/sync/pnl-history.ts
Comment thread ts-sdk/src/sync/pnl-history.ts Outdated
…the run

Two review findings. Tick lookups started alongside the watermark check,
so on a lagging indexer the reference tick was answered with the newest
indexed row and cached under the current tick; every point but the last
was then off by that tick's move, and the lag retry only refetched fills.
Now every solutions lookup awaits the watermark check, while fills still
start at once and are refetched on lag as before.

drainFlows swallowed every error, so a 503 or a timeout on the activity
route cached the range without its flows and later account values were
wrong without a warning. Only a 404 now means the node has no feed; any
other error fails the run and nothing is cached. A cache that was filled
from a node without the feed is marked, and the next run that finds the
feed drops it and fetches everything with flows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants