Skip to main content
Positions are created by placing perp orders through sdk.trading.placeLimitOrder / placeMarketOrder with tradingMode: "MARGIN" and leverage — side sets the direction (one-way), and there is no createPosition call. For lifecycle, prices, and UI patterns, see Positions.

Read

(params?: ListPositionsParams) => Promise<ListPositionsResponse>
Paginated list of positions, optionally scoped by margin account, trading pair, or status.Parameters:
  • params?: ListPositionsParams
    • marginAccountId?: string
    • tradingPairId?: string
    • status?: PositionStatusFilter — "OPEN" | "CLOSED" | "LIQUIDATING" | "LIQUIDATED"; an EXPIRED position matches under a CLOSED filter
    • pageSize?: number — 1 to 1000, defaults to 20
    • pageToken?: string — cursor pagination. Omitted or "" starts the walk (newest-opened first, except status: "OPEN", which walks in (marginAccountId, tradingPairId, positionId) order); otherwise pass the previous response’s nextPageToken. There is no page: the SDK rejects one with a ValidationError before sending, and a raw REST request carrying it gets 400. The filters must match the request that minted the token.
Returns: ListPositionsResponse:
  • positions: Position[] — order depends on the filter. status: "OPEN" is served from the matching engine’s live view, which holds no timestamp for a position the persistor has not written yet, so it is ordered by (marginAccountId, tradingPairId, positionId) ascending: stable and total, but UUID order rather than newest-first. Every other filter reads persisted rows ordered newest-opened first, on the immutable (openedAt, id) key. The two are separate cursor spaces, so a token is only valid in the one that minted it, and crossing between them is asymmetric: a persisted token offered to status: "OPEN" while the engine is reachable is served by the persisted walk (so a walk that started on persisted rows during an outage keeps going once the engine returns), but a live token cannot cross — once the engine is unreachable the request is a 400 and the walk must restart, because the two relations share no sort key and resuming at a key from the wrong one would skip or repeat rows silently. A token neither recognises is a 400
  • total: number — bounded by the server count ceiling; a lower bound when totalCapped is true
  • pageSize: number
  • nextPageToken: string — cursor for the next page, "" once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
  • totalCapped: boolean — true when more than the ceiling matched, so total is a lower bound; false when total is exact, including exactly the ceiling
Throws: REST 503 / gRPC Unavailable (code SERVICE_UNAVAILABLE) when the matching engine’s index-price read fails transiently; retry. Any other failure of that read is 500.
(positionId: string) => Promise<Position>
Full snapshot of a single position.Parameters:
  • positionId: string — UUID
Returns: Position:
  • positionId: string
  • marginAccountId: string
  • riskBucketId?: string
  • marginMode?: "ISOLATED" | "CROSS"
  • tradingPairId: string
  • side: "LONG" | "SHORT" | "NONE"
  • size: string — base-asset quantity. On an open position this is the current absolute quantity; on a terminal (closed/liquidated/expired) row it is the total quantity closed over the position’s life
  • entryPrice: string
  • markPrice: string
  • indexPrice?: string
  • unrealizedPnl: string
  • realizedPnl: string
  • isolatedMargin: string — on an open ISOLATED position, its committed position principal adjusted by settled realized PnL, fees, and funding, clamped at zero. Unfilled-order collateral is excluded. This is the unrealized-return denominator and the same backing used by isolated liquidation; live and persisted reads agree. A price gap can pass the backing before asynchronous liquidation occurs. On an open CROSS position, the posted margin. On a terminal row, the total initial margin allocated across reducing executions, used for lifetime realized return.
  • leverage?: string
  • maintenanceMarginRequired: string - position-scoped requirement at the mark price using the market maintenance-margin rate; "0" when the position has no open exposure
  • initialMarginRequired?: string - position-scoped requirement at the mark price that honors effective leverage and the market initial-margin floor; "0" when the position has no open exposure
  • liquidationPrice: string — isolated position/risk-bucket threshold; cross conditional per-position threshold, not a whole-account scalar
  • status: "OPEN" | "CLOSED" | "LIQUIDATING" | "LIQUIDATED" | "EXPIRED" — LIQUIDATED and EXPIRED are terminal statuses in the exported PositionStatus union
  • netRealizedPnl?: string — terminal rows only; lifetime realized PnL minus funding and whole-life cumFees, deducted once. Absent until the final close is recorded in its PnL sample and the reducing history is complete; poll again while pending. With no PnL sample it remains absent for 28 days after close, then falls back to the closing-execution net without unavailable opening fees. Also absent when reducing-history totals disagree with stored realized PnL.
  • realizedRoe?: string — terminal rows only; netRealizedPnl as a percentage of the total allocated initial margin (leverage-aware realized return). Absent under the same condition as netRealizedPnl
  • exitPrice?: string — terminal rows only; volume-weighted average price across the position’s reducing executions. A position closed in several fills exits at this price, not at the final fill or the mark at close. Absent when the reducing-execution history is unavailable or fails its completeness check against the stored realized PnL; unlike netRealizedPnl, it does not wait for the final close to be recorded in the PnL sample
  • fundingPaid?: string — terminal rows only; lifetime funding paid (signed; negative means received), summed over the position’s reducing executions. Absent when the reducing-execution history is unavailable or fails its completeness check against the stored realized PnL; unlike netRealizedPnl, it does not wait for the final close to be recorded in the PnL sample
  • feesPaid?: string — terminal rows only; trading fees paid on the position’s reducing executions (signed; negative means rebated). Closing-side fees only — fees on opening fills are not included. Absent when the reducing-execution history is unavailable or fails its completeness check against the stored realized PnL; unlike netRealizedPnl, it does not wait for the final close to be recorded in the PnL sample
  • cumFees?: string — total lifetime trading fees, including opening and reducing fills, application fees, liquidation fees and signed rebates (negative means net rebated). Present on open and terminal rows; serialized as null when the fee data is unavailable or incomplete, and after close until the final PnL sample arrives. Existing historical attribution errors are not repaired retroactively.
  • updatedAt: string — ISO 8601, and when the position was last mutated (a fill, collateral transfer, funding settlement or close), not when the response was built. Polling an unchanged position returns the same timestamp on every surface — list, detail, risk, and the WebSocket positions channel alike — and an oracle reprice does not move it. That holds for an open position served from a reachable matching engine; a terminal row, a position read after a matching-engine restart, and an open row read while the engine is unreachable are served from other clocks — see Inspect a Position and the positions channel. Display metadata: order by version, never by this
  • openedAt?: string | null — ISO 8601, when the position opened: the fill that took its exposure from zero to non-zero. It never changes for the life of positionId, so time held is now - openedAt while open and updatedAt - openedAt once terminal. A flip opens a new position with its own openedAt. null (or absent, from an older server) when the opening instant is unknown — never filled with the read time, so check for both before date arithmetic
  • version?: number — opaque, non-negative durable revision of this position row. Compare only within the same position; higher is newer, and never order by updatedAt. The WebSocket positions snapshot and ranked live position_update frames carry the same value for identical state, so reconcile on positionId + version. Some live frames are deliberately unranked and omit version even when their state matches this row — see WebSockets. The merge rule is per field group: on a higher version apply everything; on an equal version skip the versioned fields as an idempotent redelivery but still take markPrice, unrealizedPnl, liquidationPrice, leverage, maintenanceMarginRequired and initialMarginRequired from the newer payload, because those six are re-derived on every read rather than served from the versioned row and carry no version (five of them move with the oracle mark; leverage does not, but the live surfaces derive it while the persisted row serves its stored column, so it can still disagree at an unchanged version), and still take isolatedMargin too, because the WebSocket positions snapshot overlays it from an independent live matching-engine read rather than from the versioned row and it can therefore reflect a later command than the version names; on a lower version skip both groups, since an older command carries nothing newer — not even its mark. Discarding a whole payload on an equal version freezes displayed PnL until the position’s next real mutation. 0 is the unranked sentinel and never participates in ordering — treat it exactly as an absent version and fall back to a full field comparison. It advances on sequenced mutations (open, add, reduce, close, funding, margin transfer, liquidation, ADL). 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. 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 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. A read served from live matching-engine state reports 0: the engine holds the state but not the version, so it cannot rank itself — see WebSockets for which frames are ranked
Non-zero funding frames carry this version from captured command state. Terminal close/liquidation/ADL frames normally carry it too and retain lifetime reduced quantity — the sum of every reducing execution — and the exact CLOSED/LIQUIDATED classification. 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 size === "0".Throws: REST 503 / gRPC Unavailable (code SERVICE_UNAVAILABLE) when the matching engine’s index-price read fails transiently; retry. Any other failure of that read is 500.
For marginMode: "CROSS", a position’s liquidationPrice 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. Render cross values as conditional. Treat an absent or blank value as unavailable, never 0.
(positionId: string) => Promise<PositionRisk>
Just the volatile risk fields, refreshed. Cheaper than getPosition for live UI.Parameters:
  • positionId: string
Returns: PositionRisk:
  • positionId: string
  • markPrice: string
  • indexPrice?: string
  • unrealizedPnl: string
  • liquidationPrice: string — isolated position/risk-bucket threshold; cross conditional per-position threshold, not a whole-account scalar
  • marginRatio: string — account maintenance requirement divided by equity. At zero or negative equity with a remaining maintenance requirement it is a maximum-distress sentinel (Decimal::MAX, 79228162514264337593543950335) rather than 0 or a negative value — clamp it for display, never render it raw as a percentage
  • maintenanceMarginRequired: string - position-scoped requirement at the mark price using the market maintenance-margin rate; "0" when the position has no open exposure
  • initialMarginRequired?: string - position-scoped requirement at the mark price that honors effective leverage and the market initial-margin floor; "0" when the position has no open exposure
  • updatedAt: string — ISO 8601; the same mutation instant the matching position reports
Throws: REST 503 / gRPC Unavailable (code SERVICE_UNAVAILABLE) when the matching engine’s index-price read fails transiently; retry. Any other failure of that read is 500.Example:
PositionRisk does not include marginMode; use the matching position or risk-bucket context to label a cross value as conditional rather than account-wide.
(params?: ListPositionHistoryParams) => Promise<ListPositionHistoryResponse>
Lifecycle events for one position or all positions in scope: opens, partial fills, funding, margin changes, liquidations, closes.Parameters:
  • params?: ListPositionHistoryParams
    • positionId?: string
    • marginAccountId?: string
    • tradingPairId?: string
    • reductionOnly?: boolean — when true, returns only reductions, closes, and liquidations; omits opens, partial fills, funding, and margin-change events
    • pageSize?: number — 1 to 1000, defaults to 20
    • pageToken?: string — cursor pagination. Omitted or "" starts the walk at the newest row; otherwise pass the previous response’s nextPageToken. There is no page: the SDK rejects one with a ValidationError before sending, and a raw REST request carrying it gets 400. The filters must match the request that minted the token.
Returns: ListPositionHistoryResponse:
  • events: PositionHistoryEvent[] — each with eventId, eventType, optional quantity / price / realizedPnl, and timestamp. Reduction rows additionally include:
    • entryPrice?: string — position’s average entry price at close
    • allocatedInitialMargin?: string — initial margin committed when the position was opened
    • fundingPaid?: string — cumulative funding charges over the position’s life
    • netRealizedPnl?: string — realized PnL net of fees and funding
    • realizedRoe?: string — return on equity (net PnL ÷ allocated initial margin)
    • positionSide?: "LONG" | "SHORT" — side of the closed position
  • pageSize: number
  • nextPageToken: string — cursor for the next page, "" once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
No item count: this listing spans the hot table and the archive, and counting behind a filter would walk the wallet’s whole lifetime prefix on every page.

Modify

The positions API has no margin-adjustment method. Adjust a position’s margin through sdk.marginAccounts — see Margin Accounts. Every leg moves collateral between a risk bucket and the parent margin account, never to or from the spot wallet:
  • Add margin — transferCollateralToRiskBucket({ asset, amount, tradingPairId, marginMode: "ISOLATED" }). By default (or with applyToPositionMargin: true) the amount is committed to the open position’s stored margin — leverage drops and stays down. Pass applyToPositionMargin: false to fund the bucket with spendable free collateral instead (what order-funding loops need). (The deprecated pair-scoped transferCollateralToMarginAccount matches the default.)
  • Release collateral back to the parent — transferCollateralFromMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId }).
(positionId: string, request: AttachPositionTpSlRequest) => Promise<AttachPositionTpSlResponse>
Set take-profit, stop-loss and/or trailing-stop conditional orders bound to the position. Every leg is optional; at least one is required.Parameters:
  • positionId: string
  • request: AttachPositionTpSlRequest
    • takeProfit?: TpSlLeg
    • stopLoss?: TpSlLeg
    • trailingStop?: TrailingStopLeg — one per position (a second attach returns 409); never part of oco
    • oco?: boolean — cancel the sibling leg after one triggers. Requires both takeProfit and stopLoss
TpSlLeg:
  • triggerPrice: string
  • orderType: "LIMIT" | "MARKET"
  • limitPrice?: string — required for LIMIT
  • closePosition?: boolean — resolve the full live position size when the leg triggers
  • quantity?: string — fixed size; defaults to full position size when omitted. Required when closePosition is false; must be omitted when closePosition is true
  • timeInForce?: "GTC" | "IOC"
  • slippageToleranceBps?: number
  • expiresAt?: string — ISO 8601, auto-cancel if not triggered
TrailingStopLeg (reduce-only MARKET close whose trigger trails the best mark since arming — see Trailing Stop):
  • trailBps: number — integer, 10 to 2000 basis points behind the watermark
  • activationPrice?: string — mark level that starts tracking and seeds the watermark; rejected if already reached. Omitted, tracking starts at the current mark
  • closePosition? / quantity? — same sizing contract as TpSlLeg
  • slippageToleranceBps?: number — 0 to 10000
  • expiresAt?: string — ISO 8601
Returns: AttachPositionTpSlResponse:
  • positionId: string
  • takeProfitOrderId?: string
  • stopLossOrderId?: string
  • trailingStopOrderId?: string
  • status: "SUCCESS" | "FAILED"
  • message: string
Throws:
  • REST 409 / gRPC AlreadyExists when trailingStop is set and the position already has an active trailing stop (or a resting entry order on the same trading pair and risk bucket carries one that would arm on its fill)
  • REST 429 / gRPC ResourceExhausted when the order-creation budget is spent — each attach draws one item from the budget it shares with order creates, whatever legs it carries. See Rate Limits
Example:
Attaching a full-close (closePosition: true) leg supersedes the position’s previously active full-close legs of the same condition type: the old legs are cancelled atomically in the same engine step that creates the replacement — each emitting a conditional_order_update with reason: "cancelled" before the replacement’s "created" — so moving a TP or SL is one call, with no window in which the position holds both levels or neither. OCO siblings, fixed-quantity legs (laddered partial exits still stack), entry-order-attached legs and trailing stops survive a supersede. A trailing stop never supersedes: a second one on the same position is rejected with 409. See Order Management.Lifecycle for these conditionals streams via the conditional_order_update websocket event.Resident conditional-order caps default to 10 active legs per position, 100 resident legs per user on an engine shard (including pending entry-order legs), and 100,000 on that shard; deployments can override them. The attach subtracts full-close legs it supersedes before checking the caps. An engine cap refusal creates none of its new legs, but this adapter reports it as generic 500 MATCHING_ENGINE_ERROR / gRPC INTERNAL, not a structured cardinality error. That status alone does not identify the cause. See Conditional Perps Orders.Throws: 400 for handler-validated invalid legs or a position that is not open; 401 without valid authentication; 403 for a delegated session or denied access; 404 for a missing or unowned position; 409 for an existing or pending trailing stop; 429 when the order-creation budget is enforced and exhausted; 500 on an engine cap refusal or internal failure; 503 for an unavailable risk bucket, transient engine unavailability or maintenance. A known cardinality refusal needs fewer resident legs, not a timed retry.

Close

(positionId: string, request: ClosePositionRequest) => Promise<ClosePositionResponse>
Submit a closing order against the position. The position transitions to CLOSED only after the closing order fills.Parameters:
  • positionId: string
  • request: ClosePositionRequest
    • closeType: "MARKET" | "LIMIT" | "IOC"
    • limitPrice?: string — required for LIMIT and IOC, forbidden only for MARKET
    • quantity?: string — defaults to full position size (use for partial close)
    • slippageToleranceBps?: number — for MARKET / IOC
Throws: REST 429 / gRPC ResourceExhausted when the risk-reduction order budget is enforced and spent — each close draws one item from the budget it shares with cancels, which is twice the order-creation budget and separate from it. Nothing was submitted; retry after retryAfter. See Rate LimitsReturns: ClosePositionResponse:
  • closeOrderId: string
  • status: string
  • message: string
  • submittedQuantity: string
Examples:
(request?: BatchCloseAllRequest) => Promise<BatchCloseAllResponse>
Submit a MARKET reduce-only close for every open perp position owned by the caller — a one-call “panic close”. Best-effort and partial: each position is closed independently, so a per-position failure is reported in results rather than aborting the batch.Parameters:
  • request?: BatchCloseAllRequest
    • tradingPairId?: string — limit the close to one trading pair; if omitted, all open positions are closed
    • slippageToleranceBps?: number — slippage tolerance applied to each MARKET close
Throws: REST 429 / gRPC ResourceExhausted when the risk-reduction order budget is enforced and spent — the call draws one item per open position it closes (an empty call draws one). The first 100 are charged before anything is sent: over budget the whole call is refused and nothing is closed; retry after retryAfter. Past 100 open positions, those the budget does not cover are not sent and come back in results with RATE_LIMIT_EXCEEDED. See Rate LimitsReturns: BatchCloseAllResponse:
  • totalRequested: number — positions the batch attempted to close
  • totalClosed: number — closes accepted by the matching engine (SUCCESS, PARTIAL, or PENDING)
  • totalFailed: number
  • results: BatchCloseResult[]
BatchCloseResult:
  • positionId: string
  • closeOrderId?: string — present when accepted
  • status?: string — "SUCCESS" | "PARTIAL" | "PENDING", present when accepted
  • submittedQuantity?: string — present when accepted
  • error?: BatchCloseError — present on failure
BatchCloseError:
  • code: string — e.g. POSITION_NOT_FOUND, INVALID_REQUEST, MATCHING_ENGINE_ERROR
  • message: string
Examples:

Errors

  • 400 on order placement and modify endpoints — see rejectReason returned by the matching engine
  • 400 on closePosition when quantity exceeds remaining position size
  • 400 on attachPositionTpSl when trigger prices are on the wrong side of the position (e.g., TP below entry on a LONG)
  • 404 when the positionId does not exist