1use std::cmp::Reverse;
4use std::collections::{BTreeMap, HashMap};
5use std::fmt::Write;
6use std::str::FromStr;
7
8use chrono::{DateTime, Duration, Utc};
9use crc32fast::Hasher;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use uuid::Uuid;
14
15mod identifiers;
16
17pub use identifiers::{AssetId, ExchangeId, IdentifierParseError, Symbol};
18
19pub type Price = Decimal;
21pub type Quantity = Decimal;
23
24pub type OrderId = String;
26
27#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub enum InstrumentKind {
31 Spot,
32 LinearPerpetual,
33 InversePerpetual,
34}
35
36#[derive(Clone, Debug, Deserialize, Serialize)]
38pub struct Instrument {
39 pub symbol: Symbol,
40 pub base: AssetId,
41 pub quote: AssetId,
42 pub kind: InstrumentKind,
43 pub settlement_currency: AssetId,
44 pub tick_size: Price,
45 pub lot_size: Quantity,
46}
47
48#[derive(Clone, Debug, Deserialize, Serialize)]
50pub struct Cash {
51 pub currency: AssetId,
52 pub quantity: Quantity,
53 pub conversion_rate: Price,
54}
55
56impl Cash {
57 #[must_use]
59 pub fn value(&self) -> Price {
60 self.quantity * self.conversion_rate
61 }
62}
63
64#[derive(Clone, Debug, Default, Deserialize, Serialize)]
66pub struct CashBook(pub HashMap<AssetId, Cash>);
67
68impl CashBook {
69 #[must_use]
70 pub fn new() -> Self {
71 Self(HashMap::new())
72 }
73
74 pub fn upsert(&mut self, cash: Cash) {
75 self.0.insert(cash.currency, cash);
76 }
77
78 pub fn adjust(&mut self, currency: impl Into<AssetId>, delta: Quantity) -> Quantity {
79 let currency = currency.into();
80 let entry = self.0.entry(currency).or_insert(Cash {
81 currency,
82 quantity: Decimal::ZERO,
83 conversion_rate: Decimal::ZERO,
84 });
85 entry.quantity += delta;
86 entry.quantity
87 }
88
89 pub fn update_conversion_rate(&mut self, currency: impl Into<AssetId>, rate: Price) {
90 let currency = currency.into();
91 let entry = self.0.entry(currency).or_insert(Cash {
92 currency,
93 quantity: Decimal::ZERO,
94 conversion_rate: Decimal::ZERO,
95 });
96 entry.conversion_rate = rate;
97 }
98
99 #[must_use]
100 pub fn total_value(&self) -> Price {
101 self.0.values().map(Cash::value).sum()
102 }
103
104 #[must_use]
105 pub fn get(&self, currency: impl Into<AssetId>) -> Option<&Cash> {
106 let currency = currency.into();
107 self.0.get(¤cy)
108 }
109
110 #[must_use]
111 pub fn get_mut(&mut self, currency: impl Into<AssetId>) -> Option<&mut Cash> {
112 let currency = currency.into();
113 self.0.get_mut(¤cy)
114 }
115
116 pub fn iter(&self) -> impl Iterator<Item = (&AssetId, &Cash)> {
117 self.0.iter()
118 }
119}
120
121#[derive(Clone, Debug, Deserialize, Serialize)]
123pub enum ExecutionHint {
124 Twap { duration: Duration },
126 Vwap {
128 duration: Duration,
129 #[serde(default)]
130 participation_rate: Option<Decimal>,
131 },
132 IcebergSimulated {
134 display_size: Quantity,
135 #[serde(default)]
136 limit_offset_bps: Option<Decimal>,
137 },
138 PeggedBest {
140 offset_bps: Decimal,
141 #[serde(default)]
142 clip_size: Option<Quantity>,
143 #[serde(default)]
144 refresh_secs: Option<u64>,
145 #[serde(default)]
146 min_chase_distance: Option<Price>,
147 },
148 Sniper {
150 trigger_price: Price,
151 #[serde(default)]
152 timeout: Option<Duration>,
153 },
154 TrailingStop {
156 activation_price: Price,
157 callback_rate: Decimal,
158 },
159 Plugin {
161 name: String,
162 #[serde(default)]
163 params: Value,
164 },
165}
166
167#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
169#[serde(tag = "type", rename_all = "snake_case")]
170pub enum ExitStrategy {
171 StandardZScore { exit_z: Decimal },
173 HardTimeStop { max_duration_secs: u64 },
175 HalfLifeTimeStop {
177 half_life_candles: u32,
178 multiplier: Decimal,
179 },
180 DecayingThreshold {
182 initial_exit_z: Decimal,
183 decay_rate_per_hour: Decimal,
184 },
185}
186
187#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
189pub enum Side {
190 Buy,
192 Sell,
194}
195
196impl Side {
197 #[must_use]
199 pub fn inverse(self) -> Self {
200 match self {
201 Self::Buy => Self::Sell,
202 Self::Sell => Self::Buy,
203 }
204 }
205
206 #[must_use]
208 pub fn as_i8(self) -> i8 {
209 match self {
210 Self::Buy => 1,
211 Self::Sell => -1,
212 }
213 }
214}
215
216#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
218pub enum OrderType {
219 Market,
221 Limit,
223 StopMarket,
225}
226
227#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
229pub enum TimeInForce {
230 GoodTilCanceled,
231 ImmediateOrCancel,
232 FillOrKill,
233}
234
235#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
237pub enum Interval {
238 OneSecond,
239 OneMinute,
240 FiveMinutes,
241 FifteenMinutes,
242 OneHour,
243 FourHours,
244 OneDay,
245}
246
247impl Interval {
248 #[must_use]
250 pub fn as_duration(self) -> Duration {
251 match self {
252 Self::OneSecond => Duration::seconds(1),
253 Self::OneMinute => Duration::minutes(1),
254 Self::FiveMinutes => Duration::minutes(5),
255 Self::FifteenMinutes => Duration::minutes(15),
256 Self::OneHour => Duration::hours(1),
257 Self::FourHours => Duration::hours(4),
258 Self::OneDay => Duration::days(1),
259 }
260 }
261
262 #[must_use]
264 pub fn to_bybit(self) -> &'static str {
265 match self {
266 Self::OneSecond => "1",
267 Self::OneMinute => "1",
268 Self::FiveMinutes => "5",
269 Self::FifteenMinutes => "15",
270 Self::OneHour => "60",
271 Self::FourHours => "240",
272 Self::OneDay => "D",
273 }
274 }
275
276 pub fn to_binance(self) -> &'static str {
278 match self {
279 Self::OneSecond => "1s",
280 Self::OneMinute => "1m",
281 Self::FiveMinutes => "5m",
282 Self::FifteenMinutes => "15m",
283 Self::OneHour => "1h",
284 Self::FourHours => "4h",
285 Self::OneDay => "1d",
286 }
287 }
288}
289
290impl FromStr for Interval {
291 type Err = String;
292
293 fn from_str(value: &str) -> Result<Self, Self::Err> {
294 match value.to_lowercase().as_str() {
295 "1s" | "1sec" | "1second" | "1" => Ok(Self::OneSecond),
296 "1m" | "1min" | "1minute" => Ok(Self::OneMinute),
297 "5m" | "5min" | "5minutes" => Ok(Self::FiveMinutes),
298 "15m" | "15min" | "15minutes" => Ok(Self::FifteenMinutes),
299 "1h" | "60m" | "1hour" | "60" => Ok(Self::OneHour),
300 "4h" | "240m" | "4hours" | "240" => Ok(Self::FourHours),
301 "1d" | "day" | "d" => Ok(Self::OneDay),
302 other => Err(format!("unsupported interval '{other}'")),
303 }
304 }
305}
306
307#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
309pub struct Tick {
310 pub symbol: Symbol,
311 pub price: Price,
312 pub size: Quantity,
313 pub side: Side,
314 pub exchange_timestamp: DateTime<Utc>,
315 pub received_at: DateTime<Utc>,
316}
317
318#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
320pub struct Candle {
321 pub symbol: Symbol,
322 pub interval: Interval,
323 pub open: Price,
324 pub high: Price,
325 pub low: Price,
326 pub close: Price,
327 pub volume: Quantity,
328 pub timestamp: DateTime<Utc>,
329}
330
331#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
333pub struct OrderBookLevel {
334 pub price: Price,
335 pub size: Quantity,
336}
337
338#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
340pub struct OrderBook {
341 pub symbol: Symbol,
342 pub bids: Vec<OrderBookLevel>,
343 pub asks: Vec<OrderBookLevel>,
344 pub timestamp: DateTime<Utc>,
345 #[serde(default)]
346 pub exchange_checksum: Option<u32>,
347 #[serde(default)]
348 pub local_checksum: Option<u32>,
349}
350
351impl OrderBook {
352 #[must_use]
354 pub fn best_bid(&self) -> Option<&OrderBookLevel> {
355 self.bids.first()
356 }
357
358 #[must_use]
360 pub fn best_ask(&self) -> Option<&OrderBookLevel> {
361 self.asks.first()
362 }
363
364 #[must_use]
366 pub fn imbalance(&self, depth: usize) -> Option<Decimal> {
367 let depth = depth.max(1);
368 let bid_vol: Decimal = self.bids.iter().take(depth).map(|level| level.size).sum();
369 let ask_vol: Decimal = self.asks.iter().take(depth).map(|level| level.size).sum();
370 let denom = bid_vol + ask_vol;
371 if denom.is_zero() {
372 None
373 } else {
374 Some((bid_vol - ask_vol) / denom)
375 }
376 }
377
378 #[must_use]
380 pub fn computed_checksum(&self, depth: Option<usize>) -> u32 {
381 let mut lob = LocalOrderBook::new();
382 let bids = self
383 .bids
384 .iter()
385 .map(|level| (level.price, level.size))
386 .collect::<Vec<_>>();
387 let asks = self
388 .asks
389 .iter()
390 .map(|level| (level.price, level.size))
391 .collect::<Vec<_>>();
392 lob.load_snapshot(&bids, &asks);
393 let depth = depth.unwrap_or_else(|| bids.len().max(asks.len()).max(1));
394 lob.checksum(depth)
395 }
396}
397
398#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
400pub struct DepthUpdate {
401 pub symbol: Symbol,
402 pub bids: Vec<OrderBookLevel>,
403 pub asks: Vec<OrderBookLevel>,
404 pub timestamp: DateTime<Utc>,
405}
406
407#[derive(Clone, Debug, Default, Deserialize, Serialize)]
409pub struct LocalOrderBook {
410 bids: BTreeMap<Reverse<Price>, Quantity>,
411 asks: BTreeMap<Price, Quantity>,
412}
413
414impl LocalOrderBook {
415 #[must_use]
417 pub fn new() -> Self {
418 Self::default()
419 }
420
421 pub fn load_snapshot(&mut self, bids: &[(Price, Quantity)], asks: &[(Price, Quantity)]) {
423 self.bids.clear();
424 self.asks.clear();
425 for &(price, qty) in bids {
426 self.add_order(Side::Buy, price, qty);
427 }
428 for &(price, qty) in asks {
429 self.add_order(Side::Sell, price, qty);
430 }
431 }
432
433 pub fn add_order(&mut self, side: Side, price: Price, quantity: Quantity) {
435 if quantity <= Decimal::ZERO {
436 return;
437 }
438 match side {
439 Side::Buy => {
440 let key = Reverse(price);
441 let entry = self.bids.entry(key).or_insert(Decimal::ZERO);
442 *entry += quantity;
443 }
444 Side::Sell => {
445 let entry = self.asks.entry(price).or_insert(Decimal::ZERO);
446 *entry += quantity;
447 }
448 }
449 }
450
451 pub fn apply_delta(&mut self, side: Side, price: Price, quantity: Quantity) {
453 if quantity <= Decimal::ZERO {
454 self.clear_level(side, price);
455 return;
456 }
457 match side {
458 Side::Buy => {
459 self.bids.insert(Reverse(price), quantity);
460 }
461 Side::Sell => {
462 self.asks.insert(price, quantity);
463 }
464 }
465 }
466
467 pub fn apply_deltas(&mut self, bids: &[(Price, Quantity)], asks: &[(Price, Quantity)]) {
469 for &(price, qty) in bids {
470 self.apply_delta(Side::Buy, price, qty);
471 }
472 for &(price, qty) in asks {
473 self.apply_delta(Side::Sell, price, qty);
474 }
475 }
476
477 pub fn remove_order(&mut self, side: Side, price: Price, quantity: Quantity) {
479 if quantity <= Decimal::ZERO {
480 return;
481 }
482 match side {
483 Side::Buy => {
484 let key = Reverse(price);
485 if let Some(level) = self.bids.get_mut(&key) {
486 *level -= quantity;
487 if *level <= Decimal::ZERO {
488 self.bids.remove(&key);
489 }
490 }
491 }
492 Side::Sell => {
493 if let Some(level) = self.asks.get_mut(&price) {
494 *level -= quantity;
495 if *level <= Decimal::ZERO {
496 self.asks.remove(&price);
497 }
498 }
499 }
500 }
501 }
502
503 pub fn clear_level(&mut self, side: Side, price: Price) {
505 match side {
506 Side::Buy => {
507 self.bids.remove(&Reverse(price));
508 }
509 Side::Sell => {
510 self.asks.remove(&price);
511 }
512 }
513 }
514
515 #[must_use]
517 pub fn best_bid(&self) -> Option<(Price, Quantity)> {
518 self.bids.iter().next().map(|(price, qty)| (price.0, *qty))
519 }
520
521 #[must_use]
523 pub fn best_ask(&self) -> Option<(Price, Quantity)> {
524 self.asks.iter().next().map(|(price, qty)| (*price, *qty))
525 }
526
527 pub fn bids(&self) -> impl Iterator<Item = (Price, Quantity)> + '_ {
529 self.bids.iter().map(|(price, qty)| (price.0, *qty))
530 }
531
532 pub fn asks(&self) -> impl Iterator<Item = (Price, Quantity)> + '_ {
534 self.asks.iter().map(|(price, qty)| (*price, *qty))
535 }
536
537 pub fn take_liquidity(
539 &mut self,
540 aggressive_side: Side,
541 mut quantity: Quantity,
542 ) -> Vec<(Price, Quantity)> {
543 let mut fills = Vec::new();
544 while quantity > Decimal::ZERO {
545 let (price, available) = match aggressive_side {
546 Side::Buy => match self.best_ask() {
547 Some(level) => level,
548 None => break,
549 },
550 Side::Sell => match self.best_bid() {
551 Some(level) => level,
552 None => break,
553 },
554 };
555 let traded = quantity.min(available);
556 let contra_side = aggressive_side.inverse();
557 self.remove_order(contra_side, price, traded);
558 fills.push((price, traded));
559 quantity -= traded;
560 }
561 fills
562 }
563
564 #[must_use]
566 pub fn is_empty(&self) -> bool {
567 self.bids.is_empty() && self.asks.is_empty()
568 }
569
570 #[must_use]
572 pub fn checksum(&self, depth: usize) -> u32 {
573 if depth == 0 {
574 return 0;
575 }
576 let mut buffer = String::new();
577 let mut first = true;
578 for (price, size) in self.bids().take(depth) {
579 if !first {
580 buffer.push(':');
581 }
582 first = false;
583 write!(buffer, "{}:{}", price.normalize(), size.normalize()).ok();
584 }
585 for (price, size) in self.asks().take(depth) {
586 if !first {
587 buffer.push(':');
588 }
589 first = false;
590 write!(buffer, "{}:{}", price.normalize(), size.normalize()).ok();
591 }
592 let mut hasher = Hasher::new();
593 hasher.update(buffer.as_bytes());
594 hasher.finalize()
595 }
596
597 pub fn bid_levels(&self, depth: usize) -> Vec<(Price, Quantity)> {
599 self.bids().take(depth).collect()
600 }
601
602 pub fn ask_levels(&self, depth: usize) -> Vec<(Price, Quantity)> {
604 self.asks().take(depth).collect()
605 }
606
607 #[must_use]
609 pub fn volume_at_level(&self, side: Side, price: Price) -> Quantity {
610 match side {
611 Side::Buy => self
612 .bids
613 .get(&Reverse(price))
614 .copied()
615 .unwrap_or(Decimal::ZERO),
616 Side::Sell => self.asks.get(&price).copied().unwrap_or(Decimal::ZERO),
617 }
618 }
619}
620
621#[derive(Clone, Debug, Deserialize, Serialize)]
623pub struct OrderRequest {
624 pub symbol: Symbol,
625 pub side: Side,
626 pub order_type: OrderType,
627 pub quantity: Quantity,
628 pub price: Option<Price>,
629 pub trigger_price: Option<Price>,
630 pub time_in_force: Option<TimeInForce>,
631 pub client_order_id: Option<String>,
632 pub take_profit: Option<Price>,
633 pub stop_loss: Option<Price>,
634 pub display_quantity: Option<Quantity>,
635}
636
637#[derive(Clone, Debug, Deserialize, Serialize)]
639pub struct OrderUpdateRequest {
640 pub order_id: OrderId,
641 pub symbol: Symbol,
642 pub side: Side,
643 pub new_price: Option<Price>,
644 pub new_quantity: Option<Quantity>,
645}
646
647#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
649pub enum OrderStatus {
650 PendingNew,
651 Accepted,
652 PartiallyFilled,
653 Filled,
654 Canceled,
655 Rejected,
656}
657
658#[derive(Clone, Debug, Deserialize, Serialize)]
660pub struct Order {
661 pub id: OrderId,
662 pub request: OrderRequest,
663 pub status: OrderStatus,
664 pub filled_quantity: Quantity,
665 pub avg_fill_price: Option<Price>,
666 pub created_at: DateTime<Utc>,
667 pub updated_at: DateTime<Utc>,
668}
669
670#[derive(Clone, Debug, Deserialize, Serialize)]
672pub struct Fill {
673 pub order_id: OrderId,
674 pub symbol: Symbol,
675 pub side: Side,
676 pub fill_price: Price,
677 pub fill_quantity: Quantity,
678 pub fee: Option<Price>,
679 #[serde(default)]
680 pub fee_asset: Option<AssetId>,
681 pub timestamp: DateTime<Utc>,
682}
683
684#[derive(Clone, Debug, Deserialize, Serialize)]
686pub struct Trade {
687 pub id: Uuid,
688 pub fill: Fill,
689 pub realized_pnl: Price,
690}
691
692#[derive(Clone, Debug, Deserialize, Serialize)]
694pub struct Position {
695 pub symbol: Symbol,
696 pub side: Option<Side>,
697 pub quantity: Quantity,
698 pub entry_price: Option<Price>,
699 pub unrealized_pnl: Price,
700 pub updated_at: DateTime<Utc>,
701}
702
703impl Position {
704 pub fn mark_price(&mut self, price: Price) {
706 if let (Some(entry), Some(side)) = (self.entry_price, self.side) {
707 let delta = match side {
708 Side::Buy => price - entry,
709 Side::Sell => entry - price,
710 };
711 self.unrealized_pnl = delta * self.quantity;
712 }
713 self.updated_at = Utc::now();
714 }
715}
716
717#[derive(Clone, Debug, Deserialize, Serialize)]
719pub struct AccountBalance {
720 #[serde(default)]
721 pub exchange: ExchangeId,
722 #[serde(alias = "currency")]
723 pub asset: AssetId,
724 pub total: Price,
725 pub available: Price,
726 pub updated_at: DateTime<Utc>,
727}
728
729#[derive(Clone, Debug, Deserialize, Serialize)]
731pub struct Signal {
732 pub id: Uuid,
733 pub symbol: Symbol,
734 pub kind: SignalKind,
735 pub confidence: f64,
736 #[serde(default)]
737 pub quantity: Option<Quantity>,
738 #[serde(default)]
739 pub group_id: Option<Uuid>,
740 #[serde(default)]
741 pub panic_behavior: Option<SignalPanicBehavior>,
742 pub generated_at: DateTime<Utc>,
743 pub note: Option<String>,
744 pub stop_loss: Option<Price>,
745 pub take_profit: Option<Price>,
746 pub execution_hint: Option<ExecutionHint>,
747}
748
749#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
751pub enum SignalKind {
752 EnterLong,
753 ExitLong,
754 EnterShort,
755 ExitShort,
756 Flatten,
757}
758
759impl SignalKind {
760 #[must_use]
762 pub fn side(self) -> Side {
763 match self {
764 Self::EnterLong | Self::ExitShort => Side::Buy,
765 Self::EnterShort | Self::ExitLong => Side::Sell,
766 Self::Flatten => {
767 Side::Sell
771 }
772 }
773 }
774}
775
776impl Signal {
777 #[must_use]
779 pub fn new(symbol: impl Into<Symbol>, kind: SignalKind, confidence: f64) -> Self {
780 Self {
781 id: Uuid::new_v4(),
782 symbol: symbol.into(),
783 kind,
784 confidence,
785 quantity: None,
786 group_id: None,
787 panic_behavior: None,
788 generated_at: Utc::now(),
789 note: None,
790 stop_loss: None,
791 take_profit: None,
792 execution_hint: None,
793 }
794 }
795
796 #[must_use]
798 pub fn with_hint(mut self, hint: ExecutionHint) -> Self {
799 self.execution_hint = Some(hint);
800 self
801 }
802
803 #[must_use]
805 pub fn with_quantity(mut self, quantity: Quantity) -> Self {
806 self.quantity = Some(quantity);
807 self
808 }
809
810 #[must_use]
812 pub fn with_group(mut self, group_id: Uuid) -> Self {
813 self.group_id = Some(group_id);
814 self
815 }
816
817 #[must_use]
819 pub fn with_panic_behavior(mut self, behavior: SignalPanicBehavior) -> Self {
820 self.panic_behavior = Some(behavior);
821 self
822 }
823}
824
825#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
827pub enum SignalPanicBehavior {
828 Market,
830 AggressiveLimit { offset_bps: Decimal },
832}
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837 use rust_decimal::prelude::FromPrimitive;
838
839 #[test]
840 fn interval_duration_matches_definition() {
841 assert_eq!(Interval::OneMinute.as_duration(), Duration::minutes(1));
842 assert_eq!(Interval::FourHours.as_duration(), Duration::hours(4));
843 }
844
845 #[test]
846 fn position_mark_price_updates_unrealized_pnl() {
847 let mut position = Position {
848 symbol: Symbol::from("BTCUSDT"),
849 side: Some(Side::Buy),
850 quantity: Decimal::from_f64(0.5).unwrap(),
851 entry_price: Some(Decimal::from(60_000)),
852 unrealized_pnl: Decimal::ZERO,
853 updated_at: Utc::now(),
854 };
855 position.mark_price(Decimal::from(60_500));
856 assert_eq!(position.unrealized_pnl, Decimal::from(250));
857 }
858
859 #[test]
860 fn local_order_book_tracks_best_levels() {
861 let mut lob = LocalOrderBook::new();
862 lob.add_order(Side::Buy, Decimal::from(10), Decimal::from(2));
863 lob.add_order(Side::Buy, Decimal::from(11), Decimal::from(1));
864 lob.add_order(
865 Side::Sell,
866 Decimal::from(12),
867 Decimal::from_f64(1.5).unwrap(),
868 );
869 lob.add_order(Side::Sell, Decimal::from(13), Decimal::from(3));
870
871 assert_eq!(lob.best_bid(), Some((Decimal::from(11), Decimal::from(1))));
872 assert_eq!(
873 lob.best_ask(),
874 Some((Decimal::from(12), Decimal::from_f64(1.5).unwrap()))
875 );
876
877 let fills = lob.take_liquidity(Side::Buy, Decimal::from(2));
878 assert_eq!(
879 fills,
880 vec![
881 (Decimal::from(12), Decimal::from_f64(1.5).unwrap()),
882 (Decimal::from(13), Decimal::from_f64(0.5).unwrap())
883 ]
884 );
885 assert_eq!(
886 lob.best_ask(),
887 Some((Decimal::from(13), Decimal::from_f64(2.5).unwrap()))
888 );
889 }
890
891 #[test]
892 fn local_order_book_apply_delta_overwrites_level() {
893 let mut lob = LocalOrderBook::new();
894 lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(1));
895 lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(3));
896 assert_eq!(lob.best_bid(), Some((Decimal::from(100), Decimal::from(3))));
897
898 lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::ZERO);
899 assert!(lob.best_bid().is_none());
900 }
901
902 #[test]
903 fn local_order_book_checksum_reflects_depth() {
904 let mut lob = LocalOrderBook::new();
905 lob.apply_delta(Side::Buy, Decimal::from(10), Decimal::from(1));
906 lob.apply_delta(Side::Buy, Decimal::from(9), Decimal::from(2));
907 lob.apply_delta(Side::Sell, Decimal::from(11), Decimal::from(1));
908 lob.apply_delta(Side::Sell, Decimal::from(12), Decimal::from(2));
909
910 let checksum_full = lob.checksum(2);
911 let checksum_partial = lob.checksum(1);
912 assert_ne!(checksum_full, checksum_partial);
913 }
914
915 #[test]
916 fn local_order_book_reports_volume_at_level() {
917 let mut lob = LocalOrderBook::new();
918 lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(3));
919 lob.apply_delta(Side::Sell, Decimal::from(105), Decimal::from(2));
920
921 assert_eq!(
922 lob.volume_at_level(Side::Buy, Decimal::from(100)),
923 Decimal::from(3)
924 );
925 assert_eq!(
926 lob.volume_at_level(Side::Sell, Decimal::from(105)),
927 Decimal::from(2)
928 );
929 assert_eq!(
930 lob.volume_at_level(Side::Buy, Decimal::from(101)),
931 Decimal::ZERO
932 );
933 }
934}