use digdigdig3::core::types::{OrderbookDelta, StreamEvent};
use serde::{Deserialize, Serialize};
use crate::series::DataPoint;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObDeltaPoint {
pub ts_ms: i64,
pub bid_changes: Vec<(f64, f64)>,
pub ask_changes: Vec<(f64, f64)>,
}
const TAIL_OFFSET: usize = 8;
const HEADER_SIZE: usize = 20;
impl ObDeltaPoint {
pub fn from_delta(d: &OrderbookDelta) -> Self {
let bid_changes = d.bids.iter().map(|l| (l.price, l.size)).collect();
let ask_changes = d.asks.iter().map(|l| (l.price, l.size)).collect();
Self { ts_ms: d.timestamp, bid_changes, ask_changes }
}
}
fn encode_side_blob(levels: &[(f64, f64)], out: &mut Vec<u8>) {
out.extend_from_slice(&(levels.len() as u32).to_le_bytes());
for (p, q) in levels {
out.extend_from_slice(&p.to_le_bytes());
out.extend_from_slice(&q.to_le_bytes());
}
}
fn decode_side_blob(blob: &[u8]) -> Option<(Vec<(f64, f64)>, usize)> {
if blob.len() < 4 {
return None;
}
let count = u32::from_le_bytes(blob[0..4].try_into().ok()?) as usize;
let need = 4 + count * 16;
if blob.len() < need {
return None;
}
let mut levels = Vec::with_capacity(count);
let mut off = 4;
for _ in 0..count {
let p = f64::from_le_bytes(blob[off..off + 8].try_into().ok()?);
let q = f64::from_le_bytes(blob[off + 8..off + 16].try_into().ok()?);
levels.push((p, q));
off += 16;
}
Some((levels, need))
}
impl DataPoint for ObDeltaPoint {
const RECORD_SIZE: usize = HEADER_SIZE;
fn encode(&self, out: &mut [u8]) {
out[0..8].copy_from_slice(&(self.ts_ms as u64).to_le_bytes());
}
fn decode(bytes: &[u8]) -> Option<Self> {
if bytes.len() != Self::RECORD_SIZE {
return None;
}
Some(Self {
ts_ms: u64::from_le_bytes(bytes[0..8].try_into().ok()?) as i64,
bid_changes: Vec::new(),
ask_changes: Vec::new(),
})
}
fn timestamp_ms(&self) -> i64 { self.ts_ms }
fn from_stream_event(ev: &StreamEvent) -> Option<Self> {
if let StreamEvent::OrderbookDelta { delta, .. } = ev {
Some(Self::from_delta(delta))
} else {
None
}
}
fn encode_blob(&self) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(
8 + (self.bid_changes.len() + self.ask_changes.len()) * 16,
);
encode_side_blob(&self.bid_changes, &mut out);
encode_side_blob(&self.ask_changes, &mut out);
Some(out)
}
fn decode_blob(header: &[u8], blob: &[u8]) -> Option<Self> {
let mut p = Self::decode(header)?;
if let Some((bids, consumed)) = decode_side_blob(blob) {
p.bid_changes = bids;
if let Some((asks, _)) = decode_side_blob(&blob[consumed..]) {
p.ask_changes = asks;
}
}
Some(p)
}
fn blob_pointer_offset() -> Option<usize> { Some(TAIL_OFFSET) }
}
#[cfg(test)]
mod tests {
use super::*;
use digdigdig3::core::types::OrderBookLevel;
fn lvl(price: f64, size: f64) -> OrderBookLevel {
OrderBookLevel { price, size, order_count: None }
}
#[test]
fn from_delta_keeps_full_depth() {
let bids: Vec<OrderBookLevel> = (0..40).map(|i| lvl(100.0 - i as f64, i as f64)).collect();
let asks: Vec<OrderBookLevel> = (0..40).map(|i| lvl(101.0 + i as f64, i as f64)).collect();
let d = OrderbookDelta { bids, asks, timestamp: 1234, ..Default::default() };
let p = ObDeltaPoint::from_delta(&d);
assert_eq!(p.bid_changes.len(), 40);
assert_eq!(p.ask_changes.len(), 40);
}
#[test]
fn blob_round_trip_full_depth() {
let bids: Vec<(f64, f64)> = (0..30).map(|i| (100.0 - i as f64, i as f64)).collect();
let asks: Vec<(f64, f64)> = (0..50).map(|i| (101.0 + i as f64, i as f64)).collect();
let p = ObDeltaPoint { ts_ms: 9999, bid_changes: bids.clone(), ask_changes: asks.clone() };
let mut header = vec![0u8; ObDeltaPoint::RECORD_SIZE];
p.encode(&mut header);
let blob = p.encode_blob().expect("blob");
let back = ObDeltaPoint::decode_blob(&header, &blob).expect("decode_blob");
assert_eq!(back.ts_ms, 9999);
assert_eq!(back.bid_changes, bids);
assert_eq!(back.ask_changes, asks);
}
#[test]
fn blob_round_trip_with_removal_zero_size() {
let p = ObDeltaPoint {
ts_ms: 1,
bid_changes: vec![(99.0, 0.0), (98.5, 3.0)],
ask_changes: vec![(101.0, 0.0)],
};
let mut header = vec![0u8; ObDeltaPoint::RECORD_SIZE];
p.encode(&mut header);
let blob = p.encode_blob().expect("blob");
let back = ObDeltaPoint::decode_blob(&header, &blob).expect("decode_blob");
assert_eq!(back.bid_changes, vec![(99.0, 0.0), (98.5, 3.0)]);
assert_eq!(back.ask_changes, vec![(101.0, 0.0)]);
}
#[test]
fn header_only_decode_empty_levels() {
let p = ObDeltaPoint { ts_ms: 42, bid_changes: vec![(1.0, 2.0)], ask_changes: vec![] };
let mut header = vec![0u8; ObDeltaPoint::RECORD_SIZE];
p.encode(&mut header);
let back = ObDeltaPoint::decode(&header).expect("decode");
assert_eq!(back.ts_ms, 42);
assert!(back.bid_changes.is_empty());
assert!(back.ask_changes.is_empty());
}
}