use std::collections::BTreeMap;
use adapters::{
Client,
bybit::{BybitOption, BybitWsHandler, BybitWsUrlBase},
generics::ws::{WsConnection, WsError},
};
use jiff::Timestamp;
use v_utils::trades::Pair;
use crate::{BookShape, BookUpdate, ExchangeStream, Instrument, PrecisionPriceQty, core::Sequence};
#[derive(Debug)]
pub struct BookConnection {
connection: WsConnection<BybitWsHandler>,
pair_precisions: BTreeMap<Pair, PrecisionPriceQty>,
last_seq: BTreeMap<Pair, BybitSeq>,
}
impl BookConnection {
pub fn try_new(client: &Client, pairs: &[Pair], instrument: Instrument, pair_precisions: BTreeMap<Pair, PrecisionPriceQty>) -> Result<Self, WsError> {
let vec_topic_str = pairs.iter().map(|p| format!("orderbook.1000.{}", p.fmt_bybit())).collect::<Vec<_>>();
let url_suffix = match instrument {
Instrument::Perp => "/v5/public/linear",
Instrument::Spot => "/v5/public/spot",
_ => unimplemented!(),
};
let connection = client.ws_connection(url_suffix, vec![BybitOption::WsUrl(BybitWsUrlBase::Bybit), BybitOption::WsTopics(vec_topic_str)])?;
Ok(Self {
connection,
pair_precisions,
last_seq: BTreeMap::new(),
})
}
}
#[async_trait::async_trait]
impl ExchangeStream for BookConnection {
type Item = BookUpdate;
async fn next(&mut self) -> Result<Vec<Self::Item>, WsError> {
let batch = self.connection.next().await?;
let mut out = Vec::with_capacity(batch.len());
let now = Timestamp::now();
for content_event in batch {
let parsed: BybitBookData = serde_json::from_value(content_event.data).expect("Exchange responded with invalid book event");
let pair_str = content_event.topic.rsplit('.').next().expect("Bybit orderbook topic always contains '.'");
let pair: Pair = pair_str
.try_into()
.unwrap_or_else(|_| panic!("failed to parse pair from orderbook topic: {}", content_event.topic));
let prec = *self.pair_precisions.get(&pair).unwrap_or_else(|| panic!("{pair} not in pair_precisions"));
let parse_level = |(p, q): (String, String)| -> (i32, u32) { (prec.parse_price(&p), prec.parse_qty(&q)) };
let is_snapshot = match content_event.event_type.as_str() {
"snapshot" => true,
"delta" => false,
other => panic!("Bybit sent unexpected book event type: {other}"),
};
let seq = BybitSeq { u: parsed.u, is_snapshot };
if let Some(prev) = self.last_seq.get(&pair)
&& seq.has_gap_from_prev(prev)
{
tracing::warn!(pair = %pair, prev_u = prev.u, next_u = seq.u, "Bybit orderbook gap detected on delta chain");
}
self.last_seq.insert(pair, seq);
let shape = BookShape {
ts_event: content_event.time,
ts_init: now,
ts_last: now,
prec,
bids: parsed.b.into_iter().map(parse_level).collect(),
asks: parsed.a.into_iter().map(parse_level).collect(),
};
out.push(if is_snapshot { BookUpdate::Snapshot(shape) } else { BookUpdate::BatchDelta(shape) });
}
Ok(out)
}
}
#[derive(Clone, Copy, Debug)]
pub struct BybitSeq {
pub u: u64,
pub is_snapshot: bool,
}
#[derive(Clone, Debug, serde::Deserialize)]
struct BybitBookData {
b: Vec<(String, String)>,
a: Vec<(String, String)>,
u: u64,
}
impl Sequence for BybitSeq {
fn has_gap_from_prev(&self, prev: &Self) -> bool {
if self.is_snapshot || prev.is_snapshot {
return false;
}
self.u != prev.u + 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delta_chain_no_gap() {
let prev = BybitSeq { u: 100, is_snapshot: false };
let next = BybitSeq { u: 101, is_snapshot: false };
assert!(!next.has_gap_from_prev(&prev));
}
#[test]
fn delta_chain_gap() {
let prev = BybitSeq { u: 100, is_snapshot: false };
let next = BybitSeq { u: 102, is_snapshot: false };
assert!(next.has_gap_from_prev(&prev));
}
#[test]
fn snapshot_boundary_never_gapped() {
let prev = BybitSeq { u: 100, is_snapshot: false };
let next = BybitSeq { u: 1, is_snapshot: true };
assert!(!next.has_gap_from_prev(&prev));
let prev = BybitSeq { u: 1, is_snapshot: true };
let next = BybitSeq { u: 500, is_snapshot: false };
assert!(!next.has_gap_from_prev(&prev));
}
}