# Trading

Place, modify, cancel, and batch spot orders; use triggers with the Rust SDK.

Use an API-key `Client`. Orders spend **trading** balance.

```rust
client.wait_for_catalogs().await?;
```

## Place and cancel

```rust
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

```rust
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`, and `expires_at_ts_ns` on 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

```rust
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

```rust
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.

> **Examples**
>
> [polyester-examples-rust](https://github.com/Fabric-Labs/polyester-examples-rust)

Reference: [Orders](https://testnet.polyester.com/docs/sdk/rust/reference/orders), [Triggers](https://testnet.polyester.com/docs/sdk/rust/reference/triggers).
