1mod subscription;
4
5pub use subscription::Subscription;
6
7use std::collections::BTreeMap;
8
9use crate::codecs::decode::{decode_price_ticks, decode_qty_scaled};
10use crate::errors::{Error, Result};
11use crate::models::{OrderBookDeltaUpdate, OrderbookData, OrderbookLevel};
12
13pub type BookSide = BTreeMap<i64, i64>; pub fn apply_side_delta(book: &mut BookSide, pairs: &[(i64, i64)]) {
16 for &(price, qty) in pairs {
17 if price < 0 || qty < 0 {
19 continue;
20 }
21 if qty == 0 {
22 book.remove(&price);
23 } else {
24 book.insert(price, qty);
25 }
26 }
27}
28
29pub fn parse_bucket_ticks(bucket: &str) -> Result<i64> {
31 if bucket.trim().is_empty() {
32 return Ok(0);
33 }
34 let ticks = crate::codecs::scalars::parse_price_ticks_str(bucket, "bucket")?;
35 if ticks <= 0 {
36 return Err(Error::validation(
37 "bucket must be a positive price increment",
38 ));
39 }
40 Ok(ticks)
41}
42
43pub fn bucket_side(book: &BookSide, bucket_ticks: i64, asks: bool) -> Result<BookSide> {
48 if bucket_ticks <= 0 {
49 for (&price, &qty) in book {
50 if price < 0 {
51 return Err(Error::validation(
52 "orderbook price ticks must be non-negative",
53 ));
54 }
55 if qty < 0 {
56 return Err(Error::validation("orderbook quantity must be non-negative"));
57 }
58 }
59 return Ok(book.clone());
60 }
61 let mut out = BookSide::new();
62 for (&price, &qty) in book {
63 if price < 0 {
64 return Err(Error::validation(
65 "orderbook price ticks must be non-negative",
66 ));
67 }
68 if qty <= 0 {
69 continue;
70 }
71 let quotient = price.div_euclid(bucket_ticks);
72 let floor = quotient
73 .checked_mul(bucket_ticks)
74 .ok_or_else(|| Error::validation("orderbook bucket price overflow"))?;
75 let bucket = if asks && price.rem_euclid(bucket_ticks) != 0 {
76 floor
77 .checked_add(bucket_ticks)
78 .ok_or_else(|| Error::validation("ask bucket price overflow"))?
79 } else {
80 floor
81 };
82 let entry = out.entry(bucket).or_insert(0);
83 *entry = entry
84 .checked_add(qty)
85 .ok_or_else(|| Error::validation("orderbook bucket quantity overflow"))?;
86 }
87 Ok(out)
88}
89
90pub fn levels_from_pairs(levels: impl IntoIterator<Item = (i64, i64)>) -> BookSide {
92 let mut book = BookSide::new();
93 for (price, qty) in levels {
94 if price < 0 || qty <= 0 {
95 continue;
96 }
97 book.insert(price, qty);
98 }
99 book
100}
101
102pub fn levels_from_orderbook_side(levels: &[OrderbookLevel]) -> BookSide {
104 levels_from_pairs(levels.iter().filter_map(|l| {
105 let price = l.price.as_ref()?.as_ticks();
106 let qty = l.qty.as_ref()?.as_scaled();
107 Some((price, qty))
108 }))
109}
110
111pub fn apply_delta(
113 bids: &mut BookSide,
114 asks: &mut BookSide,
115 mut current_seq: u64,
116 delta: &OrderBookDeltaUpdate,
117) -> (u64, bool) {
118 if delta
121 .bids
122 .iter()
123 .chain(&delta.asks)
124 .any(|pair| pair.price_ticks < 0 || pair.qty_scaled < 0)
125 {
126 return (current_seq, true);
127 }
128 let seq_start = delta.book_seq_start;
131 let seq_end = delta.book_seq_end;
132 if seq_end < seq_start {
133 return (current_seq, true);
134 }
135 let comparison_seq = if delta.reset { 0 } else { current_seq };
136 if comparison_seq != 0 && seq_start > comparison_seq.saturating_add(1) {
137 return (current_seq, true);
138 }
139 if !delta.reset && seq_end <= current_seq {
140 return (current_seq, false);
141 }
142 if delta.reset {
143 bids.clear();
144 asks.clear();
145 current_seq = 0;
146 }
147 let bid_pairs: Vec<(i64, i64)> = delta
148 .bids
149 .iter()
150 .map(|p| (p.price_ticks, p.qty_scaled))
151 .collect();
152 let ask_pairs: Vec<(i64, i64)> = delta
153 .asks
154 .iter()
155 .map(|p| (p.price_ticks, p.qty_scaled))
156 .collect();
157 apply_side_delta(bids, &bid_pairs);
158 apply_side_delta(asks, &ask_pairs);
159 if seq_end > current_seq {
160 current_seq = seq_end;
161 }
162 (current_seq, false)
163}
164
165fn side_to_levels(
166 book: &BookSide,
167 side: &str,
168 limit: usize,
169 bucket_ticks: i64,
170 symbol: &str,
171 quantity_scale: u32,
172) -> Result<Vec<OrderbookLevel>> {
173 let view = bucket_side(book, bucket_ticks, side == "asks")?;
174 let mut entries: Vec<(i64, i64)> = view.into_iter().collect();
175 if side == "bids" {
176 entries.sort_by_key(|b| std::cmp::Reverse(b.0));
177 } else {
178 entries.sort_by_key(|a| a.0);
179 }
180 let limit = limit.min(entries.len());
181 let symbol = Some(symbol.to_owned());
182 let mut levels = Vec::with_capacity(limit);
183 for (price, qty) in entries.into_iter().take(limit) {
184 let price = decode_price_ticks(price, symbol.clone())
185 .ok_or_else(|| Error::validation("orderbook level has invalid or missing price"))?;
186 let qty = decode_qty_scaled(qty, Some(quantity_scale), symbol.clone(), None)
187 .ok_or_else(|| Error::validation("orderbook level has invalid or missing quantity"))?;
188 levels.push(OrderbookLevel {
189 price: Some(price),
190 qty: Some(qty),
191 });
192 }
193 Ok(levels)
194}
195
196pub fn build_orderbook_data(
198 symbol: &str,
199 depth: u32,
200 book_seq: u64,
201 bids: &BookSide,
202 asks: &BookSide,
203 bucket_ticks: i64,
204 quantity_scale: u32,
205) -> Result<OrderbookData> {
206 let limit = depth as usize;
207 Ok(OrderbookData {
208 symbol: symbol.to_owned(),
209 depth,
210 book_seq: book_seq.to_string(),
211 bids: side_to_levels(bids, "bids", limit, bucket_ticks, symbol, quantity_scale)?,
212 asks: side_to_levels(asks, "asks", limit, bucket_ticks, symbol, quantity_scale)?,
213 })
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::models::PriceQtyPair;
220
221 fn delta(
222 start: u64,
223 end: u64,
224 bids: &[(i64, i64)],
225 asks: &[(i64, i64)],
226 reset: bool,
227 ) -> OrderBookDeltaUpdate {
228 OrderBookDeltaUpdate {
229 symbol_id: 1,
230 book_seq_start: start,
231 book_seq_end: end,
232 reset,
233 bids: bids
234 .iter()
235 .map(|&(price_ticks, qty_scaled)| PriceQtyPair {
236 price_ticks,
237 qty_scaled,
238 })
239 .collect(),
240 asks: asks
241 .iter()
242 .map(|&(price_ticks, qty_scaled)| PriceQtyPair {
243 price_ticks,
244 qty_scaled,
245 })
246 .collect(),
247 }
248 }
249
250 #[test]
251 fn apply_side_delta_updates_and_deletes() {
252 let mut book = BookSide::from([(100, 5)]);
253 apply_side_delta(&mut book, &[(100, 7), (101, 2)]);
254 assert_eq!(book.get(&100), Some(&7));
255 assert_eq!(book.get(&101), Some(&2));
256 apply_side_delta(&mut book, &[(101, 0)]);
257 assert!(!book.contains_key(&101));
258 }
259
260 #[test]
261 fn apply_delta_detects_gap() {
262 let mut bids = BookSide::from([(100, 5)]);
263 let mut asks = BookSide::from([(200, 3)]);
264 let (seq, needs_refresh) = apply_delta(
265 &mut bids,
266 &mut asks,
267 3,
268 &delta(5, 6, &[(100, 7)], &[], false),
269 );
270 assert!(needs_refresh);
271 assert_eq!(seq, 3);
272 assert_eq!(bids.get(&100), Some(&5));
273 }
274
275 #[test]
276 fn apply_delta_updates_book() {
277 let mut bids = BookSide::from([(100, 5)]);
278 let mut asks = BookSide::from([(200, 3)]);
279 let (seq, needs_refresh) = apply_delta(
280 &mut bids,
281 &mut asks,
282 3,
283 &delta(3, 4, &[(100, 7)], &[], false),
284 );
285 assert!(!needs_refresh);
286 assert_eq!(seq, 4);
287 assert_eq!(bids.get(&100), Some(&7));
288 }
289
290 #[test]
291 fn apply_delta_skips_stale() {
292 let mut bids = BookSide::from([(100, 5)]);
293 let mut asks = BookSide::new();
294 let (seq, needs_refresh) = apply_delta(
295 &mut bids,
296 &mut asks,
297 5,
298 &delta(3, 4, &[(100, 9)], &[], false),
299 );
300 assert!(!needs_refresh);
301 assert_eq!(seq, 5);
302 assert_eq!(bids.get(&100), Some(&5));
303 }
304
305 #[test]
306 fn apply_delta_inverted_seq_fails_toward_refresh() {
307 let mut bids = BookSide::from([(100, 5)]);
308 let mut asks = BookSide::new();
309 let (seq, needs_refresh) = apply_delta(
310 &mut bids,
311 &mut asks,
312 3,
313 &delta(9, 2, &[(100, 9)], &[], false),
314 );
315 assert!(needs_refresh);
316 assert_eq!(seq, 3);
317 assert_eq!(bids.get(&100), Some(&5));
318 }
319
320 #[test]
321 fn apply_delta_rejects_malformed_levels_without_advancing_or_mutating() {
322 let mut bids = BookSide::from([(100, 5)]);
323 let mut asks = BookSide::from([(200, 3)]);
324 let (seq, needs_refresh) = apply_delta(
325 &mut bids,
326 &mut asks,
327 1,
328 &delta(2, 2, &[(100, -1), (101, 4)], &[], false),
329 );
330 assert!(needs_refresh);
331 assert_eq!(seq, 1);
332 assert_eq!(bids, BookSide::from([(100, 5)]));
333 assert_eq!(asks, BookSide::from([(200, 3)]));
334 }
335
336 #[test]
337 fn apply_delta_reset_clears_book() {
338 let mut bids = BookSide::from([(100, 5)]);
339 let mut asks = BookSide::from([(200, 3)]);
340 let (seq, needs_refresh) = apply_delta(
341 &mut bids,
342 &mut asks,
343 9,
344 &delta(1, 2, &[(101, 4)], &[], true),
345 );
346 assert!(!needs_refresh);
347 assert_eq!(seq, 2);
348 assert_eq!(bids.get(&101), Some(&4));
349 assert!(!bids.contains_key(&100));
350 assert!(asks.is_empty());
351 }
352
353 #[test]
354 fn bucket_side_aggregates() {
355 let book = BookSide::from([(101, 2), (105, 3)]);
356 let bucketed = bucket_side(&book, 10, false).unwrap();
357 assert_eq!(bucketed.get(&100), Some(&5));
358 let asks = bucket_side(&book, 10, true).unwrap();
359 assert_eq!(asks.get(&110), Some(&5));
360 assert!(bucket_side(&BookSide::from([(i64::MAX, 1)]), 10, true).is_err());
361 }
362
363 #[test]
364 fn bucket_side_rejects_negative_price_and_qty_overflow() {
365 assert!(bucket_side(&BookSide::from([(-1, 1)]), 10, false).is_err());
366 assert!(bucket_side(&BookSide::from([(100, i64::MAX), (101, 1)]), 10, false).is_err());
367 assert!(bucket_side(&BookSide::from([(i64::MIN + 1, 1)]), 10, false).is_err());
369 }
370
371 #[test]
372 fn apply_side_delta_ignores_negative_levels() {
373 let mut book = BookSide::from([(100, 5)]);
374 apply_side_delta(&mut book, &[(-1, 3), (100, -2), (101, 4)]);
375 assert_eq!(book.get(&100), Some(&5));
376 assert_eq!(book.get(&101), Some(&4));
377 assert!(!book.contains_key(&-1));
378 }
379
380 #[test]
381 fn build_orderbook_data_rejects_negative_levels() {
382 let bids = BookSide::from([(-5, 1)]);
383 let asks = BookSide::from([(200, 1)]);
384 assert!(build_orderbook_data("BTC-USDT", 2, 7, &bids, &asks, 0, 8).is_err());
385 }
386
387 #[test]
388 fn parse_bucket_ticks_empty_is_zero() {
389 assert_eq!(parse_bucket_ticks("").unwrap(), 0);
390 assert!(parse_bucket_ticks("nope").is_err());
391 assert!(parse_bucket_ticks("-1").is_err());
392 }
393
394 #[test]
395 fn build_orderbook_data_sorts_and_limits() {
396 let bids = BookSide::from([(100, 1), (110, 2), (120, 3)]);
397 let asks = BookSide::from([(200, 4), (210, 5)]);
398 let data = build_orderbook_data("BTC-USDT", 2, 7, &bids, &asks, 0, 8).unwrap();
399 assert_eq!(data.book_seq, "7");
400 assert_eq!(data.bids.len(), 2);
401 assert_eq!(data.bids[0].price.as_ref().unwrap().as_ticks(), 120);
402 assert_eq!(data.asks[0].price.as_ref().unwrap().as_ticks(), 200);
403 }
404}