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 |
TAKE_PROFIT | Places one child order when the price moves in your favor to a fixed level | Stop-loss and take-profit |
TRAILING_STOP | Follows the best price since activation and fires on a reversal of a set distance | Trailing stop |
TWAP | Splits a quantity into slices placed at a fixed interval | TWAP |
LADDER | Places limit orders at evenly spaced prices across a range | Ladder |
Standalone and attached triggers
A standalone trigger has its own quantity and side. Create it with TriggersService.CreateTrigger (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.
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
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.
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.
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.
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.
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.
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.
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_.
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.
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 | Every type | TRADING_CANCEL |
PauseTrigger | Stop-loss, take-profit, and trailing stop, standalone or attached | TRADING_CANCEL |
ResumeTrigger | Stop-loss, take-profit, and trailing stop, standalone or attached | TRADING_PLACE |
ModifyTrigger | Stop-loss, take-profit, and trailing stop, standalone or attached | TRADING_PLACE |
Over REST, these are DELETE /v1/triggers/{trigger_id}, POST /v1/triggers/{trigger_id}/pause, POST /v1/triggers/{trigger_id}/resume, and 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 toRUNNING, or toCREATEDbefore 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 withMIN_QTY,QTY_STEP_SIZE, orMIN_NOTIONAL, and an off-tick price withPRICE_TICK_SIZEand a field violation. A refused modify changes nothing. - Cancel ends the trigger as
CANCELEDand 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 answersCANCELEDagain; any other control on an ended trigger is refused withTRIGGER_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.
CancelTrigger.Rate limits
Triggers draw from the same TRADING_PLACE and TRADING_CANCEL pools as your orders. See 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_UNKNOWNmeans the create was not confirmed in time and may have succeeded. Read your triggers for theclient_trigger_idbefore retrying.UNAVAILABLEmeans nothing was created.
Read triggers
- One trigger with its configuration and progress:
GET /v1/triggers/{trigger_id}orGetTrigger - Your triggers, newest first, filtered by Pair, status, trigger type, or parent order:
GET /v1/triggersorListTriggers - A trigger's events, newest first, optionally filtered by event type:
GET /v1/triggers/{trigger_id}/eventsorListTriggerEvents
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. ConnectRPC uses Pair IDs from GetSpotConfig, and prices and quantities as 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.
- Spot trigger updates: the trigger's full state after each change, Protobuf only.
- Trigger events: ordered arrays of trigger events, in JSON or
Protobuf. Their JSON carries the prefixed enum names, such as
EVENT_CANCELEDandTRIGGER_CANCEL_REASON_USER_REQUEST, where REST trigger reads showCANCELEDandUSER_REQUEST. See 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 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 thesubaccount_idselector.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 byclient_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 theRetry-Afterdelay.
REST returns the matching HTTP status: 404, 400, 409, 503, 504, or 429.