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 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, nil, &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, a hard all-in quote
budget for BUY market and limit IOC orders. 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, &result.OrderID, nil, nil, &reqID, &newPrice, nil, nil, nil)
preview, err := client.Orders.CancelAll(ctx, nil, nil, &symbol, nil, true, nil)
// Final bool is allowPartial, retained for compatibility and ignored on the wire.
// Inspect per-item Accepted/Rejected on the result.
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 contracts: BatchCreate max 20; BatchReplace / BatchCancel max 50.
Use BatchReplace for same-symbol quote refresh and poll GetBatchReplaceStatus with the
admission BatchRequestID (retry briefly on 404 / not-found). Admission makes predecessor order
and client IDs stale: immediately use each 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 is live. Reuse the same requestID for an ambiguous retry and never replace against a
stale predecessor. These are API-side contracts; the SDK does not preflight every count. A batch
timeout is not proof of no commit; reconcile before retry.
After fills, prefer WaitForOrderTradesComplete because CumQty can lead trade projection.
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.
Stream orders
sub, err := client.Orders.Subscribe(ctx, accountID)
defer sub.Close()
for order := range sub.Messages() {
fmt.Println(order.Status)
break
}Triggers
// "last" is the triggerPriceSource arg, accepted for compat, ignored (not on wire).
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.
_, err = client.Triggers.Modify(ctx, nil, created.TriggerID, 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.