You can attach a take-profit, a stop-loss, or a trailing stop to an order. These legs protect what the order fills: once it fills, each leg watches the price and can close the position with an exit order on the opposite side. Other venues call this a TP/SL or bracket order.
Attach legs with the order's attached_risk field on CreateOrder and BatchCreateOrders, and change them with new_attached_risk on ModifyOrder and BatchReplaceOrders, over REST or
ConnectRPC.
A BUY sized by a quote amount (max_quote_debit_scaled, REST maxQuoteDebit), such as a market buy
for 50 USDT, can carry legs too. It has no fixed quantity, so its legs protect the quantity its fills
actually buy. Until the buy fills, a leg shows the quantity the whole amount buys at the order's
price limit; once the fills settle, it shows and protects the quantity bought. See Order sizing and the quote-amount example.
Legs
attached_risk holds at most two legs: a take-profit, and one stop leg, either a stop-loss or a
trailing stop.
| Field | Leg |
|---|---|
take_profit | trigger_price_ticks, child, and optional trigger_price_source |
stop_loss | trigger_price_ticks, child, and optional trigger_price_source |
trailing_stop | A distance, optional activation price, slippage cap, and trigger_price_source, plus limit_price_ticks on a sell order |
oco | Optional. Requires a take-profit and exactly one stop leg. See OCO |
A take-profit or stop-loss leg's child is market_ioc, a market exit with the Pair's default
price protection, or limit_gtc with price_ticks, a resting limit exit. Attached exits do not
support post-only. The trailing-stop leg follows the trailing stop rules.
The order response returns each leg's ID in take_profit_trigger_id, stop_loss_trigger_id, and trailing_stop_trigger_id. Each leg is a trigger whose parent_order_id is the order.
Order reads return the legs as you set them. A buy-back trailing leg's maximum price comes back as attached_risk.trailing_stop.policy.limit_price_ticks (REST attachedRisk.trailingStop.policy.limitPrice); it is absent on a trailing leg that sells.
Trigger prices and the entry price
On a limit order, a take-profit or stop-loss must sit on its own side of the order's price:
| Order side | Take-profit | Stop-loss |
|---|---|---|
BUY | Above the order's price | Below the order's price |
SELL | Below the order's price | Above the order's price |
A leg at or past the entry price is rejected with TRIGGER_PRICE_INVALID, and the whole order is
refused. There is no minimum distance. The same rule applies when you change the order's price or
its legs. Market orders and trailing-stop legs have no side rule.
An order is also refused when a leg's exit could never pass the Pair's order limits, even with the
order filled in full: its create, its preview, or the modify that adds the leg is rejected with that
check's code, such as MIN_NOTIONAL. For a quote-amount buy, the exit is checked at the least the
whole amount could buy: at the order's limit price, or at a market order's protected price.
Price source on legs
Each leg takes trigger_price_source, as a standalone trigger does: LAST_PRICE by default, or INDEX_PRICE. MARK_PRICE is refused with TRIGGER_PRICE_SOURCE_UNSUPPORTED. A trailing-stop leg
on INDEX_PRICE trails the index.
On a Pair without an index price, an order whose legs use INDEX_PRICE is rejected with the order
reason INDEX_PRICE_UNAVAILABLE before any fill. A ModifyOrder that adds such a leg is refused
with TRIGGER_PRICE_SOURCE_UNSUPPORTED, and the order and its legs stay unchanged. A modify that
names another source replaces the leg.
How legs protect a position
The legs are CREATED and do nothing. If the order ends without any fill, the legs are canceled
with PARENT_CANCELED_NO_FILL.
At the order's first fill, each leg records EVENT_ARMED and becomes RUNNING, protecting the
filled quantity. Later fills grow the protected quantity, including fills that settle after a leg
fired: each live leg follows the order's whole settled position, less what its exits sold. A
trailing-stop leg without an activation
price starts tracking at this point.
The first leg whose condition is met places one exit order for the whole remaining position, rounded down to the Pair's quantity step. A remainder below one step stays in your balance. Only one exit works at a time.
If the order still rests with an unfilled remainder, Polyester first cancels that remainder, then
places the exit, so the exit never trades with its own order. This happens whatever the order's
self-trade prevention mode. Fills before the cancel stand, the remainder's reserved funds are released, and the
order ends CANCELED with cancel reason ATTACHED_EXIT_FIRED. From then on the order can no longer
be modified, its legs included: a ModifyOrder answers ORDER_ALREADY_TERMINAL. An exit that the
book cannot take is refused without touching the order, which keeps resting. An account at the
Pair's open-order limit (maxOpenOrdersPerPair) is not refused a resting limit exit for that limit:
a fired exit never needs a free slot.
If another leg's condition is met while that exit works, the working exit is canceled. Once it has ended, that leg is checked again against the latest price and, if its condition still holds, exits what remains.
The legs keep protecting whatever position remains, for example after a partial fill of an IOC
exit, unless you canceled the exit yourself. An exit that Polyester cancels while it recovers your
account is not your cancel: the leg stays active and protects the position again. If the recovery
ends the leg itself, the leg ends CANCELED with ACCOUNT_REPAIR and no longer protects the
position, so place a new trigger once the account trades again.
The legs share the position whether or not oco is set, so they never sell the same quantity twice.
OCO
oco does not change when exits are placed. It decides what canceling an exit order ends:
- With
oco, canceling one leg's exit order ends that leg, withUSER_REQUEST, and the order's other leg too. - Without
oco, canceling an exit order ends only that leg. The other leg keeps protecting the remaining position.
This applies to an exit you cancel alone, with a cancel-all, or with the dead-man switch.
How legs end
| What happens | Legs end as |
|---|---|
| The order ends without a fill | CANCELED, PARENT_CANCELED_NO_FILL |
| The order has ended and an exit closed the last of the position | The leg whose exit ended last is COMPLETED; the others CANCELED, OCO |
| The order has ended, an exit sold all it claimed, and only dust is left | The leg whose exit ran is COMPLETED; the others CANCELED, OCO |
| The order ended, no exit ran, and its whole fill is below the Pair's minimums | FAILED, MIN_QUANTITY or MIN_NOTIONAL |
| You cancel a leg's exit order | That leg CANCELED, USER_REQUEST; with oco, the other leg too |
You cancel a leg with CancelTrigger, or remove it with ModifyOrder | That leg CANCELED, USER_REQUEST |
| The order ended, no exit ran, and its whole fill is below one quantity step | FAILED, STEP_SIZE |
| An exit is refused for a reason the same exit would meet again | That leg FAILED with the reason; the other leg keeps protecting the position |
Canceling the order itself after a partial fill, or its remainder being canceled when a leg fires, does not end the legs: they keep protecting the filled quantity until it is closed.
An exit refused because of the market's state, such as a halted or busy Pair, does not fail the leg:
it re-arms and fires again at the next price that meets its condition. An exit refused for the
Pair's minimums, such as MIN_NOTIONAL, waits until the position changes or you edit the leg. An
exit refused for a rate limit or an automatic account repair fires again shortly. Each such refusal
is recorded as EVENT_FIRE_REFUSED with fire_refusal_reason, and the leg keeps its status.
Once the order has ended, a remainder below the Pair's minimum quantity or minimum notional is dust, not a position the legs can exit:
- If an exit already sold everything it claimed, for example when the order's last small fill
settles while that exit works, the leg whose exit ran ends
COMPLETEDand the other legs endCANCELEDwithOCO. Nothing fires for the dust. - If no exit ever ran, each leg whose exit would miss the minimums ends
FAILEDwithMIN_QUANTITYorMIN_NOTIONAL. Each leg is judged at its own exit price, so a leg whose exit meets them keeps protecting the position. - If an exit still working when the order ends stops before selling everything it claimed, such as
an IOC exit with an unfilled remainder, the leg keeps protecting what remains; when that is dust, its next fire is refused and
the leg ends
FAILEDwith the minimums reason.
An exit that self-trade prevention expires, because it would trade with another of your resting
orders under EXPIRE_TAKER or EXPIRE_BOTH, does not end the leg either: it stays armed. The exit
ends CANCELED with its STP cancel reason, and the leg records one EVENT_FIRE_REFUSED with TRIGGER_FAILURE_REASON_SELF_TRADE_PREVENTED (REST SELF_TRADE_PREVENTED), whose child_seq names that exit. While the order that blocked it is still first in line at the best price, the leg
does not fire again, whatever its price source: new trades and index price updates do not re-fire
it. Once that order is gone, filled or canceled, or no longer first at the best price, the leg checks
the latest price once and fires if its trigger condition still holds; otherwise it waits for the next
price that meets it. See Self-trade prevention.
Funds the legs reserve
The legs reserve what their exits would sell or spend, once for all legs together:
- Selling exits (on a buy order) reserve the base Asset as each fill of the order settles.
- Buying-back exits (on a sell order) reserve quote at the highest maximum price of the legs:
the limit price of a limit exit, the protection price at the trigger price of a market exit, or a
trailing stop's
limit_price_ticks. The part of that cost that the sale itself may not return is reserved when the order is created. An order whose balance cannot cover it is rejected withINSUFFICIENT_FUNDS.
While the legs protect a position, orders and withdrawals that would use the reserved funds are
rejected with INSUFFICIENT_FUNDS. Reduce or cancel the legs first. What the legs no longer need
is released as they change, and in full when they end.
Change or cancel legs
new_attached_risk replaces the order's whole set of legs, in the same request as a price or
quantity change if you like. The order and its legs change together, or the whole modify is
refused and nothing changes.
- A leg sent with the same terms keeps its trigger ID.
- A leg sent with new terms, such as a new trigger price or trailing distance, is replaced by a new leg with a new ID.
- A leg left out is removed.
- Changing only
ocokeeps the existing legs, their IDs, and a trailing stop's tracked peak.
When the order is replaced, its legs move to the new order with their IDs.
Use the leg's trigger ID with CancelTrigger, PauseTrigger, ResumeTrigger, or ModifyTrigger. ModifyTrigger changes a take-profit or stop-loss leg's trigger price, or a
trailing leg's distance, activation price, or slippage cap.
On an order with a fill, moving a take-profit or stop-loss to a price the market has already
reached is refused with TRIGGER_WOULD_TRIGGER_IMMEDIATELY. So is moving a trailing leg's activation
price onto a price already reached, or changing an activated trailing leg's distance so its trigger
price is already reached. A ModifyOrder that moves a take-profit, stop-loss, or activation price
this way is refused whole with the same code. ModifyOrder replaces a leg, so a new distance on a
trailing leg without an activation price is accepted, and the new leg starts trailing from the latest
price.
A change is also refused when the leg's exit, valued at the order's quantity, would fall below the
Pair's minimums: MIN_QTY, QTY_STEP_SIZE, or MIN_NOTIONAL. A ModifyOrder that lowers the
order's quantity under legs it keeps is checked the same way. Nothing changes: the order, its legs,
and their reserved funds stay as they were.
While one of the order's exits is working, adding, editing, pausing, or resuming its legs is refused
with ATTACHED_EXIT_WORKING. Removing a leg is still accepted. Retry once the exit completes.
Limits and rate limits
Attached legs count toward the account's limit of 100 triggers per Pair. An order whose legs would
pass it is rejected together with its legs. Each exit order costs one unit of TRADING_PLACE when
it is placed; an exit that meets an empty budget waits and is placed when the budget allows.
Example
A limit buy of 0.5 BTC at 65,000, with a take-profit at 70,000 and a stop-loss at 62,000, both exiting at market, linked as OCO.
POST /v1/orders
{
"order": {
"symbol": "BTC-USDT",
"side": "BUY",
"baseQty": "0.5",
"limitGtc": { "price": "65000", "postOnly": false },
"clientOrderId": "entry-btc-1",
"attachedRisk": {
"takeProfit": { "triggerPrice": "70000", "child": { "marketIoc": {} } },
"stopLoss": { "triggerPrice": "62000", "child": { "marketIoc": {} } },
"oco": true
}
}
}/orders.v1.OrdersService/CreateOrder, assuming Pair ID 1 and a base_quantity_scale of 8.
{
"order": {
"symbolId": 1,
"side": "BUY",
"baseQtyScaled": "50000000",
"limitGtc": { "priceTicks": "65000000000000" },
"clientOrderId": "entry-btc-1",
"attachedRisk": {
"takeProfit": {
"triggerPriceTicks": "70000000000000",
"child": { "marketIoc": {} }
},
"stopLoss": {
"triggerPriceTicks": "62000000000000",
"child": { "marketIoc": {} }
},
"oco": true
}
}
}A limit sell of 0.5 BTC at 70,000 with a trailing stop that buys back after a 1.5% rise from the
lowest price, paying at most 72,000. A sell order's trailing leg requires limitPrice; without it,
the order is rejected with TRIGGER_BUY_REQUIRES_LIMIT.
{
"order": {
"symbol": "BTC-USDT",
"side": "SELL",
"baseQty": "0.5",
"limitGtc": { "price": "70000", "postOnly": false },
"attachedRisk": {
"trailingStop": {
"trailingDistanceBps": 150,
"limitPrice": "72000"
}
}
}
}A market buy that spends up to 50 USDT with a 2% maximum slippage, with a take-profit at 80,000 and a stop-loss at 74,000, both exiting at market. The legs protect the BTC the 50 USDT actually buys.
POST /v1/orders
{
"order": {
"symbol": "BTC-USDT",
"side": "BUY",
"maxQuoteDebit": "50",
"marketIoc": { "maxSlippageBps": 200, "clientRefPrice": "77000" },
"attachedRisk": {
"takeProfit": { "triggerPrice": "80000", "child": { "marketIoc": {} } },
"stopLoss": { "triggerPrice": "74000", "child": { "marketIoc": {} } },
"oco": true
}
}
}Over ConnectRPC, send maxQuoteDebitScaled scaled by the Pair's quote_quantity_scale from GetSpotConfig (50 USDT at a quote scale of 6 is "50000000") and the prices as *Ticks.
Errors
| Stable code | Cause |
|---|---|
TRIGGER_PRICE_INVALID | A take-profit or stop-loss leg sits at or past the entry price |
TRIGGER_BUY_REQUIRES_LIMIT | A sell order's trailing leg has no limit_price_ticks |
BAD_PRICE | A buy order's trailing leg sets limit_price_ticks, or it is below the activation price |
VALIDATION_ERROR | oco is set without a take-profit and exactly one stop leg |
ATTACHED_LEGS_EXCEEDED | The order already carries every leg it may hold. Legs being canceled count until they end |
ATTACHED_EXIT_WORKING | An exit is working, so legs cannot be added, edited, paused, or resumed now |
ATTACHED_RISK_NOT_SUPPORTED | The Pair does not take attached legs |
INSUFFICIENT_FUNDS | The balance cannot cover a buy-back's reserve, or an order would use funds the legs reserve |
MARKET_TRIGGER_LIMIT_EXCEEDED | The legs would pass the account's 100-trigger limit on the Pair |
TRIGGER_PRICE_SOURCE_UNSUPPORTED | A leg names MARK_PRICE, or a modify adds an INDEX_PRICE leg on a Pair without an index price |
TRIGGER_WOULD_TRIGGER_IMMEDIATELY | On an order with a fill, a change would move a leg onto a price the market has already reached |
MIN_QTY, QTY_STEP_SIZE, MIN_NOTIONAL | A change would leave a leg's exit below the Pair's minimums |
If the Pair refuses one of the legs after the order was accepted, the order's remainder and its
other legs are canceled, and the order's cancel reason is ATTACHED_RISK_REFUSED. See Order types and lifecycle and Error codes.