client.Orders is the spot order surface. Methods take context.Context and return (result, error).
Account scope is an AccountScope argument (nil uses the client default subaccount). Import github.com/Fabric-Labs/polyester-sdk-go/models for request helpers.
Create/modify/batch paths wait for catalog hydration when enabled. You can still call client.WaitForCatalogs(ctx) before other decimal helpers.
PostOnly: true works on limit GTC and limit GTD orders. It is rejected for market, limit IOC, and
limit FOK (*errors.ValidationError).Price on a market create is rejected by the SDK. Use MarketClientRefPrice when you need a
reservation / slippage reference.Create / BatchCreate synthesize Status: "accepted".
That is an admission ack, not a lifecycle state. Do not assert "created" on the create
response. Use ListOpen / Get / Subscribe for working /
partial / terminal statuses. Spot orders spend trading balance, not funding.Methods
| Method | Summary |
|---|---|
Create | Place a limit or market order. |
Preview | Check whether an order is admissible. |
Modify / ModifyWith | Patch price, quantity, client ID, or attached risk. |
Cancel | Cancel by order ID or client order ID. |
CancelAll / CancelAllWith | Cancel matching open orders (dryRun). |
CancelAllAfter | Dead-man switch. |
BatchCreate / BatchCancel | Batch mutations. |
BatchReplace / WaitBatchReplace | Batch quote refresh, and wait for it to settle. |
GetBatchReplaceStatus | Read one batch replace status. |
ListOpen / ListHistory | List orders. |
Get | One order plus related trades. |
Subscribe | Private order stream (Messages() channel). |
WaitForOrderTradesComplete | Poll until an ended order's trades match CumQty. |
No GetDetails; use Get. No Recv; use Messages().
An order is identified by a models.OrderKey: build one with models.OrderKeyByID(orderID) or models.OrderKeyByClientID(clientOrderID). Get, Cancel, Modify, WaitForOrderTradesComplete,
and batch items all take it. An unset key is a local validation error.
Create
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,
ClientOrderID: &clientOrderID,
}, nil)
if err != nil { log.Fatal(err) }
fmt.Println(result.Status, result.OrderID) // Status == "accepted"CreateOrderRequest fields
| Field | Contract |
|---|---|
Symbol / SymbolID | Pair name or numeric ID; one is required |
Side | "buy" or "sell" |
OrderType | "limit" or "market" |
TIF | "gtc", "ioc", "fok", or "gtd" (limit orders) |
Qty | Base quantity; exactly one of this or MaxQuoteDebitScaled |
MaxQuoteDebitScaled | All-in quote budget for BUY market or limit IOC (QtyFromQuoteDecimal) |
Price | Limit price; use PriceFromDecimal or ticks |
ExpiresAt | RFC 3339 UTC; required for "gtd", 1 second to 30 days ahead |
PostOnly | true only for limit GTC or limit GTD |
SubAccountID | Explicit subaccount override |
ClientOrderID | Account-scoped duplicate guard, 1 to 36 allowed characters |
FeeAsset | "quote" (default) or BUY-only "base" |
SelfTradePreventionMode | "expire_maker" (server default), "expire_taker", or "expire_both" |
AttachedRisk | Take-profit, stop-loss, or trailing-stop legs; see below |
MarketClientRefPrice | Reference price used for market reservation |
MaxSlippageBps / MaxSlippageTicks | Market only, at most one; bps 1 to 10,000 on a buy, 1 to 9,999 on a sell |
A decimal quantity is scaled with the pair's base quantity scale from the catalog. On a pair whose
scale is 0 it is sent as whole units ("5"), and a fractional quantity there is refused. A scaled Qty, newQty or MaxQuoteDebitScaled above 9223372036854775807 (9.223372036854775807 at
scale 18) is refused with *errors.ValidationError before anything is sent.
ClientOrderID and NewClientOrderID accept 1 to 36 ASCII letters, digits, ., _, :, /,
and -. Invalid values are rejected locally before send, including singular lookup and cancel by
client-order-id. A create request may omit the ID. Request IDs use the same character set and
accept 1 to 64 characters.
ClientOrderID is optional and the SDK does not generate one. A stable ID is strongly recommended
for every create that may need reconciliation after an unknown outcome. Reuse of a retained ID,
including an identical request, returns CONFLICT_DUPLICATE_CLIENT_ORDER_ID; it does not replay the
earlier result. Reconcile by client order ID before deciding whether to resubmit.
SelfTradePreventionMode also applies to the order's attached take-profit, stop-loss, and trailing
stop orders. Order.SelfTradePreventionMode reads it back. See Self-trade prevention.
FeeAsset replaces the prior fee-source / received vocabulary. Use "quote" or "base"; SELL
orders require "quote". A create response can include ResolvedBaseQtyScaled and, for
quote-budget sizing, SubmittedMaxQuoteDebitScaled. Call client.Orders.Preview(ctx, request, account) with the same request shape for an admissibility
check (Admissible, optional typed Rejection, resolved base size, and ProtectedPriceBound when price protection applied). Preview does not return fee or quote-debit estimates. Create
always re-evaluates the intent.
PreviewOrderResult exposes:
Admissible: whether the intent passed current admission checks.Rejection: optionalOrderErrorDetailwith a stableCodelabel such asBAD_QTYand field-levelViolations(FieldPath,RuleID,Message).ResolvedBaseQtyScaledand typedResolvedBaseQtywhen the service resolved a base size.ProtectedPriceBound: optional protective execution boundary, not an expected fill price.EvaluatedAtMs: evaluation time in Unix milliseconds.
Attached risk
Set AttachedRisk to a models.AttachedRiskInput with a take-profit and/or one stop leg (stop-loss
or trailing stop). OCO: true requires a take-profit and a stop leg. A leg without LimitPrice exits with a market child; with LimitPrice it rests a GTC limit child.
oco := &models.AttachedRiskInput{
TakeProfit: &models.RiskLegInput{TriggerPrice: models.PriceFromDecimal("72000")},
StopLoss: &models.RiskLegInput{TriggerPrice: models.PriceFromDecimal("58000")},
OCO: true,
}
symbol := "BTC-USDT"
tif := "gtc"
price := models.PriceFromDecimal("64250.5")
created, err := client.Orders.Create(ctx, models.CreateOrderRequest{
Symbol: &symbol,
Side: "buy",
OrderType: "limit",
TIF: &tif,
Qty: models.QtyFromDecimal("0.001"),
Price: &price,
AttachedRisk: oco,
}, nil)
if err != nil { log.Fatal(err) }
fmt.Println(created.TakeProfitTriggerID, created.StopLossTriggerID)A trailing stop on a sell order buys back, so TrailingStopInput.LimitPrice, the most its exit pays
per unit, is required there; a buy order's trailing stop takes none. Each leg watches the last traded
price by default; set TriggerPriceSource to "index" to watch the index price. Attached exits
never trade with the order they protect: when a leg fires while its order still rests, the order's
unfilled rest is canceled first (CancelReason attached_exit_fired).
Modify
newPrice := models.PriceFromDecimal("64100")
newQty := models.QtyFromDecimal("0.2")
requestID := "mod-1"
_, err = client.Orders.Modify(
ctx, nil, "BTC-USDT",
models.OrderKeyByID(result.OrderID), nil, &requestID,
&newPrice, &newQty, nil, nil,
)Use ModifyWith and services.ModifyParams to replace an order's attached risk with NewAttachedRisk; that replaces the whole attached policy. If you pass a nil requestID, the SDK
generates one for that single call; supply a stable value when retrying the same logical modify. See Requests & idempotency.
Modify parameters
ctx(required): One overall cancellation/deadline contextaccount(optional):niluses configured account scopesymbol(required): Pair symbol used for routing and quantity scalekey(required):models.OrderKeyof the existing ordersubAccountID(optional): Explicit scope overriderequestID(optional): SDK generates one when nil; provide and reuse one for retriesnewPrice/newQty(at least one): Replacement decimal/ticks or quantity inputbehavior(optional):amend_or_replace,amend_only, orreplace_onlynewClientOrderID(optional): Replacement client identity, locally validated
Leave balance headroom when repricing a heavily reserved book. A replacement requires sufficient available balance for the new order. Reconcile after an ambiguous response before retrying or canceling and recreating the order.
Cancel / CancelAll / CancelAllAfter
_, err = client.Orders.Cancel(ctx, nil, models.OrderKeyByClientID(clientOrderID), &symbol, nil, nil)
dry := true
preview, err := client.Orders.CancelAll(ctx, nil, nil, &symbol, nil, dry, nil)
fmt.Println(preview.MatchedOrders)
timeout := 15
_, err = client.Orders.CancelAllAfter(ctx, nil, timeout, nil, &symbol, nil, nil)If you pass a nil requestID on CancelAll / CancelAllAfter / batch helpers, the SDK generates
one for that single call; supply a stable value when retrying. Cancellation acknowledges
admission. Confirm the order has disappeared from ListOpen before releasing local state; retry
the same cancel if it remains visible after a bounded reconciliation window.
A cancel of an order that has already ended, including a repeat of the cancel that ended it, fails
with ERROR_CODE_ORDER_ALREADY_TERMINAL: errors.IsOrderAlreadyTerminal(err) reports it. Treat it
as already done and read the order for how it ended. A retry by client order ID more than a minute
after the order ended may read ERROR_CODE_ORDER_UNKNOWN, so cancel by order ID once you have it.
Every cancel, repeats included, counts against the cancel rate limit.
For process-owned cleanup, select owned client IDs and cancel them individually. Treat those two
codes from a targeted cancel as idempotent success because the order may have filled or left the
book after the preceding read; every other cancel error remains an error. Do not use account-wide CancelAll as an ownership filter.
CancelAll and CancelAllAfter cancel open orders, never triggers. To stop all automated trading in
scope, use client.StopAllTrading, which cancels every live trigger and then all open orders.
A symbol passed to Cancel must resolve in hydrated catalogs, and supplying both symbol and symbolID is rejected. CancelAllAfter accepts timeoutSec=0 to disable or 10 to 120 to arm; this range is enforced by
the API, not preflighted by the Go SDK.
CancelAll accepts only backend statuses submitted / dry_run; CancelAllAfter accepts armed / disabled. Empty or unknown statuses return *errors.ResponseContractError rather than an
ambiguous success result.
Batch
// Final bool is allowPartial, which is not sent.
batchID := "mm-batch-001"
batch, err := client.Orders.BatchCreate(ctx, nil, []models.CreateOrderRequest{
{
Symbol: &symbol,
Side: "buy",
OrderType: "limit",
TIF: &tif,
Qty: models.QtyFromDecimal("0.01"),
Price: &price,
PostOnly: true,
ClientOrderID: &batchID,
},
}, nil, &symbol, nil, false)
fmt.Println(batch.AcceptedCount, batch.RejectedCount)
cancelClientOrderID := "mm-a"
_, err = client.Orders.BatchCancel(ctx, nil, []models.BatchCancelItem{
{Key: models.OrderKeyByClientID(cancelClientOrderID)},
}, nil, nil)
newPrice := models.PriceFromDecimal("63900")
receipt, err := client.Orders.BatchReplace(ctx, nil, []models.BatchReplaceItem{
{Key: models.OrderKeyByClientID("mm-a"), NewPrice: &newPrice},
}, "BTC-USDT", nil, nil)
fmt.Println(receipt.BatchRequestID, receipt.Status, receipt.AcceptedCount)
status, err := client.Orders.WaitBatchReplace(ctx, nil, receipt.BatchRequestID, nil)
fmt.Println(status.AdmissionStatus, len(status.Items))BatchReplace is a same-symbol quote refresh only (no per-item behavior, no behavior default, no allowPartial). Each item follows the API's default amend-or-replace rule:
- An item that keeps the order's price, does not raise its total quantity and keeps its client
order ID amends the order in place.
ActionTakenisamended: the order keeps its order ID and queue priority, andReplacementOrderIDis empty ("", never"0"). A new total quantity at or below the filled quantity ends the order at its filled quantity. A rejected item has an emptyReplacementOrderIDtoo, andOldOrderIDis empty when the server named no order. - A new price, a larger total quantity or a new client order ID replaces the order.
ActionTakenisreplaced: a successor takes a new order ID, with its queue priority reset.
The write RPC returns a durable admission receipt (BatchRequestID, accepted / rejected counts),
not a final execution outcome. After a replaced item is admitted, its predecessor order and client
IDs are stale. Switch immediately to its ReplacementOrderID and new client order ID in the
receipt. A predecessor Get may return not_found / ORDER_UNKNOWN; that is expected. For an
amended item, keep tracking OldOrderID. Call WaitBatchReplace to reconcile admitted, working, rejected, and terminal. It polls GetBatchReplaceStatus through the brief not-found after admission until no item is admitted. The
status is kept for 60 seconds: when that window ends with an item still admitted, it returns the
last status and *errors.BatchReplaceWindowExpiredError, so reconcile with order reads then. An item
can be admitted and then refused when applied: it reads rejected, its order is unchanged, and ErrorCode names the refusal.
For quote-refresh bots, models.IsBatchReplaceSettled(status) and models.BatchReplaceStatusSettled(status) mean every item is working, rejected, or terminal. They are reconciliation checkpoints, not a final execution outcome: 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.
Batch size: this SDK sends at most 20 items in a BatchCreate call and at most 50 in a BatchCancel or BatchReplace call, and rejects larger calls with *errors.ValidationError before
sending. The limits are codecs.MaxBatchCreateItems, codecs.MaxBatchCancelItems and codecs.MaxBatchReplaceItems. allowPartial on BatchCreate is not sent.
Every successful batch-create response item is either accepted or rejected. The SDK reconciles
the aggregate counts for create, replace, and cancel batches with their per-item outcomes. A
malformed or inconsistent success response returns *errors.ResponseContractError instead of an
ambiguous result. Unknown rejection enums remain visible as UNKNOWN_ERROR_CODE(<number>).
Batch timeouts and reconciliation
A timeout on a batch mutation is an unknown outcome, not proof that nothing committed. Do not
blindly resubmit the batch with new identifiers. Give the batch a stable request ID, give every
create item a unique client order ID, and reconcile every item with Get, ListOpen, or order
history before retrying. The SDK sends each call once and does not promise server-side atomicity.
Trade projection after fills
Get can report CumQty before every fill is visible on the trades list. Prefer Orders.WaitForOrderTradesComplete after fills when you need trade rows to match CumQty:
orderID := "order-id-from-create"
got, err := client.Orders.WaitForOrderTradesComplete(
ctx, nil, models.OrderKeyByID(orderID), nil, 15*time.Second,
)
if err != nil { log.Fatal(err) }
fmt.Println(len(got.Trades), got.ExecutionsComplete)The wait completes only once the order has ended (filled, canceled or rejected) and its
trades add up to CumQty. A working order keeps it polling, so call it for an IOC or FOK order or
after a cancel; to check the trades so far on a working order, read it with Get. It reads every
execution page on each poll and returns the final read's ExecutionsComplete. The timeout (15
seconds when 0) bounds the whole wait, page reads included: when it passes, the wait returns the
last result with an error for which errors.Is(err, context.DeadlineExceeded) is true. A next_page_token that repeats an earlier page returns *errors.ResponseContractError.
Market BUY quantities may be normalized by the venue (for example 0.04 → 0.03997). For
cleanup, use the completed trade projection and convert FeeAmountE18 to the symbol's base
quantity scale for BUY fills whose FeeAsset is "base"; subtract when FeeIsRebate is false
and add when it is true. Sell that net received base quantity, not the requested decimal or
gross CumQty.
Reads
orderID := "order-id-from-create"
open, err := client.Orders.ListOpen(ctx, nil, nil, nil, nil, true, false, nil)
hist, err := client.Orders.ListHistory(ctx, nil, nil, &symbol, nil, nil, 100, false, false, nil)
got, err := client.Orders.Get(ctx, nil, models.OrderKeyByID(orderID), nil, true, false)
if got.Order != nil {
fmt.Println(got.Order.Status, len(got.Trades))
}
_ = open
_ = histThe final ListOpen and ListHistory argument is an optional trigger ID that returns only child
orders created by that trigger.
Subscribe
Subscribe streams an account's orders over the private WebSocket. It yields realtime.OrderStreamEvent values, not bare orders, so switch on the event type:
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.OrderReady:
fmt.Println("stream reflects every earlier acknowledgement")
case realtime.OrderUnit:
for _, order := range e.Orders {
scale, ok := client.Catalogs.BaseQuantityScaleForSymbolID(order.SymbolID)
if !ok {
return fmt.Errorf("stream symbol %d quantity scale is unavailable", order.SymbolID)
}
leaves, err := order.LeavesQty.WithScale(scale).Format()
if err != nil { return err }
fmt.Println(order.Status, order.OrderID, leaves)
}
case realtime.OrderStale:
log.Printf("account view stale=%v %s %v", e.Stale, e.Reason, e.OrderIDs)
case realtime.OrderStreamError:
log.Printf("order stream error: %v", e.Err)
}
}
if err := sub.Err(); err != nil {
log.Printf("subscription ended: %v", err)
}OrderStreamConnected: The server confirmed the subscription; repeats after a reconnectOrderStreamDisconnected: The subscription dropped; the stream resumes by itselfOrderSnapshot: The full live view; replace local state with itOrderReady: The stream reflects every acknowledgement before the subscribeOrderUnit: One change: complete orders, removed IDs, and causing eventsOrderStale: The live view is behind (Stale), or current againOrderStreamError: An error; the stream keeps running unlessMessages()closes
Subscribe returns at once; the first event is OrderStreamConnected. Keep the copy of an order with
the higher Version. An ID in RemovedOrderIDs left the live view; that is not a cancellation, and
ended orders stay readable from order history. Wait for OrderReady before trusting that an order
missing from the stream does not exist. When the server cannot resume, a new OrderSnapshot replaces
the state; the events in between are not delivered, and fills stay readable from trade history.
The stream ends, closing Messages() with Err() set, when the server refuses the subscription,
when the API key is refused, or with *errors.QueueOverflowError when the consumer falls behind.
Order quantities may omit scale metadata (Scale() == nil). Resolve scale from the hydrated catalog
by SymbolID before formatting OrigQty, CumQty, or LeavesQty. Never invent a stream scale; fail
closed when catalog lookup fails.
Order shape
Selected fields: OrderID, SymbolID, ClientOrderID, Side, Status, OrderType, TIF, OrigQty / CumQty / LeavesQty (QtyScaled), Price / AvgPx (PriceTicks), CreatedTsNs, Version, PostOnly, ExpireAt, SelfTradePreventionMode, RejectReason, CancelReason, AvgPriceE18, CumNotional, FeeTotal, AttachedRisk.
Order reads (Get, ListOpen, ListHistory, WaitForOrderTradesComplete, Subscribe) also
carry:
TerminalTsNs
When the order became terminal, Unix nanoseconds as a decimal string; empty while it is open.Origin*models.OrderOrigin, nil when the server sends none.Scopeisdirect,attached_risk,standalone_triggerorsystem. For a child of a trigger or attached-risk leg,TriggerType,TriggerID,ParentOrderIDandChildSeqname where it came from.MarketMaxSlippage/MarketMaxSlippageBps
A MARKET order's slippage cap, as a price distance (*models.PriceTicks) or in basis points (int32); nil or zero when it has none.MarketClientRefPrice
The*models.PriceTicksreference the MARKET slippage was measured from; nil when none was supplied.
These fields make an unkeyed models.Order{...} literal fail to compile; name the fields when you
build one yourself.
When includeAttachedRisk is set, decoded AttachedRisk.TrailingStop requires a positive
distance (ticks or bps). A trailing policy with missing/non-positive distance is omitted rather
than projected as a zero-distance stop. Reads report each leg's TriggerPriceSource as "last" or "index".