1#[derive(Debug, Clone, PartialEq)]
12pub struct OrderBookLevel {
13 pub price: f64,
15 pub quantity: f64,
17}
18
19impl OrderBookLevel {
20 #[inline]
22 pub fn value(&self) -> f64 {
23 self.price * self.quantity
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct OrderBook {
30 pub pair: String,
32 pub bids: Vec<OrderBookLevel>,
34 pub asks: Vec<OrderBookLevel>,
36}
37
38impl OrderBook {
39 pub fn best_bid(&self) -> Option<f64> {
41 self.bids.first().map(|l| l.price)
42 }
43
44 pub fn best_ask(&self) -> Option<f64> {
46 self.asks.first().map(|l| l.price)
47 }
48
49 pub fn mid_price(&self) -> Option<f64> {
51 match (self.best_bid(), self.best_ask()) {
52 (Some(bid), Some(ask)) => Some((bid + ask) / 2.0),
53 _ => None,
54 }
55 }
56
57 pub fn spread(&self) -> Option<f64> {
59 match (self.best_bid(), self.best_ask()) {
60 (Some(bid), Some(ask)) => Some(ask - bid),
61 _ => None,
62 }
63 }
64
65 pub fn bid_depth(&self) -> f64 {
67 self.bids.iter().map(OrderBookLevel::value).sum()
68 }
69
70 pub fn ask_depth(&self) -> f64 {
72 self.asks.iter().map(OrderBookLevel::value).sum()
73 }
74
75 pub fn estimate_buy_execution(&self, notional_usdt: f64) -> Option<ExecutionEstimate> {
78 let mid = self.mid_price()?;
79 if mid <= 0.0 {
80 return None;
81 }
82 let mut remaining = notional_usdt;
83 let mut filled_value = 0.0;
84 let mut filled_qty = 0.0;
85 for level in &self.asks {
86 let level_value = level.value();
87 if remaining <= 0.0 {
88 break;
89 }
90 let take_value = level_value.min(remaining);
91 let take_qty = if level.price > 0.0 {
92 take_value / level.price
93 } else {
94 0.0
95 };
96 filled_value += take_value;
97 filled_qty += take_qty;
98 remaining -= take_value;
99 }
100 let fillable = remaining <= 0.01;
101 let vwap = if filled_qty > 0.0 {
102 filled_value / filled_qty
103 } else {
104 mid
105 };
106 let slippage_bps = (vwap - mid) / mid * 10_000.0;
107 Some(ExecutionEstimate {
108 notional_usdt,
109 side: ExecutionSide::Buy,
110 vwap,
111 slippage_bps,
112 fillable,
113 })
114 }
115
116 pub fn estimate_sell_execution(&self, notional_usdt: f64) -> Option<ExecutionEstimate> {
118 let mid = self.mid_price()?;
119 if mid <= 0.0 {
120 return None;
121 }
122 let mut remaining = notional_usdt;
123 let mut filled_value = 0.0;
124 let mut filled_qty = 0.0;
125 for level in &self.bids {
126 if remaining <= 0.0 {
127 break;
128 }
129 let level_value = level.value();
130 let take_value = level_value.min(remaining);
131 let take_qty = if level.price > 0.0 {
132 take_value / level.price
133 } else {
134 0.0
135 };
136 filled_value += take_value;
137 filled_qty += take_qty;
138 remaining -= take_value;
139 }
140 let fillable = remaining <= 0.01;
141 let vwap = if filled_qty > 0.0 {
142 filled_value / filled_qty
143 } else {
144 mid
145 };
146 let slippage_bps = (mid - vwap) / mid * 10_000.0;
147 Some(ExecutionEstimate {
148 notional_usdt,
149 side: ExecutionSide::Sell,
150 vwap,
151 slippage_bps,
152 fillable,
153 })
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq)]
159pub enum ExecutionSide {
160 Buy,
161 Sell,
162}
163
164#[derive(Debug, Clone)]
166pub struct ExecutionEstimate {
167 pub notional_usdt: f64,
168 pub side: ExecutionSide,
169 pub vwap: f64,
170 pub slippage_bps: f64,
171 pub fillable: bool,
172}
173
174#[derive(Debug, Clone, PartialEq)]
176pub enum HealthCheck {
177 Pass(String),
178 Fail(String),
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum TradeSide {
188 Buy,
189 Sell,
190}
191
192#[derive(Debug, Clone)]
194pub struct Trade {
195 pub price: f64,
197 pub quantity: f64,
199 pub quote_quantity: Option<f64>,
201 pub timestamp_ms: u64,
203 pub side: TradeSide,
205 pub id: Option<String>,
207}
208
209#[derive(Debug, Clone)]
211pub struct Ticker {
212 pub pair: String,
214 pub last_price: Option<f64>,
216 pub high_24h: Option<f64>,
218 pub low_24h: Option<f64>,
220 pub volume_24h: Option<f64>,
222 pub quote_volume_24h: Option<f64>,
224 pub best_bid: Option<f64>,
226 pub best_ask: Option<f64>,
228}
229
230#[derive(Debug, Clone)]
232pub struct MarketSnapshot {
233 pub order_book: Option<OrderBook>,
234 pub ticker: Option<Ticker>,
235 pub recent_trades: Option<Vec<Trade>>,
236}
237
238#[derive(Debug, Clone, PartialEq)]
244pub struct Candle {
245 pub open_time: u64,
247 pub open: f64,
249 pub high: f64,
251 pub low: f64,
253 pub close: f64,
255 pub volume: f64,
257 pub close_time: u64,
259}
260
261#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_order_book_level_value() {
271 let level = OrderBookLevel {
272 price: 1.0002,
273 quantity: 100.0,
274 };
275 assert!((level.value() - 100.02).abs() < 1e-6);
276 }
277
278 #[test]
279 fn test_order_book_empty() {
280 let book = OrderBook {
281 pair: "DAI/USDT".to_string(),
282 bids: vec![],
283 asks: vec![],
284 };
285 assert!(book.best_bid().is_none());
286 assert!(book.best_ask().is_none());
287 assert!(book.mid_price().is_none());
288 assert_eq!(book.bid_depth(), 0.0);
289 assert_eq!(book.ask_depth(), 0.0);
290 }
291
292 #[test]
293 fn test_order_book_with_levels() {
294 let book = OrderBook {
295 pair: "DAI/USDT".to_string(),
296 bids: vec![
297 OrderBookLevel {
298 price: 0.9998,
299 quantity: 100.0,
300 },
301 OrderBookLevel {
302 price: 0.9997,
303 quantity: 50.0,
304 },
305 ],
306 asks: vec![
307 OrderBookLevel {
308 price: 1.0001,
309 quantity: 200.0,
310 },
311 OrderBookLevel {
312 price: 1.0002,
313 quantity: 150.0,
314 },
315 ],
316 };
317 assert_eq!(book.best_bid(), Some(0.9998));
318 assert_eq!(book.best_ask(), Some(1.0001));
319 assert_eq!(book.mid_price(), Some(0.99995));
320 assert!((book.spread().unwrap() - 0.0003).abs() < 1e-10);
321 assert!((book.bid_depth() - 99.98 - 49.985).abs() < 0.01);
322 assert!((book.ask_depth() - 200.02 - 150.03).abs() < 0.01);
323 }
324
325 #[test]
326 fn test_candle_construction() {
327 let candle = Candle {
328 open_time: 1700000000000,
329 open: 100.0,
330 high: 105.0,
331 low: 95.0,
332 close: 102.0,
333 volume: 1_000_000.0,
334 close_time: 1700003600000,
335 };
336 assert_eq!(candle.open_time, 1700000000000);
337 assert_eq!(candle.open, 100.0);
338 assert_eq!(candle.high, 105.0);
339 assert_eq!(candle.low, 95.0);
340 assert_eq!(candle.close, 102.0);
341 assert_eq!(candle.volume, 1_000_000.0);
342 assert_eq!(candle.close_time, 1700003600000);
343 }
344
345 #[test]
346 fn test_candle_partial_eq() {
347 let c1 = Candle {
348 open_time: 1000,
349 open: 1.0,
350 high: 1.1,
351 low: 0.9,
352 close: 1.0,
353 volume: 100.0,
354 close_time: 2000,
355 };
356 let c2 = c1.clone();
357 assert_eq!(c1, c2);
358 }
359
360 #[test]
361 fn test_trade_side_equality() {
362 assert_eq!(TradeSide::Buy, TradeSide::Buy);
363 assert_ne!(TradeSide::Buy, TradeSide::Sell);
364 }
365
366 #[test]
367 fn test_trade_construction() {
368 let trade = Trade {
369 price: 42_000.0,
370 quantity: 1.5,
371 quote_quantity: Some(63_000.0),
372 timestamp_ms: 1700000000000,
373 side: TradeSide::Buy,
374 id: Some("12345".to_string()),
375 };
376 assert_eq!(trade.price, 42_000.0);
377 assert_eq!(trade.quantity, 1.5);
378 assert_eq!(trade.quote_quantity, Some(63_000.0));
379 assert_eq!(trade.side, TradeSide::Buy);
380 assert_eq!(trade.id, Some("12345".to_string()));
381 }
382
383 #[test]
384 fn test_trade_optional_fields() {
385 let trade = Trade {
386 price: 1.0001,
387 quantity: 100.0,
388 quote_quantity: None,
389 timestamp_ms: 1700000000000,
390 side: TradeSide::Sell,
391 id: None,
392 };
393 assert!(trade.quote_quantity.is_none());
394 assert!(trade.id.is_none());
395 }
396
397 #[test]
398 fn test_ticker_construction() {
399 let ticker = Ticker {
400 pair: "BTC/USDT".to_string(),
401 last_price: Some(42_000.0),
402 high_24h: Some(43_000.0),
403 low_24h: Some(41_000.0),
404 volume_24h: Some(50_000.0),
405 quote_volume_24h: Some(2_100_000_000.0),
406 best_bid: Some(41_999.0),
407 best_ask: Some(42_001.0),
408 };
409 assert_eq!(ticker.pair, "BTC/USDT");
410 assert_eq!(ticker.last_price, Some(42_000.0));
411 assert_eq!(ticker.high_24h, Some(43_000.0));
412 }
413
414 #[test]
415 fn test_ticker_all_none() {
416 let ticker = Ticker {
417 pair: "UNKNOWN/USD".to_string(),
418 last_price: None,
419 high_24h: None,
420 low_24h: None,
421 volume_24h: None,
422 quote_volume_24h: None,
423 best_bid: None,
424 best_ask: None,
425 };
426 assert!(ticker.last_price.is_none());
427 assert!(ticker.volume_24h.is_none());
428 }
429
430 #[test]
431 fn test_market_snapshot_full() {
432 let snapshot = MarketSnapshot {
433 order_book: Some(OrderBook {
434 pair: "BTC/USDT".to_string(),
435 bids: vec![OrderBookLevel {
436 price: 42_000.0,
437 quantity: 1.0,
438 }],
439 asks: vec![OrderBookLevel {
440 price: 42_001.0,
441 quantity: 1.0,
442 }],
443 }),
444 ticker: Some(Ticker {
445 pair: "BTC/USDT".to_string(),
446 last_price: Some(42_000.0),
447 high_24h: None,
448 low_24h: None,
449 volume_24h: None,
450 quote_volume_24h: None,
451 best_bid: None,
452 best_ask: None,
453 }),
454 recent_trades: Some(vec![Trade {
455 price: 42_000.0,
456 quantity: 0.5,
457 quote_quantity: None,
458 timestamp_ms: 1700000000000,
459 side: TradeSide::Buy,
460 id: None,
461 }]),
462 };
463 assert!(snapshot.order_book.is_some());
464 assert!(snapshot.ticker.is_some());
465 assert_eq!(snapshot.recent_trades.as_ref().unwrap().len(), 1);
466 }
467
468 #[test]
469 fn test_market_snapshot_empty() {
470 let snapshot = MarketSnapshot {
471 order_book: None,
472 ticker: None,
473 recent_trades: None,
474 };
475 assert!(snapshot.order_book.is_none());
476 assert!(snapshot.ticker.is_none());
477 assert!(snapshot.recent_trades.is_none());
478 }
479
480 #[test]
485 fn test_estimate_buy_zero_mid_price() {
486 let book = OrderBook {
488 pair: "X/Y".to_string(),
489 bids: vec![OrderBookLevel {
490 price: 0.0,
491 quantity: 100.0,
492 }],
493 asks: vec![OrderBookLevel {
494 price: 0.0,
495 quantity: 100.0,
496 }],
497 };
498 assert!(book.estimate_buy_execution(1000.0).is_none());
499 }
500
501 #[test]
502 fn test_estimate_sell_zero_mid_price() {
503 let book = OrderBook {
504 pair: "X/Y".to_string(),
505 bids: vec![OrderBookLevel {
506 price: 0.0,
507 quantity: 100.0,
508 }],
509 asks: vec![OrderBookLevel {
510 price: 0.0,
511 quantity: 100.0,
512 }],
513 };
514 assert!(book.estimate_sell_execution(1000.0).is_none());
515 }
516
517 #[test]
518 fn test_estimate_buy_zero_price_level() {
519 let book = OrderBook {
521 pair: "X/Y".to_string(),
522 bids: vec![OrderBookLevel {
523 price: 1.0,
524 quantity: 100.0,
525 }],
526 asks: vec![
527 OrderBookLevel {
528 price: 0.0,
529 quantity: 100.0,
530 },
531 OrderBookLevel {
532 price: 1.001,
533 quantity: 10000.0,
534 },
535 ],
536 };
537 let est = book.estimate_buy_execution(50.0).unwrap();
538 assert!(est.fillable);
539 }
540
541 #[test]
542 fn test_estimate_sell_zero_price_level() {
543 let book = OrderBook {
545 pair: "X/Y".to_string(),
546 bids: vec![
547 OrderBookLevel {
548 price: 0.0,
549 quantity: 100.0,
550 },
551 OrderBookLevel {
552 price: 0.999,
553 quantity: 10000.0,
554 },
555 ],
556 asks: vec![OrderBookLevel {
557 price: 1.0,
558 quantity: 100.0,
559 }],
560 };
561 let est = book.estimate_sell_execution(50.0).unwrap();
562 assert!(est.fillable);
563 }
564
565 #[test]
566 fn test_estimate_buy_zero_filled_qty() {
567 let book = OrderBook {
569 pair: "X/Y".to_string(),
570 bids: vec![OrderBookLevel {
571 price: 1.0,
572 quantity: 100.0,
573 }],
574 asks: vec![OrderBookLevel {
575 price: 0.0,
576 quantity: 0.0,
577 }],
578 };
579 let est = book.estimate_buy_execution(50.0);
580 assert!(est.is_some());
582 let est = est.unwrap();
583 assert!(!est.fillable);
584 }
585
586 #[test]
587 fn test_estimate_sell_zero_filled_qty() {
588 let book = OrderBook {
590 pair: "X/Y".to_string(),
591 bids: vec![OrderBookLevel {
592 price: 0.0,
593 quantity: 0.0,
594 }],
595 asks: vec![OrderBookLevel {
596 price: 1.0,
597 quantity: 100.0,
598 }],
599 };
600 let est = book.estimate_sell_execution(50.0);
601 assert!(est.is_some());
602 let est = est.unwrap();
603 assert!(!est.fillable);
604 }
605}