输入/搜索内容
内置函数
Global
Log
Market
Trade
Account
Futures
Exchange
IO
Network
Storage
Threads
exchange.Go
EventLoop
ctx.subscribe
ctx.poll
ctx.order
ctx.cancel
ctx.orderAsync
ctx.cancelAsync
Subscription
threading
Thread
ThreadLock
ThreadEvent
ThreadCondition
ThreadDict
Server
TA
Talib
OS
结构体
内置变量与常量
通用约定
交易所

取出本线程所订阅事件流中的下一条推送事件,可设置等待超时。事件来自本线程通过ctx.subscribe创建的订阅,策略可根据事件的channel(或kind)字段分别处理。

ctx.poll()
ctx.poll(streams)
ctx.poll(streams, timeout)
ctx.poll(options, timeout)

示例

  • 订阅1分钟K线,仅在K线收盘时处理,同时记录网关状态的变化。

    javascript
    function onKline(ev) { if (ev.prefill) { return // 跳过首次订阅时推送的历史K线 } if (ev.closed) { Log("closed bar", _D(ev.openTimeMs), "close:", ev.close, "volume:", ev.volume) } } function main() { var k1m = ctx.subscribe(exchange, {channel: "kline", interval: 60}) while (true) { var ev = ctx.poll([], 5000) if (!ev) { Log("no event in 5 seconds") continue } if (ev.stream === k1m.id && ev.kind === 5) { onKline(ev) } else if (ev.stream === k1m.id && ev.kind === 35) { if (ev.prefillStatus === 2) { throw new Error("kline subscription failed: " + ev.message) } Log("kline history ready") } else if (ev.kind === 33) { Log("gateway status:", ev.status) } } }
    python
    def on_kline(ev): if ev.prefill: return # 跳过首次订阅时推送的历史K线 if ev.closed: Log("closed bar", _D(ev.openTimeMs), "close:", ev.close, "volume:", ev.volume) def main(): k1m = ctx.subscribe(exchange, {"channel": "kline", "interval": 60}) while True: ev = ctx.poll([], 5000) if ev is None: Log("no event in 5 seconds") continue if ev.stream == k1m.id and ev.kind == 5: on_kline(ev) elif ev.stream == k1m.id and ev.kind == 35: if ev.prefillStatus == 2: raise Exception("kline subscription failed: " + ev.message) Log("kline history ready") elif ev.kind == 33: Log("gateway status:" , ev.status)
    rust
    use sdk::ctx::EventExt; fn main() { let k1m = ctx::subscribe(exchange, ctx::SubOpts::kline(60)).unwrap(); loop { match ctx::poll(&[], Some(5000)) { ctx::Polled::Event(ev) if ev.stream() == k1m.id() => { if let Some(k) = ev.kline() { if !k.prefill && k.closed { // 跳过首次订阅时推送的历史K线 Log!("closed bar", _D(k.open_time), "close:", k.close, "volume:", k.volume); } } else if let Some(st) = ev.feed_status() { if st.failed() { panic!("kline subscription failed: {}", st.message); } Log!("kline history ready"); } } ctx::Polled::Event(ev) => { if let Some(g) = ev.gateway() { Log!("gateway status:", g.status); } } ctx::Polled::Empty => Log!("no event in 5 seconds"), ctx::Polled::Stopped => break, _ => {} } } }
  • 订阅账户私有流:待账户快照就绪后读取持仓和余额,此后根据推送更新数据,并输出每一笔成交。

    javascript
    function main() { var acct = ctx.subscribe(exchange, {channel: "private"}) while (true) { var ev = ctx.poll([], 1000) if (!ev || ev.stream !== acct.id) { continue } if (ev.kind === 35) { if (ev.prefillStatus === 1) { Log("balances:", acct.balances(), "positions:", acct.positions()) } else { Log("account snapshot failed:", ev.message) } } else if (ev.kind === 17 && !ev.snapshot) { Log("position", ev.symbol, ev.posSide, "qty:", ev.qty, "avgPx:", ev.avgPx) } else if (ev.kind === 18 && !ev.snapshot) { Log("balance", ev.asset, "free:", ev.free, "frozen:", ev.frozen) // 推送中无同口径数值时为null } else if (ev.kind === 19) { Log("fill", ev.orderId, ev.side, ev.px, ev.qty, ev.liquidity, "fee:", ev.fee, ev.feeCcy) } else if (ev.kind === 16) { Log("order", ev.id, ev.state, "filled:", ev.filledQty, "/", ev.qty) } } }
    python
    def main(): acct = ctx.subscribe(exchange, {"channel": "private"}) while True: ev = ctx.poll([], 1000) if ev is None or ev.stream != acct.id: continue if ev.kind == 35: if ev.prefillStatus == 1: Log("balances:", acct.balances(), "positions:", acct.positions()) else: Log("account snapshot failed:", ev.message) elif ev.kind == 17 and not ev.snapshot: Log("position", ev.symbol, ev.posSide, "qty:", ev.qty, "avgPx:", ev.avgPx) elif ev.kind == 18 and not ev.snapshot: Log("balance", ev.asset, "free:", ev.free, "frozen:", ev.frozen) # 推送中无同口径数值时为None elif ev.kind == 19: Log("fill", ev.orderId, ev.side, ev.px, ev.qty, "fee:", ev.get("fee"), ev.feeCcy) elif ev.kind == 16: Log("order", ev.id, ev.state, "filled:", ev.filledQty, "/", ev.qty)
    rust
    use sdk::ctx::EventExt; fn main() { let acct = ctx::subscribe(exchange, ctx::SubOpts::private()).unwrap(); loop { match ctx::poll(&[], Some(1000)) { ctx::Polled::Event(ev) if ev.stream() == acct.id() => { if let Some(st) = ev.feed_status() { if st.ready() { Log!("balances:", format!("{:?}", acct.balances()), "positions:", format!("{:?}", acct.positions())); } else { Log!("account snapshot failed:", st.message); } } else if let Some(p) = ev.position().filter(|p| !p.snapshot) { Log!("position", p.symbol, p.pos_side, "qty:", p.qty, "avgPx:", p.avg_px); } else if let Some(b) = ev.balance().filter(|b| !b.snapshot) { // free、frozen为Option类型:推送中无同口径数值时为None let (free, frozen) = (format!("{:?}", b.free), format!("{:?}", b.frozen)); Log!("balance", b.asset, "free:", free, "frozen:", frozen); } else if let Some(f) = ev.fill() { let fee = format!("{:?}", f.fee); Log!("fill", f.order_id, f.side, f.px, f.qty, "fee:", fee, f.fee_ccy); } else if let Some(o) = ev.order() { Log!("order", o.id, o.state, "filled:", o.filled_qty, "/", o.qty); } } ctx::Polled::Stopped => break, _ => {} } } }
  • 订阅全市场ticker,每分钟输出一次收到的事件数以及ctx.connectors()中的接收缓冲计数,用于判断策略的处理速度能否跟上行情推送。

    javascript
    function main() { var all = ctx.subscribe(exchange, {channel: "tickers"}) var n = 0 var last = Date.now() while (true) { var ev = ctx.poll([], 1000) if (ev && ev.kind === 1) { n++ } else if (ev && ev.kind === 35 && ev.prefillStatus === 2) { throw new Error("tickers subscription failed: " + ev.message) } if (Date.now() - last >= 60000) { var feed = ctx.connectors()[exchange.GetIndex()].feed Log("tickers/min:", n, "dropped:", feed.ticksDropped, "backpressure:", feed.backpressure, "ring:", feed.ringCapacity) n = 0 last = Date.now() } } }
    python
    import time def main(): all_tickers = ctx.subscribe(exchange, {"channel": "tickers"}) n = 0 last = time.time() while True: ev = ctx.poll([], 1000) if ev is not None and ev.kind == 1: n += 1 elif ev is not None and ev.kind == 35 and ev.prefillStatus == 2: raise Exception("tickers subscription failed: " + ev.message) if time.time() - last >= 60: feed = ctx.connectors()[all_tickers.ex].feed Log("tickers/min:", n, "dropped:", feed.ticksDropped, "backpressure:", feed.backpressure, "ring:", feed.ringCapacity) n = 0 last = time.time()
    rust
    use sdk::ctx::EventExt; fn main() { let all = ctx::subscribe(exchange, ctx::SubOpts::tickers()).unwrap(); let mut n = 0u64; let mut last = Unix(); loop { match ctx::poll(&[all.id()], Some(1000)) { ctx::Polled::Event(ev) => { if ev.kind == EV_MD_TICK { n += 1; } else if let Some(st) = ev.feed_status().filter(|st| st.failed()) { panic!("tickers subscription failed: {}", st.message); } } ctx::Polled::Stopped => break, _ => {} } if Unix() - last >= 60 { // 返回JSON数组,每个交易所对应一项,其中feed字段为接收缓冲计数 Log!("tickers/min:", n, "connectors:", ctx::connectors()); n = 0; last = Unix(); } } }

返回值

类型描述

object / 空值

事件对象,字段说明见下文;超时且没有事件时返回空值。

参数

名称类型必填描述

streams

object / number / array

否

要读取的事件流:Subscription对象或事件流ID,也可以是由它们组成的数组(支持混用和嵌套,批量订阅返回的数组可直接传入)。省略或传入[]表示本线程订阅的全部事件流,这是最常见的用法。内置事件流:ctx.STREAM_TIMER(定时器)、ctx.STREAM_STATUS(网关状态)、ctx.STREAM_ORDER_ACK(异步下单、撤单的应答)。

timeout

number

否

等待超时时间,单位为毫秒,必须为非负整数。省略时一直等待,直到有事件到达;0表示不等待;正数表示最长等待时间,超时后返回空值。

options

object

否

对象形式{streams, sources},仅JavaScript支持:streams含义同上;sources为要等待的事件来源数组,可选值为engine(推送事件,默认值)、thread(线程消息)、go(exchange.Go()等并发任务完成)、dial(Dial()连接可读)、task,或用all表示全部来源。使用此形式时,返回值包装为{source, event, seq, nano, queue},其中event为对应来源的事件内容。

参考

备注

所有事件都包含以下字段:

字段类型说明
kindnumber事件类型:1 ticker、bbo和tickers,3 深度,4 成交,5 K线,16 订单回报,17 持仓,18 余额,19 成交回报,20 异步下单应答,32 定时器,33 网关状态,35 初始化状态
streamnumber投递该事件的事件流:订阅产生的事件为本线程中对应订阅对象的id;网关状态、定时器、异步下单应答事件分别来自内置事件流ctx.STREAM_STATUS(2)、ctx.STREAM_TIMER(1)、ctx.STREAM_ORDER_ACK(4)
typestringdata(行情数据,以及私有流的订单回报、成交回报、持仓、余额)、status(网关状态、初始化状态)、timer(定时器)、ack(异步下单应答)
channelstring所属订阅的频道:ticker、bbo、tickers、depth、trade、kline;私有流为orders、fills、positions、balance;网关状态为gateway,定时器为timer,下单应答为ack;初始化状态见下文
depthnumber仅深度事件包含:所属订阅的档位数(0表示交易所默认档数)
intervalnumber仅K线事件及K线订阅的初始化状态事件包含:K线周期,单位为秒
exnumber账户在exchanges中的索引,可直接作为ctx.subscribe()的第一个参数;不属于任何账户的事件为-1
symbolstring品种代码,例如BTC_USDT、BTC_USDT.swap;tickers事件为各自对应的品种;余额事件和私有流的状态事件为空字符串
seqnumber事件序号,单调递增
timenumber事件发生时间(毫秒时间戳):交易所提供时间时取交易所时间,否则取托管者的接收时间(与ts相同);各类事件的取值方式见下文
tsnumber托管者收到交易所数据的时间(毫秒时间戳,含小数部分);Date.now() - ev.ts为托管者内部处理及取事件的延迟,不包含交易所到托管者的网络延迟

各类事件的其他字段:

kind字段
1(ticker、bbo、tickers)bid、ask、last、bidQty、askQty、volume、openInterest、open、high、low。bbo无最新价时last为0;全市场推送不含买卖价时,tickers的bid、ask为0
3(深度)不含档位字段,仅表示订单簿已更新;档位数据通过ev.book(n)读取,见下文
4(成交)px(价格)、qty(数量)、side(主动成交方向buy/sell,交易所未提供时为空字符串)、tradeId
5(K线)open、high、low、close、volume、openInterest、intervalSec、openTimeMs、closed(该K线是否已收盘)、prefill(是否为首次订阅时推送的历史K线)
16(订单回报)id(订单号)、state(acked、partial、filled、canceled、rejected)、px、qty、filledQty、avgPx、reason(拒单原因码,0表示无)、reasonText
17(持仓)side(buy为多、sell为空)、posSide(双向持仓为long、short,单向持仓为net)、qty、avgPx、profit、margin、liqPx、markPx、leverage、snapshot
18(余额)asset(资产名称)、free(可用)、frozen(冻结)、equity、upnl、margin、snapshot
19(成交回报)orderId(订单号)、tradeId(成交号)、px、qty(该笔成交的价格、数量)、fee(手续费,负数表示返佣)、feeCcy(手续费币种)、side(订单方向)、liquidity(maker或taker),见下文
20(异步下单应答)reqId、op、status、ok、via、id、clientId、error、reason、reasonText、exchTime、latencyMs,见ctx.orderAsync
32(定时器)timerId、scheduledNs(超出JavaScript安全整数范围时为字符串)
33(网关状态)status(1表示连接正常,2表示断开,3表示降级)、detail
35(初始化状态)channelCode(频道的数字代码)、intervalSec、prefillStatus(1表示就绪,2表示失败)、detail、message(失败原因)

价格和数量均为实数,已按合约精度换算。

time在各类事件中的取值方式如下(回测中time与ts均为回测的模拟时间):

事件time
ticker、bbo、tickers、成交、成交回报、持仓、余额交易所时间;交易所未提供时为接收时间(与ts相同)
K线该根K线的开始时间,与openTimeMs相同,与exchange.GetRecords返回的K线Time口径一致
深度、订单回报接收时间(与ts相同),目前无法获取这两类推送的交易所时间
网关状态、初始化状态与ts相同
异步下单应答交易所的应答时间;交易所未提供时(例如REST应答)与ts相同
定时器计划触发时间,与ts使用同一时钟(ts为实际触发时间)

ev.ts - ev.time可用于估算交易所到托管者的延迟,仅当time为交易所时间时才有意义。

成交事件的tradeId始终为字符串,格式与exchange.GetTrades返回的成交ID一致:交易所的数字成交号转换为十进制字符串,其他格式原样保留;交易所未提供成交号时,为以syn:开头的合成编号;空字符串表示连接器未提供成交号。

深度事件带有ev.book(n)方法,用于读取该事件所属订阅自身订单簿的前n档(未传入n时为订阅的档数),返回{bids: [[price, qty], ...], asks: [[price, qty], ...]},其中bids按价格从高到低排列,asks按价格从低到高排列,与订阅对象的book方法相同。ev.book不可枚举,因此JSON.stringify(ev)的结果中不包含它;所属订阅已退订后再调用会抛出异常。

持仓(kind为17)和余额(kind为18)事件来自private订阅,每条事件都是该持仓或该资产当前的完整状态,而非增量:持仓qty为0表示已平仓(单向持仓模式下平仓后side为空字符串),余额free与frozen均为0表示该资产已清空;若交易所的余额推送中没有口径一致的可用或冻结数据,对应字段为null(推送后不会再调用REST接口补齐)。字段与exchange.GetAssets、exchange.GetPositions口径一致;profit、margin、liqPx、markPx、leverage、equity、upnl在交易所未推送时不出现(而不是为0)。snapshot为true表示订阅生效或断线重连后推送的账户快照,快照之后,同一事件流上会推送一条channel为private的初始化状态事件。

成交回报(kind为19,channel为fills)来自private订阅,与订单回报位于同一个事件流上,表示自己订单的一笔成交:

  • 先推送成交,再推送反映该成交的订单回报:当收到的订单回报中filledQty增加时,构成该增量的成交均已收到;订单全部成交时,其各笔成交的qty之和等于订单的filledQty。
  • orderId与订单回报的id、ctx.order的返回值相同;tradeId始终为字符串,交易所未提供成交号时为以syn:开头的合成编号。side为订单方向,而非主动成交方向。
  • 没有快照:订阅生效和断线重连后不会补发历史成交,断线期间的成交也不会补发(订单回报中的累计成交量仍然准确),如需历史成交,请调用REST接口查询。交易所重连后重复推送的同一笔成交只会推送一次。同一账户的买单与卖单相互成交时,同一个tradeId会按两个订单各推送一条。
  • 仅当交易所的私有推送包含逐笔成交时才有成交回报,详见各交易所页面的说明;不支持的交易所只有订单回报。

异步下单应答(kind为20,type与channel均为ack)是ctx.orderAsync、ctx.cancelAsync的执行结果,位于内置事件流ctx.STREAM_ORDER_ACK上,仅投递给发起请求的线程,无需订阅:ctx.poll([ctx.STREAM_ORDER_ACK], timeout)只获取应答,ctx.poll([], timeout)也能获取到。status为unknown表示请求已通过WebSocket发出但未等到应答,订单可能已提交到交易所,请勿重复发送,应按clientId查询订单,或等待私有流的订单回报进行确认。

初始化状态事件(kind为35)与其所属订阅共用同一个事件流(stream相同),channel为所属订阅的频道(私有流快照的状态事件为private),用于表示:K线订阅已推送完首批历史K线、private订阅已推送完账户快照(prefillStatus为1),以及订阅被交易所或连接器拒绝(prefillStatus为2,message为失败原因,可能出现在任何频道)。在收到就绪状态之前,不要把已收到的少量实时K线当作完整的历史数据。

ctx.poll()只获取本线程订阅的事件:主线程以及threading.Thread()创建的线程(Python中为标准库的threading.Thread)各自订阅、各自获取,若过滤条件中使用其他线程的订阅对象或事件流ID,将永远获取不到事件。不同线程订阅了相同内容时,各自收到一份。网关状态事件会发送给在该交易所有订阅的每个线程,定时器事件只发送给主线程。

使用streams过滤仅表示本次只取这几路事件流:未指定的事件仍保留在本线程的队列中,最终需要一次不带过滤条件([])的调用将其取走。订单回报、成交回报、持仓、余额、网关状态、初始化状态、定时器、异步下单应答事件不会因读取不及时而被覆盖,无人读取时会持续积压,每个线程最多积压10240条,超出后丢弃最旧的事件。

等待期间仍会执行Promise、setTimeout回调以及WebSocket、TCP连接的读写;timeout为0时也会处理一轮这些任务。

JavaScript、Rust中,同一线程内的ctx.poll()与EventLoop从同一个事件队列获取事件,一个线程应只使用其中一种方式;JavaScript的线程可以使用options形式同时等待推送事件、线程消息等其他来源。Python的EventLoop()拥有独立的队列,与ctx.poll()互不争抢。

消费速度诊断:托管者为每个交易所分配的推送接收缓冲会随积压从1024条自动扩容至32768条,达到上限后将丢弃行情事件,并让连接器暂缓推送其他事件。ctx.connectors()返回已加载的交易所列表(顺序即ev.ex),每一项包含ex、label、name、symbol、interval、orderIdScope和feed字段,其中feed是进程启动以来的累计计数{ticksDropped, backpressure, ringCapacity}:分别为缓冲满时丢弃的行情条数、让连接器暂缓推送的次数、缓冲的当前容量(条)。这些数值增长较快说明策略处理事件过慢,应先检查每条事件的处理耗时;全市场tickers这类高频订阅应保持默认的仅保留最新模式。

Python中为ctx.poll(streams=None, timeout_ms=None):streams为None或[]时表示本线程订阅的全部事件流,否则为订阅对象、事件流ID、SubscriptionList或它们的(嵌套)列表;timeout_ms为None时一直等待,为0时不等待,为正整数时表示最长等待的毫秒数,超时返回None。事件为字典(dict的子类),字段与JavaScript的事件相同,ev["kind"]与ev.kind两种写法均可;深度事件提供ev.book(n)方法。实盘停止后(包括onexit()执行期间)调用会抛出StopError,正在等待的调用也会立即抛出。不支持options形式(传入字典会抛出TypeError):线程消息和exchange.Go()的完成通知分别通过EventLoop()和对应的对象获取。ctx.connectors()与JavaScript相同,也可以使用ctx.STREAM_STATUS、ctx.STREAM_TIMER、ctx.STREAM_ORDER_ACK。

Rust中为ctx::poll(streams: &[u32], timeout_ms: Option<u64>) -> ctx::Polled:streams传入sub.id()或内置的ctx::STREAM_TIMER、ctx::STREAM_STATUS、ctx::STREAM_ORDER_ACK,空切片表示本线程订阅的全部事件流,None表示一直等待;返回Polled::Event(ev)、Polled::Empty(超时)、Polled::Stopped(实盘停止)或Polled::Error(rc)。Rust的事件是原始结构体,引入sdk::ctx::EventExt后即可按事件类型读取,无需编写unsafe代码:ev.stream()、ev.channel()、ev.book(n)(深度事件),以及换算为浮点数的ev.ticker()、ev.trade()、ev.kline()、ev.order()、ev.position()、ev.balance()、ev.fill()(成交回报,FillView:order_id、trade_id、px、qty、fee(Option<f64>)、fee_ccy、side、liquidity、time)、ev.order_ack()(异步下单应答,OrderAckView:req_id、op、status、ok、via、id、client_id、error、reason、reason_text、exch_time(Option<i64>)、latency_ms、time)、ev.gateway()(网关状态)、ev.feed_status()(初始化状态,failed()时message为失败原因)。事件类型不符时返回None,字段与JavaScript的事件一一对应;ev.time()、ev.ts()(f64毫秒)与JavaScript事件的time、ts相同,所有事件均可调用,各类型化结果中的time字段即ev.time()。Rust的任意线程均可订阅并调用poll,各自只获取本线程订阅的事件。ctx::connectors()返回内容相同的JSON字符串。