Use an API-key client (polyester.FromEnv or polyester.New). Orders spend trading balance.
if err := client.WaitForCatalogs(ctx); err != nil { log.Fatal(err) }Place and cancel
symbol := "BTC-USDT"
tif := "gtc"
clientOrderID := "mm-bot-001"
price := models.PriceFromDecimal("64250.5")
result, err := client.Orders.Create(ctx, models.CreateOrderRequest{
Symbol: &symbol,
Side: "buy",
OrderType: "limit",
TIF: &tif,
Qty: models.QtyFromDecimal("0.25"),
Price: &price,
PostOnly: true, // limit GTC or GTD only
ClientOrderID: &clientOrderID,
}, nil)
// Create synthesizes Status: "accepted" (admission ack). Lifecycle states come
// from ListOpen / Get / Subscribe, not Create.
fmt.Println(result.Status, result.OrderID)
_, err = client.Orders.Cancel(ctx, nil, models.OrderKeyByClientID(clientOrderID), &symbol, nil, nil)Client order IDs accept 1 to 36 ASCII letters, digits, ., _, :, /, and -. Request IDs
use the same character set and accept 1 to 64 characters.
Size and preview deliberately
Create with exactly one sizing mode: base Qty, or MaxQuoteDebitScaled (models.QtyFromQuoteDecimal), a hard all-in quote budget for BUY market and limit IOC orders. To
size a buy limit order that pays its fee in the quote Asset, use the reserve rates in Fees. FeeAsset is "quote" (default) or "base" (BUY-only), replacing fee-source / received. Create responses can include ResolvedBaseQtyScaled and SubmittedMaxQuoteDebitScaled. Use client.Orders.Preview(ctx, request, account) for an
admissibility check: whether the intent is currently admissible, any typed rejection, resolved
base size, and a protected price bound when price protection applied. Preview does not return fee
or quote-debit estimates. Create always re-evaluates the intent.
Modify, cancel-all, batch
newPrice := models.PriceFromDecimal("64100")
reqID := "mod-1"
_, err = client.Orders.Modify(ctx, nil, symbol, models.OrderKeyByID(result.OrderID), nil, &reqID, &newPrice, nil, nil, nil)
// dryRun true previews what would be canceled.
preview, err := client.Orders.CancelAll(ctx, nil, nil, &symbol, nil, true, nil)
items := []models.CreateOrderRequest{ /* up to 20 orders */ }
// Final bool is allowPartial, which is not sent. Inspect per-item results.
batch, err := client.Orders.BatchCreate(ctx, nil, items, nil, &symbol, nil, false)
_, err = client.Orders.CancelAllAfter(ctx, nil, 15, nil, &symbol, nil, nil)
_ = preview
_ = batchBatch size: this SDK sends at most 20 items in a BatchCreate call and at most 50 in a BatchCancel or BatchReplace call (codecs.MaxBatchCreateItems, codecs.MaxBatchCancelItems, codecs.MaxBatchReplaceItems), and rejects larger calls with *errors.ValidationError before
sending.
Use BatchReplace for same-symbol quote refresh, then call WaitBatchReplace with the admission BatchRequestID to poll GetBatchReplaceStatus until every item settles. Each item amends its order in
place when it keeps the price, does not raise the total quantity and keeps the client order ID
(ActionTaken is amended: same order ID, queue priority kept, keep using OldOrderID).
Otherwise it replaces the order (ActionTaken is replaced), and admission makes the predecessor
order and client IDs stale: immediately use the item's ReplacementOrderID and new client order
ID from the receipt. A predecessor Get returning not_found / ORDER_UNKNOWN is expected. Poll
phases admitted, working, rejected, and terminal. For quote-refresh bots, models.IsBatchReplaceSettled(status) or models.BatchReplaceStatusSettled(status) treats working, rejected, and terminal as reconciled, not execution-final; working means the
successor, or the amended order, is live. Reuse the same requestID for an ambiguous retry and never replace against a
stale predecessor. A batch timeout is not proof of no commit; reconcile before retry.
After fills, prefer WaitForOrderTradesComplete because CumQty can lead trade projection. It
completes only once the order has ended (filled, canceled or rejected) and its trades add up to CumQty, so call it for an IOC or FOK order or after a cancel, not on an order still working.
Cancellation is an admission acknowledgement. Confirm the order has disappeared from ListOpen before releasing local state and retry the same cancel if reconciliation still shows it.
Modify and replace operations require enough available balance for the replacement order. Leave headroom when most of the trading balance is reserved, and reconcile the original order after an ambiguous response before deciding whether to retry or cancel and recreate it.
For long-running automated trading, renew CancelAllAfter continuously:
- Arm it only after startup reconciliation has confirmed open orders.
- Refresh well before
EffectiveTimeoutSec(for example every 5 seconds on a 15-second timer). - Give each deliberate refresh a new
requestID, but reuse that ID when retrying the same ambiguous refresh. - Verify
Status,EffectiveTimeoutSec, andExpiresAtTsNson every response. - Stop quoting and reconcile if a refresh fails or its deadline is uncertain.
The timer is a last-resort venue control, not a replacement for explicit shutdown cancellation.
CancelAll and the dead-man switch cancel open orders, never triggers. To stop all automated
trading in scope, call client.StopAllTrading(ctx, nil, services.StopAllTradingScope{}): it cancels
every live trigger, then all open orders.
Stream orders
import "github.com/Fabric-Labs/polyester-sdk-go/realtime"
sub, err := client.Orders.Subscribe(ctx, accountID)
if err != nil { log.Fatal(err) }
defer sub.Close()
for event := range sub.Messages() {
switch e := event.(type) {
case realtime.OrderSnapshot:
fmt.Println("live orders", len(e.Orders))
case realtime.OrderUnit:
for _, order := range e.Orders {
fmt.Println(order.Status, order.OrderID)
}
case realtime.OrderStreamError:
log.Printf("order stream error: %v", e.Err)
}
}
if err := sub.Err(); err != nil {
log.Printf("order stream ended: %v", err)
}Subscribe returns at once and reports the connection as events. See Orders.
Triggers
triggerPrice := models.PriceFromDecimal("60000")
clientTriggerID := "sl-001"
// "last" is the triggerPriceSource: the last traded price. "index" watches the index price.
created, err := client.Triggers.Create(ctx, nil, symbol, "stop_loss", &triggerPrice,
"sell", models.QtyFromDecimal("0.1"), "market", nil, "last", "ioc",
nil, &clientTriggerID, false, codecs.CreateTriggerOptions{})
// Create returns Status: "accepted" (admission). List filters still use lifecycle labels.
newTrig := models.PriceFromDecimal("59500")
_, err = client.Triggers.Modify(ctx, nil, created.TriggerID, nil, nil, nil, codecs.ModifyTriggerOptions{
TriggerPrice: &newTrig,
})
_, err = client.Triggers.Cancel(ctx, nil, created.TriggerID, nil)Status filters: created, armed, running, completed, cancelled, failed, paused.
Retry safely
Reuse ClientOrderID / requestID / clientTriggerID on transport retries. See Error handling.