client.Triggers manages standalone automations that place a child order when a condition fires.
Types: stop_loss, take_profit, trailing_stop, twap, ladder.
postOnly on conditional children is only valid for limit GTC.Methods
| Method | Summary |
|---|---|
Create | Create any of the five trigger types. |
Get | Fetch one trigger (*models.Trigger). |
List | List with symbol / status filters. |
Modify | Patch via codecs.ModifyTriggerOptions. |
Cancel / Pause / Resume | Lifecycle controls. |
ListEvents | List paginated lifecycle events. |
Subscribe / SubscribeEvents | Private streams (Messages()). |
Create
import "github.com/Fabric-Labs/polyester-sdk-go/codecs"
symbol := "BTC-USDT"
triggerPrice := models.PriceFromDecimal("60000")
qty := models.QtyFromDecimal("0.25")
clientTriggerID := "sl-001"
result, err := client.Triggers.Create(
ctx,
nil, // account scope
symbol,
"stop_loss",
&triggerPrice,
"sell",
qty,
"market",
nil, // limitPrice
"last", // triggerPriceSource: "last" (default when empty) or "index"
"ioc", // market children execute as market-IOC; tif unused for market
nil, // subAccountID
&clientTriggerID,
false, // postOnly, limit GTC children only; rejected otherwise
codecs.CreateTriggerOptions{},
)Trailing / TWAP / ladder fields go in codecs.CreateTriggerOptions (TrailingDistanceTicks, TrailingDistanceBps, ActivationPrice, MaxSlippage*, TwapDurationMs, TwapSliceIntervalMs, LadderPriceMin / Max / Levels / Distribution, FeeAsset, SelfTradePreventionMode).
triggerPriceSource applies to stop-loss, take-profit, and trailing-stop creates: "last" watches
the last traded price and "index" the market's index price, where a market without one fails the
trigger with index_price_unavailable. It is fixed at create and reads back as Details.TriggerPriceSource.
A trailing stop takes side "sell" or "buy". A sell trails the highest price since arming and
fires on a fall; a buy trails the lowest and fires on a rise. A buy requires limitPrice, its maximum
price: the most the child pays per unit, which also sizes the funds set aside for it. It must be at
least the activation price. A sell refuses limitPrice. orderType, tif, and postOnly do not
alter this strategy.
A trailing stop's MaxSlippageBps is 1 to 9,999 on either side, so the SDK rejects 10,000 bps (100%)
with *errors.ValidationError on create and on Modify. A max slippage in ticks at or above the price
at which a selling 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, and a
modify that would make it act at once is refused with ERROR_CODE_TRIGGER_WOULD_TRIGGER_IMMEDIATELY,
leaving the existing state unchanged. To start trailing from the market, clear the activation price
with a present zero (models.PriceFromTicksInt(0)).
Returns models.TriggerMutationResult (TriggerID, Status). Create synthesizes Status: "accepted" (admission only). Pause / resume / cancel / modify return lifecycle
labels such as armed, paused, cancelled.
clientTriggerID is optional, and the SDK generates one when it is nil. Supply and persist a stable
value when a create may be retried or reconciled after an unknown outcome.
Strategy capability matrix
| Strategy | Side | Child execution and required fields |
|---|---|---|
stop_loss / take_profit | buy or sell | Market or limit child; triggerPrice |
trailing_stop | buy or sell | Trailing distance in ticks or bps; limitPrice on a buy |
twap | buy or sell | Sliced market; TWAP duration and slice interval |
ladder | buy or sell | Multi-level limit; ladder min, max, levels, and distribution |
Ladder entry quantity is not guaranteed to equal the sum of child limit sizes after step / fee rounding. 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.
The SDK exposes Pause/Resume/Cancel/Modify for any trigger ID. Strategy-specific restrictions are enforced by the API.
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 and attached legs. 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.
Public IDs
TriggerID values returned by Create, List, and subscriptions are base58 encodings of the underlying uint64 (not bare decimal). Pass them back as strings to Get / Pause / Resume / Cancel / Modify / ListEvents. Use codecs.IDToInt / codecs.FormatID when you need the integer form. See Public IDs.
Canonical base58 can itself contain only digits. IDToInt first recognizes canonical base58
round-trips before treating an all-digit string as legacy decimal; do not parse returned IDs with strconv.ParseUint.
Manage
trigger, err := client.Triggers.Get(ctx, nil, result.TriggerID, nil)
var pageToken *string
for {
listed, err := client.Triggers.List(ctx, nil, nil, &symbol, []string{"created", "armed"}, 50, pageToken)
if err != nil { log.Fatal(err) }
// Unknown status labels error, they do not silently return empty.
for _, t := range listed.Triggers {
fmt.Println(t.Status, t.TriggerID)
}
if listed.NextPageToken == "" {
break
}
tok := listed.NextPageToken
pageToken = &tok
}
newTrig := models.PriceFromDecimal("59500")
_, err = client.Triggers.Modify(ctx, nil, result.TriggerID, nil, nil, nil, codecs.ModifyTriggerOptions{
TriggerPrice: &newTrig,
})
_, err = client.Triggers.Pause(ctx, nil, result.TriggerID, nil)
_, err = client.Triggers.Resume(ctx, nil, result.TriggerID, nil, nil, nil)
_, err = client.Triggers.Cancel(ctx, nil, result.TriggerID, nil)Modify and Resume take an optional symbol and symbolID after subAccountID: pass nil for both
to target the trigger's own market. An API key limited to specific markets must still pass one. A
child's limit price is fixed at create. Omit ActivationPrice or MaxSlippage* to preserve the
current values, or send an explicit zero to clear them. When you pass symbol or symbolID, Modify refuses a TriggerPrice or ActivationPrice whose ticks are bound to another market (for
example a price read from another pair's book) with *errors.ValidationError before sending; a
bound price for a symbol ID the catalogs do not know is refused too.
Get and List retry a brief UNAVAILABLE, so a read right after Modify, Pause, or Resume never returns the previous state.
Status vocabulary: created, armed, running, completed, cancelled, failed, paused.
ListEvents
fired := "fired" // optional event type filter
var eventPage *string
for {
events, err := client.Triggers.ListEvents(ctx, nil, result.TriggerID, nil, 20, &fired, eventPage)
if err != nil { log.Fatal(err) }
for _, ev := range events.Events {
fmt.Println(ev.TriggerID, ev.EventType, ev.ChildOrderID, ev.FirePrice, ev.TsNs)
}
if events.NextPageToken == "" {
break
}
tok := events.NextPageToken
eventPage = &tok
}Child orders are not on the trigger snapshot. Filter ListEvents with eventType="fired" and
read ChildOrderID / ChildSeq from each event.
Decoded TriggerEvent exposes TriggerID, SubaccountID, SymbolID, TriggerType, EventType, TsNs, ChildSeq, ChildOrderID, FirePrice, CancelReason, FailureReason, and FireRefusalReason.
Event types are fired, canceled, updated, failed, activated, armed, and fire_refused. fire_refused is a fire whose order was refused while the trigger stayed live; it is not terminal, so
read Trigger.Status rather than inferring it from the last event. FireRefusalReason says why.
Subscribe
sub, err := client.Triggers.Subscribe(ctx, accountID)
defer sub.Close()
for t := range sub.Messages() {
fmt.Println(t.Status, t.TriggerID)
break
}
evSub, err := client.Triggers.SubscribeEvents(ctx, accountID)
defer evSub.Close()
for ev := range evSub.Messages() {
fmt.Println(ev.EventType, ev.TsNs)
break
}Trigger shape
Includes TriggerID, Symbol, TriggerType, Status, child-order fields (Side, OrderType, TimeInForce, Qty, LimitPrice, PostOnly, …), timestamps, and Details (Case = stop | trailing | twap | ladder).
For TWAP, Details.ExecutedQty populates as slices fire. Right after create it is typically
unset until the first child order is placed. Child-order history comes from ListEvents.