输入/搜索内容
内置函数
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
结构体
内置变量与常量
通用约定
交易所

通过交易所的WebSocket交易接口下单、撤单:请求发送后立即返回请求号,不等待交易所应答;每笔请求的结果以一条应答事件(kind为ack)发送给调用该函数的线程,通过ctx.poll获取。可以一次发送单笔请求,也可以通过orders数组一次发送多笔请求。仅使用WebSocket:交易所不支持时返回错误,不会改用REST接口;需要等待交易所返回订单号的REST下单方式见ctx.order。

ctx.submit(ex, req)

示例

  • 通过WebSocket提交一笔仅作为maker的限价买单,收到成功应答后使用localId撤单;订单状态未知时不重复提交。

    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) // 没有发出,不会有应答 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 : "") }
    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) # 没有发出,不会有应答 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)
    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); // 没有发出,不会有应答 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), } }
  • 批量提交三个价格档位的限价买单,按请求编号收集每笔订单的应答,再批量撤销已成功挂出的订单。

    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) // 整批没有发出 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") }) }
    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) # 整批没有发出 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")
    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); // 整批没有发出 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)匹配该订单的成交回报和订单回报。

    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) // 本线程订阅的事件,加上本线程的应答 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() }
    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) # 本线程订阅的事件,加上本线程的应答 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()
    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 { // 本线程订阅的事件,加上本线程的应答 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

单笔请求返回请求号(字符串),批量请求返回与orders顺序一致的请求号数组。下单请求的请求号即发送给交易所的客户端订单号(下文称localId),后续该订单的订单回报、成交回报中的clientId也使用此值;撤单请求的请求号为新生成的编号。请求未发出时(参数错误、交易所不支持、交易连接无法建立、在途请求过多、触发本地频率限制)不抛出异常,返回Error对象,此时不会产生应答事件。

参数

名称类型必填描述

ex

object / number

是

交易账户:交易所对象(exchange、exchanges[1]等),或账户在exchanges数组中的下标(也可以传入数字字符串"0"),与ctx.order相同。

req

object

是

请求对象:op指定操作类型(order下单,cancel撤单),其余字段填写该笔请求的内容;批量请求时将各笔请求写入orders数组。字段说明见下文。

参考

备注

req的字段:

字段说明
oporder表示下单,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)同时获取应答事件与已订阅的事件,其他线程无法获取这些应答事件。除公共字段外还包含以下字段:

字段类型说明
reqIdstring请求号,即ctx.submit()的返回值(批量请求时为返回数组中的对应项)
opstringorder(下单)或cancel(撤单)
statusstringok表示成功(已获取订单号或撤单已获确认,包括通过客户端订单号查询确认的情况),failed表示失败(交易所拒绝、限频拒单、撤单时订单已成交或已处于其他终态),unknown表示状态未知(见下文)
okboolstatus为ok时为true
idstring交易所订单号:下单成功时为新订单号,撤单时为目标订单号;下单失败时为空字符串
clientIdstring目标订单的客户端订单号:下单时为localId;按localId撤单时为该localId,按交易所订单号撤单时为空字符串
errorstring失败原因,与REST接口的错误信息相同;成功时为空字符串,最长127字节
reasonnumber统一原因码:0表示无原因,3表示传输错误或状态未知,4表示交易所拒绝,5表示限频
reasonTextstringreason的说明
exchTimenumber交易所应答时间(毫秒);交易所未提供时为空值
latencyMsnumber往返耗时(毫秒):从请求写出前一刻到托管者收到交易所结果的时间间隔

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。