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
    • page?: number — at most 10,000; a larger page is rejected
    • pageSize?: number
Returns: ListPositionsResponse:
  • positions: Position[]
  • total: number — bounded by the pagination reach: exact up to pageSize × 10,000 rows and saturating there, so a caller with more matching positions than page-number mode can page through reads the cap, not its lifetime total
  • page: number
  • pageSize: number
(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, net of funding and the position’s closing-side reducing-execution fees, and — when the opening PnL sample is available — net of its opening-fill fees too, giving the full fee life (the opening-fee total is signed, so this lowers the figure for charged fees and raises it where those fills earned a maker rebate). When that sample is unavailable — before the position’s first PnL-sample flush, after its samples age out of retention, or for positions predating the PnL service — opening fees are not folded in and this falls back to the closing-execution net (no worse than before this change), with cumFees null in that same case. Absent when the reducing-execution history is unavailable, or when it is present but fails the completeness check against the stored realized PnL (executions demonstrably missing) — the stored row values are served instead
  • 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 under the same condition as netRealizedPnl
  • fundingPaid?: string — terminal rows only; lifetime funding paid (signed; negative means received), summed over the position’s reducing executions. Absent under the same condition as netRealizedPnl
  • 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 under the same condition as netRealizedPnl
  • cumFees?: string — the position’s total lifetime trading fees: opening-fill fees plus every reducing execution’s fees (decreases, close, and any liquidation penalty), signed (negative means net rebated), inclusive of the application-fee component. Unlike feesPaid (closing-side reducing executions only), this is the whole-life figure and is present on open and terminal rows alike. Serialized as null (not omitted) when the whole-life total cannot be completed — before the position’s first PnL sample, after its samples age out of PnL retention, for positions predating the PnL service, or when a legacy reducing row carries a null fee. Unlike feesPaid and netRealizedPnl, cumFees is not gated on the terminal reducing-history completeness check, so in the rare case where a reducing-history row is entirely absent (a legacy data gap, not merely a null fee) it can be non-null and understated
  • 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
  • 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
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
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
    • page?: number — at most 10,000; a larger page is rejected
    • pageSize?: number
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
  • total: number — bounded by the pagination reach: exact up to pageSize × 10,000 rows and saturating there, so a caller with more history than page-number mode can page through reads the cap, not its lifetime total
  • page: number
  • pageSize: number

Modify

addPositionMargin and reducePositionMargin were removed. 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 (the direct addPositionMargin replacement) — 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.

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
Returns: 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
Returns: 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