use crate::orderbooks::delta::NormalizedDelta;
use crate::orderbooks::target::OrderbookUpdateType;
use serde::Deserialize;
pub type BybitOrderbookType = OrderbookUpdateType;
#[derive(Deserialize, Debug, Clone)]
pub struct BybitOrderbookResponse {
pub topic: String,
#[serde(rename = "type")]
pub ty: String,
#[serde(rename = "ts")]
pub orderbook_ts: u64,
pub data: BybitOrderbookData,
#[serde(default)]
pub cts: Option<u64>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct BybitOrderbookData {
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "b")]
pub bids: Vec<BybitPriceLevel>,
#[serde(rename = "a")]
pub asks: Vec<BybitPriceLevel>,
#[serde(rename = "u")]
pub update_id: u64,
#[serde(rename = "seq")]
pub sequence: u64,
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct BybitPriceLevel(pub String, pub String);
impl BybitPriceLevel {
#[inline]
pub fn new(price: impl Into<String>, size: impl Into<String>) -> Self {
Self(price.into(), size.into())
}
#[inline]
pub fn price(&self) -> f64 {
self.0.parse().unwrap_or(0.0)
}
#[inline]
pub fn size(&self) -> f64 {
self.1.parse().unwrap_or(0.0)
}
#[inline]
pub fn price_str(&self) -> &str {
&self.0
}
#[inline]
pub fn size_str(&self) -> &str {
&self.1
}
#[inline]
pub fn is_deletion(&self) -> bool {
self.1 == "0" || self.1 == "0.0" || self.1 == "0.00"
}
}
impl BybitOrderbookData {
#[inline]
pub fn update_type(ty: &str) -> OrderbookUpdateType {
match ty {
"snapshot" => OrderbookUpdateType::Snapshot,
_ => OrderbookUpdateType::Delta,
}
}
#[inline]
pub fn best_bid(&self) -> Option<&BybitPriceLevel> {
self.bids.first()
}
#[inline]
pub fn best_ask(&self) -> Option<&BybitPriceLevel> {
self.asks.first()
}
pub fn mid_price(&self) -> Option<f64> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some((bid.price() + ask.price()) / 2.0),
_ => None,
}
}
pub fn spread(&self) -> Option<f64> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some(ask.price() - bid.price()),
_ => None,
}
}
pub fn spread_bps(&self) -> Option<f64> {
let spread = self.spread()?;
let mid = self.mid_price()?;
if mid > 0.0 {
Some((spread / mid) * 10_000.0)
} else {
None
}
}
pub fn total_bid_volume(&self) -> f64 {
self.bids.iter().map(|l| l.size()).sum()
}
pub fn total_ask_volume(&self) -> f64 {
self.asks.iter().map(|l| l.size()).sum()
}
pub fn volume_imbalance(&self) -> Option<f64> {
let bid = self.total_bid_volume();
let ask = self.total_ask_volume();
let total = bid + ask;
if total > 0.0 {
Some((bid - ask) / total)
} else {
None
}
}
}
impl BybitOrderbookResponse {
#[inline]
pub fn message_type(&self) -> OrderbookUpdateType {
if self.ty == "snapshot" {
OrderbookUpdateType::Snapshot
} else {
OrderbookUpdateType::Delta
}
}
#[inline]
pub fn is_snapshot(&self) -> bool {
self.ty == "snapshot"
}
#[inline]
pub fn is_delta(&self) -> bool {
self.ty == "delta"
}
#[inline]
pub fn symbol(&self) -> Option<&str> {
self.topic.split('.').nth(2)
}
#[inline]
pub fn depth(&self) -> Option<u32> {
self.topic.split('.').nth(1)?.parse().ok()
}
#[inline]
pub fn update_id(&self) -> u64 {
self.data.update_id
}
#[inline]
pub fn sequence(&self) -> u64 {
self.data.sequence
}
pub fn exchange_latency_ms(&self) -> Option<u64> {
self.cts.map(|cts| cts.saturating_sub(self.orderbook_ts))
}
pub fn exchange_name(&self) -> String {
"Bybit".to_string()
}
pub fn to_normalized(&self) -> Option<NormalizedDelta> {
if self.data.bids.is_empty() && self.data.asks.is_empty() && self.data.symbol.is_empty() {
return None;
}
Some(NormalizedDelta {
symbol: self.data.symbol.clone(),
bids: self
.data
.bids
.iter()
.map(|pl| (pl.0.clone(), pl.1.clone()))
.collect(),
asks: self
.data
.asks
.iter()
.map(|pl| (pl.0.clone(), pl.1.clone()))
.collect(),
update_id: self.data.update_id,
sequence: self.data.sequence,
is_snapshot: self.ty == "snapshot",
})
}
}