> ## 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, monitor, modify, and close Monaco perp positions.

A position is open directional exposure on a perp market. Positions are created by filled perp orders; there is no separate `createPosition` call.

Use `sdk.positions` for position lists, snapshots, live risk reads, margin changes, TP/SL attachment, close orders, and position history.

## Surface Map

| Surface | Use |
| - | - |
| REST | `GET /api/v1/positions`, `GET /api/v1/positions/{positionId}`, `GET /api/v1/positions/{positionId}/risk`, `POST /api/v1/positions/{positionId}/close`, `POST /api/v1/positions/{positionId}/tp-sl`, `POST /api/v1/positions/batch-close-all` |
| TypeScript SDK | `sdk.positions.listPositions()`, `getPosition()`, `getPositionRisk()`, `closePosition()`, `batchCloseAllPositions()`, `attachPositionTpSl()`, `listPositionHistory()` |
| React SDK | `usePositions()` |
| WebSocket | `sdk.ws.positions()` / `sdk.ws.liquidations()` (raw channels `positions[:{tradingPairId}]`, `liquidations[:{tradingPairId}]`); `sdk.ws.conditionalOrders()` for TP/SL lifecycle |
| gRPC | `PositionsService` |

## Lifecycle

Positions are created when a perp order (`tradingMode: "MARGIN"` with `leverage`) fills. Perps are one-way — `side` sets the direction; omit the deprecated `positionSide` compatibility field in new integrations.

```typescript theme={null}
await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
  tradingMode: "MARGIN",
  leverage: "10",
});
```

Common states:

| State | Meaning |
| - | - |
| `OPEN` | Active exposure |
| `LIQUIDATING` | A close is in flight, either user-initiated or forced by liquidation |
| `CLOSED` | Normal terminal close |
| `LIQUIDATED` | Force-closed after margin breach |
| `EXPIRED` | Time-bounded exposure expired |

Multiple fills on the same `(marginAccountId, tradingPairId)` aggregate into one position (perps are one-way, so a market has a single net position). The position's `size` and `entryPrice` update as the user adds exposure.

## List Positions

```typescript theme={null}
const { positions, nextPageToken } = await sdk.positions.listPositions({
  marginAccountId: marginAccountId,
  tradingPairId: tradingPairId,
  status: "OPEN",
  pageSize: 20,
});

// Walk every page: follow nextPageToken until it comes back empty.
let pageToken = "";
for (;;) {
  const page = await sdk.positions.listPositions({ status: "OPEN", pageToken, pageSize: 20 });
  // ... handle page.positions
  if (page.nextPageToken === "") break;
  pageToken = page.nextPageToken;
}
```

Use lists for position tables, account pages, and portfolio views.

## Inspect a Position

```typescript theme={null}
const position = await sdk.positions.getPosition(positionId);
const liquidationPrice = position.liquidationPrice?.trim();

console.log({
  side: position.side,
  size: position.size,
  entryPrice: position.entryPrice,
  markPrice: position.markPrice,
  unrealizedPnl: position.unrealizedPnl,
  liquidationPrice: liquidationPrice || "unavailable",
  liquidationPriceLabel:
    position.marginMode === "CROSS"
      ? "Conditional liquidation price"
      : "Liquidation price",
});
```

Important fields:

| Field | Meaning |
| - | - |
| `side` | `LONG`, `SHORT`, or `NONE` |
| `size` | Base-asset quantity: the current absolute quantity on an open position; on a terminal row, the total quantity closed over the position's life when its reducing executions reconcile with `realizedPnl`, and otherwise the persisted pre-close remainder |
| `entryPrice` | Weighted average of fills that opened the position |
| `markPrice` | Oracle-derived price used for PnL and liquidation |
| `indexPrice` | External fair-value reference used for funding and mark sanity checks |
| `unrealizedPnl` | PnL on open exposure |
| `realizedPnl` | Realized PnL from reducing fills, before fees and funding (see `netRealizedPnl`) |
| `cumFees` | Whole-life trading fees — opening and reducing fills, application fees and liquidation fees, signed (negative = net rebate); null while fee history is unavailable, and on an `OPEN` or `LIQUIDATING` position whose reductions do not account for its `realizedPnl` (status INCOMPLETE) rather than understated |
| `exitPrice` / `feesPaid` | Terminal rows: the reducing-execution VWAP and closing-side fees only; null when the reducing executions are unavailable or do not reconcile with `realizedPnl` |
| `fundingPaid` | Lifetime funding since the position opened, signed (positive = paid, negative = received). On a terminal row, summed over its reducing executions; null when those are unavailable or do not reconcile with `realizedPnl`. 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; null whenever `lifetimeTotalsStatus` is not COMPLETE, rather than understated |
| `netRealizedPnl` / `realizedRoe` | Terminal rows: lifetime realized PnL net of funding and `cumFees` (deducted once), and that net as a percentage of allocated initial margin. Absent until the final close is recorded; a position with no PnL sample 28 days after close falls back to the closing-execution net, while `cumFees` stays null |
| `lifetimeTotalsStatus` | Whether the lifetime totals (`exitPrice`, `realizedRoe`, `feesPaid`, `fundingPaid`, `cumFees`, `netRealizedPnl`) are served: `LIFETIME_TOTALS_STATUS_COMPLETE`, `LIFETIME_TOTALS_STATUS_PENDING` or `LIFETIME_TOTALS_STATUS_INCOMPLETE`. See [Lifetime totals status](#lifetime-totals-status). REST and gRPC only. |
| `isolatedMargin` | Open isolated: current position backing — committed position principal after settled realized PnL, signed trading fees (a taker fee reduces it, a maker rebate adds to it), and funding, clamped at zero. Collateral reserved or parked for unfilled orders is excluded, and it is the same backing isolated liquidation uses; live and persisted reads agree. Open cross: posted margin. Terminal: lifetime allocated initial margin when the reducing executions reconcile, otherwise the persisted value. |
| `marginMode` | `ISOLATED` or `CROSS` when the position belongs to a risk bucket |
| `initialMarginRequired` | Current initial margin required by this position at mark price, honoring its effective leverage and the market initial-margin floor. Zero when the position has no open exposure. |
| `maintenanceMarginRequired` | Current maintenance margin required by this position at mark price using the market maintenance-margin rate. Zero when the position has no open exposure. |
| `liquidationPrice` | Isolated: position/risk-bucket threshold. Cross: conditional per-position threshold, not a whole-account scalar. |
| `openedAt` | When the position opened: the fill that took its exposure from zero to non-zero (ISO 8601). Never changes for the life of the position id; a flip opens a new position with its own. `null` on REST reads when unknown and omitted from WebSocket frames; never filled with the read time. |
| `version` | Opaque, non-negative revision of this position row. Compare only within the same position; higher is newer. The same value the WebSocket `positions` snapshot and *ranked* live `position_update` frames carry, so reconcile on `positionId + version` (some live frames are deliberately unranked and omit it). `0` is the unranked legacy sentinel and never participates in ordering: treat it exactly as an absent version and fall back to a full field comparison rather than ranking it as oldest. A read of an `OPEN` position is normally served from live matching-engine state and reports `0`, because the engine holds the state but not the version; only a read served from the persisted row carries its version. Take the ranked version from the WebSocket `positions` snapshot or a ranked frame and never let a `0` read replace it; reconcile that read's fields by the full field comparison above, and take the valuation fields and `isolatedMargin` from it as the merge rule below describes. |

### Lifetime totals status

The lifetime totals are folded from the position's reducing history, which is written behind the matching engine, so they can trail the row you are reading. Every list and detail row carries `lifetimeTotalsStatus` to say which case you are in:

| Status | Meaning | What to do |
| - | - | - |
| `LIFETIME_TOTALS_STATUS_COMPLETE` | The position's history covers its whole lifetime. The lifetime fields are served under their own rules above, so a terminal row's `netRealizedPnl`, `realizedRoe` and `cumFees` can still wait on its PnL sample. | Render the fields that are present. |
| `LIFETIME_TOTALS_STATUS_PENDING` | History is still being written up to this row's `version`, or the row changed while it was being read. Every lifetime field is absent. Until the position-history checkpoint is seeded, PENDING arises only when the position row changed between the row read and the totals read. | Retry the read shortly — after about 1 second, backing off to a few seconds. The totals normally appear within seconds. |
| `LIFETIME_TOTALS_STATUS_INCOMPLETE` | The lifetime totals are not complete now, and a quick retry will not change that. A gap in legacy history (or reducing executions that do not reconcile with `realizedPnl`) is permanent; a maintained-totals row that is missing or has drifted is repaired by the background reconcile (at startup and daily), after which the row reads COMPLETE. Every lifetime field that is present comes from the maintained totals, which are cross-checked against each other and repaired by the background reconcile — for example `exitPrice` can be served while `cumFees` is absent. `OPEN` and `LIQUIDATING` positions are checked against `realizedPnl` too: an `OPEN` or `LIQUIDATING` row whose reductions do not account for it is INCOMPLETE with no `cumFees` or `fundingPaid`. | Render the fields that are present and show the absent ones as unavailable. Don't retry in a loop; a later read (after the daily reconcile) may come back COMPLETE. |

`OPEN` and `LIQUIDATING` positions are reconciled against `realizedPnl` like closed ones: one whose reductions do not account for it reports `LIFETIME_TOTALS_STATUS_INCOMPLETE` and returns no `cumFees` or `fundingPaid` rather than an understated one. Closed, liquidated and expired positions return the same lifetime fields as before. Never read an absent lifetime field as zero. `LIFETIME_TOTALS_STATUS_UNSPECIFIED` is never sent; treat it, and a status your client does not recognise, as unavailable. The WebSocket `positions` channel does not carry this field. Its one lifetime value, `fundingPaid`, is omitted on the snapshot and on live frames when the position's earlier reductions are not fully accounted for, so this read can omit it while the WebSocket carries it.

`version` advances on sequenced position mutations — open, add, reduce, partial and full close, funding, margin transfer, liquidation and ADL. It deliberately does NOT advance for `markPrice`, `unrealizedPnl`, `liquidationPrice`, `leverage`, `maintenanceMarginRequired` and `initialMarginRequired`, which are re-derived on every read rather than served from the versioned row and change with no position mutation behind them. Five of the six move with the oracle mark. `leverage` does not — it is opening notional over posted margin, which no reprice touches. It sits in the same group for the other reason: the live surfaces derive it from engine state while the persisted row serves its stored `leverage` column, so the two can disagree at an unchanged version. (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, like any other sequenced mutation.) 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 `position_update` 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.

Non-zero funding frames are ranked too, and funding carries state captured inside the sequencer command. Terminal close/liquidation/ADL frames are normally ranked and carry the same retained lifetime reduced quantity — the sum of every reducing execution — and exact status as this row: ordinary closes and flattened ADL counterparties are `CLOSED`; liquidation victims, backstop exits and the bankrupt ADL side are `LIQUIDATED`. 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 a zero `size`.

So the merge rule is per field group, never per payload: on a higher version apply everything; on an equal version skip the versioned fields as an idempotent redelivery but still take those six valuation fields from the newer payload — unless the payload is a WebSocket `positions` snapshot row, which wins the tie on every field (see below); on a lower version skip both groups, since an older command carries nothing newer — not even its mark.

`updatedAt` is when the position was last **mutated** — a fill, collateral transfer, funding settlement or close — carried from the producer's own record of that mutation on every read path. Polling an unchanged position returns the same timestamp, and an oracle reprice does not move it. It is display metadata; `version` is the reconciliation key.

`openedAt` is when the position **opened** — the fill that took its exposure from zero to non-zero — and it never moves afterwards. Time held is `now - openedAt` for an open position and `updatedAt - openedAt` for a terminal one, so you do not need position history (which lists reductions and closes only) or PnL history (hourly buckets) to work it out. A flip closes the position and opens a new one with a new `positionId` and its own `openedAt`. The REST read, the WebSocket `positions` snapshot and live `position_update` frames report the same instant, normally to within one matching-engine step; it can sit slightly ahead of your clock, and after a server restart it can move by the server's own clock drift, so clamp a computed time held at zero. It is absent only when the opening instant is unknown, and it is never filled with the time of the read.

The match across surfaces is exact for an **open** position. A **terminal** REST read carries the instant the matching engine emitted the close, which can differ slightly from the live frame's close instant; older terminal rows may instead carry the persistence clock and trail the close by the persistence lag. Any position read after a matching-engine restart is also served from the persistence clock and differs by that lag. Treat it as accurate to that lag.

One further case is not bounded by that lag. While the matching engine is unreachable, a REST read of an open position falls back to that row's lifecycle timestamps — close, then last funding, then open — so a position filled repeatedly since it opened reports its opening instant until funding settles. A live `position_update` frame is unaffected, being built from engine state, so a REST value older than a frame you already hold is expected in that window and the frame is the fresher one. Every list, detail and risk read also reads the market's index price from the matching engine; when that read fails transiently the request returns `503` with code `SERVICE_UNAVAILABLE` (gRPC `UNAVAILABLE`) rather than a row without it, so retry.

The WebSocket `positions` channel describes a position with these same fields — `riskBucketId`, `marginMode`, `maintenanceMarginRequired` and `initialMarginRequired` included — so the common case no longer needs a REST read after a frame arrives. Identity and scope carry the same values on the subscribe-time snapshot, on the live `position_update` frames and on this REST read; `fundingPaid` is carried too: the snapshot row serves the value the position's live frames carry, and every live frame, terminal frames included, carries the position's lifetime funding. `indexPrice`, `realizedPnl`, `netRealizedPnl`, `exitPrice`, `feesPaid` and `cumFees` remain REST-only. The five mark-derived values are priced at whichever mark the answering surface holds, so a snapshot row and a live frame can differ by one oracle tick; take those — and `leverage` with them — from the newest frame, as the merge rule above already requires.

A WebSocket `positions` snapshot row wins an equal-version tie on every field. The snapshot serves an open position from live matching-engine state under the persisted row's version, and persistence can lag the engine, so its fields can reflect a later command than the version names — take all of them rather than skipping the versioned ones. An equal-version live frame is still an idempotent redelivery. Discarding a whole payload on an equal version freezes displayed PnL until the position's next real mutation. Order position state by `version`, never by `updatedAt` or arrival order.

For an isolated risk bucket, `liquidationPrice` is the position/risk-bucket threshold. For `marginMode: "CROSS"`, render it as conditional for that position, not as a whole-account scalar: it 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. Treat an absent or blank value as unavailable; never replace it with `0`.

## Parent Accounts and Risk Buckets

A parent margin account is the collateral container for perps. Position-level exposure lives in risk buckets under that parent, scoped by trading pair and optional `strategyKey`.

A user has one parent margin account per application scope. Opening perp orders create or reuse risk buckets under that parent.

```typescript theme={null}
const summary = await sdk.marginAccounts.getParentMarginAccountSummary();

const bucket = await sdk.marginAccounts.getParentMarginAccountSummary({
  tradingPairId,
});

console.log({
  equity: summary.equity,
  freeCollateral: summary.freeCollateral,
  withdrawableCollateral: summary.withdrawableCollateral,
  initialMargin: summary.initialMarginRequired,
  maintenanceMargin: summary.maintenanceMarginRequired,
  riskBucketState: bucket.riskBucketState,
});
```

Important account fields:

| Field | Meaning |
| - | - |
| `equity` | Collateral plus realized and unrealized PnL |
| `freeCollateral` | Trading-margin headroom for existing cross exposure (credits unrealized gains) |
| `withdrawableCollateral` | Largest amount that can be transferred out now without lowering a position's posted margin, and the ceiling for funding new isolated / first-use positions (an unrealized gain does not raise it) |
| `maxRemovableCollateral` | On an isolated risk bucket with one open position, the largest removal when it may also take the position's margin down to its initial margin at the mark; the part past `withdrawableCollateral` raises the position's leverage and moves its liquidation price toward the mark. Equal to `withdrawableCollateral` on a cross bucket and on an isolated one with no open position or several. Absent on parent rows and fallback reads — handle `undefined`. See [`getMarginAccountSummary`](/sdk/typescript/margin-accounts) |
| `availableOrderCollateral` | Collateral still available to open **new** orders in this risk bucket, served by the risk engine's admission gate with parent top-up included (credits unrealized gains, unlike `withdrawableCollateral`). Not additive across buckets, and absent on parent rows and fallback reads — handle `undefined`. See [`getMarginAccountSummary`](/sdk/typescript/margin-accounts) |
| `initialMarginRequired` | Collateral required by open positions |
| `maintenanceMarginRequired` | Minimum required before liquidation |
| `totalPositionNotional` | Sum of open position notionals |
| `unrealizedPnl` | PnL from open positions |
| `realizedPnl` | PnL from closed activity |
| `subaccountKey` | The margin account's sub-account key; `default` is the default (parent) margin account. See [Margin Sub-Accounts](/developers/perps-collateral#margin-sub-accounts) |
| `marginAccountState` | The margin account's state: `ACTIVE`, or `CLOSED` for a closed sub-account |
| `riskBucketState` | Risk-bucket rows only: `NORMAL`, `PENDING_LIQUIDATION`, `LIQUIDATING`, `BAD_DEBT`, or `CLOSED`. Absent on the parent account, which has no liquidation lifecycle of its own |

List margin accounts when you need to render the parent account and its pair-scoped buckets together:

```typescript theme={null}
const { accounts, nextPageToken } = await sdk.marginAccounts.listMarginAccounts({ pageSize: 20 });
```

The list includes one row per margin account — the parent (default) margin account and each sub-account, closed sub-accounts flagged `marginAccountState: "CLOSED"` — plus separate rows for risk buckets. Bucket rows include `riskBucketId`, `marginMode`, `tradingPairId`, and optional `strategyKey`; parent rows leave those unset. A copy risk bucket row (a follow's copy account) also carries `copyFollowId` and, when the leader still holds one, `copyLeaderHandle`; account totals still include those rows.

Follow `nextPageToken` until it comes back empty to read every row. A `state` filter matches `riskBucketState`, so it keeps only risk-bucket rows and drops the parent row.

## Account States

* `NORMAL`: healthy. New positions can be opened and collateral can be transferred subject to margin requirements.
* `PENDING_LIQUIDATION`: maintenance margin was breached. New orders are rejected while positions are unwound.
* `BAD_DEBT`: post-liquidation deficit. The account cannot open new positions and may have negative equity. Collateral transfers into or out of a `BAD_DEBT` risk bucket are refused.

Render visible alert states for `PENDING_LIQUIDATION` and `BAD_DEBT`, and poll account summaries or position risk for active trading views. Do not assume ledger projections are enough for live risk UI.

## Live Risk

For active trading UIs, poll `getPositionRisk` for volatile fields instead of repeatedly fetching the full position.

```typescript theme={null}
const position = await sdk.positions.getPosition(positionId);
const risk = await sdk.positions.getPositionRisk(positionId);
const liquidationPrice = risk.liquidationPrice?.trim();

console.log({
  markPrice: risk.markPrice,
  indexPrice: risk.indexPrice,
  unrealizedPnl: risk.unrealizedPnl,
  liquidationPrice: liquidationPrice || "unavailable",
  liquidationPriceLabel:
    position.marginMode === "CROSS"
      ? "Conditional liquidation price"
      : "Liquidation price",
  marginRatio: risk.marginRatio,
});
```

`marginRatio` is the account's maintenance requirement divided by equity. It is `0` for an account with no open positions. For an account that still carries a maintenance requirement at zero or negative equity (bad debt), `getPositionRisk` reports a **maximum-distress sentinel** — a very large decimal string (`Decimal::MAX`, \~7.9e28) — rather than `0` or a negative value that would sort as safer than a healthy account. Treat any value at or above the liquidation threshold as maximal distress and clamp it for display.

Raw WebSocket clients can subscribe to `positions` or `positions:{tradingPairId}` for live position updates and `liquidations` or `liquidations:{tradingPairId}` for liquidation alerts. The TypeScript SDK exposes typed helpers for both: `sdk.ws.positions(handler, tradingPairId?, onSnapshot?, onValuation?)` and `sdk.ws.liquidations(handler, tradingPairId?)` — omit `tradingPairId` to stream all pairs. `onValuation` receives the channel's periodic `position_valuation` frames, which refresh an open position's mark-derived figures between lifecycle events; see [Position Valuation Frames](/sdk/typescript/websockets#position-valuation-frames).

## Price Fields

| Price | What it is | What it is used for |
| - | - | - |
| `entryPrice` | Weighted average open price | PnL anchor |
| `markPrice` | Oracle reference price | PnL marking and liquidation |
| `indexPrice` | Aggregated fair value | Funding and mark sanity checks |
| `liquidationPrice` | Isolated threshold; cross conditional per-position threshold | Risk display |

Avoid mixing last trade price with mark-price PnL. Perps PnL and liquidation should use mark price.

## PnL

Unrealized PnL on an open position:

```text theme={null}
LONG:  (markPrice - entryPrice) * size
SHORT: (entryPrice - markPrice) * size
```

The API returns `unrealizedPnl` already computed. The formula is mostly useful for previews and explaining the UI.

## Modify Margin

A position's margin is the collateral its **risk bucket** holds, so margin is
adjusted through the bucket rather than through the position. Both directions
move collateral between the bucket and the **parent margin account** — never to
or from your spot wallet. Fund the parent first with
`transferCollateralToParentMarginAccount` if it has no free collateral.

Allocating more lowers liquidation risk and effective leverage without changing
position size.

```typescript theme={null}
await sdk.marginAccounts.transferCollateralToRiskBucket({
  asset: "USDC",
  amount: "200",
  tradingPairId,
  marginMode: "ISOLATED",
});
```

Releasing returns collateral to the parent's free collateral, where it can be
redeployed. A release is bounded by the bucket's `withdrawableCollateral`: its
allocation plus realized PnL, less funding paid, unrealized losses and its
initial-margin requirement (resting-order reserve included). Realized profit
can leave a live bucket, so the bound is not capped at the allocation; an
unrealized gain never raises it, and it is zero while the bucket is not
`NORMAL`.

```typescript theme={null}
await sdk.marginAccounts.transferCollateralFromMarginAccount(parentMarginAccountId, {
  asset: "USDC",
  amount: "100",
  tradingPairId,
});
```

<Note>
  There is no position-level margin method. A position's margin changes only
  through the risk-bucket transfers above, so collateral already held in the
  parent margin account is what funds it.
</Note>

## Close Positions

`closePosition` submits a closing order. The position does not become `CLOSED` until that order fills.

For delegated-agent sessions, closing a position is checked like order creation because Monaco submits a close or reduce order on the owner's account. The active delegation must allow the market, action, and any applicable perps-account scope before the close is accepted.

```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
await sdk.positions.closePosition(positionId, {
  closeType: "IOC",
  limitPrice: "2050.00",
  slippageToleranceBps: 50,
});
```

An IOC close executes as a `LIMIT`+`IOC` order, so — like a `LIMIT` close — it requires `limitPrice`; only a `MARKET` close omits it.

The response carries the submitted close-order ID. Subscribe to order events or poll the position to confirm the close.

For batch close-all flows, delegated sessions must include an explicit trading-pair scope: `tradingPairId` or a non-empty `tradingPairIds`. A `side` filter on its own does not count. This keeps delegated close permission market-specific instead of allowing an agent to close every owner position across all markets.

## Batch Close All

`batchCloseAllPositions` submits a MARKET reduce-only close for every open perp position owned by the caller — a one-call "panic close" for risk-off and emergency-exit UIs.

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

// Or limit to one trading pair, with a slippage cap on each close
await sdk.positions.batchCloseAllPositions({
  tradingPairId,
  slippageToleranceBps: 50,
});

// Or close every short on a set of pairs (e.g. all the pairs a market filter
// matched) in one request
await sdk.positions.batchCloseAllPositions({
  tradingPairIds: [btcUsdcPairId, btcUsdtPairId],
  side: "SHORT",
});
```

Filters:

* `tradingPairId` closes the positions on one pair. `tradingPairIds` closes the positions on any of up to 256 pairs. Pass one or the other, not both. Repeated ids are ignored, and an id with no open position closes nothing.
* `side` (`"LONG"` or `"SHORT"`) closes only the positions on that side, on top of either pair filter.
* With no pair filter, every open position is closed. On the wire an empty `tradingPairIds` array is the same as leaving it out, so the TypeScript SDK rejects `tradingPairIds: []` with a `ValidationError`. A filter in your UI that matched no pair must not turn into a close-everything request.
* The request body is strict: a field the server does not know, such as a misspelled `tradingPairIDs`, is a `400` and closes nothing, rather than being ignored and widening the close to every position.

The batch is best-effort and partial: each position is closed independently, so a failure on one position is reported in `results` rather than aborting the rest. Inspect the response to drive UI feedback.

```typescript theme={null}
const { totalRequested, totalClosed, totalFailed, results } =
  await sdk.positions.batchCloseAllPositions();

for (const r of results) {
  if (r.error) {
    console.warn(`Failed ${r.positionId}: ${r.error.code} ${r.error.message}`);
  } else {
    console.log(`Closing ${r.positionId} (${r.status}) order ${r.closeOrderId}`);
  }
}
```

`totalClosed` counts closes accepted by the matching engine (`SUCCESS`, `PARTIAL`, or `PENDING`). As with `closePosition`, an accepted close does not mark the position `CLOSED` until the close order fills — confirm via order events or by polling the position.

## Position History

```typescript theme={null}
const { events, nextPageToken } = await sdk.positions.listPositionHistory({
  positionId: positionId,
  marginAccountId: marginAccountId,
  tradingPairId: tradingPairId,
  pageSize: 50,
});
```

History events cover opens, partial fills, funding payments, margin changes, partial closes, liquidations, and final closes.

## UI Rules

* Sort position tables by risk or unrealized PnL, not just creation time.
* Show liquidation distance and margin ratio near every open position. Label cross liquidation prices and distances as conditional, never account-wide.
* Use mark price for PnL and liquidation displays.
* Treat an absent or blank liquidation price as unavailable, never `0`.
* Default close modals to full-size market close, with optional partial size and limit price.
* Infer position side in order tickets: opening or adding to a long is `BUY`; reducing a long is `SELL` + `reduceOnly: true`. Perps are one-way — `side` is the direction; omit the deprecated `positionSide` compatibility field.
* Poll `getPositionRisk` for active positions and show stale-state handling if the read fails.

## Errors To Handle

* `400` when order placement or simulation rejects risk
* `400` when adding margin exceeds free collateral
* `400` when reducing margin would leave the risk bucket below its initial-margin requirement
* `400` when close quantity exceeds remaining position size
* `403` when a delegated-agent session attempts to close a position outside its active policy
* `404` when `positionId` does not exist
* `503` (`SERVICE_UNAVAILABLE`) from `listPositions`, `getPosition` and `getPositionRisk` when the matching engine's index-price read fails transiently; retry

## Related References

* [Perps Collateral](/developers/perps-collateral) for margin collateral funding and transfers
* [Order Management](/developers/trade) for risk-bucket routing, reduce-only orders, and TP/SL order flows
* [Margin](/trading-mechanics/perps) for margin concepts
* [TypeScript positions](/sdk/typescript/positions) for SDK method reference


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