client.triggers manages standalone automations. Types: CreateTriggerType::{StopLoss, TakeProfit, TrailingStop, Twap, Ladder}.
post_only on conditional children is only valid for limit GTC.Methods
| Method | Summary |
|---|---|
create | CreateTriggerParams → TriggerMutationResult |
get / get_by_id | Proto request or the string ID returned by the SDK |
list / list_with | Proto request or ListTriggersOpts |
modify | ModifyTriggerParams |
cancel / pause / resume | Proto request wrappers |
cancel_by_id / pause_by_id / resume_by_id | String-ID convenience helpers |
resume_with | Resume with an optional symbol |
list_events | Paginated events via ListTriggerEventsRequest |
subscribe / subscribe_events | Private streams (recv_result) |
Create
use polyester::models::{
CreateOrderType, CreateSide, CreateTimeInForce, CreateTriggerParams, CreateTriggerType,
};
use polyester::{Price, Quantity};
let quantity_scale = client
.catalogs
.base_quantity_scale_for_symbol("BTC-USDT")
.ok_or_else(|| polyester::Error::validation("BTC-USDT quantity scale is unavailable"))?;
let mut params = CreateTriggerParams::new(
"BTC-USDT",
CreateTriggerType::StopLoss,
CreateSide::Sell,
CreateOrderType::Market,
Quantity::from_decimal_str("0.25", quantity_scale, Some("BTC-USDT".into()), None)?,
);
params.trigger_price = Some(Price::from_decimal_str("60000", Some("BTC-USDT".into()))?);
params.time_in_force = Some(CreateTimeInForce::Ioc); // market → market-IOC; tif unused for market
params.client_trigger_id = "sl-001".into();
let result = client.triggers.create(params).await?;CreateTriggerParams is #[non_exhaustive]: build it with CreateTriggerParams::new, then set
optional fields. trigger_price_source takes last or index; None watches the last trade price.
A mark source returns Error::Validation.
Stop-loss and take-profit triggers with a market child accept a slippage cap, max_slippage_ticks or max_slippage_bps (at most one; a SELL child takes fewer than 10,000 bps). Without one, the Pair's
default market slippage applies. A limit child takes no cap.
Trailing stop requires exactly one of trailing_distance_ticks or trailing_distance_bps, and takes
either side. A SELL trailing stop tracks the highest price and fires a market-IOC child; it takes no limit_price. A BUY trailing stop tracks the lowest price and requires limit_price: the most its
child pays per unit, fixed at create, and at least its activation_price. A trailing stop's max_slippage_bps takes at most 9,999, whatever its side, so the SDK returns Error::Validation above that and the API rejects 10,000 (100%) with MAX_SLIPPAGE_INVALID on create and on modify. On
a SELL trailing stop, a max_slippage_ticks at or above the price at which the stop fires is not
rejected; the stop then sells at the lowest valid price.
A trailing stop created with an activation price the market has already reached ends failed with TRIGGER_FAILURE_REASON_TRIGGER_WOULD_TRIGGER_IMMEDIATELY (244). A modify that would make it act at
once is refused with ERROR_CODE_TRIGGER_WOULD_TRIGGER_IMMEDIATELY (302), leaving the existing state
unchanged. To start trailing from the market, clear the activation price with a present zero.
client_trigger_id is required, and the SDK never generates it. Create and persist one stable ID
per logical trigger, then reuse it when reconciling or retrying an ambiguous result.
Create returns TriggerMutationResult with status: "accepted" (admission only, the wire
create response no longer carries a lifecycle enum). Pause / resume / cancel / modify return
lifecycle labels such as armed, paused, cancelled.
Strategies
All five support pause, resume, cancel, and modify through the *_by_id helpers and modify, but
the server may reject a lifecycle operation that does not apply mid-run.
StopLoss/TakeProfit: buy or sell. The child is market-IOC or limit (+ TIF). Requirestrigger_price.TrailingStop: buy or sell. The child is market-IOC when selling, or IOC limit atlimit_pricewhen buying. Requires a trailing distance in ticks or bps. Modify patches distance, activation, and slippage.Twap: buy or sell. The slices are market-IOC or limit-GTC. Requirestwap_duration_msandtwap_slice_interval_ms.Ladder: buy or sell. The children are multi-level limit orders. Requires the ladder min, max, and levels, with distributionlinear.
Ladder entry quantity is not guaranteed to equal the sum of child limit sizes after step / fee rounding, so child aggregate qty may be lower than the requested entry. TWAP / ladder creates are not guaranteed bulk atomic placement of every child; if a mid-run lifecycle op is rejected or children land partially, fall back to single-order placement and explicit cleanup of residual children / open orders. Pause may be rejected for TWAP even when pause appears in generic lifecycle helpers.
Limits per Pair
Each trading account (Main Account or Subaccount) can hold a limited number of triggers that have not ended on each Pair. Other accounts and other Pairs are unaffected. A create past a limit is rejected and nothing is created; retry once one of the account's triggers on that Pair ends.
- 100: All strategies, attached take-profit, stop-loss, and trailing legs included. Rejected with
MARKET_TRIGGER_LIMIT_EXCEEDED(293). - 20: TWAP. Rejected with
MARKET_TWAP_LIMIT_EXCEEDED(294). - 5: Ladder. Rejected with
MARKET_LADDER_LIMIT_EXCEEDED(295).
TWAPs and ladders also count toward the 100-trigger limit, which is checked first, so a TWAP or
ladder create past it is rejected with MARKET_TRIGGER_LIMIT_EXCEEDED. Each TWAP or ladder counts
once, however many slices or levels it has. An order whose attached legs would pass the 100-trigger
limit is rejected with MARKET_TRIGGER_LIMIT_EXCEEDED, and neither the order nor its legs is
placed.
Get by ID
// Canonical base58 string returned by create / list / subscribe.
let trigger_id = "3nK8xQmP2aBcDeFgHiJkLmNoPqRsTuVwXy";
let trigger = client.triggers.get_by_id(trigger_id, None).await?;The convenience helper accepts the base58 string returned by create, list, and subscriptions.
Canonical base58 can consist only of digits, so never decide that an ID is decimal from its
characters. Reuse IDs returned by the SDK. Use the lower-level get method only when you already
have a typed proto u64 ID.
get / get_by_id and list / list_with read your own writes: once a create, modify, pause, or
resume has answered, they return the trigger as changed. Until the change is readable the server
answers unavailable, and these reads retry it with the same request, giving up after about 2
seconds with Error::Server. Trigger.version is 1 for a trigger's first state and higher for every
later state change; keep the copy with the highest version.
List triggers
use polyester::models::ListTriggersOpts;
let mut page_token = None;
loop {
let listed = client.triggers.list_with(ListTriggersOpts {
symbol: Some("BTC-USDT".into()),
status: vec!["created".into(), "armed".into()],
limit: Some(50),
page_token,
..Default::default()
}).await?;
// Unknown status labels → Error::Validation
for t in &listed.triggers {
println!("{} {}", t.status, t.trigger_id);
}
if listed.next_page_token.is_empty() {
break;
}
page_token = Some(listed.next_page_token);
}Modify
use polyester::models::ModifyTriggerParams;
use polyester::Price;
let trigger_id = "3nK8xQmP2aBcDeFgHiJkLmNoPqRsTuVwXy".to_owned();
client.triggers.modify(ModifyTriggerParams {
trigger_id,
subaccount_id: None,
symbol: None,
symbol_id: None,
trigger_price: Some(Price::from_decimal_str("59500", Some("BTC-USDT".into()))?),
activation_price: None,
trailing_distance_ticks: None,
trailing_distance_bps: None,
max_slippage_ticks: None,
max_slippage_bps: None,
}).await?;Omit activation_price / max_slippage_ticks / max_slippage_bps to preserve the current values,
and send an explicit zero to clear one. A trigger's child limit price is fixed at create: cancel the
trigger and create a new one to change it. Leave out symbol and symbol_id to target the trigger's
own market; an API key limited to some markets must name the market.
A trigger_price or activation_price bound to another market than the symbol or symbol_id you pass returns Error::Validation before sending, as on trigger creation and order writes. With
only symbol_id and a bound price, the SDK waits for the catalogs to learn that market's name, and
it refuses a bound price for a symbol_id the catalogs don't know. Without either, the trigger's
market isn't known locally and the price is sent as is.
Pause, resume, and cancel
let trigger_id = "3nK8xQmP2aBcDeFgHiJkLmNoPqRsTuVwXy";
client.triggers.pause_by_id(trigger_id, None).await?;
client.triggers.resume_by_id(trigger_id, None, None).await?;
client.triggers.cancel_by_id(trigger_id, None).await?;List events
use polyester::proto::triggers::v1::{ListTriggerEventsRequest, TriggerEventType};
use polyester::codecs::scalars::id_to_u64;
let trigger_id = id_to_u64("3nK8xQmP2aBcDeFgHiJkLmNoPqRsTuVwXy", "trigger_id")?;
let mut page_token = String::new();
loop {
let events = client.triggers.list_events(ListTriggerEventsRequest {
trigger_id,
limit: 20,
// optional filter: EventFired, EventCanceled, EventUpdated, EventFailed,
// EventActivated, EventArmed, EventFireRefused
event_type: TriggerEventType::EventFired.into(),
page_token: page_token.clone(),
..Default::default()
}).await?;
for ev in &events.events {
println!(
"{} {} {:?} {}",
ev.event_type, ev.child_order_id, ev.fire_price, ev.ts_ns
);
}
if events.next_page_token.is_empty() {
break;
}
page_token = events.next_page_token;
}Child orders are not on the trigger snapshot. Set event_type to EVENT_FIRED and read child_order_id / child_seq from each decoded event.
list_events is currently a proto-level method, so decode the SDK-returned canonical base58 ID
with id_to_u64 (returns polyester::Result, so ? maps decode failures into Error). Do not
parse an all-digit canonical base58 ID as decimal.
Subscribe
let account_id = client.default_account_id.as_deref();
let mut sub = client.triggers.subscribe(account_id).await?;
while let Some(t) = sub.recv_result().await? {
println!("{} {}", t.status, t.trigger_id);
break;
}
let mut ev = client.triggers.subscribe_events(account_id).await?;
while let Some(e) = ev.recv_result().await? {
println!("{} {}", e.event_type, e.ts_ns);
break;
}Decoded TriggerEvent exposes trigger_id, subaccount_id, symbol_id, trigger_type, event_type (fired, canceled, updated, failed, activated, armed, fire_refused), ts_ns, child_seq, child_order_id, fire_price, cancel_reason, failure_reason, and fire_refusal_reason.
A fire_refused event records a fire refused while the trigger stayed live; it is not terminal, so
read the trigger's status rather than inferring it from its last event. fire_refusal_reason says
why, such as market_halted, min_notional, rate_limited, or self_trade_prevented, and child_order_id is empty unless an order was placed. fire_price is None for time-scheduled TWAP
slice fires.
Decoded Trigger includes, for TWAP, details → Twap → executed_qty. That field populates
as slices fire; right after create it is typically unset until the first child order is placed.
Child-order history comes from list_events.