Streaming model
July 1, 2026 · View on GitHub
wickra-exchange is pull-based. The public API is synchronous — a real client drives an async socket internally, but every call (and every language binding) blocks, so the consumer owns its loop. This is what lets the C ABI carry streaming to every binding, including single-threaded R, as a plain call.
Subscribe, then poll
use wickra_exchange::{MarketData, Symbol};
exchange.subscribe_trades(&Symbol::new("BTC", "USDT"))?;
loop {
for event in exchange.poll_events() {
// handle Trade / BookSnapshot / BookDelta / OrderUpdate / BalanceUpdate
}
}
subscribe_trades/subscribe_book/subscribe_tickeropen a subscription that fills an internal buffer.poll_eventsdrains everything buffered since the last call and returns an empty vector when nothing is pending (never blocks).- Order-book streams maintain a local ladder via
OrderBookBuilder, which detects sequence gaps and signals a resync (BookUpdate::Gap).
Events
Event is a tagged enum: Trade, Ticker, BookSnapshot, BookDelta,
OrderUpdate, BalanceUpdate, Subscribed, Disconnected, Reconnected.
Execution events (order/balance updates) flow through the same poll_events
drain as market data.
Reconnect and the dead-man's-switch
When the peer closes a stream, the client transparently reconnects and replays
every subscription — the consumer only sees a Disconnected followed by a
Reconnected event, and the buffer keeps filling.
For live trading, pair that with a dead-man's-switch (DeadMansSwitch): arm
it and feed it a heartbeat on every message; if the deadline passes without one,
is_expired fires and you cancel every resting order (via the venue's cancel-all
endpoint, or PaperExchange::cancel_all in simulation) so nothing works
unattended after a disconnect.
use wickra_exchange::DeadMansSwitch;
use std::time::Duration;
let mut guard = DeadMansSwitch::new(Duration::from_secs(10));
guard.heartbeat(now_ms); // on every successful message
if guard.is_expired(now_ms) {
exchange_cancel_all(); // heartbeat lost -> pull all orders
}