Skip to content
Skip to main content

P&L Stream

Beta

The WebSocket API is currently in beta. Features and endpoints are subject to change.

Overview

The P&L stream delivers continuous updates to account value, leverage, and per-position unrealized P&L. Every time the market price of a held security changes, the stream pushes both an aggregate account-level update and a position-level update for the affected position.

Endpoint: wss://webapi.tradezero.com/stream/pnl


Usage

Step 1 - Connect and authenticate

Connect to the endpoint and complete the authentication handshake described in About WebSocket API. Once you receive CONNECTED, proceed to step 2.

Step 2 - Subscribe to an account

Send a JSON message with the account ID you want to stream:

{
"account": "YOUR_ACCOUNT_ID"
}
Multiple accounts

You can subscribe to additional accounts at any time by sending more { "account": "..." } messages on the same connection. Each account sends its own initial snapshot followed by its own stream of updates.

Forgiving subscribe payload

The P&L stream ignores any extra fields you include in the subscribe payload. Only the account field is read; all other fields are discarded. This makes it safe to send a richer object without stripping fields first.

Step 3 - Receive the initial snapshot

On successful subscription the server immediately sends a full snapshot of the account's current state:

{
"ts": 1700000000240,
"action": "init",
"target": "pnlReturn",
"pnlReturn": {
"accountValue": 996452.64,
"availableCash": 967066.39,
"optionCashUsed": 2293.00,
"usedLeverage": 0.03,
"allowedLeverage": 1,
"sharesTraded": 2546,
"exposure": 27093.25,
"dayUnrealized": 2731.58,
"dayRealized": 0,
"dayPnl": 2731.58,
"totalUnrealized": 5004.98,
"equityRatio": 1,
"positions": [
{
"positionId": "2260512190020399758",
"symbol": "AAPL",
"pnlCalc": {
"unrealizedPnL": 4.26,
"dayUnrealizedPnL": 3.88,
"pctPnLMove": 1.45,
"dayPctPnLMove": 1.32,
"exposure": 298.68
},
"realizedPnl": 0,
"dayRealizedPnl": 0
},
{
"positionId": "2260513134257710616",
"symbol": "NVDA260513P00232500",
"pnlCalc": {
"unrealizedPnL": -115.00,
"dayUnrealizedPnL": -115.00,
"pctPnLMove": -16.2,
"dayPctPnLMove": -16.2,
"exposure": -825.00
},
"realizedPnl": 0,
"dayRealizedPnl": 0
}
]
}
}
Options positions

Option positions appear alongside equity positions in the positions array. The symbol field contains the full OCC-format symbol (e.g. NVDA260513P00232500). The exposure value for a short option position is negative.


Real-time updates

After the initial snapshot, the server pushes two types of update messages whenever a price change affects a held position.

Aggregate account update

The aggCalcs update carries account-level values. Only fields that changed from the previous snapshot are included - treat any absent field as unchanged.

{
"ts": 1700000000262,
"action": "update",
"target": "aggCalcs",
"aggCalcs": {
"accountValue": 996455.79,
"exposure": 27096.40,
"dayUnrealized": 2734.73,
"dayPnl": 2734.73,
"totalUnrealized": 5008.13
}
}

Position-level update

The position update carries P&L data for the single position whose price changed. Only fields within pnlCalc that changed from the prior snapshot are included.

{
"ts": 1700000000262,
"action": "update",
"target": "position",
"position": {
"positionId": "2260421072916766412",
"symbol": "NVDA",
"pnlCalc": {
"unrealizedPnL": 2562.00,
"dayUnrealizedPnL": 579.60,
"pctPnLMove": 12.09,
"dayPctPnLMove": 2.50,
"exposure": 23761.50
}
}
}
Update cadence

Aggregate and position updates are sent in pairs: for any given price tick the server sends one aggCalcs and one position message with matching ts values. The two messages may arrive consecutively or interleaved if multiple positions move simultaneously.

After regular trading hours

Outside regular trading hours the connection stays open and the initial snapshot is delivered normally. Price-driven updates arrive rarely or not at all while markets are closed. Premarket and after-hours sessions may produce updates at reduced frequency depending on venue activity.


Message type reference

actiontargetDescription
"init""pnlReturn"Full account snapshot - sent once per { account } subscribe
"update""aggCalcs"Partial account-level P&L update
"update""position"Partial position-level P&L update
"error"-Subscription-level error (e.g. account not found)

Field reference

pnlReturn (initial snapshot)

FieldTypeDescription
accountValuenumberTotal account value
availableCashnumberCash available for trading
optionCashUsednumberCash reserved for open option positions
usedLeveragenumberCurrent leverage multiplier in use
allowedLeveragenumberMaximum permitted leverage
sharesTradednumberTotal shares traded today
exposurenumberTotal market exposure (long − short)
dayUnrealizednumberUnrealized P&L for the current session
dayRealizednumberRealized P&L for the current session
dayPnlnumberdayUnrealized + dayRealized
totalUnrealizednumberTotal unrealized P&L across all open positions
equityRationumberEquity-to-account-value ratio
positionsarrayPer-position P&L entries (see below)

Each entry in positions

FieldTypeDescription
positionIdstringUnique position identifier
symbolstringTicker symbol; OCC format for options (e.g. NVDA260513P00232500)
pnlCalcobjectCurrent P&L calculations for this position
pnlCalc.unrealizedPnLnumberTotal unrealized P&L
pnlCalc.dayUnrealizedPnLnumberUnrealized P&L since the session open
pnlCalc.pctPnLMovenumberTotal unrealized P&L as a percentage
pnlCalc.dayPctPnLMovenumberDay unrealized P&L as a percentage
pnlCalc.exposurenumberMarket value of the position (negative for short/sold options)
realizedPnlnumberTotal realized P&L for this position
dayRealizedPnlnumberRealized P&L from today's activity

aggCalcs update fields

All fields are identical to the matching pnlReturn fields. Any field may be absent from an update - only values that changed since the last message are included.


Error handling

Subscription errors

If the { "account": "..." } payload is invalid, the server sends an error data message (not a system message):

{
"ts": 1700000008729,
"action": "error",
"message": "account not found"
}
ScenarioResponse typemessage value
Account ID does not existaction: "error" data message"account not found"
Account not owned by authenticated credentialsaction: "error" data message"account not found"
Numeric value sent as account (e.g. 12345)action: "error" data message"account not found"

Empty account - TERMINATED

Sending an empty string as the account value returns a system-level TERMINATED message rather than a data error:

{
"@system": true,
"ts": 1700000004324,
"status": "TERMINATED",
"message": "Account value needs to be non-empty"
}

After TERMINATED the connection remains open. You can send a corrected { "account": "..." } payload without reconnecting.

Malformed subscribe payload - INVALID_DATA

If you send a value that cannot be deserialized as a subscribe request (for example, a plain string instead of a JSON object) the server sends INVALID_DATA with a description of the parse error:

{
"@system": true,
"ts": 1700000011585,
"status": "INVALID_DATA",
"message": "Error converting value \"this-is-not-a-valid-message\" to type 'TradeZero.Api.PnL.Models.PnlSubscribeRequest'. Path '', line 1, position 29."
}

The connection is not closed after INVALID_DATA. If you already have an active subscription, updates continue flowing. You can send a corrected subscribe payload at any time.


State management pattern

The P&L stream is designed around a local copy of pnlReturn that you seed from the initial snapshot and keep in sync using partial updates:

interface PnlState {
accountValue: number;
availableCash: number;
optionCashUsed: number;
usedLeverage: number;
allowedLeverage: number;
sharesTraded: number;
exposure: number;
dayUnrealized: number;
dayRealized: number;
dayPnl: number;
totalUnrealized: number;
equityRatio: number;
positions: Map<string, {
symbol: string;
pnlCalc: {
unrealizedPnL: number;
dayUnrealizedPnL: number;
pctPnLMove: number;
dayPctPnLMove: number;
exposure: number;
};
realizedPnl: number;
dayRealizedPnl: number;
}>;
}

let state: PnlState | null = null;

const ws = new WebSocket('wss://webapi.tradezero.com/stream/pnl');

ws.onmessage = (event) => {
const msg = JSON.parse(event.data);

if (msg['@system']) {
switch (msg.status) {
case 'PENDING_AUTH':
ws.send(JSON.stringify({ key: 'YOUR_TZ-API-KEY-ID', secret: 'YOUR_TZ-API-SECRET-KEY' }));
break;
case 'CONNECTED':
ws.send(JSON.stringify({ account: 'YOUR_ACCOUNT_ID' }));
break;
case 'FAILED_AUTH':
console.error('Auth failed:', msg.message);
ws.close();
break;
case 'TERMINATED':
console.error('Subscribe rejected:', msg.message);
break;
case 'INVALID_DATA':
// Note: connection stays open - existing subscription keeps delivering updates.
console.warn('Invalid subscribe payload:', msg.message);
break;
}
return;
}

if (msg.action === 'init' && msg.target === 'pnlReturn') {
// Build the initial state - seed the positions map.
// Keep `pnlCalc` nested so update messages (which carry the same shape)
// can be merged in directly without a flat/nested mismatch.
const snap = msg.pnlReturn;
state = {
...snap,
positions: new Map(
snap.positions.map((p: any) => [
p.positionId,
{
symbol: p.symbol,
pnlCalc: { ...p.pnlCalc },
realizedPnl: p.realizedPnl,
dayRealizedPnl: p.dayRealizedPnl,
},
]),
),
};
onStateChange(state);
return;
}

if (!state) return; // Updates before snapshot are discarded.

if (msg.action === 'update' && msg.target === 'aggCalcs') {
// Merge only the fields present in the update.
Object.assign(state, msg.aggCalcs);
onStateChange(state);
return;
}

if (msg.action === 'update' && msg.target === 'position') {
const { positionId, pnlCalc } = msg.position;
const existing = state.positions.get(positionId);
if (existing) {
Object.assign(existing.pnlCalc, pnlCalc);
}
onStateChange(state);
return;
}

if (msg.action === 'error') {
console.error('P&L subscription error:', msg.message);
}
};
Paired updates

For each price tick the server sends one aggCalcs and one position message with matching ts values. If you render UI after every message you may want to batch these: buffer both and render once after both have arrived within the same ts.

For end-to-end implementations on top of these messages, see:

  • P&L Stream Dashboard - dedicated walk-through of the WebSocket handshake, initial snapshot, and incremental merge.
  • Live Account Dashboard - composite dashboard that combines the P&L stream with REST snapshots from /account, /positions, and /pnl, with auto-reconnect and a clean state machine.