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 result = client.orders.create(CreateOrderParams {
symbol: "BTC-USDT".into(),
side: CreateSide::Buy,
order_type: CreateOrderType::Limit,
quantity: Some(Quantity::from_decimal_str("0.25", quantity_scale, Some("BTC-USDT".into()), None)?),
max_quote_debit_scaled: None,
price: Some(Price::from_decimal_str("64250.5", Some("BTC-USDT".into()))?),
time_in_force: Some(CreateTimeInForce::Gtc),
client_order_id: Some("mm-bot-001".into()),
subaccount_id: None,
post_only: Some(true), // limit GTC only
market_client_ref_price: None,
fee_asset: None,
self_trade_prevention: None,
market_max_slippage: None,
attached_risk: None,
}).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. fee_asset is FeeAsset::Quote (default) or
BUY-only FeeAsset::Base, replacing fee_source / received. Create responses can include resolved_base_qty and submitted_max_quote_debit_scaled. Use client.orders.preview(PreviewOrderParams { ... }) 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};
use polyester::Price;
let order_id = "order-id-from-create".to_owned();
client.orders.modify(ModifyOrderParams {
symbol: "BTC-USDT".into(),
order_id: Some(order_id),
client_order_id: None,
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. Use batch_replace for same-symbol quote refresh and
poll get_batch_replace_status with the admission batch_request_id (retry briefly on not-found).
Admission makes predecessor order and client IDs stale: immediately use each replacement_order_id 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; retry a transient 404 immediately after admission. 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 is live. Reuse the
same request_id for an ambiguous retry and never replace against a stale predecessor. The SDK
does not preflight those counts. 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 waits for a terminal order and applies one overall deadline.
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.
Stream
let mut sub = client
.orders
.subscribe(client.default_account_id.as_deref())
.await?;
while let Some(order) = sub.recv_result().await? {
println!("{}", order.status);
break;
}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 created = client.triggers.create(CreateTriggerParams {
symbol: "BTC-USDT".into(),
trigger_type: CreateTriggerType::StopLoss,
side: CreateSide::Sell,
order_type: CreateOrderType::Market,
qty: Quantity::from_decimal_str("0.1", quantity_scale, Some("BTC-USDT".into()), None)?,
trigger_price: Some(Price::from_decimal_str("60000", Some("BTC-USDT".into()))?),
limit_price: None,
trigger_price_source: None, // unsupported values fail instead of being discarded
time_in_force: Some(CreateTimeInForce::Ioc),
subaccount_id: None,
client_trigger_id: "sl-1".into(),
post_only: false,
activation_price: None,
trailing_distance_ticks: None,
trailing_distance_bps: None,
max_slippage_ticks: None,
max_slippage_bps: None,
twap_duration_ms: None,
twap_slice_interval_ms: None,
ladder_price_min: None,
ladder_price_max: None,
ladder_levels: None,
ladder_distribution: None,
fee_asset: None,
self_trade_prevention_mode: None,
}).await?;
// Create returns status: "accepted" (admission).
client.triggers.modify(ModifyTriggerParams {
trigger_id: created.trigger_id,
subaccount_id: None,
trigger_price: Some(Price::from_decimal_str("59500", Some("BTC-USDT".into()))?),
limit_price: None,
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.