Type/to search
Built-in Functions
Global
Log
Market
Trade
Account
Futures
Exchange
IO
Network
Storage
Threads
exchange.Go
EventLoop
ctx.subscribe
ctx.poll
ctx.order
ctx.cancel
ctx.submit
Subscription
threading
Thread
ThreadLock
ThreadEvent
ThreadCondition
ThreadDict
Server
TA
Talib
OS
Structures
Built-in Variables and Constants
Conventions
Exchanges

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.

    javascript
    function 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 : "") }
    python
    import 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)
    rust
    use 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.

    javascript
    function 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") }) }
    python
    import 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")
    rust
    use 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).

    javascript
    function 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() }
    python
    import 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()
    rust
    use 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

TypeDescription

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 orders. For an order placement request, the request ID is the client order ID sent to the exchange (referred to below as localId); subsequent order updates and trade execution reports for that order also use this value for clientId. For a cancellation request, the request ID is a newly generated identifier. If the request is not sent (invalid parameters, unsupported exchange, failure to establish a trading connection, too many in-flight requests, or a local rate limit being triggered), no exception is thrown; an Error object is returned, and no response event is generated.

Arguments

NameTypeRequiredDescription

ex

object / number

Yes

Trading account: an exchange object (exchange, exchanges[1], etc.), or the account's index in the exchanges array (a numeric string such as "0" is also accepted), as in ctx.order.

req

object

Yes

Request object: op specifies the operation type (order to place an order, cancel to cancel an order), and the remaining fields specify the details of the request; for batch requests, put each request in the orders array. See below for field descriptions.

See Also

Remarks

Fields of req:

FieldDescription
oporder places an order; cancel cancels an order. Required
symbolInstrument symbol, such as BTC_USDT or BTC_USDT.swap. Required
sideOrder side: buy for buy, sell for sell
priceOrder price; may be omitted for market orders
amountOrder quantity, which must be greater than 0. Uses the same units as qty in ctx.order
type、tif、reduceOnly、postOnly、extraOrder options, identical to options in ctx.order
idIdentifier of the order to cancel: the exchange order ID or a localId returned by ctx.submit() during the current run
ordersArray 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 Error object 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 call exchange.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 rules also apply to ctx.submit(), with order placement counted under CreateOrder and order cancellation under CancelOrder.
  • 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:

FieldTypeDescription
reqIdstringRequest ID, i.e., the return value of ctx.submit() (the corresponding element in the returned array for batch requests)
opstringorder (order placement) or cancel (order cancellation)
statusstringok 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)
okbooltrue when status is ok
idstringExchange order ID: the new order ID upon successful order placement, or the target order ID for cancellation; an empty string if order placement fails
clientIdstringClient 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
errorstringFailure reason, identical to the REST API error message; an empty string on success, with a maximum length of 127 bytes
reasonnumberUnified reason code: 0 indicates no reason, 3 indicates a transport error or unknown status, 4 indicates exchange rejection, and 5 indicates rate limiting
reasonTextstringDescription of reason
exchTimenumberExchange response time (milliseconds); null if not provided by the exchange
latencyMsnumberRound-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:

ExchangeBatching MethodPer-Batch Limit
Binance SpotSent individually in sequence10
Binance USDⓈ-M FuturesSent individually in sequence5
OKX Spot and DerivativesNative batching20
Bybit Spot and DerivativesNative batching (orders across different categories are sent individually)Spot: 10; derivatives: 20; options: 20 for placement, 5 for cancellation
Gate Spot and Perpetual FuturesNative batching10 for placement, 20 for cancellation
Bitget Spot and DerivativesNative batching for unified accounts; sent individually in sequence for classic accountsUnified accounts: 20; classic accounts: 10
Hyperliquid Spot and DerivativesNative batching; cancellation requests using order IDs and those using localId are each sent in a separate frame20
Deribit DerivativesSent individually in sequence10
Kraken SpotNative 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 localId15 for placement, 50 for cancellation
KuCoin Spot, FuturesSent individually in sequence10
Crypto.com Spot and DerivativesNative 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 localId10
Bitfinex Spot and DerivativesNative batching75
HitBTC SpotSent individually in sequence10
Poloniex SpotPlacement requests sent individually in sequence; native batching for cancellations10 for placement, 20 for cancellation
Lighter DerivativesNative batching; the exchange accepts or rejects the entire batch15

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.