ctx.submit
通过交易所的WebSocket交易接口下单、撤单:请求发送后立即返回请求号,不等待交易所应答;每笔请求的结果以一条应答事件(kind为ack)发送给调用该函数的线程,通过ctx.poll获取。可以一次发送单笔请求,也可以通过orders数组一次发送多笔请求。仅使用WebSocket:交易所不支持时返回错误,不会改用REST接口;需要等待交易所返回订单号的REST下单方式见ctx.order。
ctx.submit(ex, req)示例
-
通过WebSocket提交一笔仅作为maker的限价买单,收到成功应答后使用localId撤单;订单状态未知时不重复提交。
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) // 没有发出,不会有应答 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) // 不要重发 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) # 没有发出,不会有应答 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) # 不要重发 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); // 没有发出,不会有应答 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); // 不要重发 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), } } -
批量提交三个价格档位的限价买单,按请求编号收集每笔订单的应答,再批量撤销已成功挂出的订单。
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) // 整批没有发出 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:不要重发 } }) 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) # 整批没有发出 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:不要重发 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); // 整批没有发出 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:不要重发 } } 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), } } -
订阅私有流后通过WebSocket提交一笔IOC买单,在同一循环中处理应答,并根据
clientId(即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) // 本线程订阅的事件,加上本线程的应答 if (!ev) { continue } if (ev.kind === "ack" && ev.reqId === lid) { if (ev.status === "unknown") { Log("state unknown, wait for the private stream", ev.clientId) // 不要重发 } 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) # 本线程订阅的事件,加上本线程的应答 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) # 不要重发 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 { // 本线程订阅的事件,加上本线程的应答 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); // 不要重发 } 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(); }
返回值
| 类型 | 描述 |
string / array / object | 单笔请求返回请求号(字符串),批量请求返回与 |
参数
| 名称 | 类型 | 必填 | 描述 |
ex | object / number | 是 | 交易账户:交易所对象( |
req | object | 是 | 请求对象: |
参考
备注
req的字段:
| 字段 | 说明 |
|---|---|
| op | order表示下单,cancel表示撤单,必填 |
| symbol | 品种代码,例如BTC_USDT、BTC_USDT.swap,必填 |
| side | 下单方向:buy表示买入,sell表示卖出 |
| price | 委托价格;市价单可省略 |
| amount | 委托数量,必须大于0,计量口径与ctx.order的qty相同 |
| type、tif、reduceOnly、postOnly、extra | 下单选项,与ctx.order的options相同 |
| id | 待撤销订单的标识:交易所订单号,或本次运行中ctx.submit()返回的localId |
| orders | 批量请求数组,每一项包含一笔请求的字段(不填写op),所有请求均使用外层的op |
下单需要symbol、side、amount(限价单还需要price),撤单需要symbol、id。仅接受上表中的字段:存在未知字段、字段类型不正确时不发送请求,并返回错误。客户端订单号只能由托管者生成:传入clientId字段,或在extra中包含交易所的客户端订单号字段(例如币安的newClientOrderId、OKX的clOrdId)均返回错误。
调用过程:
- 先校验整批请求:任何一笔请求不合法(包括整批请求数量超过该交易所的单批上限)时整批返回
Error对象,不发送任何请求。 - 每个账户使用一条独立的交易连接,与行情、私有流的推送连接相互独立。首次调用时建立连接,最多等待约5秒,连接失败时返回错误,不会无限排队;可以提前调用
exchange.IO("wsTrade", "connect")建立连接,调用exchange.IO("wsTrade", "status")查看连接状态。 - 请求写出后立即返回请求号,不等待交易所应答。同时在途(已发送、尚未收到结果)的请求最多256个,超过时返回错误。
- 交易所的下单数量限额通常与REST接口共享:本地估算将超限、或交易所报告的用量已达上限时直接返回限频错误,不排队等待。
限流规则同样适用于ctx.submit(),下单按CreateOrder、撤单按CancelOrder计数。 - 交易所提供原生批量接口时,一批请求在一帧中发送;未提供时在同一条连接上逐笔连续发送。两种情况均不保证原子性,可能部分成功,每笔请求各有一条应答事件。
应答事件(kind为ack,type、channel均为ack)通过内置事件流ctx.STREAM_ORDER_ACK(值为4)投递,每笔请求对应一条,仅投递给调用ctx.submit()的线程,无需订阅:ctx.poll([ctx.STREAM_ORDER_ACK], timeout)仅获取应答事件,ctx.poll([], timeout)同时获取应答事件与已订阅的事件,其他线程无法获取这些应答事件。除公共字段外还包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| reqId | string | 请求号,即ctx.submit()的返回值(批量请求时为返回数组中的对应项) |
| op | string | order(下单)或cancel(撤单) |
| status | string | ok表示成功(已获取订单号或撤单已获确认,包括通过客户端订单号查询确认的情况),failed表示失败(交易所拒绝、限频拒单、撤单时订单已成交或已处于其他终态),unknown表示状态未知(见下文) |
| ok | bool | status为ok时为true |
| id | string | 交易所订单号:下单成功时为新订单号,撤单时为目标订单号;下单失败时为空字符串 |
| clientId | string | 目标订单的客户端订单号:下单时为localId;按localId撤单时为该localId,按交易所订单号撤单时为空字符串 |
| error | string | 失败原因,与REST接口的错误信息相同;成功时为空字符串,最长127字节 |
| reason | number | 统一原因码:0表示无原因,3表示传输错误或状态未知,4表示交易所拒绝,5表示限频 |
| reasonText | string | reason的说明 |
| exchTime | number | 交易所应答时间(毫秒);交易所未提供时为空值 |
| latencyMs | number | 往返耗时(毫秒):从请求写出前一刻到托管者收到交易所结果的时间间隔 |
symbol、ex分别为该笔请求的品种代码与账户下标;time为交易所应答时间,交易所未提供时与ts相同。
status为unknown表示请求已发送,但未能确认执行结果(请求超时、连接中途断开、交易所回复执行状态未知)。托管者不会重发:先按客户端订单号执行一次只读订单查询,查到即为ok;仍无法确认时才报告unknown。此时订单可能已被交易所接受,请勿重发,应按clientId查询订单,或等待私有流的订单回报确认。
localId由托管者生成,重启后也不会重复;生成过程也会应用exchange.IO("extend", ...)中目标字符串为ws:order的规则(见请求参数注入)。Hyperliquid、Bitfinex、Lighter的客户端订单号由托管者直接生成,无法通过ws:order规则修改。本次运行中发出的localId会被记录,可以直接作为ctx.cancel或ctx.submit()撤单时的id(按客户端订单号撤单);重新启动后请使用交易所订单号撤单。
每个线程的应答队列最多保存1024条应答,队列满时丢弃最旧的一条,累计丢弃数量记录在ctx.connectors()[i].acksDropped中;从不读取应答的策略不会因应答积压而持续增加内存占用,但会丢失应答,应持续调用ctx.poll()读取应答。线程结束后到达的应答会被丢弃。
支持WebSocket下单的交易所:
| 交易所 | 批量方式 | 单批上限 |
|---|---|---|
币安现货 | 逐笔连续发送 | 10 |
币安U本位合约 | 逐笔连续发送 | 5 |
OKX现货、合约 | 原生批量 | 20 |
Bybit现货、合约 | 原生批量(品类不同的订单逐笔发送) | 现货10;合约20;期权下单20、撤单5 |
Gate现货、永续合约 | 原生批量 | 下单10、撤单20 |
Bitget现货、合约 | 统一账户原生批量,经典账户逐笔连续发送 | 统一账户20,经典账户10 |
Hyperliquid现货、合约 | 原生批量;撤单时按订单号撤单的请求与按localId撤单的请求各发送一帧 | 20 |
Deribit合约 | 逐笔连续发送 | 10 |
Kraken现货 | 同一交易对的下单请求使用原生批量,交易对不同时逐笔发送;撤单请求全部使用订单号时使用原生批量,包含localId时逐笔发送 | 下单15、撤单50 |
KuCoin现货、合约 | 逐笔连续发送 | 10 |
Crypto.com现货、合约 | 各订单均仅使用列表接口支持的字段时使用原生批量,否则逐笔发送;撤单请求全部使用订单号时使用原生批量,包含localId时逐笔发送 | 10 |
Bitfinex现货、合约 | 原生批量 | 75 |
HitBTC现货 | 逐笔连续发送 | 10 |
Poloniex现货 | 下单逐笔连续发送,撤单使用原生批量 | 下单10、撤单20 |
Lighter合约 | 原生批量,交易所整批受理或整批拒绝 | 15 |
Bybit模拟盘、Gate交割合约与期权、币安杠杆账户、币本位合约等不提供WebSocket下单接口的账户类型在调用时返回错误;表中未列出的交易所(例如Kraken期货)同样返回错误。HitBTC、Lighter的订单号就是localId。各交易所的具体规则(限额、支持的账户与合约)请参阅对应交易所页面。
Python中调用方式为ctx.submit(ex, req),req为字典,字段与上述定义相同;单笔请求返回请求号字符串,批量请求返回列表,失败时返回CtxError对象(不抛出异常),可用isinstance(r, CtxError)判断;应答事件为字典,字段与上述定义相同。Rust中单笔请求调用方式为ctx::submit(ex, req_json: &str) -> Result<String, String>,批量请求调用方式为ctx::submit_batch(ex, req_json: &str) -> Result<Vec<String>, String>,请求以JSON字符串形式传入;应答事件通过ev.order_ack()读取(OrderAckView:req_id、op、status、ok、id、client_id、symbol、error、reason、reason_text、exch_time(Option<i64>)、latency_ms、time),事件流ID为ctx::STREAM_ORDER_ACK。