1use crate::error::{Error, Result};
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Level {
14 pub price: f64,
16 pub size: f64,
18}
19
20impl Level {
21 pub fn new(price: f64, size: f64) -> Result<Self> {
29 if !price.is_finite() || price <= 0.0 {
30 return Err(Error::InvalidOrderBook {
31 message: "level price must be finite and positive",
32 });
33 }
34 if !size.is_finite() || size < 0.0 {
35 return Err(Error::InvalidOrderBook {
36 message: "level size must be finite and non-negative",
37 });
38 }
39 Ok(Self { price, size })
40 }
41
42 pub const fn new_unchecked(price: f64, size: f64) -> Self {
45 Self { price, size }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq)]
55pub struct OrderBook {
56 pub bids: Vec<Level>,
58 pub asks: Vec<Level>,
60}
61
62impl OrderBook {
63 pub fn new(bids: Vec<Level>, asks: Vec<Level>) -> Result<Self> {
73 if bids.is_empty() || asks.is_empty() {
74 return Err(Error::InvalidOrderBook {
75 message: "order book must have at least one bid and one ask",
76 });
77 }
78 for level in bids.iter().chain(asks.iter()) {
79 if !level.price.is_finite() || level.price <= 0.0 {
80 return Err(Error::InvalidOrderBook {
81 message: "level price must be finite and positive",
82 });
83 }
84 if !level.size.is_finite() || level.size < 0.0 {
85 return Err(Error::InvalidOrderBook {
86 message: "level size must be finite and non-negative",
87 });
88 }
89 }
90 for pair in bids.windows(2) {
91 if pair[0].price <= pair[1].price {
92 return Err(Error::InvalidOrderBook {
93 message: "bids must be strictly descending in price",
94 });
95 }
96 }
97 for pair in asks.windows(2) {
98 if pair[0].price >= pair[1].price {
99 return Err(Error::InvalidOrderBook {
100 message: "asks must be strictly ascending in price",
101 });
102 }
103 }
104 if bids[0].price >= asks[0].price {
105 return Err(Error::InvalidOrderBook {
106 message: "order book must be uncrossed (best_bid < best_ask)",
107 });
108 }
109 Ok(Self { bids, asks })
110 }
111
112 pub const fn new_unchecked(bids: Vec<Level>, asks: Vec<Level>) -> Self {
115 Self { bids, asks }
116 }
117
118 pub fn best_bid(&self) -> Option<Level> {
120 self.bids.first().copied()
121 }
122
123 pub fn best_ask(&self) -> Option<Level> {
125 self.asks.first().copied()
126 }
127
128 pub fn mid(&self) -> Option<f64> {
131 match (self.best_bid(), self.best_ask()) {
132 (Some(bid), Some(ask)) => Some(f64::midpoint(bid.price, ask.price)),
133 _ => None,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum Side {
141 Buy,
143 Sell,
145}
146
147impl Side {
148 pub const fn sign(self) -> f64 {
151 match self {
152 Side::Buy => 1.0,
153 Side::Sell => -1.0,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct Trade {
161 pub price: f64,
163 pub size: f64,
165 pub side: Side,
167 pub timestamp: i64,
169}
170
171impl Trade {
172 pub fn new(price: f64, size: f64, side: Side, timestamp: i64) -> Result<Self> {
180 if !price.is_finite() || price <= 0.0 {
181 return Err(Error::InvalidTrade {
182 message: "trade price must be finite and positive",
183 });
184 }
185 if !size.is_finite() || size < 0.0 {
186 return Err(Error::InvalidTrade {
187 message: "trade size must be finite and non-negative",
188 });
189 }
190 Ok(Self {
191 price,
192 size,
193 side,
194 timestamp,
195 })
196 }
197
198 pub const fn new_unchecked(price: f64, size: f64, side: Side, timestamp: i64) -> Self {
201 Self {
202 price,
203 size,
204 side,
205 timestamp,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq)]
216pub struct TradeQuote {
217 pub trade: Trade,
219 pub mid: f64,
221}
222
223impl TradeQuote {
224 pub fn new(trade: Trade, mid: f64) -> Result<Self> {
232 if !mid.is_finite() || mid <= 0.0 {
233 return Err(Error::InvalidTrade {
234 message: "trade-quote mid must be finite and positive",
235 });
236 }
237 Ok(Self { trade, mid })
238 }
239
240 pub const fn new_unchecked(trade: Trade, mid: f64) -> Self {
243 Self { trade, mid }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn level_new_accepts_valid() {
253 let level = Level::new(100.5, 2.0).unwrap();
254 assert_eq!(level.price, 100.5);
255 assert_eq!(level.size, 2.0);
256 }
257
258 #[test]
259 fn level_new_accepts_zero_size() {
260 assert!(Level::new(100.0, 0.0).is_ok());
261 }
262
263 #[test]
264 fn level_new_rejects_non_finite_price() {
265 assert!(matches!(
266 Level::new(f64::NAN, 1.0),
267 Err(Error::InvalidOrderBook { .. })
268 ));
269 assert!(matches!(
270 Level::new(f64::INFINITY, 1.0),
271 Err(Error::InvalidOrderBook { .. })
272 ));
273 }
274
275 #[test]
276 fn level_new_rejects_non_positive_price() {
277 assert!(matches!(
278 Level::new(0.0, 1.0),
279 Err(Error::InvalidOrderBook { .. })
280 ));
281 assert!(matches!(
282 Level::new(-1.0, 1.0),
283 Err(Error::InvalidOrderBook { .. })
284 ));
285 }
286
287 #[test]
288 fn level_new_rejects_bad_size() {
289 assert!(matches!(
290 Level::new(100.0, -1.0),
291 Err(Error::InvalidOrderBook { .. })
292 ));
293 assert!(matches!(
294 Level::new(100.0, f64::NAN),
295 Err(Error::InvalidOrderBook { .. })
296 ));
297 }
298
299 #[test]
300 fn level_new_unchecked_preserves_fields() {
301 let level = Level::new_unchecked(-5.0, -2.0);
302 assert_eq!(level.price, -5.0);
303 assert_eq!(level.size, -2.0);
304 }
305
306 fn lvl(price: f64, size: f64) -> Level {
307 Level::new(price, size).unwrap()
308 }
309
310 #[test]
311 fn order_book_new_accepts_valid() {
312 let book = OrderBook::new(
313 vec![lvl(100.0, 2.0), lvl(99.0, 3.0)],
314 vec![lvl(101.0, 1.0), lvl(102.0, 4.0)],
315 )
316 .unwrap();
317 assert_eq!(book.best_bid(), Some(lvl(100.0, 2.0)));
318 assert_eq!(book.best_ask(), Some(lvl(101.0, 1.0)));
319 assert_eq!(book.mid(), Some(100.5));
320 }
321
322 #[test]
323 fn order_book_new_rejects_empty_side() {
324 assert!(matches!(
325 OrderBook::new(vec![], vec![lvl(101.0, 1.0)]),
326 Err(Error::InvalidOrderBook { .. })
327 ));
328 assert!(matches!(
329 OrderBook::new(vec![lvl(100.0, 1.0)], vec![]),
330 Err(Error::InvalidOrderBook { .. })
331 ));
332 }
333
334 #[test]
335 fn order_book_new_rejects_bad_level() {
336 assert!(matches!(
337 OrderBook::new(
338 vec![Level::new_unchecked(100.0, -1.0)],
339 vec![lvl(101.0, 1.0)]
340 ),
341 Err(Error::InvalidOrderBook { .. })
342 ));
343 assert!(matches!(
344 OrderBook::new(
345 vec![lvl(100.0, 1.0)],
346 vec![Level::new_unchecked(f64::NAN, 1.0)]
347 ),
348 Err(Error::InvalidOrderBook { .. })
349 ));
350 }
351
352 #[test]
353 fn order_book_new_rejects_misordered_bids() {
354 assert!(matches!(
355 OrderBook::new(vec![lvl(99.0, 1.0), lvl(100.0, 1.0)], vec![lvl(101.0, 1.0)]),
356 Err(Error::InvalidOrderBook { .. })
357 ));
358 }
359
360 #[test]
361 fn order_book_new_rejects_misordered_asks() {
362 assert!(matches!(
363 OrderBook::new(
364 vec![lvl(100.0, 1.0)],
365 vec![lvl(102.0, 1.0), lvl(101.0, 1.0)]
366 ),
367 Err(Error::InvalidOrderBook { .. })
368 ));
369 }
370
371 #[test]
372 fn order_book_new_rejects_crossed() {
373 assert!(matches!(
374 OrderBook::new(vec![lvl(101.0, 1.0)], vec![lvl(101.0, 1.0)]),
375 Err(Error::InvalidOrderBook { .. })
376 ));
377 assert!(matches!(
378 OrderBook::new(vec![lvl(102.0, 1.0)], vec![lvl(101.0, 1.0)]),
379 Err(Error::InvalidOrderBook { .. })
380 ));
381 }
382
383 #[test]
384 fn order_book_new_unchecked_allows_empty() {
385 let book = OrderBook::new_unchecked(vec![], vec![]);
386 assert_eq!(book.best_bid(), None);
387 assert_eq!(book.best_ask(), None);
388 assert_eq!(book.mid(), None);
389 }
390
391 #[test]
392 fn side_sign() {
393 assert_eq!(Side::Buy.sign(), 1.0);
394 assert_eq!(Side::Sell.sign(), -1.0);
395 }
396
397 #[test]
398 fn trade_new_accepts_valid() {
399 let trade = Trade::new(100.0, 1.5, Side::Buy, 42).unwrap();
400 assert_eq!(trade.price, 100.0);
401 assert_eq!(trade.size, 1.5);
402 assert_eq!(trade.side, Side::Buy);
403 assert_eq!(trade.timestamp, 42);
404 }
405
406 #[test]
407 fn trade_new_rejects_bad_price() {
408 assert!(matches!(
409 Trade::new(0.0, 1.0, Side::Buy, 0),
410 Err(Error::InvalidTrade { .. })
411 ));
412 assert!(matches!(
413 Trade::new(f64::NAN, 1.0, Side::Sell, 0),
414 Err(Error::InvalidTrade { .. })
415 ));
416 }
417
418 #[test]
419 fn trade_new_rejects_bad_size() {
420 assert!(matches!(
421 Trade::new(100.0, -1.0, Side::Buy, 0),
422 Err(Error::InvalidTrade { .. })
423 ));
424 assert!(matches!(
425 Trade::new(100.0, f64::INFINITY, Side::Buy, 0),
426 Err(Error::InvalidTrade { .. })
427 ));
428 }
429
430 #[test]
431 fn trade_new_unchecked_preserves_fields() {
432 let trade = Trade::new_unchecked(-1.0, -2.0, Side::Sell, 7);
433 assert_eq!(trade.price, -1.0);
434 assert_eq!(trade.size, -2.0);
435 assert_eq!(trade.side, Side::Sell);
436 assert_eq!(trade.timestamp, 7);
437 }
438
439 #[test]
440 fn trade_quote_new_accepts_valid() {
441 let trade = Trade::new(100.0, 1.0, Side::Buy, 0).unwrap();
442 let tq = TradeQuote::new(trade, 99.5).unwrap();
443 assert_eq!(tq.trade, trade);
444 assert_eq!(tq.mid, 99.5);
445 }
446
447 #[test]
448 fn trade_quote_new_rejects_bad_mid() {
449 let trade = Trade::new(100.0, 1.0, Side::Buy, 0).unwrap();
450 assert!(matches!(
451 TradeQuote::new(trade, 0.0),
452 Err(Error::InvalidTrade { .. })
453 ));
454 assert!(matches!(
455 TradeQuote::new(trade, f64::NAN),
456 Err(Error::InvalidTrade { .. })
457 ));
458 }
459
460 #[test]
461 fn trade_quote_new_unchecked_preserves_fields() {
462 let trade = Trade::new_unchecked(100.0, 1.0, Side::Buy, 0);
463 let tq = TradeQuote::new_unchecked(trade, -1.0);
464 assert_eq!(tq.mid, -1.0);
465 assert_eq!(tq.trade, trade);
466 }
467}