createPosition call.
Use sdk.positions for position lists, snapshots, live risk reads, margin changes, TP/SL attachment, close orders, and position history.
Surface Map
Lifecycle
Positions are created when a perp order (tradingMode: "MARGIN" with leverage) fills. Perps are one-way — side sets the direction; omit the deprecated positionSide compatibility field in new integrations.
Multiple fills on the same
(marginAccountId, tradingPairId) aggregate into one position (perps are one-way, so a market has a single net position). The position’s size and entryPrice update as the user adds exposure.
List Positions
Inspect a Position
version advances on sequenced position mutations — open, add, reduce, partial and full close, funding, margin transfer, liquidation and ADL. It deliberately does NOT advance for markPrice, unrealizedPnl, liquidationPrice, leverage, maintenanceMarginRequired and initialMarginRequired, which are re-derived on every read rather than served from the versioned row and change with no position mutation behind them. Five of the six move with the oracle mark. leverage does not — it is opening notional over posted margin, which no reprice touches. It sits in the same group for the other reason: the live surfaces derive it from engine state while the persisted row serves its stored leverage column, so the two can disagree at an unchanged version. (A valuation-only oracle reprice persists nothing and advances no version. A state-transitioning oracle command — a maintenance-margin breach or recovery — persists the account’s snapshot and restamps the positions it changed, like any other sequenced mutation.) It advances exactly when the row’s versioned content changed at that sequence, a change undone within one persistence batch included; a position a command left unchanged keeps its version, so a ranked live position_update frame for an untouched sibling of a filled position can carry a higher version than this row while describing identical content — rank it as newer, and nothing changes.
Non-zero funding frames are ranked too, and funding carries state captured inside the sequencer command. Terminal close/liquidation/ADL frames are normally ranked and carry the same retained lifetime reduced quantity — the sum of every reducing execution — and exact status as this row: ordinary closes and flattened ADL counterparties are CLOSED; liquidation victims, backstop exits and the bankrupt ADL side are LIQUIDATED. After restart, a terminal frame rebuilt from pre-upgrade reduction history omits version when that history lacks the size or margin fields needed to reconstruct complete lifetime values; incomplete size history carries only the known partial total. Reconcile that frame by full field comparison. Detect closure from status, never from a zero size.
So the merge rule is per field group, never per payload: on a higher version apply everything; on an equal version skip the versioned fields as an idempotent redelivery but still take those six valuation fields from the newer payload; on a lower version skip both groups, since an older command carries nothing newer — not even its mark.
updatedAt is when the position was last mutated — a fill, collateral transfer, funding settlement or close — carried from the producer’s own record of that mutation on every read path. Polling an unchanged position returns the same timestamp, and an oracle reprice does not move it. It is display metadata; version is the reconciliation key.
openedAt is when the position opened — the fill that took its exposure from zero to non-zero — and it never moves afterwards. Time held is now - openedAt for an open position and updatedAt - openedAt for a terminal one, so you do not need position history (which lists reductions and closes only) or PnL history (hourly buckets) to work it out. A flip closes the position and opens a new one with a new positionId and its own openedAt. The REST read, the WebSocket positions snapshot and live position_update frames report the same instant, normally to within one matching-engine step; it can sit slightly ahead of your clock, and after a server restart it can move by the server’s own clock drift, so clamp a computed time held at zero. It is absent only when the opening instant is unknown, and it is never filled with the time of the read.
The match across surfaces is exact for an open position. Two cases are served from the persistence clock instead and differ by the persistence lag: a terminal row — the live frame carries the engine’s close instant while a terminal REST read carries the stored one — and any position read after a matching-engine restart. Treat it as accurate to that lag.
One further case is not bounded by that lag. While the matching engine is unreachable, a REST read of an open position falls back to that row’s lifecycle timestamps — close, then last funding, then open — so a position filled repeatedly since it opened reports its opening instant until funding settles. A live position_update frame is unaffected, being built from engine state, so a REST value older than a frame you already hold is expected in that window and the frame is the fresher one. Every list, detail and risk read also reads the market’s index price from the matching engine; when that read fails transiently the request returns 503 with code SERVICE_UNAVAILABLE (gRPC UNAVAILABLE) rather than a row without it, so retry.
The WebSocket positions channel describes a position with these same fields — riskBucketId, marginMode, maintenanceMarginRequired and initialMarginRequired included — so the common case no longer needs a REST read after a frame arrives. Identity and scope carry the same values on the subscribe-time snapshot, on the live position_update frames and on this REST read; indexPrice, realizedPnl, netRealizedPnl, exitPrice, fundingPaid, feesPaid and cumFees remain REST-only. The five mark-derived values are priced at whichever mark the answering surface holds, so a snapshot row and a live frame can differ by one oracle tick; take those — and leverage with them — from the newest frame, as the merge rule above already requires.
isolatedMargin is the one exception among the versioned fields. The WebSocket positions snapshot overlays it from an independent live matching-engine read rather than reading it from the versioned row, so it can reflect a later command than the version names — take it from the newest payload rather than skipping it on an equal version. Discarding a whole payload on an equal version freezes displayed PnL until the position’s next real mutation. Order position state by version, never by updatedAt or arrival order.
For an isolated risk bucket, liquidationPrice is the position/risk-bucket threshold. For marginMode: "CROSS", render it as conditional for that position, not as a whole-account scalar: it varies only that position’s mark while all other marks in the cross risk bucket remain unchanged. Other position marks, funding, realized PnL, fees/reserves, and collateral can change it. Treat an absent or blank value as unavailable; never replace it with 0.
Parent Accounts and Risk Buckets
A parent margin account is the collateral container for perps. Position-level exposure lives in risk buckets under that parent, scoped by trading pair and optionalstrategyKey.
A user has one parent margin account per application scope. Opening perp orders create or reuse risk buckets under that parent.
List margin accounts when you need to render the parent account and its pair-scoped buckets together:
marginAccountState: "CLOSED" — plus separate rows for risk buckets. Bucket rows include riskBucketId, marginMode, tradingPairId, and optional strategyKey; parent rows leave those unset. A copy risk bucket row (a follow’s copy account) also carries copyFollowId and, when the leader still holds one, copyLeaderHandle; account totals still include those rows.
Follow nextPageToken until it comes back empty to read every row. A state filter matches riskBucketState, so it keeps only risk-bucket rows and drops the parent row.
Account States
NORMAL: healthy. New positions can be opened and collateral can be transferred subject to margin requirements.PENDING_LIQUIDATION: maintenance margin was breached. New orders are rejected while positions are unwound.BAD_DEBT: post-liquidation deficit. The account cannot open new positions and may have negative equity. Collateral transfers into or out of aBAD_DEBTrisk bucket are refused.
PENDING_LIQUIDATION and BAD_DEBT, and poll account summaries or position risk for active trading views. Do not assume ledger projections are enough for live risk UI.
Live Risk
For active trading UIs, pollgetPositionRisk for volatile fields instead of repeatedly fetching the full position.
marginRatio is the account’s maintenance requirement divided by equity. It is 0 for an account with no open positions. For an account that still carries a maintenance requirement at zero or negative equity (bad debt), getPositionRisk reports a maximum-distress sentinel — a very large decimal string (Decimal::MAX, ~7.9e28) — rather than 0 or a negative value that would sort as safer than a healthy account. Treat any value at or above the liquidation threshold as maximal distress and clamp it for display.
Raw WebSocket clients can subscribe to positions or positions:{tradingPairId} for live position updates and liquidations or liquidations:{tradingPairId} for liquidation alerts. The TypeScript SDK exposes typed helpers for both: sdk.ws.positions(handler, tradingPairId?, onSnapshot?, onValuation?) and sdk.ws.liquidations(handler, tradingPairId?) — omit tradingPairId to stream all pairs. onValuation receives the channel’s periodic position_valuation frames, which refresh an open position’s mark-derived figures between lifecycle events; see Position Valuation Frames.
Price Fields
Avoid mixing last trade price with mark-price PnL. Perps PnL and liquidation should use mark price.
PnL
Unrealized PnL on an open position:unrealizedPnl already computed. The formula is mostly useful for previews and explaining the UI.
Modify Margin
A position’s margin is the collateral its risk bucket holds, so margin is adjusted through the bucket rather than through the position. Both directions move collateral between the bucket and the parent margin account — never to or from your spot wallet. Fund the parent first withtransferCollateralToParentMarginAccount if it has no free collateral.
Allocating more lowers liquidation risk and effective leverage without changing
position size.
withdrawableCollateral: its
allocation plus realized PnL, less funding paid, unrealized losses and its
initial-margin requirement (resting-order reserve included). Realized profit
can leave a live bucket, so the bound is not capped at the allocation; an
unrealized gain never raises it, and it is zero while the bucket is not
NORMAL.
There is no position-level margin method. A position’s margin changes only
through the risk-bucket transfers above, so collateral already held in the
parent margin account is what funds it.
Close Positions
closePosition submits a closing order. The position does not become CLOSED until that order fills.
For delegated-agent sessions, closing a position is checked like order creation because Monaco submits a close or reduce order on the owner’s account. The active delegation must allow the market, action, and any applicable perps-account scope before the close is accepted.
LIMIT+IOC order, so — like a LIMIT close — it requires limitPrice; only a MARKET close omits it.
The response carries the submitted close-order ID. Subscribe to order events or poll the position to confirm the close.
For batch close-all flows, delegated sessions should include an explicit trading-pair scope. This keeps delegated close permission market-specific instead of allowing an agent to close every owner position across all markets.
Batch Close All
batchCloseAllPositions submits a MARKET reduce-only close for every open perp position owned by the caller — a one-call “panic close” for risk-off and emergency-exit UIs.
results rather than aborting the rest. Inspect the response to drive UI feedback.
totalClosed counts closes accepted by the matching engine (SUCCESS, PARTIAL, or PENDING). As with closePosition, an accepted close does not mark the position CLOSED until the close order fills — confirm via order events or by polling the position.
Position History
UI Rules
- Sort position tables by risk or unrealized PnL, not just creation time.
- Show liquidation distance and margin ratio near every open position. Label cross liquidation prices and distances as conditional, never account-wide.
- Use mark price for PnL and liquidation displays.
- Treat an absent or blank liquidation price as unavailable, never
0. - Default close modals to full-size market close, with optional partial size and limit price.
- Infer position side in order tickets: opening or adding to a long is
BUY; reducing a long isSELL+reduceOnly: true. Perps are one-way —sideis the direction; omit the deprecatedpositionSidecompatibility field. - Poll
getPositionRiskfor active positions and show stale-state handling if the read fails.
Errors To Handle
400when order placement or simulation rejects risk400when adding margin exceeds free collateral400when reducing margin would leave the risk bucket below its initial-margin requirement400when close quantity exceeds remaining position size403when a delegated-agent session attempts to close a position outside its active policy404whenpositionIddoes not exist503(SERVICE_UNAVAILABLE) fromlistPositions,getPositionandgetPositionRiskwhen the matching engine’s index-price read fails transiently; retry
Related References
- Perps Collateral for margin collateral funding and transfers
- Order Management for risk-bucket routing, reduce-only orders, and TP/SL order flows
- Margin for margin concepts
- TypeScript positions for SDK method reference

