Skip to content
Skip to main content

Open Positions

A position is whatever you currently hold in a TradeZero account: every long, every short, every option leg, plus the per-position P&L. The Positions API exposes that view through two read-only endpoints:

  • GET /v1/api/accounts/{accountId}/positions - the holdings themselves: symbol, side, share count, average price, day-vs-overnight tag, plus option metadata (tradedSymbol, priceStrike, putCall) when the row is an option.
  • GET /v1/api/accounts/{accountId}/pnl - per-position realized / unrealized P&L plus the account-level totals (accountValue, availableCash, dayPnl, usedLeverage, exposure).

Both endpoints share row identity through positionId. The recommended way to correlate them is by positionId first, then tradedSymbol, then symbol - that fallback order handles every scenario the API can produce, including option rows where one side carries the OCC and the other carries the root.

What /positions does not include

The Positions API only describes what is currently open. Use a different endpoint for everything else:

  • Closed trades / fill history - not exposed here. Use GET /v1/api/accounts/{accountId}/orders (today's working + recently-closed orders; filter by executed > 0) or GET /v1/api/accounts/{accountId}/orders/start-date/{startDate} (up to 1 week of historical orders, where each row is one fill - filter by !canceled).
  • Pending / working orders - not in /positions. Use GET /orders, filter by orderStatus{"New", "PendingNew", "Accepted", "PartiallyFilled"}.
  • Account balances and buying power - aggregate values are in /pnl (accountValue, availableCash, usedLeverage). The richer per-account view (bp, overnightBp, optLevel, marginRatio) lives on GET /v1/api/account/{accountId} - that endpoint is path /account/, singular, not /accounts/.
  • Real-time fill / price updates - the REST endpoints are snapshots. For incremental changes use the Portfolio and P&L WebSocket streams (covered below).
Paper vs live - mostly the same, with a few real differences

The request and response format for /positions and /pnl is identical on paper (TZP* accounts) and live: authentication, headers, error codes, and JSON response structure all match. The behavioral differences to know about:

  • Paper accounts hold positions across sessions. Anything you bought on a previous paper session is still there next time you read /positions. Use the Flatten All Positions recipe to reset between exercises.
  • Paper P&L is simulated, not real. accountValue, dayPnl, unrealizedPnL etc. reprice against live quotes, but the cash and equity totals are reset to a fixed paper balance and do not reflect any actual money. Margin / borrow / commissions are similarly simulated.
  • Paper option pricing is incomplete. Option lots on paper sometimes have no current quote; those rows show in /positions but not in /pnl[]. Live accounts generally show every lot in both endpoints.
  • Order-acceptance rules differ where the docs explicitly say so. Most basic equity orders behave identically. But several things are paper-only or live-only - locates only work on live (Account Types), some routes / TIFs / Mleg strategies are restricted on paper (Trading → Time in force, Options → Multi-leg paper restrictions), and GET /orders/start-date/{date} returns { "orders": [] } on paper because paper has no historical-order archive.

The no-market-orders-outside-regular-hours rule (covered below) applies to both paper and live identically.


Quick start

The whole working API is two GETs. Both take the account ID in the URL path. Both return JSON. Neither has a body.

Read positions and PnL
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/positions' \
-H 'Accept: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'

curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/pnl' \
-H 'Accept: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'

Authentication uses the same key / secret pair you use for the rest of the trading API. The endpoints accept only GET; non-GET methods return 404 or 405. OPTIONS is supported as a CORS preflight.

If you need real-time updates instead of point-in-time snapshots, skip the polling section below entirely and go straight to Live updates via WebSocket - the Portfolio and P&L streams give you the same data incrementally.


Order placement and off-hours trading

Pre-market (before 9:30 AM ET), post-market (after 4:00 PM ET), and weekends are quiet windows. The position endpoints keep working, but POST /order behavior changes - on both paper and live accounts identically:

  • Market orders are rejected outside regular hours. The HTTP response is still 200 OK, but the body carries orderStatus: "Rejected", executed: 0, and text: null. Nothing fills, nothing rests, no position appears. Market orders cannot execute when the matching venues are closed.
  • Limit orders with timeInForce: "Day_Plus" rest into the extended-hours session and may fill outside regular hours.
  • Limit orders with timeInForce: "Day" placed pre-market are queued for the regular session open and fill at 9:30 AM ET if the price is touched.

An off-hours market-order therefore returns 200 OK at the HTTP layer but nothing changes: the gateway accepts the request, the matching engine declines the order, and /positions keeps reporting your existing overnight rows. Always inspect orderStatus in the response body before assuming a fill - the HTTP status code alone does not confirm execution. The full order-type and time-in-force matrix lives on the Equity Trading page.


Endpoint reference

The base URL for production is https://webapi.tradezero.com. The TZ-API-KEY-ID and TZ-API-SECRET-KEY headers are required on every call. Both endpoints always respond with Content-Type: application/json; charset=utf-8; supplying a different Accept header has no effect on the response format.

Read open positions

GET /v1/api/accounts/{accountId}/positions

Returns every open row on the account: stocks (long or short), single-leg option contracts, and any positions that came from filled multi-leg orders (the legs surface individually with their own positionIds).

Response envelope

Every response is wrapped in a positions object whose value is the array of position rows - the top-level response is always { "positions": [...] }. Read response.positions to access the rows.

Response - mixed stock + option positions
{
"positions": [
{
"accountId": "TZP12345678",
"createdDate": "2026-05-12T19:00:20.4271758+00:00",
"dayOvernight": "Overnight",
"maintenanceRequirement": 0,
"marginRequirement": 0,
"positionId": "2260512190020399758",
"priceAvg": 294.42,
"priceClose": 0,
"priceOpen": 294.42,
"priceStrike": 0,
"putCall": "None",
"rootSymbol": null,
"securityType": "Stock",
"shares": 1,
"side": "Long",
"symbol": "AAPL",
"tradedSymbol": null,
"updatedDate": "2026-05-12T19:00:20.4307353+00:00"
},
{
"accountId": "TZP12345678",
"createdDate": "2026-05-12T19:31:12.4810776+00:00",
"dayOvernight": "Overnight",
"maintenanceRequirement": 0,
"marginRequirement": 0,
"positionId": "2260512193112437606",
"priceAvg": 5.72,
"priceClose": 0,
"priceOpen": 5.72,
"priceStrike": 703,
"putCall": "Call",
"rootSymbol": null,
"securityType": "Option",
"shares": 2,
"side": "Long",
"symbol": "QQQ",
"tradedSymbol": "QQQ260514C00703000",
"updatedDate": "2026-05-12T19:31:17.0098659+00:00"
}
]
}

If the account has no open positions, the server returns { "positions": [] }. The envelope is never omitted, even on a brand-new account.

Position-row fields

FieldTypeStock valueOption value
accountIdstringthe account ID you queriedsame
positionIdstringnumeric-looking string (do not parse as int)same; the stable join key for /pnl and websockets
symbolstringthe ticker (e.g. "AAPL")the underlying root (e.g. "QQQ"), not the OCC
tradedSymbolstringnullthe OCC contract (e.g. "QQQ260514C00703000")
rootSymbolstringnull in current production datanull in current production data - parse tradedSymbol instead
securityTypestring"Stock""Option"
sidestring"Long" or "Short""Long" or "Short" (e.g. a sold-to-open call is "Short")
sharesnumbersize of the position (see "Short rows" note below). Usually integer-valued; treat as number (the wire type is JSON number, not int) so fractional-share rows on supported securities round-trip cleanly.size in contracts (1 contract = 100 shares); always integer-valued
priceAvgnumbervolume-weighted average entry priceaverage premium paid (or received, for shorts)
priceOpennumbersession open price snapshot at row creationsame
priceClosenumber0 while the position is still open - not a real closesame
priceStrikenumber0the strike price as a plain number (e.g. 703, not 00703000)
putCallstring"None""Call" or "Put"
dayOvernightstring"Day" for trades opened today, "Overnight" once they rollsame
marginRequirementnumbermargin reserved for the position; 0 on cash accountsame
maintenanceRequirementnumbermaintenance reserved; 0 on cash accountsame
createdDatestringISO 8601 with offset (e.g. "...+00:00")same
updatedDatestringISO 8601, refreshed on every fill that moves the rowsame

A few behaviors worth noting when you build against these fields:

  • priceClose is 0 while a position is open. It is populated only after the position has closed, so for any open row treat it as "not applicable" and use real-time quotes for current pricing.
  • Options carry the root in symbol and the OCC in tradedSymbol. Use symbol for the underlying ticker and tradedSymbol for the contract identifier. The OCC string in tradedSymbol follows the standard OCC layout: 1-6 characters of root, 6-digit YYMMDD expiry, single-character C/P, then an 8-digit strike where the last 3 digits are the milli-cents fractional component (00703000 = $703.000). For example, QQQ260717C00703000 is a QQQ 2026-07-17 Call at strike 703. The OCC option symbol recipe shows a parser.
  • priceStrike is the human strike, not the OCC fragment. For QQQ260514C00703000 the row reports priceStrike: 703, not 703000 and not 00703000. Fractional strikes are returned as numbers - a $7.50 strike comes back as 7.5.
  • Read side to determine direction; use Math.abs(shares) for size. side is the canonical indicator of whether a position is "Long" or "Short"; shares carries the size of the position. Treating shares as a magnitude alongside side produces a clean direction-and-size pair that is robust across stocks and options.
  • Multi-leg option fills land as individual rows. A securityType: "Mleg" order does not produce a single combined position. Each filled leg appears as its own option row with its own positionId. To reconstruct a spread or covered call in the UI, group rows by the originating clientOrderId from /orders, or join on root symbol + same-day fills.
  • Sub-second timestamps. createdDate and updatedDate come back with seven fractional digits and a +00:00 suffix. Standard JSON / ISO 8601 time parsers handle this correctly; libraries that truncate to millisecond precision are still fine for display, but pay attention if you are diffing timestamps for strict ordering.

Filtering positions

The /positions endpoint always returns the complete list of open positions for the account - there are no server-side filters and no querystring parameters. Apply any filtering you need on the client side after receiving the response.

Common filtering patterns
// Stocks vs options
const stocks = positions.filter((p) => p.securityType === 'Stock');
const options = positions.filter((p) => p.securityType === 'Option');

// Direction
const longs = positions.filter((p) => p.side === 'Long');
const shorts = positions.filter((p) => p.side === 'Short');

// Day trades vs overnight holds
const dayTrades = positions.filter((p) => p.dayOvernight === 'Day');
const overnights = positions.filter((p) => p.dayOvernight === 'Overnight');

// All option legs on a specific underlying (symbol = root on option rows)
const qqqLegs = positions.filter(
(p) => p.securityType === 'Option' && p.symbol === 'QQQ',
);

// Net share delta for NVDA stock lots
const nvdaSize = positions
.filter((p) => p.symbol === 'NVDA' && p.securityType === 'Stock')
.reduce((total, p) => total + p.shares, 0);

Read per-position PnL

GET /v1/api/accounts/{accountId}/pnl

Returns account-level totals plus a pnl[] array of per-lot P&L rows. Rows are keyed by positionId and join cleanly with /positions. The two endpoints describe overlapping but not identical sets:

  • /positions returns every open lot the account holds, including positions the system has no current quote for (e.g. paper option positions on illiquid contracts, expired options awaiting settlement).
  • /pnl returns rows for every lot the system can currently mark to market - i.e. lots with a usable quote - plus any lots that have been closed earlier today (those rows carry exposure: 0 and continue to report realized P&L for the rest of the session).

In typical operation on a live account the two are 1:1 by positionId. On paper - and for any option lot that doesn't currently have a market price - /pnl can have fewer rows than /positions. Always join by positionId and treat a missing row as "no current P&L data".

note

This is the same endpoint documented under Account Information → Retrieve Account Values and Profit/Loss. The account-level aggregate fields (accountValue, availableCash, usedLeverage, etc.) are covered in more detail there.

Response - account with three open positions
{
"accountValue": 994282.71,
"allowedLeverage": 1,
"availableCash": 968814.74,
"dayPnl": 561.65,
"dayRealized": 0,
"dayUnrealized": 561.65,
"equityRatio": 1,
"exposure": 24038.35,
"optionCashUsed": 0,
"pnl": [
{
"positionId": "2260512190020399758",
"symbol": "AAPL",
"exposure": 293.65,
"realizedPnl": 0,
"unrealizedPnL": -0.77,
"dayRealizedPnl": 0,
"dayUnrealizedPnL": -1.15,
"pctPnLMove": -0.26,
"dayPctPnLMove": -0.39
},
{
"positionId": "2260512193112437606",
"symbol": "QQQ260514C00703000",
"exposure": 1429.62,
"realizedPnl": 0,
"unrealizedPnL": 285.62,
"dayRealizedPnl": 0,
"dayUnrealizedPnL": 0,
"pctPnLMove": 24.97,
"dayPctPnLMove": 0
}
],
"sharesTraded": 0,
"totalUnrealized": 2830.05,
"usedLeverage": 0.02
}

If there are no open positions, pnl is [] and the day totals are mostly 0; accountValue and availableCash still reflect the cash balance.

Account-level totals

FieldDescription
accountValueTotal equity: cash + market value of open positions, refreshed in real time.
availableCashCash that could be deployed into a new order today, before borrow / margin.
allowedLeverageMaximum gross leverage permitted on the account (1 on cash, higher on margin).
usedLeverageCurrent gross exposure / equity. 0 on a flat account; equal to exposure / accountValue otherwise.
equityRatioEquity-to-account-value ratio. 1 on a typical account; drops below 1 when you carry credit.
exposureTotal absolute notional across every open position (sum of per-row exposure).
optionCashUsedCash currently tied up in open option positions (premium paid for longs, collateral for shorts).
dayPnldayRealized + dayUnrealized for the current trading day.
dayRealizedRealized P&L closed out today.
dayUnrealizedMark-to-market P&L on positions still open at the end of the read.
totalUnrealizedLifetime unrealized P&L across every still-open row.
sharesTradedTotal shares that traded today across all symbols (informational; not a position count).

Per-position P&L row

FieldDescription
positionIdSame key as the /positions row - join here.
symbolThe OCC for option positions, the equity ticker for stocks. Equals tradedSymbol from /positions for options, or symbol for stocks.
exposureAbsolute market value of the position right now.
realizedPnlLifetime realized P&L on this position (typically 0 while still open).
unrealizedPnLLifetime unrealized P&L on this position.
dayRealizedPnlRealized portion attributable to today.
dayUnrealizedPnLUnrealized portion attributable to today's price move.
pctPnLMoveunrealizedPnL as a percentage of cost basis, in percent (12.0 means +12%).
dayPctPnLMoveSame percentage scoped to today's move.

Note the casing: unrealizedPnL and dayUnrealizedPnL are camelCase with capital L, while realizedPnl and dayRealizedPnl end in lowercase Pnl. Keep the casing exact when reading these fields from the response.


Joining /positions and /pnl

The two endpoints are intentionally complementary. To build a unified table - the kind a trading screen renders - zip them together on three keys in fallback order:

  1. positionId - identical between the two endpoints. Use this first.
  2. tradedSymbol - matches pnl[].symbol for option rows.
  3. symbol - matches pnl[].symbol for stock rows when positionId is unavailable for some reason.
Join pattern (TypeScript pseudocode)
type Position = { positionId: string; symbol: string; tradedSymbol: string | null; /* ... */ };
type PnlRow = { positionId: string; symbol: string; unrealizedPnL: number; /* ... */ };

function joinPositionsAndPnl(positions: Position[], pnl: PnlRow[]) {
const byPositionId = new Map(pnl.map((r) => [r.positionId, r]));
const bySymbol = new Map(pnl.map((r) => [r.symbol, r]));

return positions.map((p) => {
const match =
byPositionId.get(p.positionId) ??
(p.tradedSymbol ? bySymbol.get(p.tradedSymbol) : undefined) ??
bySymbol.get(p.symbol);
return { ...p, pnl: match };
});
}
Join on positionId, not symbol

positionId is the only reliable join key. The bySymbol fallbacks in the snippet above are defensive guards for the narrow race window where a fresh fill appears in one endpoint but not the other yet. In normal operation, positionId resolves every row.

symbol alone is not unique - an account with two open NVDA lots carries two /positions rows and two /pnl rows, both with symbol: "NVDA" but different positionId values. A symbol-keyed Map keeps only the last entry, so the bySymbol fallback would misattribute P&L if more than one lot exists. Always prefer positionId; treat the other fallbacks as last resorts.

Reading both endpoints back-to-back is cheap (median round-trip is well under 200 ms in production), but the two snapshots are not guaranteed to be from the same instant. If a fill lands between them, /positions may show a row that /pnl does not yet, or vice versa, for one or two seconds. Treat any unmatched row as "still settling" and reconcile on the next poll.


Live updates via WebSocket

The REST endpoints are snapshots. Anything that needs to react in real time - a portfolio screen, a P&L ticker, a stop-out monitor - should subscribe to the WebSocket streams instead of polling /positions. There are two streams, and they intentionally split position changes by what causes the change:

StreamEndpointWhat it pushes
Portfoliowss://webapi.tradezero.com/stream/portfolioOrder state changes (Accepted, Filled, Canceled, ...) plus the position rows produced by those fills. Driven by order activity.
P&Lwss://webapi.tradezero.com/stream/pnlAccount-level totals and per-position P&L, recomputed on every price tick. Driven by price movement (and order fills).

Both streams use the same authentication flow described on the WebSocket introduction page: connect, send { "key": ..., "secret": ... }, then send the per-stream subscribe message.

The bootstrap pattern

The two streams behave differently on connect:

  • Portfolio sends incremental updates only - it never delivers a full snapshot when you subscribe. To know about orders and positions that already exist, you must read REST first.
  • P&L sends an init message with the full account snapshot and every per-position P&L row, so a separate REST call is technically optional - but reading GET /pnl is still useful as a fallback if you reconnect.

A stable client is built like this:

  1. Open both WebSockets and authenticate. Start buffering incoming messages immediately - do not process them yet.
  2. Read the REST snapshots in parallel. For the Portfolio stream, you need GET /orders (working orders) and GET /positions (open positions). For the P&L stream, the init WebSocket message itself carries the account totals; GET /pnl is the equivalent REST snapshot if you prefer to seed from REST.
  3. Apply buffered WebSocket messages in arrival order on top of the REST snapshot. A Portfolio Order update for a clientOrderId you don't have yet is a new order; a Position update for a positionId you don't have yet is a new fill creating a position. A P&L position update for a positionId is a price refresh; an aggCalcs update is an account-totals refresh.
  4. From then on, react to messages live. Fall back to a one-shot REST refresh (GET /orders + GET /positions) if the connection drops or you detect a sequence gap.

The reason for buffering before the REST call (rather than the more obvious "REST first, WebSocket second") is that changes that land between the snapshot and the subscription would otherwise be lost - a fill that happens during the GET /positions round-trip would never be reflected in your client.

Bootstrap skeleton
const portfolioWS = await connectAndAuth('/stream/portfolio');
const pnlWS = await connectAndAuth('/stream/pnl');

// Buffer first - do NOT process yet.
const buffered: Message[] = [];
portfolioWS.on('message', (m) => buffered.push(m));
pnlWS.on('message', (m) => buffered.push(m));

// Subscribe with the field name each stream actually requires:
// - Portfolio uses `accountId`
// - P&L uses `account`
portfolioWS.send({accountId, subscriptions: ['Order', 'Position']});
pnlWS.send({account: accountId});

// Bootstrap from REST: orders + positions for the Portfolio stream.
// (The P&L stream sends its own `init` snapshot, so you can skip GET /pnl
// here unless you want a REST fallback path on reconnect.)
const headers = {
'TZ-API-KEY-ID': '…YOUR_KEY…',
'TZ-API-SECRET-KEY': '…YOUR_SECRET…',
};
const [ordersRes, positionsRes] = await Promise.all([
fetch(`https://webapi.tradezero.com/v1/api/accounts/${accountId}/orders`, {headers}).then((r) => r.json()),
fetch(`https://webapi.tradezero.com/v1/api/accounts/${accountId}/positions`, {headers}).then((r) => r.json()),
]);

const state = seedFromRestSnapshot(ordersRes.orders, positionsRes.positions);
for (const m of buffered) state.apply(m);
buffered.length = 0;
portfolioWS.on('message', (m) => state.apply(m));
pnlWS.on('message', (m) => state.apply(m));

REST and WebSocket field reference

REST and WebSocket payloads describe the same positions, with each surface tuned for its delivery model: REST returns full snapshots, the Portfolio stream pushes order-driven changes, and the P&L stream pushes price-driven recalculations. Use the table below as a quick reference when you build a client that consumes both.

ConceptREST /positions rowPortfolio WS position updateP&L WS position update
Position identitypositionIdidpositionId
Symbol (stock)symbolsymbolsymbol
Symbol (option)symbol = root, tradedSymbol = OCCsymbolsymbol = OCC
Short encodingside: "Short" is the canonical direction. shares may come back as a signed number (negative for short option positions) or unsigned for stocks - always pair side with Math.abs(shares) for size.side: "Short", signed shares permittedn/a (P&L stream is unsigned)
Margin / maintenance fieldsmarginRequirement, maintenanceRequirementnot deliverednot delivered
P&L row shapeflat: { unrealizedPnL, dayUnrealizedPnL, ... }n/anested: { pnlCalc: { unrealizedPnL, dayUnrealizedPnL, ... } }
Snapshot array namepnln/a (incremental only)positions (inside pnlReturn on the initial init message)
Account-id field on subscribepath parameter accountIdsubscribe body accountIdsubscribe body account

A few field-name details to map carefully when you bridge the two surfaces:

  • The Portfolio stream uses id as the position identifier on its Position updates, while REST and the P&L stream use positionId. Normalize on receipt.
  • The P&L stream's subscribe message uses the field name account; the Portfolio stream's subscribe message uses accountId. Use each as documented per stream.

Map both surfaces to a common internal format and the rest of your client can treat all three as a single shape.

When REST polling is still appropriate

Pure REST polling is the right call when:

  • You only need an occasional snapshot (a daily report, a webhook handler, a scheduled cron).
  • You cannot maintain a long-lived WebSocket (constrained runtime, serverless, restrictive corporate proxy).
  • You want a simple read-only audit script that does not need to track changes.

Both REST endpoints are idempotent, have no published rate limit, and tolerate parallel reads (4-way concurrency returns consistent snapshots). A reasonable default cadence is 5 seconds for a background portfolio refresh, with 1.5 to 2 second bursts for the 30 seconds immediately after a POST /order so a freshly-filled row appears quickly. Anything tighter than ~1 second adds load without giving you newer data, since position state propagates through the gateway in that range.


Error responses

ConditionStatusBody
Missing TZ-API-KEY-ID or TZ-API-SECRET-KEY404Not found\n (plain text)
Both auth headers present but values are invalid404Not found\n
Account ID belongs to another customer (foreign account)404Not found\n
Account ID does not exist (e.g. TZP99999X)404Not found\n
Account ID is empty (/accounts//positions)404404 page not found
Account ID has whitespace, quotes, or extra trailing characters404Not found\n
POST / PUT / DELETE / PATCH against /positions404 or 405empty or 405 method not allowed
HEAD against /positions405empty
OPTIONS against /positions or /pnl200empty (CORS preflight)

Two things worth knowing when you handle errors:

  • Auth failures and "account not yours" failures both come back as 404 Not Found. This is a deliberate choice that prevents account-ID enumeration - the response does not distinguish "account does not exist" from "you are not authorized for this account." If a previously-working call starts returning 404, audit your credentials first.
  • Error bodies are text/plain, not JSON. Do not call JSON.parse() on an error response. Read response.text() and surface it directly, or branch on the content-type header before parsing.

URL handling is permissive: lowercase or mixed-case account IDs (tzp12345678), lowercase header names (tz-api-key-id), and trailing slashes on the path all return 200 OK with the same positions list.


Recipes

A few flows from the Quickstart Recipes gallery exercise these endpoints end-to-end:

  • Flatten All Positions. Read /positions, send a closing POST /order for every row, re-read until the list is empty. The cleanest way to reset a paper account between exercises.
  • Live Account Dashboard. Combines /account/{accountId}, /positions, and /pnl into a single periodic snapshot - a useful starting point for a polling-style monitor.
  • Stop-Loss on a Long Position. Reads /positions, attaches a stop order at a configurable percentage below priceAvg, and watches the row disappear when the stop is hit. The cleanest single-position-lifecycle example in the gallery.
  • OCC Option Symbol. Parses the tradedSymbol field on option rows into root + expiry + put/call + strike. Pairs with the option-row notes above.
  • Track Orders Across Sessions. Pairs /orders with /positions to reconstruct what filled when, including dayOvernight rollovers.

If you are building a brand-new client and want a quick end-to-end sanity check, the simplest exercise is: read /account/{accountId} for balances, read /positions for rows, read /pnl for P&L, then check that accountValue ≈ availableCash + sum(per-row exposure) + (option premium balance). The numbers will not match to the cent across snapshots taken seconds apart, but they should match within a handful of dollars on a quiet account.