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?:ListPositionsParamsmarginAccountId?: stringtradingPairId?: stringstatus?:PositionStatusFilter—"OPEN" | "CLOSED" | "LIQUIDATING" | "LIQUIDATED"; anEXPIREDposition matches under aCLOSEDfilterpageSize?: number — 1 to 1000, defaults to 20pageToken?: string — cursor pagination. Omitted or""starts the walk (newest-opened first, exceptstatus: "OPEN", which walks in(marginAccountId, tradingPairId, positionId)order); otherwise pass the previous response’snextPageToken. There is nopage: the SDK rejects one with aValidationErrorbefore sending, and a raw REST request carrying it gets400. The filters must match the request that minted the token.
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 tostatus: "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 a400and 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 a400total: number — bounded by the server count ceiling; a lower bound whentotalCappedistruepageSize: numbernextPageToken: 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 onetotalCapped: boolean —truewhen more than the ceiling matched, sototalis a lower bound;falsewhentotalis exact, including exactly the ceiling
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
Position:positionId: stringmarginAccountId: stringriskBucketId?: stringmarginMode?:"ISOLATED" | "CROSS"tradingPairId: stringside:"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 lifeentryPrice: stringmarkPrice: stringindexPrice?: stringunrealizedPnl: stringrealizedPnl: stringisolatedMargin: 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?: stringmaintenanceMarginRequired: string - position-scoped requirement at the mark price using the market maintenance-margin rate;"0"when the position has no open exposureinitialMarginRequired?: 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 exposureliquidationPrice: string — isolated position/risk-bucket threshold; cross conditional per-position threshold, not a whole-account scalarstatus:"OPEN" | "CLOSED" | "LIQUIDATING" | "LIQUIDATED" | "EXPIRED"—LIQUIDATEDandEXPIREDare terminal statuses in the exportedPositionStatusunionnetRealizedPnl?: string — terminal rows only; lifetime realized PnL minus funding and whole-lifecumFees, 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;netRealizedPnlas a percentage of the total allocated initial margin (leverage-aware realized return). Absent under the same condition asnetRealizedPnlexitPrice?: 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; unlikenetRealizedPnl, it does not wait for the final close to be recorded in the PnL samplefundingPaid?: 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; unlikenetRealizedPnl, it does not wait for the final close to be recorded in the PnL samplefeesPaid?: 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; unlikenetRealizedPnl, it does not wait for the final close to be recorded in the PnL samplecumFees?: 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 asnullwhen 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 WebSocketpositionschannel 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 thepositionschannel. Display metadata: order byversion, never by thisopenedAt?: 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 ofpositionId, so time held isnow - openedAtwhile open andupdatedAt - openedAtonce terminal. A flip opens a new position with its ownopenedAt.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 arithmeticversion?: number — opaque, non-negative durable revision of this position row. Compare only within the same position; higher is newer, and never order byupdatedAt. The WebSocketpositionssnapshot and ranked liveposition_updateframes carry the same value for identical state, so reconcile onpositionId + version. Some live frames are deliberately unranked and omitversioneven 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 takemarkPrice,unrealizedPnl,liquidationPrice,leverage,maintenanceMarginRequiredandinitialMarginRequiredfrom 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;leveragedoes 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 takeisolatedMargintoo, 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.0is 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 reports0: the engine holds the state but not the version, so it cannot rank itself — see WebSockets for which frames are ranked
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.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
PositionRisk:positionId: stringmarkPrice: stringindexPrice?: stringunrealizedPnl: stringliquidationPrice: string — isolated position/risk-bucket threshold; cross conditional per-position threshold, not a whole-account scalarmarginRatio: 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 than0or a negative value — clamp it for display, never render it raw as a percentagemaintenanceMarginRequired: string - position-scoped requirement at the mark price using the market maintenance-margin rate;"0"when the position has no open exposureinitialMarginRequired?: 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 exposureupdatedAt: string — ISO 8601; the same mutation instant the matching position reports
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?:ListPositionHistoryParamspositionId?: stringmarginAccountId?: stringtradingPairId?: stringreductionOnly?: boolean — whentrue, returns only reductions, closes, and liquidations; omits opens, partial fills, funding, and margin-change eventspageSize?: number — 1 to 1000, defaults to 20pageToken?: string — cursor pagination. Omitted or""starts the walk at the newest row; otherwise pass the previous response’snextPageToken. There is nopage: the SDK rejects one with aValidationErrorbefore sending, and a raw REST request carrying it gets400. The filters must match the request that minted the token.
ListPositionHistoryResponse:events:PositionHistoryEvent[]— each witheventId,eventType, optionalquantity/price/realizedPnl, andtimestamp. Reduction rows additionally include:entryPrice?: string — position’s average entry price at closeallocatedInitialMargin?: string — initial margin committed when the position was openedfundingPaid?: string — cumulative funding charges over the position’s lifenetRealizedPnl?: string — realized PnL net of fees and fundingrealizedRoe?: string — return on equity (net PnL ÷ allocated initial margin)positionSide?:"LONG" | "SHORT"— side of the closed position
pageSize: numbernextPageToken: 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
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 withapplyToPositionMargin: true) the amount is committed to the open position’s stored margin — leverage drops and stays down. PassapplyToPositionMargin: falseto fund the bucket with spendable free collateral instead (what order-funding loops need). (The deprecated pair-scopedtransferCollateralToMarginAccountmatches 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:Attaching a full-close (
positionId: stringrequest:AttachPositionTpSlRequesttakeProfit?:TpSlLegstopLoss?:TpSlLegtrailingStop?:TrailingStopLeg— one per position (a second attach returns409); never part ofocooco?: boolean — cancel the sibling leg after one triggers. Requires bothtakeProfitandstopLoss
TpSlLeg:triggerPrice: stringorderType:"LIMIT" | "MARKET"limitPrice?: string — required for LIMITclosePosition?: boolean — resolve the full live position size when the leg triggersquantity?: string — fixed size; defaults to full position size when omitted. Required whenclosePositionisfalse; must be omitted whenclosePositionistruetimeInForce?:"GTC" | "IOC"slippageToleranceBps?: numberexpiresAt?: 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 watermarkactivationPrice?: string — mark level that starts tracking and seeds the watermark; rejected if already reached. Omitted, tracking starts at the current markclosePosition?/quantity?— same sizing contract asTpSlLegslippageToleranceBps?: number — 0 to 10000expiresAt?: string — ISO 8601
AttachPositionTpSlResponse:positionId: stringtakeProfitOrderId?: stringstopLossOrderId?: stringtrailingStopOrderId?: stringstatus:"SUCCESS" | "FAILED"message: string
- REST
409/ gRPCAlreadyExistswhentrailingStopis 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/ gRPCResourceExhaustedwhen 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
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: stringrequest:ClosePositionRequestcloseType:"MARKET" | "LIMIT" | "IOC"limitPrice?: string — required forLIMITandIOC, forbidden only forMARKETquantity?: string — defaults to full position size (use for partial close)slippageToleranceBps?: number — forMARKET/IOC
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: stringstatus: stringmessage: stringsubmittedQuantity: string
(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?:BatchCloseAllRequesttradingPairId?: string — limit the close to one trading pair; if omitted, all open positions are closedslippageToleranceBps?: number — slippage tolerance applied to each MARKET close
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 closetotalClosed: number — closes accepted by the matching engine (SUCCESS,PARTIAL, orPENDING)totalFailed: numberresults:BatchCloseResult[]
BatchCloseResult:positionId: stringcloseOrderId?: string — present when acceptedstatus?: string —"SUCCESS" | "PARTIAL" | "PENDING", present when acceptedsubmittedQuantity?: string — present when acceptederror?:BatchCloseError— present on failure
BatchCloseError:code: string — e.g.POSITION_NOT_FOUND,INVALID_REQUEST,MATCHING_ENGINE_ERRORmessage: string
Errors
400on order placement and modify endpoints — seerejectReasonreturned by the matching engine400onclosePositionwhenquantityexceeds remaining position size400onattachPositionTpSlwhen trigger prices are on the wrong side of the position (e.g., TP below entry on a LONG)404when thepositionIddoes not exist

