# TP/SL Orders

Standalone take-profit and stop-loss orders attached to an open position. Unlike the
[embedded TP/SL fields](/onchain/manage-position#take-profit-and-stop-loss), these support **partial
quantities and multiple legs** — scale out of a position at several price levels.

Internally these are called *decrease orders*.

::: info Merged positions only
TP/SL orders require a position with a deterministic key. Legacy positions keep using
`updateTradeTp` / `updateTradeSl`. See
[Positions are merged slots](/introduction/concepts#positions-are-merged-slots).
:::

## Rules

| | Take-profit (`kind = 0`) | Stop-loss (`kind = 1`) |
| :--- | :--- | :--- |
| Orders per position | Multiple legs | Multiple legs |
| Partial `closeQty` | Yes | Yes |
| Trigger side | Profit side, bounded by leverage | Loss side, validated on write |

Both kinds go through the same quantity rule — `0 < closeQty <= position.qty` — and there is no
per-kind limit on how many you create. Do not assume a position carries at most one stop-loss, or
that a stop-loss closes the full size: if you are reading orders back, handle several partial legs
of either kind.

::: info The stop-loss from the open parameters is a different object
The `stopLoss` you pass when opening is a protocol-managed order: it is always sized to the full
position and is resized automatically as the position grows. It is not reachable through
`createDecreaseOrder` / `batchCreateDecreaseOrders`, and it can coexist with stop-loss orders you
create yourself — so a position may legitimately hold more than one.
:::

Additional behaviour:

- Orders do **not** reserve quantity against each other. Each only requires
  `0 < closeQty <= position.qty` at creation; execution clamps to whatever quantity remains. Active
  legs can therefore add up to more than the position size — the surplus simply never fills.
- There is a per-position cap on active orders — exceeding it reverts with
  `TooManyActiveDecreaseOrders`.
- Fully closing the position cancels all of its orders.

## Data types

```solidity
enum DecreaseOrderKind { TP, SL }

struct DecreaseOrderInput {
    DecreaseOrderKind kind;
    uint128 triggerPrice; // 1e18
    uint128 closeQty;     // 1e10
    uint24 broker;        // close-fee referral, applied when the keeper executes
}


struct DecreaseOrderUpdateInput {
    bytes32 orderHash;
    uint128 triggerPrice;
    uint128 closeQty;
    uint24 broker;
}
```

`broker` is carried **on the order**, not taken from the position, and is applied when the keeper
executes it. A broker integration must set it on every order it creates and on every update — see
[Brokers & Referrals](/introduction/brokers#open-and-close-are-credited-separately).

## Creating orders

```solidity
function createDecreaseOrder(bytes32 positionHash, DecreaseOrderInput calldata order)
    external returns (bytes32 orderHash);

function batchCreateDecreaseOrders(bytes32 positionHash, DecreaseOrderInput[] calldata orders)
    external returns (bytes32[] memory orderHashes);
```

No oracle data required.

```ts
import { parseUnits } from 'viem'
import { publicClient, walletClient, DIAMOND } from './config'

const CREATE_ORDERS_ABI = [{
  type: 'function',
  name: 'batchCreateDecreaseOrders',
  stateMutability: 'nonpayable',
  inputs: [
    { name: 'positionHash', type: 'bytes32' },
    {
      name: 'orders', type: 'tuple[]', components: [
        { name: 'kind', type: 'uint8' },
        { name: 'triggerPrice', type: 'uint128' },
        { name: 'closeQty', type: 'uint128' },
        { name: 'broker', type: 'uint24' },
      ],
    },
  ],
  outputs: [{ name: 'orderHashes', type: 'bytes32[]' }],
}] as const

// Scale out: half at $110k, the rest at $130k, full stop at $90k.
const hash = await walletClient.writeContract({
  address: DIAMOND,
  abi: CREATE_ORDERS_ABI,
  functionName: 'batchCreateDecreaseOrders',
  args: [
    positionHash,
    [
      { kind: 0, triggerPrice: parseUnits('110000', 18), closeQty: position.qty / 2n, broker: 0 },
      { kind: 0, triggerPrice: parseUnits('130000', 18), closeQty: position.qty / 2n, broker: 0 },
      { kind: 1, triggerPrice: parseUnits('90000', 18), closeQty: position.qty, broker: 0 },
    ],
  ],
})

await publicClient.waitForTransactionReceipt({ hash })
```

Order hashes are returned by the call and emitted in `DecreaseOrderCreated`.

## Updating orders

```solidity
function batchUpdateDecreaseOrders(DecreaseOrderUpdateInput[] calldata updates) external;
```

Updates are addressed by `orderHash`, so a single call can modify orders across different positions.

```ts
const UPDATE_ORDERS_ABI = [{
  type: 'function',
  name: 'batchUpdateDecreaseOrders',
  stateMutability: 'nonpayable',
  inputs: [{
    name: 'updates', type: 'tuple[]', components: [
      { name: 'orderHash', type: 'bytes32' },
      { name: 'triggerPrice', type: 'uint128' },
      { name: 'closeQty', type: 'uint128' },
      { name: 'broker', type: 'uint24' },
    ],
  }],
  outputs: [],
}] as const

await walletClient.writeContract({
  address: DIAMOND,
  abi: UPDATE_ORDERS_ABI,
  functionName: 'batchUpdateDecreaseOrders',
  args: [[{
    orderHash,
    triggerPrice: parseUnits('115000', 18),
    closeQty: position.qty / 4n,
    broker: 0,
  }]],
})
```

## Cancelling

```solidity
function cancelDecreaseOrder(bytes32 orderHash) external;
function cancelAllDecreaseOrders(bytes32 positionHash) external;
```

```ts
const CANCEL_ORDERS_ABI = [{
  type: 'function',
  name: 'cancelAllDecreaseOrders',
  stateMutability: 'nonpayable',
  inputs: [{ name: 'positionHash', type: 'bytes32' }],
  outputs: [],
}] as const

await walletClient.writeContract({
  address: DIAMOND,
  abi: CANCEL_ORDERS_ABI,
  functionName: 'cancelAllDecreaseOrders',
  args: [positionHash],
})
```

`cancelAllDecreaseOrders` removes every decrease order on the position — take-profit legs and the
stop-loss alike — emitting one `DecreaseOrderCancelled` per order, and leaves the position itself
untouched. On a position that exists but has no orders it succeeds and does nothing. It reverts with
[`NonexistentTrade`](/reference/errors) if the position no longer exists (already fully closed, or
the wrong hash), and with [`UnauthorizedOperation`](/reference/errors) if the caller is not the
position owner — so cancelling after a full close is an error, not a no-op.

Both are available gaslessly as
[`CANCEL_DECREASE_ORDER` (10) and `CANCEL_ALL_DECREASE_ORDERS` (13)](/gasless/actions#action-list).

## Reading orders

```solidity
function getDecreaseOrders(bytes32 positionHash) external view returns (DecreaseOrderInfo[] memory);
function getTraderDecreaseOrders(address user) external view returns (DecreaseOrderInfo[] memory);
function getDecreaseOrder(bytes32 orderHash) external view returns (DecreaseOrderInfo memory);
```

```solidity
struct DecreaseOrderInfo {
    bytes32 orderHash;
    bytes32 positionHash;
    address user;
    address pairBase;
    bool isLong;
    DecreaseOrderKind kind;  // 0 = TP, 1 = SL
    uint128 triggerPrice;    // 1e18
    uint128 closeQty;        // 1e10
    uint24 broker;
    uint32 createdAt;
    bool active;
}
```

`getTraderDecreaseOrders` returns every live order across all of a trader's positions — the usual
starting point for a dashboard. A missing or removed order reads back as the zero struct
(`user == address(0)`).

```ts
const READ_ORDERS_ABI = [{
  type: 'function',
  name: 'getTraderDecreaseOrders',
  stateMutability: 'view',
  inputs: [{ name: 'user', type: 'address' }],
  outputs: [{
    type: 'tuple[]', name: '', components: [
      { name: 'orderHash', type: 'bytes32' },
      { name: 'positionHash', type: 'bytes32' },
      { name: 'user', type: 'address' },
      { name: 'pairBase', type: 'address' },
      { name: 'isLong', type: 'bool' },
      { name: 'kind', type: 'uint8' },
      { name: 'triggerPrice', type: 'uint128' },
      { name: 'closeQty', type: 'uint128' },
      { name: 'broker', type: 'uint24' },
      { name: 'createdAt', type: 'uint32' },
      { name: 'active', type: 'bool' },
    ],
  }],
}] as const

const orders = await publicClient.readContract({
  address: DIAMOND,
  abi: READ_ORDERS_ABI,
  functionName: 'getTraderDecreaseOrders',
  args: [account.address],
})
```

## Events

| Event | Emitted when |
| :--- | :--- |
| `DecreaseOrderCreated(user, positionHash, orderHash, kind, triggerPrice, closeQty, broker)` | Order created |
| `DecreaseOrderUpdated(orderHash, triggerPrice, closeQty, broker)` | Order modified |
| `DecreaseOrderCancelled(user, positionHash, orderHash)` | Cancelled by the trader, swept by a full close or liquidation, or cleaned up as stale |
| `ExecuteDecreaseOrderSuccessful` | Trigger hit and the reduction settled |

See [Events](/onchain/events#tpsl-orders).
