Use an API-key Client. Orders spend trading balance.
client.wait_for_catalogs().await?;Place and cancel
use polyester::models::{CreateOrderParams, CreateOrderType, CreateSide, CreateTimeInForce};
use polyester::{Price, Quantity};
let quantity_scale = client
.catalogs
.base_quantity_scale_for_symbol("BTC-USDT")
.ok_or_else(|| polyester::Error::validation("BTC-USDT quantity scale is unavailable"))?;
let mut params = CreateOrderParams::new("BTC-USDT", CreateSide::Buy, CreateOrderType::Limit);
params.quantity = Some(Quantity::from_decimal_str("0.25", quantity_scale, Some("BTC-USDT".into()), None)?);
params.price = Some(Price::from_decimal_str("64250.5", Some("BTC-USDT".into()))?);
params.time_in_force = Some(CreateTimeInForce::Gtc);
params.client_order_id = Some("mm-bot-001".into());
params.post_only = Some(true); // limit GTC or GTD only
let result = client.orders.create(params).await?;
// Create synthesizes status: "accepted" (admission ack). Lifecycle states come
// from list_open / get / subscribe, not create.
println!("{} {}", result.status, result.order_id);
client.orders.cancel_by_client_order_id("mm-bot-001", Some("BTC-USDT"), None).await?;Size and preview deliberately
Create with exactly one sizing mode: base quantity, or max_quote_debit_scaled, a hard all-in
quote budget for BUY market and limit IOC orders, built with Quantity::from_quote_decimal_str. fee_asset is FeeAsset::Quote (default) or BUY-only FeeAsset::Base. Create responses can include resolved_base_qty and submitted_max_quote_debit_scaled. Size a buy limit order that pays its fee
in the quote Asset with the reserve rate from Fees. Use client.orders.preview(PreviewOrderParams::new(...)) 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, and dead-man timer
use polyester::models::{CancelAllOpts, ModifyOrderParams, OrderKey};
use polyester::Price;
let order_id = "order-id-from-create".to_owned();
client.orders.modify(ModifyOrderParams {
symbol: "BTC-USDT".into(),
key: OrderKey::OrderId(order_id),
request_id: Some("mod-1".into()),
new_price: Some(Price::from_decimal_str("64100", Some("BTC-USDT".into()))?),
new_qty: None,
new_attached_risk: None,
behavior: None,
new_client_order_id: None,
subaccount_id: None,
}).await?;
let preview = client.orders.cancel_all_with(CancelAllOpts {
symbol: Some("BTC-USDT".into()),
dry_run: true,
request_id: Some("cancel-all-preview-1".into()),
..Default::default()
}).await?;
client.orders.cancel_all_after(
15,
Some("BTC-USDT"),
None,
Some("cancel-after-arm-1".into()),
).await?;Cancellation is an admission acknowledgement. Confirm the order has disappeared from list_open before releasing local state; if it remains visible after a bounded reconciliation window, retry
the same cancel.
Batch size contracts: the API declares batch_create max 20 and batch_replace / batch_cancel max 50. The Rust SDK accepts the same limits: at most 20 items in batch_create and 50 in batch_replace or batch_cancel. It returns Error::Validation above
that before sending, so split larger batches. Use batch_replace for same-symbol quote
refresh, then wait_batch_replace with the admission batch_request_id: it polls get_batch_replace_status through the brief not-found answers after admission until no item is admitted. Each item amends its order in place when it keeps the price, does not raise the total
quantity and keeps the client order ID (action_taken is "amended": same order ID, queue priority
kept, keep using old_order_id). Otherwise it replaces the order (action_taken is "replaced").
An admitted item can still be refused when it is applied, so the admission's planned replacement_order_id is not proof of an order: only the batch status or order reads confirm one.
A predecessor get returning ERROR_CODE_ORDER_UNKNOWN is expected. Phases are admitted, working, rejected, and terminal. For quote-refresh bots, status.is_settled() or is_batch_replace_settled(&status) treats working, rejected, and terminal as reconciled, not
execution-final; working means the successor, or the amended order, is live. Reuse the same request_id 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 wait_for_order_trades_complete because cum_qty can lead trade projection.
The helper completes only once the order has ended (filled, canceled or rejected) and its
trades add up to cum_qty; executions_complete alone is not completion, because it is also true
for a working order whose fills so far are all recorded. It applies one overall deadline to every
page read, treats a read that finishes after it as a timeout, and returns Error::ResponseContract when a next_page_token repeats.
Client order IDs accept 1 to 36 ASCII letters, digits, ., _, :, /, and -. Request IDs
use the same character set and accept 1 to 64 characters. Invalid values fail locally with Error::Validation.
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. Do not assume a replacement reserves only the incremental collateral difference.
For long-running automated trading, renew cancel_all_after continuously:
- Arm it only after startup reconciliation has confirmed open orders.
- Refresh well before
effective_timeout_sec(for example every 5 seconds on a 15-second timer). - Give each deliberate refresh a new
request_id, but reuse that ID when retrying the same ambiguous refresh. - Verify
status,effective_timeout_sec, andexpires_at_ts_nson 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.
Cancel-all and the dead-man switch cancel open orders, never triggers. To stop all automated
trading, call client.stop_all_trading(StopAllTradingScope::default()), which cancels every live
trigger and then every open order.
Stream
orders.subscribe yields PrivateOrdersEvent values, not bare orders. The call returns at once:
wait for Ready before relying on the stream.
use polyester::models::PrivateOrdersEvent;
let mut sub = client
.orders
.subscribe(client.default_account_id.as_deref())
.await?;
while let Some(event) = sub.recv_result().await? {
if let PrivateOrdersEvent::Unit(unit) = event {
for order in unit.orders {
println!("{} {}", order.order_id, order.status);
}
break;
}
}See Streaming.
Triggers
use polyester::models::{
CreateOrderType, CreateSide, CreateTimeInForce, CreateTriggerParams, CreateTriggerType,
ModifyTriggerParams,
};
use polyester::{Price, Quantity};
let quantity_scale = client
.catalogs
.base_quantity_scale_for_symbol("BTC-USDT")
.ok_or_else(|| polyester::Error::validation("BTC-USDT quantity scale is unavailable"))?;
let mut params = CreateTriggerParams::new(
"BTC-USDT",
CreateTriggerType::StopLoss,
CreateSide::Sell,
CreateOrderType::Market,
Quantity::from_decimal_str("0.1", quantity_scale, Some("BTC-USDT".into()), None)?,
);
params.trigger_price = Some(Price::from_decimal_str("60000", Some("BTC-USDT".into()))?);
params.time_in_force = Some(CreateTimeInForce::Ioc);
params.client_trigger_id = "sl-1".into();
let created = client.triggers.create(params).await?;
// Create returns status: "accepted" (admission).
client.triggers.modify(ModifyTriggerParams {
trigger_id: created.trigger_id,
subaccount_id: None,
symbol: None,
symbol_id: None,
trigger_price: Some(Price::from_decimal_str("59500", Some("BTC-USDT".into()))?),
activation_price: None,
trailing_distance_ticks: None,
trailing_distance_bps: None,
max_slippage_ticks: None,
max_slippage_bps: None,
}).await?;Status filters: created, armed, running, completed, cancelled, failed, paused.
Retry safely
Persist client_order_id for reconciliation, and reuse replayable request_id / client_trigger_id values for the same logical mutation. Reusing a retained client order ID returns
a duplicate conflict rather than the original create result. When mutation_outcome_unknown() is
true, reconcile first. In particular, Error::ResponseContract is non-retryable even though the mutation outcome is unknown.