digdigdig3_station/data/
ob_delta.rs1use digdigdig3::core::types::{OrderbookDelta, StreamEvent};
2use serde::{Deserialize, Serialize};
3
4use crate::series::DataPoint;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ObDeltaPoint {
24 pub ts_ms: i64,
25 pub bid_changes: Vec<(f64, f64)>,
27 pub ask_changes: Vec<(f64, f64)>,
29}
30
31const TAIL_OFFSET: usize = 8;
33const HEADER_SIZE: usize = 20;
34
35impl ObDeltaPoint {
36 pub fn from_delta(d: &OrderbookDelta) -> Self {
37 let bid_changes = d.bids.iter().map(|l| (l.price, l.size)).collect();
40 let ask_changes = d.asks.iter().map(|l| (l.price, l.size)).collect();
41 Self { ts_ms: d.timestamp, bid_changes, ask_changes }
42 }
43}
44
45fn encode_side_blob(levels: &[(f64, f64)], out: &mut Vec<u8>) {
47 out.extend_from_slice(&(levels.len() as u32).to_le_bytes());
48 for (p, q) in levels {
49 out.extend_from_slice(&p.to_le_bytes());
50 out.extend_from_slice(&q.to_le_bytes());
51 }
52}
53
54fn decode_side_blob(blob: &[u8]) -> Option<(Vec<(f64, f64)>, usize)> {
57 if blob.len() < 4 {
58 return None;
59 }
60 let count = u32::from_le_bytes(blob[0..4].try_into().ok()?) as usize;
61 let need = 4 + count * 16;
62 if blob.len() < need {
63 return None;
64 }
65 let mut levels = Vec::with_capacity(count);
66 let mut off = 4;
67 for _ in 0..count {
68 let p = f64::from_le_bytes(blob[off..off + 8].try_into().ok()?);
69 let q = f64::from_le_bytes(blob[off + 8..off + 16].try_into().ok()?);
70 levels.push((p, q));
71 off += 16;
72 }
73 Some((levels, need))
74}
75
76impl DataPoint for ObDeltaPoint {
77 const RECORD_SIZE: usize = HEADER_SIZE;
78
79 fn encode(&self, out: &mut [u8]) {
80 out[0..8].copy_from_slice(&(self.ts_ms as u64).to_le_bytes());
83 }
84
85 fn decode(bytes: &[u8]) -> Option<Self> {
86 if bytes.len() != Self::RECORD_SIZE {
89 return None;
90 }
91 Some(Self {
92 ts_ms: u64::from_le_bytes(bytes[0..8].try_into().ok()?) as i64,
93 bid_changes: Vec::new(),
94 ask_changes: Vec::new(),
95 })
96 }
97
98 fn timestamp_ms(&self) -> i64 { self.ts_ms }
99
100 fn from_stream_event(ev: &StreamEvent) -> Option<Self> {
101 if let StreamEvent::OrderbookDelta { delta, .. } = ev {
102 Some(Self::from_delta(delta))
103 } else {
104 None
105 }
106 }
107
108 fn encode_blob(&self) -> Option<Vec<u8>> {
109 let mut out = Vec::with_capacity(
110 8 + (self.bid_changes.len() + self.ask_changes.len()) * 16,
111 );
112 encode_side_blob(&self.bid_changes, &mut out);
113 encode_side_blob(&self.ask_changes, &mut out);
114 Some(out)
115 }
116
117 fn decode_blob(header: &[u8], blob: &[u8]) -> Option<Self> {
118 let mut p = Self::decode(header)?;
119 if let Some((bids, consumed)) = decode_side_blob(blob) {
120 p.bid_changes = bids;
121 if let Some((asks, _)) = decode_side_blob(&blob[consumed..]) {
122 p.ask_changes = asks;
123 }
124 }
125 Some(p)
126 }
127
128 fn blob_pointer_offset() -> Option<usize> { Some(TAIL_OFFSET) }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use digdigdig3::core::types::OrderBookLevel;
135
136 fn lvl(price: f64, size: f64) -> OrderBookLevel {
137 OrderBookLevel { price, size, order_count: None }
138 }
139
140 #[test]
141 fn from_delta_keeps_full_depth() {
142 let bids: Vec<OrderBookLevel> = (0..40).map(|i| lvl(100.0 - i as f64, i as f64)).collect();
144 let asks: Vec<OrderBookLevel> = (0..40).map(|i| lvl(101.0 + i as f64, i as f64)).collect();
145 let d = OrderbookDelta { bids, asks, timestamp: 1234, ..Default::default() };
146 let p = ObDeltaPoint::from_delta(&d);
147 assert_eq!(p.bid_changes.len(), 40);
148 assert_eq!(p.ask_changes.len(), 40);
149 }
150
151 #[test]
152 fn blob_round_trip_full_depth() {
153 let bids: Vec<(f64, f64)> = (0..30).map(|i| (100.0 - i as f64, i as f64)).collect();
154 let asks: Vec<(f64, f64)> = (0..50).map(|i| (101.0 + i as f64, i as f64)).collect();
155 let p = ObDeltaPoint { ts_ms: 9999, bid_changes: bids.clone(), ask_changes: asks.clone() };
156
157 let mut header = vec![0u8; ObDeltaPoint::RECORD_SIZE];
158 p.encode(&mut header);
159 let blob = p.encode_blob().expect("blob");
160
161 let back = ObDeltaPoint::decode_blob(&header, &blob).expect("decode_blob");
162 assert_eq!(back.ts_ms, 9999);
163 assert_eq!(back.bid_changes, bids);
164 assert_eq!(back.ask_changes, asks);
165 }
166
167 #[test]
168 fn blob_round_trip_with_removal_zero_size() {
169 let p = ObDeltaPoint {
171 ts_ms: 1,
172 bid_changes: vec![(99.0, 0.0), (98.5, 3.0)],
173 ask_changes: vec![(101.0, 0.0)],
174 };
175 let mut header = vec![0u8; ObDeltaPoint::RECORD_SIZE];
176 p.encode(&mut header);
177 let blob = p.encode_blob().expect("blob");
178 let back = ObDeltaPoint::decode_blob(&header, &blob).expect("decode_blob");
179 assert_eq!(back.bid_changes, vec![(99.0, 0.0), (98.5, 3.0)]);
180 assert_eq!(back.ask_changes, vec![(101.0, 0.0)]);
181 }
182
183 #[test]
184 fn header_only_decode_empty_levels() {
185 let p = ObDeltaPoint { ts_ms: 42, bid_changes: vec![(1.0, 2.0)], ask_changes: vec![] };
186 let mut header = vec![0u8; ObDeltaPoint::RECORD_SIZE];
187 p.encode(&mut header);
188 let back = ObDeltaPoint::decode(&header).expect("decode");
189 assert_eq!(back.ts_ms, 42);
190 assert!(back.bid_changes.is_empty());
191 assert!(back.ask_changes.is_empty());
192 }
193}