# Triggers overview

How standalone and attached triggers watch a price or a schedule, place child orders, report their status, and count against limits.

A trigger is an instruction that Polyester holds and evaluates server-side. It waits for a price condition or a schedule, then places one or more ordinary orders, called child orders, on your behalf. Closing your connection does not stop a trigger.

Other venues call these conditional, algo, or plan orders. Polyester keeps the trigger and its child orders as separate records: the trigger has its own ID, status, and events, and each child order has its own order ID, fills, and final state.

## Trigger types

| Type            | What it does                                                                      | Details                                                                                                                           |
| --------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `STOP_LOSS`     | Places one child order when the price moves against you to a fixed level          | [Stop-loss and take-profit](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/stop-loss-and-take-profit) |
| `TAKE_PROFIT`   | Places one child order when the price moves in your favor to a fixed level        | [Stop-loss and take-profit](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/stop-loss-and-take-profit) |
| `TRAILING_STOP` | Follows the best price since activation and fires on a reversal of a set distance | [Trailing stop](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/trailing-stop)                         |
| `TWAP`          | Splits a quantity into slices placed at a fixed interval                          | [TWAP](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/twap)                                           |
| `LADDER`        | Places limit orders at evenly spaced prices across a range                        | [Ladder](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/ladder)                                       |

## Standalone and attached triggers

A **standalone trigger** has its own quantity and side. Create it with [`TriggersService.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)). All five types can be standalone.

An **attached trigger**, or leg, belongs to an order. A take-profit, a stop-loss, or a trailing stop can be attached to an order through its `attached_risk` field. A leg protects the quantity its parent order fills, and its `parent_order_id` names that order. The order response returns each leg's trigger ID. See [Attached take-profit and stop-loss](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/attached-tp-sl).

Both kinds are read, paused, resumed, modified, and canceled through `TriggersService` with the trigger ID.

## The price a trigger watches

Stop-loss, take-profit, and trailing-stop triggers watch one price, chosen with `trigger_price_source` when you create them. TWAPs and ladders do not watch a price.

| Source        | Meaning                                                                                                         |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| `LAST_PRICE`  | The Pair's last traded price. The default when the field is omitted or unspecified                              |
| `INDEX_PRICE` | The Pair's index price, aggregated from external exchanges. Available only on Pairs that publish an index price |
| `MARK_PRICE`  | Not supported on spot Pairs. A create that names it is rejected with `TRIGGER_PRICE_SOURCE_UNSUPPORTED`         |

The source is fixed for the life of the trigger. To change it, cancel the trigger and create a new one. For an attached leg, a `ModifyOrder` that names another source replaces the leg.

A trigger on `INDEX_PRICE` acts only on a current index value. While the index is stale or unavailable, the trigger stays armed and waits; it never fires on an outdated value. A standalone trigger on a Pair without an index price fails with `INDEX_PRICE_UNAVAILABLE`.

## From trigger to child order

1. Create

   `CreateTrigger` answers with `trigger_id` once Polyester admits the trigger. Admission checks the request, the account's limits and rate limits, and reserves funds where the type needs them. A stop, take-profit, trailing stop, or TWAP whose child order could never pass the Pair's minimum quantity, step size, or minimum notional, valued where its fire would value it, is refused at create with that check's code, such as `MIN_NOTIONAL`, and nothing is held. The answer does not mean the trigger is armed: arming, firing, and child orders follow asynchronously.

2. Arm

   The trigger starts evaluating. A stop-loss or take-profit whose trigger price is already reached when it is installed, or a trailing stop whose activation price is already reached, fails at once with `TRIGGER_WOULD_TRIGGER_IMMEDIATELY` and places nothing. Its reserved funds are released.

3. Fire

   When the watched price reaches the trigger price, the trigger fires. The comparison includes the trigger price itself. Polyester records an `EVENT_FIRED` event with the price that fired it and the child order's ID.

4. Place the child order

   The child order goes through the same admission checks as an order you send. If it is admitted, a stop-loss, take-profit, or trailing stop is `COMPLETED`. If it is rejected, the trigger is `FAILED` with a `failure_reason` naming the cause, with two exceptions that keep the trigger live and record `EVENT_FIRE_REFUSED`: a refusal for a rate limit or an automatic account repair, after which the trigger fires again shortly, and the attached-leg cases described in [Attached take-profit and stop-loss](https://testnet.polyester.com/docs/developer-docs/shared-concepts/triggers/attached-tp-sl).

5. Follow the child order

   The child order is an ordinary order. Read its fills and final state with the order APIs and the private order stream. Its client order ID is `trg:<trigger_id>:<child_seq>`, with the trigger ID in decimal.

> **Completed is not filled**
>
> A completed stop has placed its child order. The child can still fill partly, fill at several prices, or not fill at all, according to its own type and price protection. Check the child order for the outcome.

## Limits

Each account has its own limits. The Main Account and each Subaccount are separate accounts.

| Limit                                                            | Most at once | Refused with                    |
| ---------------------------------------------------------------- | ------------ | ------------------------------- |
| Triggers on one Pair, attached legs, TWAPs, and ladders included | 100          | `MARKET_TRIGGER_LIMIT_EXCEEDED` |
| TWAPs on one Pair                                                | 20           | `MARKET_TWAP_LIMIT_EXCEEDED`    |
| Ladders on one Pair                                              | 5            | `MARKET_LADDER_LIMIT_EXCEEDED`  |
| Triggers on all Pairs, live or ended within the last 60 seconds  | 1,024        | `TRIGGER_LIMIT_EXCEEDED`        |

A trigger stops counting toward a Pair's limits once it ends. A create that would pass a limit is rejected and nothing is created. An order whose attached legs would pass the Pair limit is rejected together with its legs. A ladder places 2 to 100 orders, and an order carries at most a take-profit and one stop leg.

Polyester also limits the number of triggers across all accounts. When that capacity is reached, creates are refused with `TRIGGER_CAPACITY_REACHED` or `TWAP_CAPACITY_REACHED`; retry later.

## Statuses

REST returns the short name. ConnectRPC returns the `STATUS_` name, for example `STATUS_ARMED`.

| Status      | Meaning                                                                                                                   |
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| `CREATED`   | Installed but not yet evaluating: a trailing stop before its activation price, or an attached leg before its parent fills |
| `ARMED`     | Evaluating its condition. A trigger whose fire is in progress is still `ARMED`                                            |
| `RUNNING`   | Executing: a TWAP or a ladder, or an attached leg protecting its parent's filled quantity                                 |
| `PAUSED`    | Not evaluating until you resume it                                                                                        |
| `COMPLETED` | Ended after it did its work. For a stop, its child order was placed                                                       |
| `CANCELED`  | Ended without finishing. `cancel_reason` says why                                                                         |
| `FAILED`    | Ended because a child order could not be placed or the trigger could not run. `failure_reason` says why                   |

`COMPLETED`, `CANCELED`, and `FAILED` are final. `completed_at` holds the time the trigger ended, whatever the final status.

### Cancel reasons

- **`USER_REQUEST`**\
  You canceled the trigger, the working exit of an attached leg, or any level of a ladder that filled nothing.
- **`OCO`**\
  Another leg of the same order made the last exit, so no position is left to protect.
- **`PARENT_CANCELED_NO_FILL`**\
  The parent order ended without a fill.
- **`SELF_TRADE_PREVENTION`**\
  Self-trade prevention canceled the trigger's child orders.
- **`MARKET_HALTED`**\
  The Pair was halted. Every open trigger on it ended and released its funds.
- **`PAIR_DRAIN_TRIGGER`, `PAIR_DRAIN_TWAP`, `PAIR_DRAIN_LADDER`, `PAIR_DRAIN_ATTACHED_LEG`**\
  The Pair was disabled, and Polyester ended the trigger.
- **`ACCOUNT_REPAIR`**\
  Polyester ended the trigger while it recovered your account.

ConnectRPC prefixes each name with `TRIGGER_CANCEL_REASON_`.

### Failure reasons

Most failure reasons repeat the reason the child order was rejected for, such as `INSUFFICIENT_FUNDS`, `MIN_NOTIONAL`, `POST_ONLY_CROSS`, or `MARKET_HALTED`. Others describe the trigger itself, such as `TRIGGER_WOULD_TRIGGER_IMMEDIATELY`, `INDEX_PRICE_UNAVAILABLE`, or `STEP_SIZE` for a TWAP slice that rounds down to zero. ConnectRPC prefixes each name with `TRIGGER_FAILURE_REASON_`. See [Error codes](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes).

## Events

Every change is also recorded as an event. Events are append-only, so you can rebuild what a trigger did. ConnectRPC returns the names below; REST returns the short name, for example `FIRED`.

| Event                | When                                                                                                |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `EVENT_ACTIVATED`    | A trailing stop's activation condition was met and it started tracking                              |
| `EVENT_ARMED`        | An attached leg started protecting its parent's filled quantity, once                               |
| `EVENT_FIRED`        | The trigger placed a child order: a stop's child, a TWAP slice, a ladder level, or an attached exit |
| `EVENT_UPDATED`      | The trigger was paused, resumed, or modified                                                        |
| `EVENT_CANCELED`     | The trigger was canceled. Carries `cancel_reason`                                                   |
| `EVENT_FAILED`       | The trigger failed. Carries `failure_reason`                                                        |
| `EVENT_FIRE_REFUSED` | A fire's child order was refused and the trigger stayed live. Carries `fire_refusal_reason`         |

`EVENT_FIRE_REFUSED` is not final: the trigger keeps its status, the refused attempt uses up its `child_seq`, and the next fire takes the next one. `fire_refusal_reason` says why, for example `RATE_LIMITED`, `ACCOUNT_REPAIRING`, or, for an attached leg, the exit's own refusal or `SELF_TRADE_PREVENTED`. Only a `SELF_TRADE_PREVENTED` refusal has a placed order behind its `child_seq`: the attached exit that self-trade prevention expired. A refusal that ends the trigger is `EVENT_FAILED` instead.

`EVENT_FIRED` carries `child_seq`, `child_order_id`, and `fire_price_ticks`. The fire price is the price that crossed the trigger price, a trailing stop's threshold when it fired, or a ladder level's price. A market child's slippage cap is measured from it.

## Pause, resume, modify, and cancel

| Operation                                                                                                        | Supported for                                                     | Rate-limit pool  |
| ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------- |
| [`CancelTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/CancelTrigger) | Every type                                                        | `TRADING_CANCEL` |
| [`PauseTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/PauseTrigger)   | Stop-loss, take-profit, and trailing stop, standalone or attached | `TRADING_CANCEL` |
| [`ResumeTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/ResumeTrigger) | Stop-loss, take-profit, and trailing stop, standalone or attached | `TRADING_PLACE`  |
| [`ModifyTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/ModifyTrigger) | Stop-loss, take-profit, and trailing stop, standalone or attached | `TRADING_PLACE`  |

Over REST, these are [`DELETE /v1/triggers/{trigger_id}`](https://testnet.polyester.com/docs/api-docs/rest/DELETE/v1/triggers/trigger_id), [`POST /v1/triggers/{trigger_id}/pause`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/triggers/trigger_id/pause), [`POST /v1/triggers/{trigger_id}/resume`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/triggers/trigger_id/resume), and [`PUT /v1/triggers/{trigger_id}`](https://testnet.polyester.com/docs/api-docs/rest/PUT/v1/triggers/trigger_id).

TWAPs and ladders take only a cancel. Pausing, resuming, or modifying them is refused with `TRIGGER_NOT_MODIFIABLE`.

- **Pause** stops evaluation. A paused trigger keeps its reserved funds and still counts toward your limits.
- **Resume** returns a paused standalone trigger to `ARMED`, and an attached leg to `RUNNING`, or to `CREATED` before its parent fills. A resumed standalone stop-loss or take-profit waits for the next price; it does not fire on a price seen while it was paused. A resumed standalone trailing stop is checked against the latest price at once.
- **Modify** changes a trigger price, a trailing distance, an activation price, or a slippage cap. The side, quantity, type, price source, and a child's limit price cannot change. A change that would make the trigger act at once, such as a trigger or activation price the watched price has already reached, is refused with `TRIGGER_WOULD_TRIGGER_IMMEDIATELY`. A change whose child order would fall below the Pair's minimums is refused with `MIN_QTY`, `QTY_STEP_SIZE`, or `MIN_NOTIONAL`, and an off-tick price with `PRICE_TICK_SIZE` and a field violation. A refused modify changes nothing.
- **Cancel** ends the trigger as `CANCELED` and releases its reserved funds. A child order that already exists is not canceled with a stop; cancel it separately. Canceling a TWAP or a ladder also cancels its open child orders. Canceling a trigger that is already canceled answers `CANCELED` again; any other control on an ended trigger is refused with `TRIGGER_STATUS_INVALID`.

A trigger takes one change at a time. A change sent while the previous one is still in progress is refused with `TRIGGER_CONTROL_IN_PROGRESS`. A change that is not confirmed in time fails with `TRIGGER_CONTROL_OUTCOME_UNKNOWN`: it may still apply, so read the trigger before retrying.

> **Cancel-all leaves triggers in place**
>
> [Cancel all orders](https://testnet.polyester.com/docs/developer-docs/shared-concepts/cancel-all-orders) cancels open orders, including child orders that triggers placed, but never a trigger, attached legs included. Cancel triggers with `CancelTrigger`.

## Rate limits

Triggers draw from the same `TRADING_PLACE` and `TRADING_CANCEL` pools as your orders. See [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits).

| Action                                                                 | Pool             |                Cost |
| ---------------------------------------------------------------------- | ---------------- | ------------------: |
| Create a stop, trailing stop, or TWAP                                  | `TRADING_PLACE`  |                   1 |
| Create a ladder                                                        | `TRADING_PLACE`  | 1, plus 1 per level |
| Each child order a trigger places: a fired stop, a TWAP slice, an exit | `TRADING_PLACE`  |                   1 |
| Modify or resume                                                       | `TRADING_PLACE`  |                   1 |
| Cancel or pause                                                        | `TRADING_CANCEL` |                   1 |

A request is charged even when it is then refused for another reason. When your placement budget is empty, a trigger that fires is not failed: it stays `ARMED` (a TWAP stays `RUNNING`), records `EVENT_FIRE_REFUSED` with `RATE_LIMITED`, and retries when the budget allows. A retried stop fires only if the latest price still meets its condition. Leave headroom in your placement budget for your triggers.

## Create once, retry safely

`client_trigger_id` is required: 1 to 36 bytes, unique per account, Pair, and trigger type.

- While the trigger is live, and for 60 seconds after it ends, a retry with the same ID and the same request returns the same trigger. The same ID with a different request is refused with `CONFLICT_DUPLICATE_CLIENT_TRIGGER_ID`.
- After those 60 seconds, the ID is free and a create with it makes a new trigger. To learn the outcome of an older create, list your triggers and match `client_trigger_id`.
- `TRIGGER_CREATE_OUTCOME_UNKNOWN` means the create was not confirmed in time and may have succeeded. Read your triggers for the `client_trigger_id` before retrying. `UNAVAILABLE` means nothing was created.

## Read triggers

- **One trigger with its configuration and progress:** [`GET /v1/triggers/{trigger_id}`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/triggers/trigger_id) or [`GetTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/GetTrigger)
- **Your triggers, newest first,** filtered by Pair, status, trigger type, or parent order: [`GET /v1/triggers`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/triggers) or [`ListTriggers`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/ListTriggers)
- **A trigger's events, newest first,** optionally filtered by event type: [`GET /v1/triggers/{trigger_id}/events`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/triggers/trigger_id/events) or [`ListTriggerEvents`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/ListTriggerEvents)

A trigger you cannot read, because no trigger has the ID, it belongs to another account, or it is outside the selected `subaccount_id`, answers `NOT_FOUND` (REST `404`) on its events as on the trigger read. A trigger you can read with no events yet answers an empty list. Right after a create, both reads can answer a retryable `UNAVAILABLE` (REST `503`) until they catch up; retry the read.

REST takes the Pair `symbol` and returns prices and quantities as decimal strings and IDs as [public IDs](https://testnet.polyester.com/docs/developer-docs/shared-concepts/public-ids). ConnectRPC uses Pair IDs from `GetSpotConfig`, and prices and quantities as [scaled integers](https://testnet.polyester.com/docs/developer-docs/connectrpc/scaled-integers).

Every trigger read names the self-trade prevention mode `selfTradePreventionMode`, as the create request does: the REST list and single-trigger reads and ConnectRPC alike. REST order reads name the same mode `stpMode`.

List methods return 100 items by default and at most 1,000. Pass `next_page_token` back as `page_token` for the next page.

A trigger carries its configuration in one of `stop_loss`, `take_profit`, `trailing_stop`, `twap`, or `ladder`, the same shape you created it with, including every change a modify applied, and its progress in `stop`, `trailing`, `twap_state`, or `ladder_state`. `version` starts at 1 and rises with every change. Keep the copy with the highest version, because a list can lag a realtime update. Once `ModifyTrigger`, `PauseTrigger`, or `ResumeTrigger` has answered, `GetTrigger` and `ListTriggers` return the trigger as changed, or a retryable `UNAVAILABLE` until they can; retry the read.

### Realtime channels

Two private streams carry trigger changes for one account. Subscribe as described in the [WebSocket session model](https://testnet.polyester.com/docs/developer-docs/shared-concepts/websocket-session-model).

- [Spot trigger updates](https://testnet.polyester.com/docs/api-docs/streams/private-spot-triggers): the trigger's full state after each change, Protobuf only.
- [Trigger events](https://testnet.polyester.com/docs/api-docs/streams/private-spot-trigger-events): ordered arrays of trigger events, in JSON or Protobuf. Their JSON carries the prefixed enum names, such as `EVENT_CANCELED` and `TRIGGER_CANCEL_REASON_USER_REQUEST`, where REST trigger reads show `CANCELED` and `USER_REQUEST`. See [Enum values](https://testnet.polyester.com/docs/developer-docs/rest/getting-started#enum-values).

An event is identified by `trigger_id`, `event_type`, `child_seq`, and `ts_ns`. Apply events in order and upsert by that identity. After a gap the subscription cannot recover, reload with the list methods above.

## Errors

These codes are common to every trigger type. Each type's page lists its own. See [Error codes](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes) for the full list. Each entry gives the stable code and the ConnectRPC code, then what to do.

- **`TRIGGER_NOT_FOUND`** · `NotFound`\
  Check the trigger ID and the account.
- **`NOT_FOUND`** · `NotFound`\
  A trigger read or its events name a trigger you cannot read. Check the ID and the `subaccount_id` selector.
- **`TRIGGER_NOT_MODIFIABLE`** · `InvalidArgument`\
  Cancel the TWAP or ladder instead.
- **`TRIGGER_STATUS_INVALID`** · `InvalidArgument`\
  The trigger's status does not allow the change; read it first.
- **`TRIGGER_CHILD_PRICE_NOT_MODIFIABLE`** · `InvalidArgument`\
  Cancel the trigger and create a new one with the new limit price.
- **`TRIGGER_WOULD_TRIGGER_IMMEDIATELY`** · `FailedPrecondition`\
  Choose a trigger price, activation price, or trailing distance the market has not reached.
- **`TRIGGER_CONTROL_IN_PROGRESS`** · `Unavailable`\
  Retry once the earlier change completes.
- **`TRIGGER_SYMBOL_MISMATCH`** · `InvalidArgument`\
  Name the trigger's own Pair, or omit it.
- **`CONFLICT_DUPLICATE_CLIENT_TRIGGER_ID`** · `AlreadyExists`\
  List triggers by `client_trigger_id`, then use a new ID.
- **`TRIGGER_CREATE_OUTCOME_UNKNOWN`** · `DeadlineExceeded`\
  Read your triggers before retrying the create.
- **`TRIGGER_CONTROL_OUTCOME_UNKNOWN`** · `DeadlineExceeded`\
  Read the trigger before retrying the change.
- **`MARKET_TRIGGER_LIMIT_EXCEEDED`** · `ResourceExhausted`\
  Cancel a trigger on the Pair or wait for one to end.
- **`TRIGGER_LIMIT_EXCEEDED`** · `ResourceExhausted`\
  Cancel triggers you no longer need.
- **`RATE_LIMIT_EXCEEDED`** · `ResourceExhausted`\
  Retry after the `Retry-After` delay.

REST returns the matching HTTP status: `404`, `400`, `409`, `503`, `504`, or `429`.
