# Attached take-profit and stop-loss

Attach take-profit, stop-loss, and trailing-stop legs to an order, and know how they arm on fills, share one exit, and end when the order is canceled or filled.

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`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersService/CreateOrder) and [`BatchCreateOrders`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersService/BatchCreateOrders), and change them with `new_attached_risk` on [`ModifyOrder`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersService/ModifyOrder) and [`BatchReplaceOrders`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersService/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](https://testnet.polyester.com/docs/developer-docs/shared-concepts/order-sizing#maximum-quote-debit) and the [quote-amount example](#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](#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](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/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

### Before the order fills

The legs are `CREATED` and do nothing. If the order ends without any fill, the legs are canceled with `PARENT_CANCELED_NO_FILL`.

### When the order fills

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.

### When a leg fires

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.

### After an exit ends

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, with `USER_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 `COMPLETED` and the other legs end `CANCELED` with `OCO`. Nothing fires for the dust.
- If no exit ever ran, each leg whose exit would miss the minimums ends `FAILED` with `MIN_QUANTITY` or `MIN_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 `FAILED` with 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](https://testnet.polyester.com/docs/developer-docs/shared-concepts/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 with `INSUFFICIENT_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

## With ModifyOrder

`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 `oco` keeps 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.

## With TriggersService

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.

## REST JSON

`POST /v1/orders`

```json
{
	"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
		}
	}
}
```

## ConnectRPC ProtoJSON

`/orders.v1.OrdersService/CreateOrder`, assuming Pair ID `1` and a `base_quantity_scale` of 8.

```json
{
	"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 sell order with a trailing buy-back

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`.

```json
{
	"order": {
		"symbol": "BTC-USDT",
		"side": "SELL",
		"baseQty": "0.5",
		"limitGtc": { "price": "70000", "postOnly": false },
		"attachedRisk": {
			"trailingStop": {
				"trailingDistanceBps": 150,
				"limitPrice": "72000"
			}
		}
	}
}
```

### A market buy for a quote amount with OCO legs

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`

```json
{
	"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](https://testnet.polyester.com/docs/developer-docs/shared-concepts/order-types-and-lifecycle) and [Error codes](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes).
