Skip to content

TP/SL Orders

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

Internally these are called decrease orders.

Merged positions only

TP/SL orders require a position with a deterministic key. Legacy positions keep using updateTradeTp / updateTradeSl. See Positions are merged slots.

Rules

Take-profit (kind = 0)Stop-loss (kind = 1)
Orders per positionMultiple legsMultiple legs
Partial closeQtyYesYes
Trigger sideProfit side, bounded by leverageLoss 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.

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.

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 if the position no longer exists (already fully closed, or the wrong hash), and with UnauthorizedOperation 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).

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

EventEmitted 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
ExecuteDecreaseOrderSuccessfulTrigger hit and the reduction settled

See Events.

Trading perpetuals involves risk. Nothing here is financial advice.