use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PriceLevel {
pub price: f64,
pub qty: f64,
#[serde(default)]
pub timestamp: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum OrderbookUpdateType {
Snapshot,
Update,
}
impl std::fmt::Display for OrderbookUpdateType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OrderbookUpdateType::Snapshot => write!(f, "snapshot"),
OrderbookUpdateType::Update => write!(f, "update"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderbookUpdate {
#[serde(default)]
pub channel: String,
#[serde(rename = "type")]
pub update_type: OrderbookUpdateType,
pub data: Vec<OrderbookData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderbookData {
pub symbol: String,
#[serde(default)]
pub bids: Vec<PriceLevelRaw>,
#[serde(default)]
pub asks: Vec<PriceLevelRaw>,
#[serde(default)]
pub checksum: u32,
#[serde(default)]
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceLevelRaw {
#[serde(deserialize_with = "deserialize_number")]
pub price: f64,
#[serde(deserialize_with = "deserialize_number")]
pub qty: f64,
}
fn deserialize_number<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct NumberVisitor;
impl<'de> Visitor<'de> for NumberVisitor {
type Value = f64;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a number or string representation of a number")
}
fn visit_f64<E>(self, value: f64) -> Result<f64, E> {
Ok(value)
}
fn visit_i64<E>(self, value: i64) -> Result<f64, E> {
Ok(value as f64)
}
fn visit_u64<E>(self, value: u64) -> Result<f64, E> {
Ok(value as f64)
}
fn visit_str<E>(self, value: &str) -> Result<f64, E>
where
E: de::Error,
{
value.parse::<f64>().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(NumberVisitor)
}
impl PriceLevelRaw {
pub fn to_price_level(&self) -> PriceLevel {
PriceLevel {
price: self.price,
qty: self.qty,
timestamp: 0.0,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Orderbook {
pub symbol: String,
pub bids: BTreeMap<OrderedFloat, f64>,
pub asks: BTreeMap<OrderedFloat, f64>,
pub timestamp: String,
pub sequence: u64,
#[cfg(feature = "checksum")]
#[serde(default)]
pub last_checksum: u32,
#[cfg(feature = "checksum")]
#[serde(default = "default_checksum_valid")]
pub checksum_valid: bool,
}
#[cfg(feature = "checksum")]
fn default_checksum_valid() -> bool {
true
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct OrderedFloat(pub f64);
impl PartialEq for OrderedFloat {
fn eq(&self, other: &Self) -> bool {
self.0.to_bits() == other.0.to_bits()
}
}
impl Eq for OrderedFloat {}
impl PartialOrd for OrderedFloat {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedFloat {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0
.partial_cmp(&other.0)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
impl std::hash::Hash for OrderedFloat {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
impl Orderbook {
pub fn new(symbol: String) -> Self {
Self {
symbol,
bids: BTreeMap::new(),
asks: BTreeMap::new(),
timestamp: String::new(),
sequence: 0,
#[cfg(feature = "checksum")]
last_checksum: 0,
#[cfg(feature = "checksum")]
checksum_valid: true,
}
}
pub fn apply_update(&mut self, data: &OrderbookData) {
self.timestamp = data.timestamp.clone();
self.sequence += 1;
for level in &data.bids {
if level.qty == 0.0 {
self.bids.remove(&OrderedFloat(level.price));
} else {
self.bids.insert(OrderedFloat(level.price), level.qty);
}
}
for level in &data.asks {
if level.qty == 0.0 {
self.asks.remove(&OrderedFloat(level.price));
} else {
self.asks.insert(OrderedFloat(level.price), level.qty);
}
}
#[cfg(feature = "checksum")]
if data.checksum != 0 {
self.last_checksum = data.checksum;
self.checksum_valid = self.validate_checksum(data.checksum);
}
}
#[cfg(feature = "checksum")]
pub fn apply_update_validated(&mut self, data: &OrderbookData) -> bool {
self.apply_update(data);
self.checksum_valid
}
#[cfg(not(feature = "checksum"))]
pub fn apply_update_validated(&mut self, data: &OrderbookData) -> bool {
self.apply_update(data);
true
}
pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
self.bids
.iter()
.rev()
.take(n)
.map(|(price, qty)| PriceLevel {
price: price.0,
qty: *qty,
timestamp: 0.0,
})
.collect()
}
pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
self.asks
.iter()
.take(n)
.map(|(price, qty)| PriceLevel {
price: price.0,
qty: *qty,
timestamp: 0.0,
})
.collect()
}
pub fn best_bid(&self) -> Option<f64> {
self.bids.keys().next_back().map(|p| p.0)
}
pub fn best_ask(&self) -> Option<f64> {
self.asks.keys().next().map(|p| p.0)
}
pub fn spread(&self) -> Option<f64> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some(ask - bid),
_ => None,
}
}
pub fn mid_price(&self) -> Option<f64> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some((bid + ask) / 2.0),
_ => None,
}
}
pub fn total_bid_volume(&self) -> f64 {
self.bids.values().sum()
}
pub fn total_ask_volume(&self) -> f64 {
self.asks.values().sum()
}
#[cfg(feature = "analytics")]
pub fn imbalance(&self) -> f64 {
let bid_vol = self.total_bid_volume();
let ask_vol = self.total_ask_volume();
let total = bid_vol + ask_vol;
if total == 0.0 {
return 0.0;
}
(bid_vol - ask_vol) / total
}
#[cfg(feature = "analytics")]
pub fn imbalance_top_n(&self, n: usize) -> f64 {
let bid_vol: f64 = self.bids.iter().rev().take(n).map(|(_, qty)| qty).sum();
let ask_vol: f64 = self.asks.iter().take(n).map(|(_, qty)| qty).sum();
let total = bid_vol + ask_vol;
if total == 0.0 {
return 0.0;
}
(bid_vol - ask_vol) / total
}
#[cfg(feature = "analytics")]
pub fn imbalance_within_depth(&self, depth_percent: f64) -> Option<f64> {
let mid = self.mid_price()?;
let lower_bound = mid * (1.0 - depth_percent);
let upper_bound = mid * (1.0 + depth_percent);
let bid_vol: f64 = self
.bids
.iter()
.filter(|(price, _)| price.0 >= lower_bound)
.map(|(_, qty)| qty)
.sum();
let ask_vol: f64 = self
.asks
.iter()
.filter(|(price, _)| price.0 <= upper_bound)
.map(|(_, qty)| qty)
.sum();
let total = bid_vol + ask_vol;
if total == 0.0 {
return Some(0.0);
}
Some((bid_vol - ask_vol) / total)
}
#[cfg(feature = "analytics")]
pub fn imbalance_metrics(&self) -> ImbalanceMetrics {
let bid_vol = self.total_bid_volume();
let ask_vol = self.total_ask_volume();
let total = bid_vol + ask_vol;
ImbalanceMetrics {
bid_volume: bid_vol,
ask_volume: ask_vol,
imbalance_ratio: if total > 0.0 {
(bid_vol - ask_vol) / total
} else {
0.0
},
bid_ask_ratio: if ask_vol > 0.0 {
bid_vol / ask_vol
} else {
f64::INFINITY
},
bid_levels: self.bids.len(),
ask_levels: self.asks.len(),
}
}
#[cfg(feature = "checksum")]
pub fn calculate_checksum(&self) -> u32 {
let mut data = String::new();
for (price, qty) in self.asks.iter().take(10) {
data.push_str(&Self::format_for_checksum(price.0));
data.push_str(&Self::format_for_checksum(*qty));
}
for (price, qty) in self.bids.iter().rev().take(10) {
data.push_str(&Self::format_for_checksum(price.0));
data.push_str(&Self::format_for_checksum(*qty));
}
crc32fast::hash(data.as_bytes())
}
#[cfg(feature = "checksum")]
pub fn validate_checksum(&self, expected: u32) -> bool {
self.calculate_checksum() == expected
}
#[cfg(feature = "checksum")]
pub fn checksum_validation(&self, expected: u32) -> ChecksumValidation {
let calculated = self.calculate_checksum();
ChecksumValidation {
expected,
calculated,
valid: expected == calculated,
bid_count: self.bids.len(),
ask_count: self.asks.len(),
}
}
#[cfg(feature = "checksum")]
fn format_for_checksum(value: f64) -> String {
let formatted = format!("{:.10}", value);
let without_decimal = formatted.replace('.', "");
let trimmed = without_decimal.trim_start_matches('0');
if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.trim_end_matches('0').to_string()
}
}
}
#[cfg(feature = "analytics")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImbalanceMetrics {
pub bid_volume: f64,
pub ask_volume: f64,
pub imbalance_ratio: f64,
pub bid_ask_ratio: f64,
pub bid_levels: usize,
pub ask_levels: usize,
}
#[cfg(feature = "analytics")]
impl ImbalanceMetrics {
pub fn is_bullish(&self, threshold: f64) -> bool {
self.imbalance_ratio > threshold
}
pub fn is_bearish(&self, threshold: f64) -> bool {
self.imbalance_ratio < -threshold
}
pub fn signal(&self, threshold: f64) -> ImbalanceSignal {
if self.imbalance_ratio > threshold {
ImbalanceSignal::Bullish
} else if self.imbalance_ratio < -threshold {
ImbalanceSignal::Bearish
} else {
ImbalanceSignal::Neutral
}
}
}
#[cfg(feature = "analytics")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImbalanceSignal {
Bullish,
Bearish,
Neutral,
}
#[cfg(feature = "checksum")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChecksumValidation {
pub expected: u32,
pub calculated: u32,
pub valid: bool,
pub bid_count: usize,
pub ask_count: usize,
}
#[cfg(feature = "checksum")]
impl ChecksumValidation {
pub fn is_valid(&self) -> bool {
self.valid
}
pub fn is_corrupted(&self) -> bool {
!self.valid
}
}
#[cfg(feature = "checksum")]
impl std::fmt::Display for ChecksumValidation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.valid {
write!(f, "✓ Checksum valid (0x{:08X})", self.expected)
} else {
write!(
f,
"✗ Checksum mismatch: expected 0x{:08X}, got 0x{:08X}",
self.expected, self.calculated
)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderbookSnapshot {
pub id: String,
pub symbol: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub bids: Vec<PriceLevel>,
pub asks: Vec<PriceLevel>,
pub sequence: u64,
}
impl OrderbookSnapshot {
pub fn from_orderbook(orderbook: &Orderbook, depth: usize) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
symbol: orderbook.symbol.clone(),
timestamp: chrono::Utc::now(),
bids: orderbook.top_bids(depth),
asks: orderbook.top_asks(depth),
sequence: orderbook.sequence,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_orderbook_new() {
let ob = Orderbook::new("BTC/USD".to_string());
assert_eq!(ob.symbol, "BTC/USD");
assert!(ob.bids.is_empty());
assert!(ob.asks.is_empty());
assert_eq!(ob.sequence, 0);
}
#[test]
fn test_orderbook_apply_update() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![
PriceLevelRaw {
price: 50000.0,
qty: 1.5,
},
PriceLevelRaw {
price: 49900.0,
qty: 2.0,
},
],
asks: vec![
PriceLevelRaw {
price: 50100.0,
qty: 1.0,
},
PriceLevelRaw {
price: 50200.0,
qty: 0.5,
},
],
checksum: 0,
timestamp: "2024-01-01T00:00:00Z".to_string(),
};
ob.apply_update(&update);
assert_eq!(ob.bids.len(), 2);
assert_eq!(ob.asks.len(), 2);
assert_eq!(ob.sequence, 1);
}
#[test]
fn test_orderbook_best_bid_ask() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![
PriceLevelRaw {
price: 50000.0,
qty: 1.5,
},
PriceLevelRaw {
price: 49900.0,
qty: 2.0,
},
],
asks: vec![
PriceLevelRaw {
price: 50100.0,
qty: 1.0,
},
PriceLevelRaw {
price: 50200.0,
qty: 0.5,
},
],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
assert_eq!(ob.best_bid(), Some(50000.0));
assert_eq!(ob.best_ask(), Some(50100.0));
}
#[test]
fn test_orderbook_spread() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 50000.0,
qty: 1.0,
}],
asks: vec![PriceLevelRaw {
price: 50100.0,
qty: 1.0,
}],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
assert_eq!(ob.spread(), Some(100.0));
assert_eq!(ob.mid_price(), Some(50050.0));
}
#[test]
fn test_orderbook_remove_level() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update1 = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 50000.0,
qty: 1.0,
}],
asks: vec![],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update1);
assert_eq!(ob.bids.len(), 1);
let update2 = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 50000.0,
qty: 0.0,
}],
asks: vec![],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update2);
assert_eq!(ob.bids.len(), 0);
}
#[test]
fn test_top_bids_asks() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![
PriceLevelRaw {
price: 50000.0,
qty: 1.0,
},
PriceLevelRaw {
price: 49900.0,
qty: 2.0,
},
PriceLevelRaw {
price: 49800.0,
qty: 3.0,
},
],
asks: vec![
PriceLevelRaw {
price: 50100.0,
qty: 1.0,
},
PriceLevelRaw {
price: 50200.0,
qty: 2.0,
},
PriceLevelRaw {
price: 50300.0,
qty: 3.0,
},
],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
let top_bids = ob.top_bids(2);
assert_eq!(top_bids.len(), 2);
assert_eq!(top_bids[0].price, 50000.0); assert_eq!(top_bids[1].price, 49900.0);
let top_asks = ob.top_asks(2);
assert_eq!(top_asks.len(), 2);
assert_eq!(top_asks[0].price, 50100.0); assert_eq!(top_asks[1].price, 50200.0);
}
#[test]
fn test_ordered_float() {
let a = OrderedFloat(1.5);
let b = OrderedFloat(2.5);
let c = OrderedFloat(1.5);
assert!(a < b);
assert_eq!(a, c);
assert!(b > a);
}
#[test]
fn test_price_level_raw_conversion() {
let raw = PriceLevelRaw {
price: 50000.50,
qty: 1.25,
};
let level = raw.to_price_level();
assert_eq!(level.price, 50000.50);
assert_eq!(level.qty, 1.25);
}
#[test]
fn test_deserialize_number_formats() {
let json = r#"{"price": 50000.0, "qty": 1.5}"#;
let level: PriceLevelRaw = serde_json::from_str(json).unwrap();
assert_eq!(level.price, 50000.0);
assert_eq!(level.qty, 1.5);
let json_str = r#"{"price": "49999.99", "qty": "2.5"}"#;
let level_str: PriceLevelRaw = serde_json::from_str(json_str).unwrap();
assert_eq!(level_str.price, 49999.99);
assert_eq!(level_str.qty, 2.5);
}
#[test]
#[cfg(feature = "analytics")]
fn test_orderbook_imbalance_bullish() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![
PriceLevelRaw {
price: 50000.0,
qty: 5.0,
},
PriceLevelRaw {
price: 49900.0,
qty: 5.0,
},
],
asks: vec![PriceLevelRaw {
price: 50100.0,
qty: 2.0,
}],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
let imbalance = ob.imbalance();
assert!(imbalance > 0.0, "Imbalance should be positive (bullish)");
assert!((imbalance - 0.666666).abs() < 0.001);
let metrics = ob.imbalance_metrics();
assert_eq!(metrics.bid_volume, 10.0);
assert_eq!(metrics.ask_volume, 2.0);
assert_eq!(metrics.signal(0.1), ImbalanceSignal::Bullish);
}
#[test]
#[cfg(feature = "analytics")]
fn test_orderbook_imbalance_bearish() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 50000.0,
qty: 1.0,
}],
asks: vec![
PriceLevelRaw {
price: 50100.0,
qty: 4.0,
},
PriceLevelRaw {
price: 50200.0,
qty: 4.0,
},
],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
let imbalance = ob.imbalance();
assert!(imbalance < 0.0, "Imbalance should be negative (bearish)");
assert!((imbalance - (-0.777777)).abs() < 0.001);
let metrics = ob.imbalance_metrics();
assert_eq!(metrics.signal(0.1), ImbalanceSignal::Bearish);
}
#[test]
#[cfg(feature = "analytics")]
fn test_orderbook_imbalance_neutral() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 50000.0,
qty: 5.0,
}],
asks: vec![PriceLevelRaw {
price: 50100.0,
qty: 5.0,
}],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
assert_eq!(ob.imbalance(), 0.0);
let metrics = ob.imbalance_metrics();
assert_eq!(metrics.signal(0.1), ImbalanceSignal::Neutral);
}
#[test]
#[cfg(feature = "analytics")]
fn test_orderbook_imbalance_top_n() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![
PriceLevelRaw {
price: 50000.0,
qty: 10.0,
}, PriceLevelRaw {
price: 49900.0,
qty: 1.0,
},
PriceLevelRaw {
price: 49800.0,
qty: 1.0,
},
],
asks: vec![
PriceLevelRaw {
price: 50100.0,
qty: 2.0,
}, PriceLevelRaw {
price: 50200.0,
qty: 10.0,
},
PriceLevelRaw {
price: 50300.0,
qty: 10.0,
},
],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
assert!(ob.imbalance() < 0.0);
let top1_imbalance = ob.imbalance_top_n(1);
assert!(top1_imbalance > 0.0);
assert!((top1_imbalance - 0.666666).abs() < 0.001);
}
#[test]
#[cfg(feature = "checksum")]
fn test_checksum_format_for_checksum() {
assert_eq!(Orderbook::format_for_checksum(50000.0), "5");
assert_eq!(Orderbook::format_for_checksum(0.001234), "1234");
assert_eq!(Orderbook::format_for_checksum(123.456), "123456");
assert_eq!(Orderbook::format_for_checksum(0.0), "0");
}
#[test]
#[cfg(feature = "checksum")]
fn test_checksum_calculate() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![
PriceLevelRaw {
price: 50000.0,
qty: 1.0,
},
PriceLevelRaw {
price: 49900.0,
qty: 2.0,
},
],
asks: vec![
PriceLevelRaw {
price: 50100.0,
qty: 1.5,
},
PriceLevelRaw {
price: 50200.0,
qty: 2.5,
},
],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
let checksum1 = ob.calculate_checksum();
let checksum2 = ob.calculate_checksum();
assert_eq!(checksum1, checksum2);
let update2 = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![
PriceLevelRaw {
price: 50000.0,
qty: 3.0,
}, ],
asks: vec![],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update2);
let checksum3 = ob.calculate_checksum();
assert_ne!(checksum1, checksum3);
}
#[test]
#[cfg(feature = "checksum")]
fn test_checksum_validation() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 50000.0,
qty: 1.0,
}],
asks: vec![PriceLevelRaw {
price: 50100.0,
qty: 1.0,
}],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update);
let correct_checksum = ob.calculate_checksum();
assert!(ob.validate_checksum(correct_checksum));
assert!(!ob.validate_checksum(correct_checksum + 1));
let validation = ob.checksum_validation(correct_checksum);
assert!(validation.is_valid());
assert!(!validation.is_corrupted());
assert_eq!(validation.expected, correct_checksum);
assert_eq!(validation.calculated, correct_checksum);
let bad_validation = ob.checksum_validation(12345);
assert!(!bad_validation.is_valid());
assert!(bad_validation.is_corrupted());
}
#[test]
#[cfg(feature = "checksum")]
fn test_checksum_in_apply_update() {
let mut ob = Orderbook::new("BTC/USD".to_string());
let update1 = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 50000.0,
qty: 1.0,
}],
asks: vec![PriceLevelRaw {
price: 50100.0,
qty: 1.0,
}],
checksum: 0,
timestamp: "".to_string(),
};
ob.apply_update(&update1);
assert!(ob.checksum_valid);
let _checksum = ob.calculate_checksum();
assert!(_checksum != 0 || (ob.bids.is_empty() && ob.asks.is_empty()));
let update2 = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 49900.0,
qty: 2.0,
}],
asks: vec![],
checksum: 0, timestamp: "".to_string(),
};
ob.apply_update(&update2);
let correct2 = ob.calculate_checksum();
let mut ob2 = Orderbook::new("BTC/USD".to_string());
ob2.apply_update(&update1);
let update3 = OrderbookData {
symbol: "BTC/USD".to_string(),
bids: vec![PriceLevelRaw {
price: 49900.0,
qty: 2.0,
}],
asks: vec![],
checksum: correct2,
timestamp: "".to_string(),
};
let valid = ob2.apply_update_validated(&update3);
assert!(valid);
assert!(ob2.checksum_valid);
assert_eq!(ob2.last_checksum, correct2);
}
}