# Stop-loss and take-profit

Create standalone stop-loss and take-profit triggers on either side, with a market or limit child order, and know how they reserve funds and fire.

A stop-loss or take-profit watches one price and places one child order when that price reaches its trigger price. Create one with [`CreateTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/CreateTrigger) ([`POST /v1/triggers`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/triggers)). Both work on either side: a sell protects or closes a holding, and a buy enters or buys back. This page covers standalone triggers. For legs attached to an order, see [Attached take-profit and stop-loss](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/attached-tp-sl).

## Which way the price must move

You do not set a direction. Polyester derives it from the trigger type and the child order's side, and returns it as `stop.trigger_direction`.

| Type and side        | Fires when the price          | Typical use                    |
| -------------------- | ----------------------------- | ------------------------------ |
| `STOP_LOSS` `SELL`   | Falls to or below the trigger | Limit the loss on a holding    |
| `STOP_LOSS` `BUY`    | Rises to or above the trigger | Buy on a breakout, or buy back |
| `TAKE_PROFIT` `SELL` | Rises to or above the trigger | Lock in a gain                 |
| `TAKE_PROFIT` `BUY`  | Falls to or below the trigger | Buy at a lower price           |

The price is the one named by `trigger_price_source`: the last traded price by default, or the index price. See [The price a trigger watches](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/overview#the-price-a-trigger-watches).

If the price has already reached the trigger price when the trigger is installed, it fails with `TRIGGER_WOULD_TRIGGER_IMMEDIATELY` and places nothing. Use an ordinary order to trade at once.

## Child order

`child` selects the order placed when the trigger fires. Either side can use any of them.

- **`market_ioc`** · Fields: `max_slippage_ticks` or `max_slippage_bps`\
  Takes liquidity at once, within a slippage cap measured from the fire price.
- **`limit_gtc`** · Fields: `price_ticks`, `post_only`\
  Rests at your limit price until it fills or you cancel it.
- **`limit_ioc`** · Fields: `price_ticks`\
  Takes what it can at your limit price or better, then cancels the rest.
- **`limit_fok`** · Fields: `price_ticks`\
  Fills completely at your limit price or better, or not at all.

A market child without a cap uses the Pair's default market slippage for its side, as a market order does. See [Market order price protection](https://testnet.polyester.com/docs/developer-docs/shared-concepts/market-order-price-protection). On a price gap past the cap, a market child can fill partly or not at all.

> **The trigger price is not the fill price**
>
> The trigger price decides when the child is placed. The child's fill price depends on the book at that moment and on its own limit or slippage cap.

## Fields and validation

| Field                               | Rule                                                                    |
| ----------------------------------- | ----------------------------------------------------------------------- |
| `qty_scaled`                        | Required, positive. The child order's quantity in the Pair's base scale |
| `trigger_price_ticks`               | Required, positive, and on the Pair's tick size                         |
| `side`                              | Required: `BUY` or `SELL`                                               |
| `child`                             | Required: exactly one of the four children                              |
| `price_ticks` (limit children)      | Required, positive, and on the Pair's tick size                         |
| `max_slippage_bps` (market child)   | Optional. `1` to `10000` for a buy, `1` to `9999` for a sell            |
| `max_slippage_ticks` (market child) | Optional, positive. Set at most one of the two caps                     |
| `trigger_price_source`              | Optional: `LAST_PRICE` (default) or `INDEX_PRICE`                       |
| `fee_asset`                         | Optional. `BASE` only for a buy; a sell must use `QUOTE`                |
| `self_trade_prevention_mode`        | Optional. Defaults to `EXPIRE_MAKER`                                    |
| `client_trigger_id`                 | Required, 1 to 36 bytes                                                 |

Limit children take no slippage cap. In REST, prices and the quantity are decimal strings, such as `"triggerPrice": "60000"` and `"qty": "0.5"`, and the Pair is named by `symbol`.

## Funds reserved at creation

A standalone stop-loss or take-profit reserves its funds when it is created, so the child can be placed when it fires. A create that the available balance cannot cover is rejected with `INSUFFICIENT_FUNDS`. A create whose child could never pass the Pair's minimum quantity, step size, or minimum notional is rejected with that check's code, such as `MIN_NOTIONAL`. The child is valued where the fire would value it: a limit child at its limit price, a market child at its slippage bound from the trigger price.

| Child                   | Reserved                                                                                                 |
| ----------------------- | -------------------------------------------------------------------------------------------------------- |
| Any sell                | The quantity, in the base Asset                                                                          |
| Buy with a limit child  | The limit price times the quantity, plus the fee, in the quote Asset                                     |
| Buy with a market child | The trigger price raised by the slippage cap, or by the Pair's default, times the quantity, plus the fee |

A buy with a market child never pays more than the trigger price raised by its cap. The reserve moves to the child order when the trigger fires, and is released if the trigger is canceled or fails.

## Examples

A stop-loss that sells 0.5 BTC at market if BTC-USDT trades at 60,000 or lower, with a 1% slippage cap.

## REST JSON

`POST /v1/triggers`

```json
{
	"trigger": {
		"symbol": "BTC-USDT",
		"qty": "0.5",
		"clientTriggerId": "sl-btc-1",
		"stopLoss": {
			"side": "SELL",
			"triggerPrice": "60000",
			"triggerPriceSource": "LAST_PRICE",
			"child": { "marketIoc": { "maxSlippageBps": 100 } }
		}
	}
}
```

## ConnectRPC ProtoJSON

`/triggers.v1.TriggersService/CreateTrigger`. The example assumes BTC-USDT has Pair ID `1` and a `base_quantity_scale` of 8; read both from `GetSpotConfig`.

```json
{
	"trigger": {
		"symbolId": 1,
		"qtyScaled": "50000000",
		"clientTriggerId": "sl-btc-1",
		"stopLoss": {
			"side": "SELL",
			"triggerPriceTicks": "60000000000000",
			"triggerPriceSource": "LAST_PRICE",
			"child": { "marketIoc": { "maxSlippageBps": 100 } }
		}
	}
}
```

The response returns the trigger ID and the time it was accepted:

```json
{
	"triggerId": "2NEpo7TZRRrLZSi2W",
	"clientTriggerId": "sl-btc-1",
	"acceptedAt": "2026-11-03T09:15:02.481Z",
	"acceptedAtTsNs": "1793697302481000000"
}
```

### Take-profit that sells with a resting limit order

Sells 0.5 BTC with a GTC limit order at 72,000 once the last price reaches 71,500. The limit order rests on the book if it cannot fill at once.

```json
{
	"trigger": {
		"symbol": "BTC-USDT",
		"qty": "0.5",
		"clientTriggerId": "tp-btc-1",
		"takeProfit": {
			"side": "SELL",
			"triggerPrice": "71500",
			"child": { "limitGtc": { "price": "72000", "postOnly": false } }
		}
	}
}
```

### Stop-loss that buys on the index price

Buys 0.5 BTC with an IOC limit order at up to 66,200 once the index price rises to 66,000. On a Pair without an index price, the trigger fails with `INDEX_PRICE_UNAVAILABLE`.

```json
{
	"trigger": {
		"symbol": "BTC-USDT",
		"qty": "0.5",
		"clientTriggerId": "sl-btc-buy-1",
		"stopLoss": {
			"side": "BUY",
			"triggerPrice": "66000",
			"triggerPriceSource": "INDEX_PRICE",
			"child": { "limitIoc": { "price": "66200" } }
		}
	}
}
```

## When it fires

When the watched price reaches the trigger price, the trigger places its child order once and records `EVENT_FIRED` with the child's order ID and the fire price.

- **Child admitted:** the trigger is `COMPLETED`. Follow the child as an ordinary order. Canceling the trigger no longer affects it.
- **Child rejected:** the trigger is `FAILED`, its `failure_reason` is the child's rejection, such as `MARKET_HALTED` or `POST_ONLY_CROSS`, and its reserved funds are released. It does not re-arm.
- **Placement budget empty, or the account under an automatic repair:** the trigger stays `ARMED`, records `EVENT_FIRE_REFUSED` with `RATE_LIMITED` or `ACCOUNT_REPAIRING`, and fires again shortly if the latest price still meets its condition.

## Modify

[`ModifyTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/ModifyTrigger) ([`PUT /v1/triggers/{trigger_id}`](https://testnet.polyester.com/docs/api-docs/rest/PUT/v1/triggers/trigger_id)) changes:

- `trigger_price_ticks`. A price the watched price has already reached is refused with `TRIGGER_WOULD_TRIGGER_IMMEDIATELY`.
- The slippage cap of a market child, with `max_slippage_ticks` or `max_slippage_bps`. A value of `0` clears the cap, and the Pair's default applies again.

For a buy with a market child, changing the trigger price or the cap re-sizes the reserved funds. A change that needs more than your available balance is refused, and the trigger is unchanged.

A modify meets the same minimums as a create. The child order is valued on the terms the change leaves: a market buy at its maximum price, a market sell at its protection price, and a limit child at its limit price. A change that would put the child below the Pair's minimum quantity, step size, or minimum notional is refused with `MIN_QTY`, `QTY_STEP_SIZE`, or `MIN_NOTIONAL`, and the trigger keeps its previous price, cap, and reserved funds.

A trigger price off the Pair's tick size is refused with `PRICE_TICK_SIZE` and a `tick_size` violation on `trigger_price_ticks` (REST `triggerPrice`), with or without the optional `symbol_id`. The trigger is unchanged.

The child's limit price cannot change: setting `limit_price_ticks` is refused with `TRIGGER_CHILD_PRICE_NOT_MODIFIABLE`. Cancel the trigger and create a new one instead.

## Errors

- **`PRICE_TICK_SIZE`**\
  A trigger or limit price is not on the Pair's tick size.
- **`MAX_SLIPPAGE_INVALID`**\
  A sell's slippage cap is `10000` bps.
- **`TRIGGER_PRICE_SOURCE_UNSUPPORTED`**\
  The source is `MARK_PRICE` or not a defined source.
- **`FEE_ASSET_NOT_ALLOWED`**\
  A sell set `fee_asset` to `BASE`, or the Pair does not allow it for a buy.
- **`INSUFFICIENT_FUNDS`**\
  The available balance cannot cover the reserve.
- **`MIN_QUANTITY`, `STEP_SIZE`, `MIN_NOTIONAL`, `MAX_NOTIONAL`**\
  The child order could never pass the Pair's order limits.
- **`MIN_QTY`, `QTY_STEP_SIZE`, `MIN_NOTIONAL`** on a modify\
  The change would leave the child order below the Pair's minimums. The trigger is unchanged.
- **`VALIDATION_ERROR`**\
  A required field is missing, such as `child`, or a price, quantity, or cap is not positive.

See [Error codes](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes) for HTTP and ConnectRPC statuses, and [Order types and lifecycle](https://testnet.polyester.com/docs/developer-docs/shared-concepts/order-types-and-lifecycle) for how the child order behaves after it is placed.
