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 aCLOSEDfilterpage?: number — at most 10,000; a larger page is rejectedpageSize?: number
ListPositionsResponse:positions:Position[]total: number — bounded by the pagination reach: exact up topageSize× 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 totalpage: numberpageSize: number
(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, 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), withcumFeesnullin 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 insteadrealizedRoe?: 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 under the same condition asnetRealizedPnlfundingPaid?: string — terminal rows only; lifetime funding paid (signed; negative means received), summed over the position’s reducing executions. Absent under the same condition asnetRealizedPnlfeesPaid?: 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 asnetRealizedPnlcumFees?: 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. UnlikefeesPaid(closing-side reducing executions only), this is the whole-life figure and is present on open and terminal rows alike. Serialized asnull(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. UnlikefeesPaidandnetRealizedPnl,cumFeesis 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 understatedupdatedAt: 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 thisversion?: 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
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
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 eventspage?: number — at most 10,000; a larger page is rejectedpageSize?: number
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
total: number — bounded by the pagination reach: exact up topageSize× 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 totalpage: numberpageSize: 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
addPositionMarginreplacement) —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.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
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
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

