> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xmonaco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Positions

> Inspect, modify, and close perp positions

```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";

// Initialize SDK with wallet client (see Initialization guide)

// Read
await sdk.positions.listPositions(params?);
await sdk.positions.getPosition(positionId);
await sdk.positions.getPositionRisk(positionId);
await sdk.positions.listPositionHistory(params?);

// Modify
await sdk.positions.attachPositionTpSl(positionId, { takeProfit?, stopLoss?, trailingStop?, oco? });

// Close
await sdk.positions.closePosition(positionId, { closeType, ... });
await sdk.positions.batchCloseAllPositions(request?);
```

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](/developers/positions).

***

## Read

<ResponseField name="listPositions" type="(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-closed first for `status: "CLOSED"` and `status: "LIQUIDATED"`; `(marginAccountId, tradingPairId, positionId)` order for `status: "OPEN"`; newest-opened first otherwise); 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. `status: "CLOSED"` and `status: "LIQUIDATED"` read persisted rows ordered newest-closed first, on `(closedAt, id)`, so a position held for weeks and closed today leads the list (a legacy terminal row with no close time sorts by its open time). Every other filter reads persisted rows ordered newest-opened first, on the immutable `(openedAt, id)` key. Each is a separate cursor space, so a token is only valid in the one that minted it, and crossing between the live and persisted walks 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`.
</ResponseField>

<ResponseField name="getPosition" type="(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 — lifetime funding paid since the position opened (signed; negative means received). On a terminal row, 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, and unlike `netRealizedPnl` it does not wait for the final close to be recorded in the PnL sample. On an `OPEN` or `LIQUIDATING` row, the funding accrued on the open size plus the funding its partial closes settled, so the last open value equals the terminal value; absent whenever `lifetimeTotalsStatus` is not COMPLETE, rather than understated
  * `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; on an `OPEN` or `LIQUIDATING` position it is also `null` when its reductions do not account for its `realizedPnl` (`lifetimeTotalsStatus` INCOMPLETE), rather than understated. Existing historical attribution errors are not repaired retroactively.
  * `lifetimeTotalsStatus?`: `LifetimeTotalsStatus` | null — whether the lifetime totals (`exitPrice`, `realizedRoe`, `feesPaid`, `fundingPaid`, `cumFees`, `netRealizedPnl`) are served. `"LIFETIME_TOTALS_STATUS_COMPLETE"`: history covers the whole lifetime and the fields follow their own rules above. `"LIFETIME_TOTALS_STATUS_PENDING"`: history is still catching up to this row's `version`, or the row changed while it was being read; every lifetime field is absent — call again after a short backoff (about 1 second, growing to a few). `"LIFETIME_TOTALS_STATUS_INCOMPLETE"`: the lifetime totals are not complete now and a quick retry will not change that — a legacy-history gap (or reductions that do not reconcile) is permanent, while a missing or drifted maintained-totals row is repaired by the background reconcile (at startup and daily), after which the row reads COMPLETE; every field that is present comes from the maintained totals, which are cross-checked against each other and repaired by the background reconcile, e.g. `exitPrice` with no `cumFees`; `OPEN` and `LIQUIDATING` positions are checked against `realizedPnl` too and are INCOMPLETE with no `cumFees` or `fundingPaid` when their reductions fall short. Until the position-history checkpoint is seeded, PENDING arises only when the position row changed between the row read and the totals read. Never read an absent total as zero. Typed with a `string` fallback so a newer status passes through; `null` or absent from an older server. See [Lifetime totals status](/developers/positions#lifetime-totals-status)
  * `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](/developers/positions#inspect-a-position) and the [`positions` channel](/sdk/typescript/websockets#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](/sdk/typescript/websockets#positions-channel). 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), except that a WebSocket `positions` snapshot row wins the tie on every field, because it is built from live matching-engine state under the persisted row's version and 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](/sdk/typescript/websockets#positions-channel) 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`.
</ResponseField>

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`.

<ResponseField name="getPositionRisk" type="(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:**

  ```typescript theme={null}
  // Live liquidation-distance display, polled every 2 seconds
  const position = await sdk.positions.getPosition(positionId);

  setInterval(async () => {
    const risk = await sdk.positions.getPositionRisk(positionId);
    const liquidationPrice = risk.liquidationPrice?.trim();

    if (!liquidationPrice) {
      setLiquidationDisplay("unavailable");
      return;
    }

    const mark = parseFloat(risk.markPrice);
    const liq = parseFloat(liquidationPrice);
    const distance = (mark - liq) / mark; // for a LONG
    setLiquidationDisplay(
      position.marginMode === "CROSS"
        ? `Conditional liquidation price: ${liquidationPrice}`
        : liquidationPrice,
    );
    setDistance(distance);
  }, 2000);
  ```

  `PositionRisk` does not include `marginMode`; use the matching position or risk-bucket context to label a cross value as conditional rather than account-wide.
</ResponseField>

<ResponseField name="listPositionHistory" type="(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.
</ResponseField>

***

## Modify

<Note>
  The positions API has no margin-adjustment method. Adjust a position's
  margin through `sdk.marginAccounts` — see
  [Margin Accounts](/sdk/typescript/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 })`.
</Note>

<ResponseField name="attachPositionTpSl" type="(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](/developers/trade#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](/developers/rate-limits)

  **Example:**

  ```typescript theme={null}
  await sdk.positions.attachPositionTpSl(positionId, {
    takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
    stopLoss: {
      triggerPrice: "1900",
      orderType: "LIMIT",
      limitPrice: "1895",
    },
  });

  // Full-position OCO bracket: whichever leg triggers closes the whole
  // position and cancels its sibling.
  await sdk.positions.attachPositionTpSl(positionId, {
    takeProfit: { triggerPrice: "2200", orderType: "MARKET", closePosition: true },
    stopLoss: { triggerPrice: "1900", orderType: "MARKET", closePosition: true },
    oco: true,
  });
  ```

  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) and trailing stops survive a supersede. An entry order's full-close legs compete for the same slot once armed, and the leg set most recently wins: an attach retires an armed entry-order leg, and a leg still pending on an unfilled entry order is cancelled instead of armed when that order fills, if the attach came later. A trailing stop never supersedes: a second one on the same position is rejected with `409`. See [Order Management](/developers/trade#conditional-perps-orders).

  Lifecycle for these conditionals streams via the [`conditional_order_update`](/sdk/typescript/websockets) 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](/developers/trade#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.
</ResponseField>

***

## Close

<ResponseField name="closePosition" type="(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 Limits](/developers/rate-limits)

  **Returns:** `ClosePositionResponse`:

  * `closeOrderId`: string
  * `status`: string
  * `message`: string
  * `submittedQuantity`: string

  **Examples:**

  ```typescript theme={null}
  // Full market close
  await sdk.positions.closePosition(positionId, { closeType: "MARKET" });

  // Partial limit close
  await sdk.positions.closePosition(positionId, {
    closeType: "LIMIT",
    limitPrice: "2050.00",
    quantity: "0.5",
  });

  // IOC close with 0.5% slippage cap. An IOC close executes as a LIMIT+IOC
  // order, so limitPrice is required.
  await sdk.positions.closePosition(positionId, {
    closeType: "IOC",
    limitPrice: "2050.00",
    slippageToleranceBps: 50,
  });
  ```
</ResponseField>

<ResponseField name="batchCloseAllPositions" type="(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. Cannot be combined with `tradingPairIds`
    * `tradingPairIds?`: string\[] — limit the close to any of these trading pairs (1 to 256 UUIDs, repeats ignored) in one request. Cannot be combined with `tradingPairId`. The SDK rejects an empty array with a `ValidationError`, because on the wire an empty set means no pair filter
    * `side?`: `"LONG" | "SHORT"` — limit the close to one side, on top of either pair filter
    * `slippageToleranceBps?`: number — slippage tolerance applied to each MARKET close

    With no pair filter, every open position is closed. Delegated sessions must pass `tradingPairId` or a non-empty `tradingPairIds`; `side` alone is refused with `403`.

  **Throws:** `ValidationError` before sending when both pair filters are set, `tradingPairIds` is empty, longer than 256, or holds a non-UUID, `side` is not `LONG`/`SHORT`, or the request has a field the SDK does not know (a misspelled filter would otherwise be dropped and close every position). The server answers these with `400`, with two exceptions: it reads an empty `tradingPairIds` as no pair filter, and it accepts `side` in either case (`"short"` works over raw REST or gRPC; the SDK takes only `"LONG"`/`"SHORT"`). 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 Limits](/developers/rate-limits)

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

  ```typescript theme={null}
  // Close every open position
  const result = await sdk.positions.batchCloseAllPositions();

  // Or limit to one pair with slippage tolerance
  await sdk.positions.batchCloseAllPositions({
    tradingPairId,
    slippageToleranceBps: 50,
  });

  // Or close the longs on several pairs in one request
  await sdk.positions.batchCloseAllPositions({
    tradingPairIds: [btcUsdcPairId, btcUsdtPairId],
    side: "LONG",
  });
  ```
</ResponseField>

***

## 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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.