client.orders is the spot order surface: create, modify, cancel, list, batch, and stream your
orders. Every method is authenticated and account-scoped. Pass optional account ("main", "active", or {"sub_account_id": "..."}) or sub_account_id when you need a scope other than the
client default. See Accounts & balances.
Prices and quantities should be decimal strings (or Price / Quantity). Excess precision relative
to the catalog should fail validation rather than silently round. Create/modify/batch paths
auto-await catalog hydration when enabled; you can still call await client.wait_for_catalogs() before other decimal helpers.
post_only=True is valid only for limit GTC and limit GTD orders. Market, limit IOC, and limit FOK
raise PolyesterValidationError before the request is sent.price on a market create is rejected by the SDK. Use market_client_ref_price when you need a
reservation / slippage reference. The server may still accept a stray price if you bypass the SDK.create / batch_create synthesize status="accepted".
That is an admission ack, not a lifecycle state. Do not assert "created" on the create
response. Use list_open / get / subscribe for lifecycle statuses (pending, pending_cancel, working, filled, canceled, rejected). Spot orders spend trading balance, not funding.Methods
create: Place a limit or market order, optionally with attached risk.modify: Patch price, quantity, client id, behavior, or attached risk.cancel: Cancel one order bykey=OrderId(...)orClientOrderId(...).cancel_all: Cancel matching open orders (supportsdry_run).cancel_all_after: Arm a dead-man switch that cancels aftertimeout_sec.stop_all_trading: Cancel every live trigger, then every open order.preview_order: Check admissibility of a create without placing it.batch_create: Create many orders in one RPC.batch_replace: Same-symbol quote refresh; returns an admission receipt.get_batch_replace_status: Read recoverable execution status for one batch.wait_batch_replace: Poll that status until no item isadmitted.batch_cancel: Cancel many orders in one RPC.list_open: List currently open orders.list_history: List historical orders (paginated).get: Fetch one order by key, plus related user trades.wait_for_order_trades_complete: Pollgetuntil the order has ended and its trades add up tocum_qty.subscribe: Stream private order events (returns once confirmed).
There is no get_details name in Python, use get.
Create
Places a spot order and returns OrderMutationResult (status, order_id, client_order_id).
You can pass a CreateOrderRequest or keyword arguments.
result = await client.orders.create(
symbol="BTC-USDT",
side="buy",
order_type="limit",
tif="gtc",
qty="0.25",
price="64250.5",
post_only=True,
client_order_id="mm-bot-001",
)
print(result.status, result.order_id, result.client_order_id) # status == "accepted"Create fields
| Field | Notes |
|---|---|
symbol | Required, e.g. "BTC-USDT". |
side | Required: "buy" or "sell". |
order_type | Required: "limit" or "market". |
tif | "gtc" (default), "ioc", "fok", or "gtd". |
expires_at | Required with tif="gtd": RFC3339 UTC, 1 second to 30 days ahead. |
qty | Base quantity. Exactly one of qty or max_quote_debit. |
max_quote_debit | Hard all-in quote budget for a buy market or limit IOC order. |
price | Limit price: decimal string or Price. |
post_only | Default False. Limit GTC or GTD only. |
fee_asset | "quote" (default) or "base" (buy only). |
self_trade_prevention_mode | "expire_taker", "expire_maker", or "expire_both". |
client_order_id | 1 to 36 characters; account-scoped duplicate guard. See below. |
sub_account_id / account | Account override. Pass one, not both. |
attached_risk | Take-profit, stop-loss, or trailing stop. See Attached risk. |
market_client_ref_price | Market only: reservation reference price. |
max_slippage_ticks / max_slippage_bps | Market only, at most one. Bps is 1 to 10000 to buy, 1 to 9999 to sell. |
Omit self_trade_prevention_mode for the server default. The order's attached take-profit, stop-loss,
and trailing stop exits use the same mode; they have none of their own. Order.self_trade_prevention_mode reads it back.
The SDK does not generate client_order_id. Omission is valid, but production callers should
provide and persist a stable value whenever a create might need reconciliation. 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.
fee_asset replaces the prior fee_source / "received" vocabulary. Use "quote" or "base";
SELL orders require "quote". A create response can include the resolved base quantity and, for
quote-budget sizing, the submitted maximum quote debit. Call preview_order with the same create
shape for an admissibility check (admissible, optional typed rejection, resolved base size,
and protected_price_bound 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(field_path,rule_id,message).resolved_base_qty_scaledand typedresolved_base_qtywhen the service resolved a base size.protected_price_bound: optional protective execution boundary, not an expected fill price.evaluated_at_ms: evaluation time in Unix milliseconds.
Modify
Patches an open order. Identify it with key=OrderId(...) or key=ClientOrderId(...). Supply at
least one of new_price, new_qty, or new_attached_risk.
from polyester.models import OrderId
await client.orders.modify(
symbol="BTC-USDT",
key=OrderId(result.order_id),
new_price="64100",
new_qty="0.2",
request_id="mod-stable-1", # reuse on retry
)Optional: behavior ("amend_or_replace", "amend_only", "replace_only"), new_client_order_id, new_attached_risk, request_id. If you omit request_id, the SDK
generates one for that single call; supply a stable value when retrying the same logical modify.
See Requests & idempotency. Returns ModifyOrderResult (action_taken, old_order_id, final_order_id, code).
Modify parameters
key: Required:OrderId(...)orClientOrderId(...).symbol: Required: Pair symbol used for routing and scale.account: Uses the configured account when omitted.sub_account_id: Explicit scope override.request_id: SDK generates one when absent; provide and reuse one for retries.new_price: Exact replacement price.*new_qty: Exact replacement quantity.*new_attached_risk: Replacement risk: dict orAttachedRisk.*behavior:"amend_or_replace","amend_only", or"replace_only".new_client_order_id: Replacement identity, locally validated.
* At least one of new_price, new_qty, or new_attached_risk is required.
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
from polyester.models import ClientOrderId, OrderId
await client.orders.cancel(key=OrderId(result.order_id))
await client.orders.cancel(key=ClientOrderId("mm-bot-001"), symbol="BTC-USDT")Optional symbol / symbol_id speeds routing. Omit both to send zero/unspecified routing. If
supplied, pass exactly one; an unknown symbol is rejected locally rather than silently becoming
zero. Returns OrderMutationResult.
Cancellation acknowledges admission. Confirm the order has disappeared from list_open 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, a repeat of the cancel that ended it included, raises PolyesterApiError with code ERROR_CODE_ORDER_ALREADY_TERMINAL; is_order_already_terminal(err) tells it apart. Treat it as already done and read the order for how it ended. A client order ID
names its order for at least a minute after the order ends. A later cancel by client order ID may
read ERROR_CODE_ORDER_UNKNOWN, so retry by order ID once you have it. Every cancel, a repeat
included, counts against the cancel rate limit.
For process-owned cleanup, select owned client IDs and cancel them individually. Treat ERROR_CODE_ORDER_ALREADY_TERMINAL and ERROR_CODE_ORDER_UNKNOWN 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 cancel_all as an ownership
filter.
key is required. A value that is not an OrderId or ClientOrderId raises PolyesterValidationError.
Cancel all
preview = await client.orders.cancel_all(symbol="BTC-USDT", dry_run=True)
print(preview.matched_orders)
await client.orders.cancel_all(symbol="BTC-USDT", side="buy", request_id="ca-1")If you omit request_id, the SDK generates one for that single call; supply a stable value when
retrying the same logical bulk cancel. Returns CancelAllOrdersResult (status, matched_orders, submitted_cancels, failed_cancels, cancelled_orders, total_cancelled, truncated, listing_status).
cancelled_orders lists up to 200 orders, oldest first; truncated is true when the list is shorter
than total_cancelled. Each order's final state comes from the order stream and reads. A dry run
lists none. A retry with the same request_id within 120 seconds returns the original counts with
an empty list and listing_status="not_retained_read_open_orders", which does not mean nothing was
cancelled; read open orders instead.
cancel_all accepts only backend statuses submitted / dry_run; cancel_all_after accepts armed / disabled. Empty or unknown statuses raise PolyesterResponseContractError rather than
returning an ambiguous success result.
cancel_all and cancel_all_after cancel open orders only, never triggers: an armed stop still
fires later. To stop all automated trading, stop_all_trading() lists the live triggers, cancels
each, and only then cancels all orders; symbol= limits it to one market. Check trigger_failures on the result, because such a trigger may still be live.
Cancel all after
armed = await client.orders.cancel_all_after(timeout_sec=15, symbol="BTC-USDT")
print(armed.status, armed.effective_timeout_sec, armed.expires_at_ts_ns)Use timeout_sec=0 to disable or 10 to 120 to arm. The API enforces this range; the Python SDK
currently passes it through without local preflight.
armed.effective_scope is set after an arm or refresh. It reports the recorded account or Subaccount (account_id), symbol_id, and side that expiry will cancel. A symbol_id of 0 means all symbols. An empty side means both sides. A disabled switch has no effective_scope.
The scope covers working orders, including working child orders. Waiting triggers remain active and may create further orders. A cancellation that already began at expiry is unaffected by a later refresh or disable.
Batch helpers
batch = await client.orders.batch_create(
items=[
{
"symbol": "BTC-USDT",
"side": "buy",
"order_type": "limit",
"tif": "gtc",
"qty": "0.01",
"price": "64000",
"client_order_id": "mm-a",
"post_only": True,
},
{
"symbol": "BTC-USDT",
"side": "sell",
"order_type": "limit",
"tif": "gtc",
"qty": "0.01",
"price": "65000",
"client_order_id": "mm-b",
"post_only": True,
},
],
)
# allow_partial is retained for compatibility but ignored on the current wire.
print(batch.accepted_count, batch.rejected_count)
from polyester.models import ClientOrderId
await client.orders.batch_cancel(
items=[{"key": ClientOrderId("mm-a")}, {"key": ClientOrderId("mm-b")}],
request_id="bc-1",
)
receipt = await client.orders.batch_replace(
symbol="BTC-USDT",
items=[
{"key": ClientOrderId("mm-a"), "new_price": "63900"},
{"key": ClientOrderId("mm-b"), "new_price": "66100"},
],
request_id="br-1",
)
print(receipt.batch_request_id, receipt.status, receipt.accepted_count)
status = await client.orders.wait_batch_replace(
batch_request_id=receipt.batch_request_id,
)
for item in status.items:
if item.phase == "rejected":
print(item.old_order_id, "unchanged:", item.error_code)batch_replace is a same-symbol quote refresh only (no per-item behavior, no behavior_default, no allow_partial). 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.
action_takenis"amended": the order keeps its order ID and queue priority, andreplacement_order_idis empty. A new total quantity at or below the filled quantity ends the order at its filled quantity. - A new price, a larger total quantity or a new client order ID replaces the order.
action_takenis"replaced": a successor takes a new order ID, with its queue priority reset.
The write RPC returns a durable admission receipt (batch_request_id, accepted/rejected counts),
not a final execution outcome. An admitted item can still be refused when it is applied, for example
a post-only price that would now cross: its status then reads rejected with error_code (and raw code), the order keeps its previous price and quantity, and no successor exists. So the receipt's replacement_order_id is a planned ID, not proof of an order. Once a replaced item is working,
its predecessor order and client IDs are stale: switch to its replacement_order_id and the new
client order ID. A predecessor get may return not_found / ORDER_UNKNOWN; that is expected. For
an amended item, keep tracking old_order_id.
wait_batch_replace(batch_request_id=...) polls get_batch_replace_status with a short backoff
until no item is admitted, through the phases admitted, working, rejected, and terminal.
The status appears a few seconds after admission, so the wait polls through not_found, unavailable, and other server or network errors; a rate limit or any other error is raised. The
server keeps a batch's status for 60 seconds from acceptance, so the wait stops there (or at timeout=) with PolyesterBatchReplaceTimeoutError, whose last_status is the last status read.
Reconcile the orders themselves with order reads in that case.
For quote-refresh bots, is_batch_replace_settled(status) means every item is working, rejected, or terminal. It is a reconciliation checkpoint, not a final execution outcome: working means the successor, or the amended order, is live. Reuse the same request_id for an
ambiguous retry and
never submit another replacement against a stale predecessor.
Batch size: the Python SDK accepts at most 20 items in batch_create and at most 50 in batch_replace and batch_cancel, and raises PolyesterValidationError above that before sending.
Split larger sets into several calls. polyester.codecs.orders.BATCH_MAX_ITEMS holds each limit,
keyed by operation name ("batch_create", "batch_replace", "batch_cancel"). allow_partial on batch_create is accepted but is not encoded. Batch items carry no account scope of their own:
pass sub_account_id once for the batch, as an item that names one raises PolyesterValidationError.
A dictionary item takes only the fields its operation supports, and any other field raises PolyesterValidationError naming it before anything is sent, so a misspelt field is never dropped:
batch_replaceitemkey,new_price,new_qty,new_client_order_id, andnew_attached_risk(or aBatchReplaceItemwith the same fields).batch_cancelitemkeyand an optional integersymbol_idfor routing; asymbolname is refused.
new_attached_risk on a replace item takes the same dictionary or AttachedRisk as modify. An
item may give new_attached_risk alone, without a new price or quantity.
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 raises PolyesterResponseContractError instead of
returning an ambiguous result. Unknown rejection enums remain visible as unknown_error_code(<number>).
Read orders
open_orders = await client.orders.list_open(include_attached_risk=True)
for order in open_orders.orders:
print(order.order_id, order.status, order.leaves_qty)
page_token = None
while True:
page = await client.orders.list_history(
symbol="BTC-USDT",
limit=100,
page_token=page_token,
)
for order in page.orders:
print(order.order_id, order.status)
page_token = page.next_page_token or None
if not page_token:
break
from polyester.models import ClientOrderId, OrderId
details = await client.orders.get(key=OrderId("order-id-from-create"), include_attached_risk=True)
if details.order:
print(details.order.status, len(details.trades))
by_client_id = await client.orders.get(key=ClientOrderId("mm-bot-001"))list_history can filter by symbol or symbol_id. get takes key=OrderId(...) or key=ClientOrderId(...).
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, list_open, 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 cum_qty before every fill is visible on the trades list. Prefer orders.wait_for_order_trades_complete (also exported as polyester.wait_for_order_trades_complete)
after fills when you need trade rows to match cum_qty. It completes only once the order has ended
(filled, canceled, or rejected) and its trades add up to cum_qty, so call it when the order
is expected to end: an IOC or FOK order, or after a cancel. A working order keeps it polling until
the timeout. To check the trades recorded so far on a working order, read it with get and compare
its trades to cum_qty.
from polyester.models import OrderId
details = await client.orders.wait_for_order_trades_complete(
key=OrderId("order-id-from-create"),
timeout=15.0,
)
print(details.order.cum_qty if details.order else None, len(details.trades))timeout (seconds, default 10) bounds the whole wait, every page read and the sleeps between polls
included; when it passes, the wait raises PolyesterTransportError. Pass a timeout long enough to
read every execution page. A next_page_token that repeats a page token of the same read raises PolyesterResponseContractError; handle it like any other failed read.
Market BUY quantities may be normalized by the venue (for example 0.04 → 0.03997). For
cleanup, use the completed trade projection and convert fee_amount_e18 to the symbol's base
quantity scale for BUY fills whose fee_asset is "base"; subtract when fee_is_rebate is
false and add when it is true. Sell that net received base quantity, not the requested decimal
or gross cum_qty.
Subscribe
Streams an account's orders over the private WebSocket. It returns once the server has confirmed the
subscription, and needs an Account ID (account_id= or client default_account_id). It yields
private-stream events, not bare orders:
PrivateOrdersConnected: Subscription confirmed; again after each reconnect.PrivateOrdersSnapshot: The full live view inorders: replace local state.PrivateOrdersUnit:orders,removed_order_ids, andeventsof one change.PrivateOrdersReady: The stream reflects every order read made before the subscribe.PrivateOrdersStale: The live view is behind (stale=True) or caught up.PrivateOrdersDisconnected: The connection dropped; the stream resumes on its own.PrivateOrdersError: A failure the stream recovers from (kind,error).
from polyester.models import (
PrivateOrdersReady,
PrivateOrdersSnapshot,
PrivateOrdersUnit,
)
orders = {order.order_id: order for order in (await client.orders.list_open()).orders}
sub = await client.orders.subscribe(account_id=account_id)
async with sub:
async for event in sub:
if isinstance(event, PrivateOrdersSnapshot):
orders = {order.order_id: order for order in event.orders}
elif isinstance(event, PrivateOrdersUnit):
for order in event.orders:
held = orders.get(order.order_id)
if held is None or order.version > held.version:
orders[order.order_id] = order
for order_id in event.removed_order_ids:
orders.pop(order_id, None) # left the live view; not a cancel
for change in event.events:
print(change) # PrivateOrderEvent or PrivateOrderCommandOutcome
elif isinstance(event, PrivateOrdersReady):
print("stream caught up with earlier reads")Orders in events are the same Order model the reads return. A higher version is newer, and equal
versions are identical. To merge with a read: read list_open(), subscribe, wait for PrivateOrdersReady, and keep the higher version of each order.
A PrivateOrderEvent has kind (accepted, filled, canceled, rejected, replaced, or attached_risk), order_id, version, reject_reason, cancel_reason, replacement_order_id,
and fill. A PrivateOrderCommandOutcome reports a command that changed no order, with kind and reason.
A sequence gap, an expired token, or a dropped connection resumes from the last applied position.
Outcomes between the two states are not replayed; fills stay readable from trade history. A failure
that ends the stream (a refused token, a revoked permission, close code 4400 or 4403) is raised from
iteration, with PolyesterRealtimeError.code or .close_code set. Batch-replace progress has no
stream: use wait_batch_replace and watch each order's revisions here.
See Realtime for overflow and delivery rules.
Stream order quantities may omit their scale (scale is None). Resolve scale from the hydrated
catalog with client.catalogs.base_quantity_scale_for_symbol_id(order.symbol_id) before formatting orig_qty, cum_qty, or leaves_qty. Never invent a stream scale; fail closed when the lookup
fails.
Identifier constraints
client_order_id and new_client_order_id accept 1 to 36 characters. Allowed characters are
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.
Attached risk
Pass attached_risk on create (and new_attached_risk on modify) as a dict or AttachedRisk.
Legs use the same friendly keys as the read model (trigger_price, trigger_price_source, order_type, limit_price):
await client.orders.create(
symbol="BTC-USDT",
side="buy",
order_type="limit",
tif="gtc",
price="64000",
qty="0.1",
attached_risk={
"take_profit": {"trigger_price": "70000", "order_type": "market"},
"stop_loss": {"trigger_price": "60000", "order_type": "market"},
"oco": True,
},
)Child execution is market (default) or limit, with limit_price for a limit child. Legs watch
the last trade price unless trigger_price_source names another: "last" or "index". Spot markets
have no mark price, so "mark" raises PolyesterValidationError.
Attached trailing_stop requires a positive trailing_distance_ticks or trailing_distance_bps (exactly one). Optional max_slippage_ticks must be positive when set; max_slippage_bps is 1 to 9999. order_type under trailing_stop is rejected: the child is always market. A trailing stop
attached to an order that sells buys back, so it requires limit_price, the most its exit pays per
unit, at least activation_price when that is set. A trailing stop attached to an order that buys
takes none. On decode, a trailing leg with a missing or non-positive distance is omitted (no
zero-distance fabrication).
A buy sized by a quote amount (max_quote_debit) takes attached_risk too. Its legs arm as its
fills settle and follow each later fill.
For standalone automations not tied to a parent order, use triggers.
The Order shape
# msgspec struct fields (selected)
order.order_id # str
order.symbol_id # int
order.client_order_id # str
order.side # "buy" | "sell" | ...
order.status # pending / pending_cancel / working / filled / canceled / rejected
order.version # higher is newer
order.order_type
order.tif
order.orig_qty # Quantity | None
order.cum_qty
order.leaves_qty
order.price # Price | None
order.avg_price # Decimal | None, exact average; prefer it to avg_px
order.avg_px
order.reject_reason # why a rejected order was refused, e.g. "POST_ONLY_CROSS"
order.cancel_reason # why a canceled order ended, e.g. "USER_REQUEST"
order.created_ts_ns # str
order.terminal_ts_ns # str, Unix ns when the order ended; "" while open
order.post_only
order.attached_risk # AttachedRisk | None when requested
order.origin # OrderOrigin | None, how the order was created
order.market_client_ref_price # Price | None, MARKET slippage reference
order.market_max_slippage # Price | None, MARKET slippage cap as a price distance
order.market_max_slippage_bps # int, MARKET slippage cap in bps; 0 when noneList responses return { orders, next_page_token }.
These fields are on every Order that get, list_open, list_history, wait_for_order_trades_complete, and subscribe return:
terminal_ts_ns
When the order became terminal, in Unix nanoseconds as a decimal string; empty while it is open.origin
AnOrderOrigin, orNonewhen the server sends none.scopeisdirect,attached_risk,standalone_trigger, orsystem. For a child of a trigger or attached-risk leg,trigger_typenames the strategy,trigger_idthe trigger,parent_order_idthe parent order, andchild_seqthe child's sequence number; each is empty or 0 when it does not apply.market_client_ref_price,market_max_slippage,market_max_slippage_bps
A MARKET order's slippage protection: the reference price it was measured from and its cap, as a price distance or in basis points.Noneor 0 when the order has none of that form.