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.orderAsync
ctx.cancelAsync
Subscription
threading
Thread
ThreadLock
ThreadEvent
ThreadCondition
ThreadDict
Server
TA
Talib
OS
Structures
Built-in Variables and Constants
Conventions
Exchanges

Subscribes to WebSocket push data. You can subscribe to market data for a symbol, to the market-wide ticker, or to an account's private data stream. Returns a Subscription object. For a single symbol you can subscribe to the ticker, best bid/ask, order book, trades and K-lines. Market-wide, you can subscribe to the ticker. For the whole account you can subscribe to order updates, fill updates, positions and balances. When symbol is an array, one call subscribes to several symbols and returns an array of subscription objects. Retrieve events with ctx.poll and handle each event according to its channel. A subscription belongs to the thread that created it. Both the main thread and threads created with threading.Thread() can subscribe.

ctx.subscribe(ex, options)

Examples

  • Use the exchange object to subscribe to the ticker and the 20-level order book for the current trading pair, subscribe to the account's private stream as well, and handle each type of event separately according to channel.

    javascript
    function main() { // Pass the exchange object as the first argument; when a market data channel does not specify a symbol, the exchange object's current trading pair is used var tk = ctx.subscribe(exchange, {channel: "ticker"}) var book = ctx.subscribe(exchange, {channel: "depth", depth: 20}) // The private stream does not specify a symbol and covers the entire account: order updates, fill reports, positions and balances are all pushed through this stream var acct = ctx.subscribe(exchange, {channel: "private"}) Log("subscribed:", JSON.stringify(tk), JSON.stringify(book), JSON.stringify(acct)) while (true) { var ev = ctx.poll([], 1000) // [] means all event streams subscribed by this thread; wait at most 1 second if (!ev) { continue } if (ev.type === "status") { // Check gateway status and initialization status events first; their channel may be the same as that of data events if (ev.kind === 35 && ev.channel === "private") { Log("account snapshot ready:", ev.prefillStatus === 1, ev.message) } continue } if (ev.channel === "ticker") { Log("ticker", ev.symbol, "bid:", ev.bid, "ask:", ev.ask, "last:", ev.last) } else if (ev.channel === "depth") { var b = book.book(1) // Reads the same order book as ev.book(1) if (b.bids.length > 0 && b.asks.length > 0) { Log("bid1:", b.bids[0][0], "ask1:", b.asks[0][0]) } } else if (ev.channel === "fills") { Log("fill", ev.orderId, ev.side, ev.px, ev.qty, "fee:", ev.fee, ev.feeCcy, ev.liquidity) } else if (ev.channel === "orders") { Log("order", ev.id, ev.state, "filled:", ev.filledQty, "/", ev.qty) } } }
    python
    def main(): # Pass the exchange object as the first argument; when a market data channel does not specify a symbol, the exchange object's current trading pair is used tk = ctx.subscribe(exchange, {"channel": "ticker"}) book = ctx.subscribe(exchange, {"channel": "depth", "depth": 20}) # The private stream does not specify a symbol and covers the entire account: order updates, fill reports, positions and balances are all pushed through this stream acct = ctx.subscribe(exchange, {"channel": "private"}) Log("subscribed:", tk.info(), book.info(), acct.info()) while True: ev = ctx.poll([], 1000) # [] means all event streams subscribed by this thread; wait at most 1 second if ev is None: continue if ev.type == "status": # Check gateway status and initialization status events first if ev.kind == 35 and ev.channel == "private": Log("account snapshot ready:", ev.prefillStatus == 1, ev.message) continue if ev.channel == "ticker": Log("ticker", ev.symbol, "bid:", ev.bid, "ask:", ev.ask, "last:", ev.last) elif ev.channel == "depth": b = book.book(1) # Reads the same order book as ev.book(1) if b["bids"] and b["asks"]: Log("bid1:", b["bids"][0][0], "ask1:", b["asks"][0][0]) elif ev.channel == "fills": # When the exchange does not provide the fee, the event has no fee field Log("fill", ev.orderId, ev.side, ev.px, ev.qty, "fee:", ev.get("fee"), ev.feeCcy) elif ev.channel == "orders": Log("order", ev.id, ev.state, "filled:", ev.filledQty, "/", ev.qty)
    rust
    use sdk::ctx::EventExt; fn main() { // Exchange object as the first argument: when a market data channel does not specify a symbol, its current trading pair is used let book = ctx::subscribe(exchange, ctx::SubOpts::depth(20)).unwrap(); let acct = ctx::subscribe(exchange, ctx::SubOpts::private()).unwrap(); loop { match ctx::poll(&[], Some(1000)) { ctx::Polled::Event(ev) => match ev.channel() { "depth" => { if let Ok(b) = book.book(1) { if !b.bids.is_empty() && !b.asks.is_empty() { Log!("bid1:", b.bids[0].0, "ask1:", b.asks[0].0); } } } "fills" => { 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); } } "positions" | "balance" => { Log!("positions:", format!("{:?}", acct.positions())); } channel => Log!("event:", channel, "kind:", ev.kind), }, ctx::Polled::Stopped => break, _ => {} } } }
  • Batch-subscribe to tick-by-tick trades for three symbols: the call returns an array of subscription objects, and the whole array can be passed directly to ctx.poll(); after counting each symbol's trades over 1 minute, all subscriptions are cancelled at once.

    javascript
    function main() { // When symbol is an array, an array of subscription objects is returned in the same order; if any symbol is invalid, none of the batch is subscribed var symbols = ["BTC_USDT", "ETH_USDT", "SOL_USDT"] var subs = ctx.subscribe(exchange, {channel: "trade", symbol: symbols}) subs.forEach(function(s) { Log(s.symbol, "stream:", s.id) }) var count = {} var t0 = Date.now() while (Date.now() - t0 < 60000) { var ev = ctx.poll(subs, 1000) // An array of subscription objects can be passed directly to ctx.poll if (!ev) { continue } if (ev.kind === 35 && ev.prefillStatus === 2) { Log("subscription failed:", ev.symbol, ev.message) // Only this symbol's subscription failed; the other symbols are unaffected } else if (ev.kind === 4) { count[ev.symbol] = (count[ev.symbol] || 0) + 1 } } Log("trades in 60 seconds:", count) subs.unsubscribe() // Cancel all subscriptions in the array at once }
    python
    import time def main(): # When symbol is a list, a SubscriptionList is returned in the same order; if any symbol is invalid, none of the batch is subscribed symbols = ["BTC_USDT", "ETH_USDT", "SOL_USDT"] subs = ctx.subscribe(exchange, {"channel": "trade", "symbol": symbols}) for s in subs: Log(s.symbol, "stream:", s.id) count = {} t0 = time.time() while time.time() - t0 < 60: ev = ctx.poll(subs, 1000) # A SubscriptionList can be passed directly to ctx.poll if ev is None: continue if ev.kind == 35 and ev.prefillStatus == 2: Log("subscription failed:", ev.symbol, ev.message) elif ev.kind == 4: count[ev.symbol] = count.get(ev.symbol, 0) + 1 Log("trades in 60 seconds:", count) subs.unsubscribe() # Cancel all subscriptions in the list at once
    rust
    use sdk::ctx::EventExt; use std::collections::HashMap; fn main() { let symbols = ["BTC_USDT", "ETH_USDT", "SOL_USDT"]; // Returns handles in the same order as symbols; if any symbol is invalid, the whole batch returns Err let subs = ctx::subscribe_many(exchange, ctx::SubOpts::trade(), &symbols).unwrap(); let ids: Vec<u32> = subs.iter().map(|s| s.id()).collect(); let mut count: HashMap<u32, u64> = HashMap::new(); let t0 = Unix(); while Unix() - t0 < 60 { match ctx::poll(&ids, Some(1000)) { ctx::Polled::Event(ev) if ev.kind == EV_MD_TRADE => { *count.entry(ev.stream()).or_insert(0) += 1; } ctx::Polled::Stopped => return, _ => {} } } for (s, symbol) in subs.iter().zip(symbols) { Log!(symbol, "trades in 60 seconds:", count.get(&s.id()).copied().unwrap_or(0)); s.unsubscribe().unwrap(); } }
  • Subscribing in a thread: the child thread subscribes to the order books of two symbols on its own, and the related events are delivered only to that thread; the main thread subscribes to its own ticker, and the two do not interfere with each other.

    javascript
    function main() { var t = threading.Thread(function(symbols, ms) { // Subscribe inside the thread; events from these two subscriptions are delivered only to this thread var subs = ctx.subscribe(0, {channel: "depth", symbol: symbols, depth: 5}) var n = 0 var t0 = Date.now() while (Date.now() - t0 < ms) { var ev = ctx.poll([], 1000) // Only receives events subscribed by this thread if (ev && ev.kind === 3) { var b = ev.book(1) if (b.bids.length > 0 && b.asks.length > 0) { Log("thread", ev.symbol, "bid1:", b.bids[0][0], "ask1:", b.asks[0][0]) } n++ } } subs.unsubscribe() // Can only unsubscribe in the thread that created the subscription return n }, ["ETH_USDT" , "SOL_USDT"], 30000) var tk = ctx.subscribe(0, {channel: "ticker" , symbol: "BTC_USDT"}) var t0 = Date.now() while (Date.now() - t0 < 30000) { var ev = ctx.poll([], 1000) // Cannot receive depth events subscribed by the child thread if (ev && ev.kind === 1) { Log("main", ev.symbol, "last:", ev.last) } } tk.unsubscribe() Log("depth updates in the thread:", t.join().ret) }
    python
    import threading import time def depth_worker(symbols, seconds): # Subscribe inside the thread; events from these two subscriptions are delivered only to this thread subs = ctx.subscribe(0, {"channel": "depth" , "symbol": symbols, "depth": 5}) t0 = time.time() while time.time() - t0 < seconds: ev = ctx.poll([], 1000) # Only receives events subscribed by this thread if ev is not None and ev.kind == 3: b = ev.book(1) if b["bids"] and b["asks"]: Log("thread" , ev.symbol, "bid1:", b["bids"][0][0], "ask1:", b["asks"][0][0]) subs.unsubscribe() # Also unsubscribed automatically when the thread exits def main(): t = threading.Thread(target=depth_worker, args=(["ETH_USDT", "SOL_USDT" ], 30)) t.start() tk = ctx.subscribe(0, {"channel": "ticker", "symbol": "BTC_USDT"}) t0 = time.time() while time.time() - t0 < 30: ev = ctx.poll([], 1000) # Cannot receive depth events subscribed by the child thread if ev is not None and ev.kind == 1: Log("main", ev.symbol, "last:", ev.last) tk.unsubscribe() t.join()
    rust
    fn main() { let worker = std::thread::spawn(|| { // Subscribe inside the thread; events from these two subscriptions are delivered only to this thread let symbols = ["ETH_USDT", "SOL_USDT"]; let subs = ctx::subscribe_many(0u16, ctx::SubOpts::depth(5), &symbols).unwrap(); let mut n = 0u64; let t0 = Unix(); while Unix() - t0 < 30 { match ctx::poll(&[], Some(1000)) { // Only receives events subscribed by this thread ctx::Polled::Event(ev) if ev.kind == EV_MD_DEPTH5 => n += 1, ctx::Polled::Stopped => break, _ => {} } } for s in subs { let _ = s.unsubscribe(); // Can only unsubscribe in the thread that created the subscription } n }); let tk = ctx::subscribe(0u16, ctx::SubOpts::ticker().symbol("BTC_USDT")).unwrap(); let t0 = Unix(); while Unix() - t0 < 30 { match ctx::poll(&[tk.id()], Some(1000)) { ctx::Polled::Event(ev) => Log!("main ticker event, seq:", ev.seq), ctx::Polled::Stopped => return, _ => {} } } let _ = tk.unsubscribe(); Log!("depth updates in the thread:", worker.join().unwrap_or(0)); }
  • Subscribe to tickers for the whole market: one subscription receives tickers for every symbol on the exchange, and the number of symbols received is counted after 10 seconds. If the exchange does not support whole-market push, the subscription receives a status event that reports the failure.

    javascript
    function main() { // No symbol specified: one subscription receives tickers for every symbol on the exchange var all = ctx.subscribe(exchange, {channel: "tickers"}) var last = {} var t0 = Date.now() while (Date.now() - t0 < 10000) { var ev = ctx.poll(all, 1000) if (!ev) { continue } if (ev.kind === 35 && ev.prefillStatus === 2) { Log("tickers not supported:", ev.message) // The exchange does not support whole-market push return } if (ev.kind === 1) { last[ev.symbol] = ev.last // symbol is the symbol this ticker belongs to } } Log("symbols:" , Object.keys(last).length, "BTC_USDT last:", last["BTC_USDT"]) all.unsubscribe() }
    python
    import time def main(): # No symbol specified: one subscription receives tickers for every symbol on the exchange all_tickers = ctx.subscribe(exchange, {"channel": "tickers"}) last = {} t0 = time.time() while time.time() - t0 < 10: ev = ctx.poll(all_tickers, 1000) if ev is None: continue if ev.kind == 35 and ev.prefillStatus == 2: Log("tickers not supported:", ev.message) # The exchange does not support whole-market push return if ev.kind == 1: last[ev.symbol] = ev.last # symbol is the symbol this ticker belongs to Log("symbols:", len(last), "BTC_USDT last:" , last.get("BTC_USDT")) all_tickers.unsubscribe()
    rust
    use sdk::ctx::EventExt; use std::collections::HashMap; fn main() { // No symbol specified: one subscription receives tickers for every symbol on the exchange let all = ctx::subscribe(exchange, ctx::SubOpts::tickers()).unwrap(); let mut last: HashMap<String, f64> = HashMap::new(); let t0 = Unix(); while Unix() - t0 < 10 { match ctx::poll(&[all.id()], Some(1000)) { ctx::Polled::Event(ev) => { if let Some(st) = ev.feed_status() { if st.failed() { Log!("tickers not supported:" , st.message); // The exchange does not support whole-market push return; } } else if let Some(t) = ev.ticker() { last.insert(t.symbol, t.last); // symbol is the symbol this ticker belongs to } } ctx::Polled::Stopped => return, _ => {} } } Log!("symbols:", last.len(), "BTC_USDT last:", format!("{:?}", last.get("BTC_USDT"))); let _ = all.unsubscribe(); }
  • Identical subscriptions share one event stream: both subscription objects have the same id, and after one of them unsubscribes, the other keeps receiving events.

    javascript
    function main() { var a = ctx.subscribe(0, {channel: "trade", symbol: "BTC_USDT"}) var b = ctx.subscribe(0, {channel: "trade", symbol: "BTC_USDT"}) var c = ctx.subscribe(0, {channel: "ticker", symbol: "BTC_USDT"}) Log("a.id:", a.id, "b.id:", b.id, "c.id:", c.id) // a.id and b.id are the same, c.id is different a.unsubscribe() // b still references this subscription, so the trade feed is not interrupted var n = 0 while (n < 10) { var ev = ctx.poll(b, 5000) if (!ev) { Log("no trade in 5 seconds") continue } if (ev.kind === 4) { Log("trade", ev.tradeId, ev.side, ev.px, ev.qty) n++ } } b.unsubscribe() // Releases the last reference; only now is the subscription actually cancelled on the exchange c.unsubscribe() }
    python
    def main(): a = ctx.subscribe(0, {"channel": "trade", "symbol": "BTC_USDT"}) b = ctx.subscribe(0, {"channel": "trade", "symbol": "BTC_USDT"}) c = ctx.subscribe(0, {"channel": "ticker", "symbol": "BTC_USDT"}) Log("a.id:", a.id, "b.id:", b.id, "c.id:", c.id) # a.id and b.id are the same, c.id is different a.unsubscribe() # b still references this subscription, so the trade feed is not interrupted n = 0 while n < 10: ev = ctx.poll(b, 5000) if ev is None: Log("no trade in 5 seconds") continue if ev.kind == 4: Log("trade", ev.tradeId, ev.side, ev.px, ev.qty) n += 1 b.unsubscribe() # Releases the last reference; only now is the subscription actually cancelled on the exchange c.unsubscribe()
  • Handling a subscription rejected by the exchange: subscribing to a K-line interval the exchange does not support does not throw an exception; the reason for the failure is reported through that subscription's own event stream.

    javascript
    function main() { // Most exchanges do not support a 7-second K-line interval var k = ctx.subscribe(exchange, {channel: "kline", interval: 7}) while (true) { var ev = ctx.poll(k, 3000) if (!ev) { continue } if (ev.kind === 35) { if (ev.prefillStatus === 2) { Log("kline subscription failed:", ev.message) break } Log("kline history ready") } else if (ev.kind === 5 && ev.closed) { Log("closed bar", _D(ev.openTimeMs), ev.close) } } }
    python
    def main(): # Most exchanges do not support a 7-second K-line interval k = ctx.subscribe(exchange, {"channel": "kline", "interval": 7}) while True: ev = ctx.poll(k, 3000) if ev is None: continue if ev.kind == 35: if ev.prefillStatus == 2: Log("kline subscription failed:", ev.message) break Log("kline history ready") elif ev.kind == 5 and ev.closed: Log("closed bar", _D(ev.openTimeMs), ev.close)
  • Depth subscription at any number of levels: when subscribing to 7 levels, the smallest native exchange depth tier that is not less than 7 levels is selected, and the data is truncated to 7 levels before being pushed; an exception is thrown immediately if the number of levels exceeds 255; if it exceeds the maximum supported by the exchange, that subscription receives a failure status event.

    javascript
    function main() { var d7 = ctx.subscribe(exchange, {channel: "depth", depth: 7}) try { ctx.subscribe(exchange, {channel: "depth", depth: 300}) // More than 255 levels, throws an exception } catch (e) { Log("depth 300 rejected:", String(e)) } var n = 0 while (n < 10) { var ev = ctx.poll(d7, 3000) if (!ev) { continue } if (ev.kind === 35 && ev.prefillStatus === 2) { Log("depth 7 subscription failed:", ev.message) // e.g. the exchange does not support this number of levels break } if (ev.kind === 3) { var b = ev.book() // When n is not passed, returns the subscribed number of levels Log("levels:", b.bids.length, b.asks.length) n++ } } d7.unsubscribe() }
    python
    def main(): d7 = ctx.subscribe(exchange, {"channel": "depth", "depth": 7}) try: ctx.subscribe(exchange, {"channel": "depth", "depth": 300}) # More than 255 levels, throws an exception except Exception as e: Log("depth 300 rejected:", e) n = 0 while n < 10: ev = ctx.poll(d7, 3000) if ev is None: continue if ev.kind == 35 and ev.prefillStatus == 2: Log("depth 7 subscription failed:", ev.message) # e.g. the exchange does not support this number of levels break if ev.kind == 3: b = ev.book() # When n is not passed, returns the subscribed number of levels Log("levels:", len(b["bids"]), len(b["asks"])) n += 1 d7.unsubscribe()
    rust
    use sdk::ctx::EventExt; fn main() { let d7 = ctx::subscribe(exchange, ctx::SubOpts::depth(7)).unwrap(); if let Err(e) = ctx::subscribe(exchange, ctx::SubOpts::depth(300)) { Log!("depth 300 rejected:", e); // More than 255 levels, returns Err } let mut n = 0; while n < 10 { match ctx::poll(&[d7.id()], Some(3000)) { ctx::Polled::Event(ev) => { if let Some(st) = ev.feed_status() { if st.failed() { Log!("depth 7 subscription failed:", st.message); break; } } else if let Ok(b) = ev.book(7) { Log!("levels:", b.bids.len(), b.asks.len()); n += 1; } } ctx::Polled::Stopped => break, _ => {} } } let _ = d7.unsubscribe(); }

Returns

TypeDescription

Subscription object / Subscription array

The subscription object. Its properties are id (the event stream ID, which is the ev.stream of the events this subscription produces), ex, channel, symbol, depth and interval. Its methods are book(), positions(), balances() and unsubscribe(). Pass it directly to ctx.poll() to retrieve only this subscription's events. When options.symbol is an array, the call returns an array of subscription objects in the same order.

Arguments

NameTypeRequiredDescription

ex

object / number

Yes

The account to subscribe to. Pass an exchange object (exchange, exchanges[1], etc.) or the account's index in the exchanges array (0 means exchanges[0]; the numeric string "0" also works). Account labels are not supported.

options

object

Yes

Subscription options {channel, symbol, depth, interval, mode, buffer}. This is the only way to pass parameters, and the symbol is also set here. When symbol is an array, the call is a batch subscription. Each field is described below.

See Also

Remarks

options fields:

FieldDescription
channelChannel: ticker (default), bbo (best bid/offer), depth (order book), trade (tick-by-tick trades), kline (candlesticks), tickers (market-wide tickers), private (private account stream)
symbolSymbol code, e.g. BTC_USDT, BTC_USDT.swap; an array of symbol codes can also be passed (batch subscription). Required for market data channels; may be omitted when ex is an exchange object, in which case the exchange object's current trading pair is used. Not passed for the tickers and private channels
depthNumber of levels for the depth channel: if omitted or 0, the exchange's default depth is used; an integer from 1 to 255 specifies the number of levels, see below for details
intervalCandlestick period for the kline channel, in seconds, e.g. 60
modeWhen set to latest, only the most recent event is kept; older events not consumed in time are overwritten
bufferCapacity of the bounded buffer (positive integer, in events); on overflow, the oldest events are dropped

For symbol notation, see Symbol Codes. When ex is an exchange object and a market data channel is subscribed without symbol, the trading pair of that exchange object at the time ctx.subscribe() is called is used; for futures, this also includes the contract type (e.g. BTC_USDT.swap). Subsequent calls to exchange.SetCurrency() or exchange.SetContractType() do not change subscriptions already established. When ex is an index, symbol is not filled in automatically; the tickers (entire market) and private (entire account) channels never take a symbol.

Passing an array as symbol creates a batch subscription, subscribing to multiple symbols in a single call:

  • Returns an array of subscription objects in the same order as the input array; an array is returned even if the input array has only one element. When symbol is a string, a single subscription object is still returned.
  • The whole batch is validated up front: if any symbol or option is invalid, an exception is thrown for the entire batch and no symbol is subscribed.
  • Once validation passes, each element is an independent subscription with its own id, order book and reference count; if the exchange rejects one of the symbols, only that subscription receives a failure status event, while the others continue streaming as normal.
  • The returned array has an unsubscribe() method (non-enumerable) that unsubscribes all elements at once; the array can also be passed directly to ctx.poll().

A subscription's identity is determined jointly by the account, symbol, channel, depth and interval:

  • Within the same thread, subscriptions with exactly the same identity share one event stream, and the returned subscription objects have the same id. The docker manages them by reference counting: each call to ctx.subscribe() adds one reference, and each call to unsubscribe() on a subscription object removes one reference. The exchange is only actually unsubscribed when the last reference is released, so unsubscribing in one place does not interrupt the same subscription still in use elsewhere.
  • If any of these differs, it is a separate subscription with a separate event stream: ticker and bbo for the same symbol are two subscriptions; depth of 5 and 20 are also two subscriptions, each maintaining its own order book (exchanges may push different depth levels at different frequencies, so the docker does not merge them into the largest depth).
  • Event stream IDs only increase within a single run and are never reused; resubscribing after unsubscribing everything yields a new ID.

A subscription belongs to the thread that called ctx.subscribe(). Both the main thread and threads created by threading.Thread() (the standard library threading.Thread in Python, any thread in Rust) can subscribe and receive events:

  • Events of a subscription are delivered only to the thread that subscribed to it, and ctx.poll() only receives events of subscriptions made by the current thread. If multiple threads each need to receive their own events, each thread should subscribe to the streams it needs.
  • When different threads subscribe to content with the same identity, the exchange is subscribed only once, and there is only one copy of the order book, candlestick cache and account state; however, each thread gets its own subscription object (with a different id) and its own independent copy of the events, with independent delivery modes, so a slow consumer in one thread does not affect the others. The exchange is only actually unsubscribed after all threads have unsubscribed.
  • When a thread subscribes later to a candlestick stream that is already streaming, it first receives the cached candlesticks (with prefill set to true) and the most recent initialization status event; the same applies to the private stream: if the account snapshot is ready, the thread immediately receives the full snapshot and the ready event, and positions() and balances() can be read right away. Other channels start receiving from the next push.
  • Only the thread that owns a subscription can unsubscribe it: if another thread calls unsubscribe() on that subscription object, an exception is thrown and the subscription remains active. When a thread exits, all its subscriptions are automatically unsubscribed; the same streams shared with other threads are unaffected.
  • Gateway status events are sent to every thread that has subscriptions on that exchange; timer events are sent only to the main thread.

The tickers channel subscribes to tickers for the entire market: no symbol is needed, and a single subscription receives tickers for all symbols on that exchange. The event format is the same as ticker events (kind is 1), with channel set to tickers and symbol set to the corresponding symbol of each event.

  • Supported only when the exchange provides a market-wide push; for each exchange's support, see the "Push" column of the market data group in the Capability Matrix and the individual exchange pages. On unsupported exchanges, the subscription receives a failure status event (message begins with unsupported channel: tickers), and it does not automatically fall back to per-symbol subscriptions.
  • On some exchanges the market-wide push does not include the best bid and best ask prices, in which case bid and ask are 0; if you need bid/ask prices, subscribe to ticker or bbo per symbol.
  • By default only the latest event is kept per symbol: even with a slow consumer, you always get the latest ticker for every symbol.

private is the account's private stream. It takes no symbol, covers the whole account, and each account has only one such stream. Order updates, fill updates, positions and balances are all pushed on this one event stream, and the matching event channel values are orders, fills, positions and balance:

  • When the subscription takes effect (and after every reconnect), an account snapshot (positions and balances, with snapshot set to true) is pushed first. It is followed by a status event whose kind is 35 and whose channel is private. A prefillStatus of 1 means the snapshot is ready, and 2 means the snapshot failed (message gives the reason, and the docker backs off and retries automatically). Real-time pushes (snapshot is false) start only after this. Order updates and fill updates have no snapshot.
  • Every position or balance push carries the full current state of that position or asset, not an increment: a position quantity of 0 means the position is closed, and available and frozen both at 0 mean the asset has been fully cleared.
  • A fill update (kind is 19, channel is fills) is one execution of your own order. It contains the order ID, the trade ID, the price, quantity and fee of that execution, and whether it was maker or taker. The fill update is pushed first, followed by the order update that reflects the fill. Historical fills are not resent when the subscription takes effect or after a reconnect, and fills are pushed only when the exchange's private feed includes individual executions. See ctx.poll for the field descriptions.
  • The positions and balances methods of the subscription object return the latest positions and balances that the docker maintains from the pushes.
  • The docker does not call REST endpoints after a push to fill in missing data: after the snapshot, only what the exchange pushes is delivered. Changes the exchange does not push do not appear in the pushes (for example, some exchanges do not push balances when orders are placed or canceled, and some do not push positions and only provide a snapshot). If a balance push has no available and frozen values on the same basis as exchange.GetAssets, those fields are null. When you need the latest values, call exchange.GetAssets or exchange.GetPositions. See each exchange's page for what its real-time pushes cover.

Subscriptions are lazy: ctx.subscribe() returns immediately and registers the subscription, and the connection to the exchange is made and the subscription request sent only on the first call to ctx.poll(). Changes in connection state are reported through gateway status events (kind is 33, channel is gateway).

The level count of the depth channel can be any integer from 0 to 255:

  • Omitting it or passing 0 uses the exchange's default level count, and passing 1 to 255 sets the number of levels you need. Each level count is a separate subscription: for the same symbol, depth of 7 and 10 are two subscriptions with two order books.
  • The docker picks the smallest native exchange depth that covers the requested level count, then truncates it to that count before pushing. For example, if the exchange only provides 5-, 10- and 20-level snapshots, a subscription with depth of 7 uses the 10-level snapshot and keeps only the top 7 levels. Several subscriptions on the same symbol can share one native subscription, each truncated separately.
  • If the level count is above 255 or is not an integer, ctx.subscribe() throws an exception. If it is above the maximum the exchange supports, no exception is thrown; instead that subscription receives a failed status event whose message starts with unsupported depth. See the exchange pages for each exchange's native depths and limits.

A subscription can fail in two ways:

  • Invalid arguments throw an exception, for example: not exactly 2 arguments, a second argument that is not an object, depth above 255, an unknown field in options (such as a misspelled intervel), an unknown channel, a market data channel with no symbol, a symbol passed to the tickers or private channel, mode and buffer passed together, or an invalid element in the symbol array (in which case nothing in the batch is subscribed).
  • When the exchange or the connector rejects the subscription (for example, an unsupported channel, K-line period or depth level count), ctx.subscribe() does not throw. Instead, that subscription's own event stream receives a status event whose kind is 35 and prefillStatus is 2, with the failure reason in message. The subscription is then dead, and calling unsubscribe() does nothing.

What happens when events are not taken out in time (delivery mode): by default ticker, bbo and depth keep only the latest event, and tickers keeps only the latest event per symbol. trade and kline buffer the latest 1024 events by default and drop the oldest on overflow. You can change this with mode or buffer (use one, not both). Order updates, fill updates, positions, balances, gateway status and initialization status events are never dropped, so you must keep calling ctx.poll() after subscribing. The order book is updated continuously inside the docker no matter how fast events are taken out, so book() always returns the latest complete order book. To see whether the docker's push receive buffer is backing up, check the ctx.connectors() counters described in ctx.poll.

Pushes and REST endpoints are completely independent: subscriptions only affect the events that ctx.poll() receives. Functions such as exchange.GetTicker, exchange.GetDepth, exchange.GetTrades, exchange.GetRecords and exchange.GetAccount call the exchange's REST endpoints directly every time and do not read pushed data. Whether you subscribe, and to what, does not change their results.

In JavaScript and Rust strategies, ctx.poll() and EventLoop read events from the same event queue when they run in the same thread. A thread should therefore use only one of the two to read events. In Python, EventLoop() has its own queue. That queue holds only the market data and order updates that brokerage-type exchanges subscribe to automatically, plus completion notices from concurrent tasks such as exchange.Go(). So it does not compete with ctx.poll(). To find out whether an exchange supports push data and which channels it offers, see the Capability Matrix and each exchange's own page.

Python strategies are written the same way as JavaScript ones. ctx.subscribe(ex, options) accepts only these two positional arguments and raises TypeError if you pass more or fewer arguments or use keyword arguments. options is a dictionary with the same fields and validation rules, and the dictionary you pass in is not modified. When options["symbol"] is a list (or tuple), the call returns a SubscriptionList. This is a subclass of list whose elements are in the same order as the input, and calling its unsubscribe() unsubscribes all of its elements at once. Subscription object attributes are read-only, and info() returns a dictionary with the same fields. When a thread created with threading.Thread ends, its subscriptions are unsubscribed automatically. Threads created without threading.Thread, for example with _thread.start_new_thread(), can also subscribe. However, their subscriptions are not unsubscribed automatically when they end, so you must call unsubscribe() yourself.

Rust strategies use the functions of the same name in the ctx module: ctx::subscribe(ex, opts: ctx::SubOpts) -> Result<ctx::Sub, String>. Here ex can be an exchange object (exchange, exchanges[i]) or a u16 index. On failure the function returns Err and does not panic. Subscription options are built with ctx::SubOpts::ticker(), bbo(), trade(), depth(n), kline(seconds), tickers() or private(), followed by chained calls to .symbol("BTC_USDT"), .latest() and .buffer(n). For batch subscriptions, use ctx::subscribe_many(ex, opts, &["BTC_USDT", "ETH_USDT"]) -> Result<Vec<ctx::Sub>, String>, which also validates the whole batch first. The returned ctx::Sub can be copied. Its methods are id() (pass it to ctx::poll(&[sub.id()], ...)), book(n), positions(), balances() and unsubscribe(). Reference counts are based on the number of ctx::subscribe() calls, so each call must be matched by one call to unsubscribe(). Any Rust thread can subscribe, and the subscription belongs to the thread that created it.