ctx.submit
Places and cancels orders through the exchange's WebSocket trading API: returns a request ID immediately after sending the request, without waiting for the exchange's response; the result of each request is sent as a response event (with kind set to ack) to the thread that called this function and is retrieved through ctx.poll. You can send a single request or multiple requests at once through the orders array. Uses WebSocket only: returns an error if the exchange does not support it, without falling back to REST; for REST order placement that waits for the exchange to return an order ID, see ctx.order.
ctx.submit(ex, req)Examples
-
Submit a maker-only limit buy order via WebSocket and cancel it using localId after receiving a successful acknowledgment; do not resubmit when the order status is unknown.
javascriptfunction waitAck(reqId) { var t0 = Date.now() while (Date.now() - t0 < 10000) { var ev = ctx.poll([ctx.STREAM_ORDER_ACK], 1000) if (ev && ev.kind === "ack" && ev.reqId === reqId) { return ev } } return null } function main() { var t = exchange.GetTicker("BTC_USDT") if (!t) { return } var lid = ctx.submit(exchange, {op: "order", symbol: "BTC_USDT", side: "buy", price: _N(t.Buy * 0.98, 2), amount: 0.001, postOnly: true}) if (lid instanceof Error) { Log("not sent:", lid.message) // Not sent; no acknowledgment will be received return } var ack = waitAck(lid) if (!ack) { Log("no ack in 10 seconds") return } if (ack.status === "unknown") { Log("state unknown, check the order by clientId", ack.clientId) // Do not resubmit return } if (!ack.ok) { Log("order failed:", ack.error, ack.reasonText) return } Log("placed", ack.id, "localId:", lid, "latency:", ack.latencyMs, "ms") Sleep(5000) var creq = ctx.submit(exchange, {op: "cancel", symbol: "BTC_USDT", id: lid}) if (creq instanceof Error) { Log("cancel not sent:", creq.message) return } var cack = waitAck(creq) Log("cancel:", cack ? cack.status : "no ack", cack ? cack.error : "") }pythonimport time def wait_ack(req_id): t0 = time.time() while time.time() - t0 < 10: ev = ctx.poll([ctx.STREAM_ORDER_ACK], 1000) if ev is not None and ev.kind == "ack" and ev.reqId == req_id: return ev return None def main(): t = exchange.GetTicker("BTC_USDT") if not t: return lid = ctx.submit(exchange, {"op": "order", "symbol": "BTC_USDT", "side": "buy", "price": _N(t["Buy"] * 0.98, 2), "amount": 0.001, "postOnly": True}) if isinstance(lid, CtxError): Log("not sent:", lid) # Not sent; no acknowledgment will be received return ack = wait_ack(lid) if ack is None: Log("no ack in 10 seconds") return if ack.status == "unknown": Log("state unknown, check the order by clientId", ack.clientId) # Do not resubmit return if not ack.ok: Log("order failed:", ack.error, ack.reasonText) return Log("placed", ack.id, "localId:", lid, "latency:", ack.latencyMs, "ms") Sleep(5000) creq = ctx.submit(exchange, {"op": "cancel", "symbol": "BTC_USDT", "id": lid}) if isinstance(creq, CtxError): Log("cancel not sent:", creq) return cack = wait_ack(creq) if cack is None: Log("cancel: no ack") else: Log("cancel:", cack.status, cack.error)rustuse sdk::ctx::{EventExt, OrderAckView}; fn wait_ack(req_id: &str) -> Option<OrderAckView> { let t0 = Unix(); while Unix() - t0 < 10 { match ctx::poll(&[ctx::STREAM_ORDER_ACK], Some(1000)) { ctx::Polled::Event(ev) => { if let Some(ack) = ev.order_ack().filter(|a| a.req_id == req_id) { return Some(ack); } } ctx::Polled::Stopped => return None, _ => {} } } None } fn main() { let Ok(t) = exchange.GetTicker("BTC_USDT") else { return }; let req = format!( r#"{{"op":"order","symbol":"BTC_USDT","side":"buy","price":{},"amount":0.001, "postOnly":true}}"#, _N(t.Buy * 0.98, 2) ); let lid = match ctx::submit(exchange, &req) { Ok(lid) => lid, Err(e) => { Log!("not sent:", e); // Not sent; no acknowledgment will be received return; } }; let Some(ack) = wait_ack(&lid) else { Log!("no ack in 10 seconds"); return; }; if ack.status == "unknown" { Log!("state unknown, check the order by clientId", ack.client_id); // Do not resubmit return; } if !ack.ok { Log!("order failed:", ack.error, ack.reason_text); return; } Log!("placed", ack.id, "localId:", lid, "latency:", ack.latency_ms, "ms"); Sleep(5000); let creq = format!(r#"{{"op":"cancel","symbol":"BTC_USDT","id":"{}"}}"#, lid); match ctx::submit(exchange, &creq) { Ok(req_id) => match wait_ack(&req_id) { Some(c) => Log!("cancel:", c.status, c.error), None => Log!("cancel: no ack"), }, Err(e) => Log!("cancel not sent:", e), } } -
Submit a batch of limit buy orders at three price levels, collect the acknowledgment for each order by request ID, then cancel the successfully placed orders in a batch.
javascriptfunction collectAcks(reqIds) { var acks = {} var left = reqIds.length var t0 = Date.now() while (left > 0 && Date.now() - t0 < 10000) { var ev = ctx.poll([ctx.STREAM_ORDER_ACK], 1000) if (ev && ev.kind === "ack" && reqIds.indexOf(ev.reqId) >= 0 && !acks[ev.reqId]) { acks[ev.reqId] = ev left-- } } return acks } function main() { var t = exchange.GetTicker("BTC_USDT") if (!t) { return } var ids = ctx.submit(exchange, {op: "order", orders: [ {symbol: "BTC_USDT", side: "buy", price: _N(t.Buy * 0.99, 2), amount: 0.001}, {symbol: "BTC_USDT", side: "buy", price: _N(t.Buy * 0.98, 2), amount: 0.001}, {symbol: "BTC_USDT", side: "buy", price: _N(t.Buy * 0.97, 2), amount: 0.001} ]}) if (ids instanceof Error) { Log("batch not sent:", ids.message) // The entire batch was not sent return } var acks = collectAcks(ids) var placed = [] ids.forEach(function (lid) { var a = acks[lid] if (!a) { Log(lid, "no ack") } else if (a.ok) { placed.push(lid) Log(lid, "placed", a.id, a.latencyMs, "ms") } else { Log(lid, a.status, a.error) // unknown: do not resubmit } }) if (placed.length === 0) { return } Sleep(5000) var cids = ctx.submit(exchange, {op: "cancel", orders: placed.map(function (lid) { return {symbol: "BTC_USDT", id: lid} })}) if (cids instanceof Error) { Log("cancel not sent:", cids.message) return } var cacks = collectAcks(cids) cids.forEach(function (r) { Log("cancel", r, cacks[r] ? cacks[r].status : "no ack") }) }pythonimport time def collect_acks(req_ids): acks = {} t0 = time.time() while len(acks) < len(req_ids) and time.time() - t0 < 10: ev = ctx.poll([ctx.STREAM_ORDER_ACK], 1000) if ev is not None and ev.kind == "ack" and ev.reqId in req_ids: acks[ev.reqId] = ev return acks def main(): t = exchange.GetTicker("BTC_USDT") if not t: return ids = ctx.submit(exchange, {"op": "order", "orders": [ {"symbol": "BTC_USDT", "side": "buy", "price": _N(t["Buy"] * 0.99, 2), "amount": 0.001}, {"symbol": "BTC_USDT", "side": "buy", "price": _N(t["Buy"] * 0.98, 2), "amount": 0.001}, {"symbol": "BTC_USDT", "side": "buy", "price": _N(t["Buy"] * 0.97, 2), "amount": 0.001} ]}) if isinstance(ids, CtxError): Log("batch not sent:", ids) # The entire batch was not sent return acks = collect_acks(ids) placed = [] for lid in ids: a = acks.get(lid) if a is None: Log(lid, "no ack") elif a.ok: placed.append(lid) Log(lid, "placed", a.id, a.latencyMs, "ms") else: Log(lid, a.status, a.error) # unknown: do not resubmit if not placed: return Sleep(5000) items = [{"symbol": "BTC_USDT", "id": lid} for lid in placed] cids = ctx.submit(exchange, {"op": "cancel", "orders": items}) if isinstance(cids, CtxError): Log("cancel not sent:", cids) return cacks = collect_acks(cids) for r in cids: Log("cancel", r, cacks[r].status if r in cacks else "no ack")rustuse sdk::ctx::{EventExt, OrderAckView}; use std::collections::HashMap; fn collect_acks(req_ids: &[String]) -> HashMap<String, OrderAckView> { let mut acks = HashMap::new(); let t0 = Unix(); while acks.len() < req_ids.len() && Unix() - t0 < 10 { match ctx::poll(&[ctx::STREAM_ORDER_ACK], Some(1000)) { ctx::Polled::Event(ev) => { if let Some(a) = ev.order_ack().filter(|a| req_ids.contains(&a.req_id)) { acks.insert(a.req_id.clone(), a); } } ctx::Polled::Stopped => break, _ => {} } } acks } fn main() { let Ok(t) = exchange.GetTicker("BTC_USDT") else { return }; let items: Vec<String> = (1..=3) .map(|i| { let px = _N(t.Buy * (1.0 - 0.01 * i as f64), 2); format!( r#"{{"symbol":"BTC_USDT","side":"buy","price":{},"amount":0.001,"postOnly":true}}"#, px ) }) .collect(); let req = format!(r#"{{"op":"order","orders":[{}]}}"#, items.join(",")); let ids = match ctx::submit_batch(exchange, &req) { Ok(ids) => ids, Err(e) => { Log!("batch not sent:", e); // The entire batch was not sent return; } }; let acks = collect_acks(&ids); let mut placed = Vec::new(); for lid in &ids { match acks.get(lid) { None => Log!(lid, "no ack"), Some(a) if a.ok => { placed.push(lid.clone()); Log!(lid, "placed", a.id, a.latency_ms, "ms"); } Some(a) => Log!(lid, a.status, a.error), // unknown: do not resubmit } } if placed.is_empty() { return; } Sleep(5000); let items: Vec<String> = placed .iter() .map(|lid| format!(r#"{{"symbol":"BTC_USDT","id":"{}"}}"#, lid)) .collect(); let creq = format!(r#"{{"op":"cancel","orders":[{}]}}"#, items.join(",")); match ctx::submit_batch(exchange, &creq) { Ok(cids) => { let cacks = collect_acks(&cids); for r in &cids { let st = cacks.get(r).map(|a| a.status).unwrap_or("no ack"); Log!("cancel", r, st); } } Err(e) => Log!("cancel not sent:", e), } } -
After subscribing to the private stream, submit an IOC buy order via WebSocket, process acknowledgments in the same loop, and match the order's fill reports and order updates using
clientId(i.e., localId).javascriptfunction main() { var acct = ctx.subscribe(exchange, {channel: "private"}) var t = exchange.GetTicker("BTC_USDT") if (!t) { return } var lid = ctx.submit(exchange, {op: "order", symbol: "BTC_USDT", side: "buy", price: _N(t.Sell * 1.001, 2), amount: 0.001, tif: "ioc"}) if (lid instanceof Error) { Log("not sent:", lid.message) return } var t0 = Date.now() while (Date.now() - t0 < 10000) { var ev = ctx.poll([], 1000) // Events subscribed to by this thread, plus acknowledgments for this thread if (!ev) { continue } if (ev.kind === "ack" && ev.reqId === lid) { if (ev.status === "unknown") { Log("state unknown, wait for the private stream", ev.clientId) // Do not resend } else if (!ev.ok) { Log("order failed:", ev.error) break } else { Log("ack", ev.id, ev.latencyMs, "ms") } } else if (ev.kind === "fill" && ev.clientId === lid) { Log("fill", ev.tradeId, ev.px, ev.qty, ev.liquidity) } else if (ev.kind === "order" && ev.clientId === lid) { if (ev.state === "filled" || ev.state === "canceled") { Log("order done:", ev.state, "filled:", ev.filledQty, "avgPx:", ev.avgPx) break } } } acct.unsubscribe() }pythonimport time def main(): acct = ctx.subscribe(exchange, {"channel": "private"}) t = exchange.GetTicker("BTC_USDT") if not t: return lid = ctx.submit(exchange, {"op": "order", "symbol": "BTC_USDT", "side": "buy", "price": _N(t["Sell"] * 1.001, 2), "amount": 0.001, "tif": "ioc"}) if isinstance(lid, CtxError): Log("not sent:", lid) return t0 = time.time() while time.time() - t0 < 10: ev = ctx.poll([], 1000) # Events subscribed to by this thread, plus acknowledgments for this thread if ev is None: continue if ev.kind == "ack" and ev.reqId == lid: if ev.status == "unknown": Log("state unknown, wait for the private stream", ev.clientId) # Do not resend elif not ev.ok: Log("order failed:", ev.error) break else: Log("ack", ev.id, ev.latencyMs, "ms") elif ev.kind == "fill" and ev.clientId == lid: Log("fill", ev.tradeId, ev.px, ev.qty, ev.liquidity) elif ev.kind == "order" and ev.clientId == lid and ev.state in ("filled", "canceled"): Log("order done:", ev.state, "filled:", ev.filledQty, "avgPx:", ev.avgPx) break acct.unsubscribe()rustuse sdk::ctx::EventExt; fn main() { let acct = ctx::subscribe(exchange, ctx::SubOpts::private()).unwrap(); let Ok(t) = exchange.GetTicker("BTC_USDT") else { return }; let req = format!( r#"{{"op":"order","symbol":"BTC_USDT","side":"buy","price":{},"amount":0.001, "tif":"ioc"}}"#, _N(t.Sell * 1.001, 2) ); let lid = match ctx::submit(exchange, &req) { Ok(lid) => lid, Err(e) => { Log!("not sent:", e); return; } }; let t0 = Unix(); while Unix() - t0 < 10 { // Events subscribed to by this thread, plus acknowledgments for this thread let ev = match ctx::poll(&[], Some(1000)) { ctx::Polled::Event(ev) => ev, ctx::Polled::Stopped => break, _ => continue, }; if let Some(a) = ev.order_ack().filter(|a| a.req_id == lid) { if a.status == "unknown" { Log!("state unknown, wait for the private stream", a.client_id); // Do not resend } else if !a.ok { Log!("order failed:", a.error); break; } else { Log!("ack", a.id, a.latency_ms, "ms"); } } else if let Some(f) = ev.fill().filter(|f| f.client_id == lid) { Log!("fill", f.trade_id, f.px, f.qty, f.liquidity); } else if let Some(o) = ev.order().filter(|o| o.client_id == lid) { if o.state == "filled" || o.state == "canceled" { Log!("order done:", o.state, "filled:", o.filled_qty, "avgPx:", o.avg_px); break; } } } let _ = acct.unsubscribe(); }
Returns
| Type | Description |
string / array / object | A single request returns a request ID (string), while a batch request returns an array of request IDs in the same order as |
Arguments
| Name | Type | Required | Description |
ex | object / number | Yes | Trading account: an exchange object ( |
req | object | Yes | Request object: |
See Also
Remarks
Fields of req:
| Field | Description |
|---|---|
| op | order places an order; cancel cancels an order. Required |
| symbol | Instrument symbol, such as BTC_USDT or BTC_USDT.swap. Required |
| side | Order side: buy for buy, sell for sell |
| price | Order price; may be omitted for market orders |
| amount | Order quantity, which must be greater than 0. Uses the same units as qty in ctx.order |
| type、tif、reduceOnly、postOnly、extra | Order options, identical to options in ctx.order |
| id | Identifier of the order to cancel: the exchange order ID or a localId returned by ctx.submit() during the current run |
| orders | Array of batch requests. Each item contains the fields for one request (without op); all requests use the outer op |
Placing an order requires symbol, side, and amount (limit orders also require price); canceling an order requires symbol and id. Only the fields listed above are accepted: if unknown fields or incorrect field types are present, no request is sent and an error is returned. Client order IDs can only be generated by the docker: passing a clientId field or including an exchange-specific client order ID field in extra (such as Binance's newClientOrderId or OKX's clOrdId) returns an error.
Call procedure:
- The entire batch is validated first: if any request is invalid (including when the batch size exceeds the exchange's per-batch limit), an
Errorobject is returned for the entire batch, and no requests are sent. - Each account uses a dedicated trading connection, separate from the push connections for market data and private streams. The connection is established on the first call, with a maximum wait of approximately 5 seconds. If the connection fails, an error is returned; requests are not queued indefinitely. You can call
exchange.IO("wsTrade", "connect")in advance to establish the connection and callexchange.IO("wsTrade", "status")to check its status. - A request ID is returned immediately after the request is written, without waiting for the exchange's response. A maximum of 256 requests may be in flight (sent but awaiting results) simultaneously; exceeding this limit returns an error.
- Exchange order submission limits are typically shared with the REST API: if local estimates indicate that the limit would be exceeded, or exchange-reported usage has reached the limit, a rate limit error is returned immediately, without queuing.
Rate limiting rulesalso apply toctx.submit(), with order placement counted underCreateOrderand order cancellation underCancelOrder. - When the exchange provides a native batch API, a batch of requests is sent in a single frame; otherwise, requests are sent consecutively over the same connection. Neither case guarantees atomicity, and partial success is possible. Each request has its own acknowledgment event.
Acknowledgment events (with kind set to ack and both type and channel set to ack) are delivered through the built-in event stream ctx.STREAM_ORDER_ACK (value 4), one per request, exclusively to the thread that called ctx.submit(), with no subscription required: ctx.poll([ctx.STREAM_ORDER_ACK], timeout) retrieves only acknowledgment events, while ctx.poll([], timeout) retrieves both acknowledgment events and subscribed events. Other threads cannot retrieve these acknowledgment events. In addition to the common fields, the following fields are included:
| Field | Type | Description |
|---|---|---|
| reqId | string | Request ID, i.e., the return value of ctx.submit() (the corresponding element in the returned array for batch requests) |
| op | string | order (order placement) or cancel (order cancellation) |
| status | string | ok indicates success (an order ID has been obtained or cancellation has been confirmed, including confirmation by querying the client order ID); failed indicates failure (exchange rejection, rate limit rejection, or an order that was already filled or in another terminal state when cancellation was attempted); unknown indicates an unknown status (see below) |
| ok | bool | true when status is ok |
| id | string | Exchange order ID: the new order ID upon successful order placement, or the target order ID for cancellation; an empty string if order placement fails |
| clientId | string | Client order ID of the target order: localId for order placement; the specified localId when canceling by localId, or an empty string when canceling by exchange order ID |
| error | string | Failure reason, identical to the REST API error message; an empty string on success, with a maximum length of 127 bytes |
| reason | number | Unified reason code: 0 indicates no reason, 3 indicates a transport error or unknown status, 4 indicates exchange rejection, and 5 indicates rate limiting |
| reasonText | string | Description of reason |
| exchTime | number | Exchange response time (milliseconds); null if not provided by the exchange |
| latencyMs | number | Round-trip latency (milliseconds): the interval from immediately before the request is written until the docker receives the result from the exchange |
symbol and ex are the instrument symbol and account index for the request, respectively; time is the exchange response time, or the same as ts if not provided by the exchange.
A status of unknown indicates that the request was sent, but its execution result could not be confirmed (request timeout, connection interruption, or an exchange response indicating an unknown execution status). The docker does not resend the request: it first performs a single read-only order query using the client order ID and reports ok if the order is found; it reports unknown only if the result still cannot be confirmed. At this point, the order may already have been accepted by the exchange. Do not resend it; query the order using clientId or wait for confirmation from an order update on the private stream.
localId is generated by the docker and remains unique across restarts; generation also applies the rules in exchange.IO("extend", ...) whose target string is ws:order (see Request Parameter Injection). Client order IDs for Hyperliquid, Bitfinex, and Lighter are generated directly by the docker and cannot be modified through ws:order rules. localId values issued during the current run are recorded and can be used directly as the id when canceling orders with ctx.cancel or ctx.submit() (cancellation by client order ID); after a restart, use exchange order IDs to cancel orders.
Each thread's acknowledgment queue holds up to 1024 acknowledgments. When the queue is full, the oldest acknowledgment is discarded, and the cumulative number of discarded acknowledgments is recorded in ctx.connectors()[i].acksDropped. Strategies that never read acknowledgments will not experience continuous memory growth due to acknowledgment accumulation, but will lose acknowledgments. Call ctx.poll() continuously to read acknowledgments. Acknowledgments arriving after the thread terminates are discarded.
Exchanges supporting order placement via WebSocket:
| Exchange | Batching Method | Per-Batch Limit |
|---|---|---|
Binance Spot | Sent individually in sequence | 10 |
Binance USDⓈ-M Futures | Sent individually in sequence | 5 |
OKX Spot and Derivatives | Native batching | 20 |
Bybit Spot and Derivatives | Native batching (orders across different categories are sent individually) | Spot: 10; derivatives: 20; options: 20 for placement, 5 for cancellation |
Gate Spot and Perpetual Futures | Native batching | 10 for placement, 20 for cancellation |
Bitget Spot and Derivatives | Native batching for unified accounts; sent individually in sequence for classic accounts | Unified accounts: 20; classic accounts: 10 |
Hyperliquid Spot and Derivatives | Native batching; cancellation requests using order IDs and those using localId are each sent in a separate frame | 20 |
Deribit Derivatives | Sent individually in sequence | 10 |
Kraken Spot | Native batching for placement requests for the same trading pair; sent individually for different trading pairs; native batching when all cancellation requests use order IDs; sent individually when any request uses localId | 15 for placement, 50 for cancellation |
KuCoin Spot, Futures | Sent individually in sequence | 10 |
Crypto.com Spot and Derivatives | Native batching when every order uses only fields supported by the list endpoint; otherwise sent individually; native batching when all cancellation requests use order IDs; sent individually when any request uses localId | 10 |
Bitfinex Spot and Derivatives | Native batching | 75 |
HitBTC Spot | Sent individually in sequence | 10 |
Poloniex Spot | Placement requests sent individually in sequence; native batching for cancellations | 10 for placement, 20 for cancellation |
Lighter Derivatives | Native batching; the exchange accepts or rejects the entire batch | 15 |
Account types that do not provide a WebSocket order placement API, such as Bybit demo trading, Gate delivery futures and options, and Binance margin accounts and COIN-M futures, return an error when called; exchanges not listed in the table (such as Kraken Futures) also return an error. For HitBTC and Lighter, the order ID is the localId. For each exchange's specific rules (limits, supported accounts, and contracts), refer to the corresponding exchange page.
In Python, call ctx.submit(ex, req), where req is a dictionary with the same fields as defined above; a single request returns a request ID string, a batch request returns a list, and a failure returns a CtxError object (without raising an exception), which can be checked using isinstance(r, CtxError); acknowledgment events are dictionaries with the same fields as defined above. In Rust, call ctx::submit(ex, req_json: &str) -> Result<String, String> for a single request and ctx::submit_batch(ex, req_json: &str) -> Result<Vec<String>, String> for a batch request, passing requests as JSON strings; read acknowledgment events through ev.order_ack() (OrderAckView: req_id, op, status, ok, id, client_id, symbol, error, reason, reason_text, exch_time (Option<i64>), latency_ms, time), with event stream ID ctx::STREAM_ORDER_ACK.