ctx.subscribe
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.javascriptfunction 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) } } }pythondef 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)rustuse 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.javascriptfunction 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 }pythonimport 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 oncerustuse 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.
javascriptfunction 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) }pythonimport 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()rustfn 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.
javascriptfunction 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() }pythonimport 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()rustuse 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.javascriptfunction 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() }pythondef 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.
javascriptfunction 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) } } }pythondef 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.javascriptfunction 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() }pythondef 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()rustuse 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
| Type | Description |
| The subscription object. Its properties are |
Arguments
| Name | Type | Required | Description |
ex | object / number | Yes | The account to subscribe to. Pass an exchange object ( |
options | object | Yes | Subscription options |
See Also
Remarks
options fields:
| Field | Description |
|---|---|
| channel | Channel: ticker (default), bbo (best bid/offer), depth (order book), trade (tick-by-tick trades), kline (candlesticks), tickers (market-wide tickers), private (private account stream) |
| symbol | Symbol 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 |
| depth | Number 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 |
| interval | Candlestick period for the kline channel, in seconds, e.g. 60 |
| mode | When set to latest, only the most recent event is kept; older events not consumed in time are overwritten |
| buffer | Capacity 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
symbolis 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 toctx.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 toctx.subscribe()adds one reference, and each call tounsubscribe()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:
tickerandbbofor the same symbol are two subscriptions;depthof5and20are 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
prefillset totrue) 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, andpositions()andbalances()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 Matrixand the individual exchange pages. On unsupported exchanges, the subscription receives a failure status event (messagebegins withunsupported 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
bidandaskare0; if you need bid/ask prices, subscribe totickerorbboper 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
snapshotset totrue) is pushed first. It is followed by a status event whosekindis35and whosechannelisprivate. AprefillStatusof1means the snapshot is ready, and2means the snapshot failed (messagegives the reason, and the docker backs off and retries automatically). Real-time pushes (snapshotisfalse) 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
0means the position is closed, and available and frozen both at0mean the asset has been fully cleared. - A fill update (
kindis19,channelisfills) 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. Seectx.pollfor the field descriptions. - The
positionsandbalancesmethods 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 arenull. When you need the latest values, callexchange.GetAssetsorexchange.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
0uses the exchange's default level count, and passing1to255sets the number of levels you need. Each level count is a separate subscription: for the same symbol,depthof7and10are 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
depthof7uses 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
255or 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 whosemessagestarts withunsupported 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,
depthabove255, an unknown field inoptions(such as a misspelledintervel), an unknownchannel, a market data channel with nosymbol, asymbolpassed to thetickersorprivatechannel,modeandbufferpassed together, or an invalid element in thesymbolarray (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 whosekindis35andprefillStatusis2, with the failure reason inmessage. The subscription is then dead, and callingunsubscribe()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.