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 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.
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.qtyat 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
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
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.
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
function batchUpdateDecreaseOrders(DecreaseOrderUpdateInput[] calldata updates) external;Updates are addressed by orderHash, so a single call can modify orders across different positions.
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
function cancelDecreaseOrder(bytes32 orderHash) external;
function cancelAllDecreaseOrders(bytes32 positionHash) external;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
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);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)).
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.