1use crate::types::{NanoTimestamp, Price, Quantity, Side, Symbol};
18use rust_decimal::Decimal;
19
20#[derive(Debug, Clone)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct Tick {
24 pub symbol: Symbol,
26 pub price: Price,
28 pub quantity: Quantity,
30 pub side: Side,
32 pub timestamp: NanoTimestamp,
34}
35
36impl Tick {
37 pub fn new(
39 symbol: Symbol,
40 price: Price,
41 quantity: Quantity,
42 side: Side,
43 timestamp: NanoTimestamp,
44 ) -> Self {
45 Self {
46 symbol,
47 price,
48 quantity,
49 side,
50 timestamp,
51 }
52 }
53
54 pub fn notional(&self) -> Decimal {
56 self.price.value() * self.quantity.value()
57 }
58
59 pub fn notional_checked(&self) -> Option<Decimal> {
61 self.price.checked_mul(self.quantity)
62 }
63
64 pub fn is_buy_aggressor(&self) -> bool {
66 self.side == Side::Bid
67 }
68
69 pub fn is_sell_aggressor(&self) -> bool {
71 self.side == Side::Ask
72 }
73
74 pub fn is_buy(&self) -> bool {
76 self.side == Side::Bid
77 }
78
79 pub fn is_sell(&self) -> bool {
81 self.side == Side::Ask
82 }
83
84 pub fn is_uptick(&self, prev: &Tick) -> bool {
86 self.price.value() > prev.price.value()
87 }
88
89 pub fn is_downtick(&self, prev: &Tick) -> bool {
91 self.price.value() < prev.price.value()
92 }
93
94 pub fn delta(ticks: &[Tick]) -> Decimal {
99 ticks.iter().map(|t| {
100 match t.side {
101 Side::Bid => t.quantity.value(),
102 Side::Ask => -t.quantity.value(),
103 }
104 }).sum()
105 }
106
107 pub fn cumulative_delta(ticks: &[Tick]) -> Vec<Decimal> {
113 let mut running = Decimal::ZERO;
114 ticks
115 .iter()
116 .map(|t| {
117 match t.side {
118 Side::Bid => running += t.quantity.value(),
119 Side::Ask => running -= t.quantity.value(),
120 }
121 running
122 })
123 .collect()
124 }
125
126 pub fn average_price(ticks: &[Tick]) -> Option<Decimal> {
130 if ticks.is_empty() {
131 return None;
132 }
133 #[allow(clippy::cast_possible_truncation)]
134 let sum: Decimal = ticks.iter().map(|t| t.price.value()).sum();
135 Some(sum / Decimal::from(ticks.len() as u32))
136 }
137
138 pub fn buy_volume(ticks: &[Tick]) -> Decimal {
142 ticks
143 .iter()
144 .filter(|t| t.side == Side::Bid)
145 .map(|t| t.quantity.value())
146 .sum()
147 }
148
149 pub fn sell_volume(ticks: &[Tick]) -> Decimal {
153 ticks
154 .iter()
155 .filter(|t| t.side == Side::Ask)
156 .map(|t| t.quantity.value())
157 .sum()
158 }
159
160 pub fn vwap_from_slice(ticks: &[Tick]) -> Option<Decimal> {
166 let total_qty: Decimal = ticks.iter().map(|t| t.quantity.value()).sum();
167 if total_qty.is_zero() {
168 return None;
169 }
170 let weighted: Decimal = ticks.iter().map(|t| t.price.value() * t.quantity.value()).sum();
171 Some(weighted / total_qty)
172 }
173
174 pub fn max_price(ticks: &[Tick]) -> Option<Price> {
176 ticks.iter().map(|t| t.price).max_by(|a, b| a.value().cmp(&b.value()))
177 }
178
179 pub fn min_price(ticks: &[Tick]) -> Option<Price> {
181 ticks.iter().map(|t| t.price).min_by(|a, b| a.value().cmp(&b.value()))
182 }
183
184 pub fn time_weighted_avg_price(ticks: &[Tick]) -> Option<Decimal> {
190 if ticks.len() < 2 {
191 return None;
192 }
193 let mut total_weight = 0u128;
194 let mut weighted_sum = Decimal::ZERO;
195 for i in 1..ticks.len() {
196 let elapsed = ticks[i].timestamp.nanos()
197 .saturating_sub(ticks[i - 1].timestamp.nanos())
198 .max(0) as u128;
199 total_weight = total_weight.saturating_add(elapsed);
200 #[allow(clippy::cast_possible_truncation)]
201 let w = Decimal::from(elapsed as u64);
202 weighted_sum += ticks[i].price.value() * w;
203 }
204 if total_weight == 0 {
205 return None;
206 }
207 #[allow(clippy::cast_possible_truncation)]
208 Some(weighted_sum / Decimal::from(total_weight as u64))
209 }
210
211 pub fn largest_trade(ticks: &[Tick]) -> Option<&Tick> {
215 ticks.iter().max_by(|a, b| {
216 let na = a.price.value() * a.quantity.value();
217 let nb = b.price.value() * b.quantity.value();
218 na.cmp(&nb)
219 })
220 }
221
222 pub fn classify_aggressor(&self) -> &'static str {
229 match self.side {
230 Side::Bid => "market_buy",
231 Side::Ask => "market_sell",
232 }
233 }
234
235 pub fn imbalance_ratio(ticks: &[Tick]) -> Option<Decimal> {
240 let total: Decimal = ticks.iter().map(|t| t.quantity.value()).sum();
241 if total.is_zero() {
242 return None;
243 }
244 let buy_vol = Self::buy_volume(ticks);
245 Some(buy_vol / total)
246 }
247
248 pub fn count_by_side(ticks: &[Tick]) -> (usize, usize) {
252 let buy = ticks.iter().filter(|t| t.side == Side::Bid).count();
253 let sell = ticks.len() - buy;
254 (buy, sell)
255 }
256
257 pub fn notional_volume(ticks: &[Tick]) -> Decimal {
261 ticks.iter().map(|t| t.price.value() * t.quantity.value()).sum()
262 }
263
264 pub fn tick_direction_series(ticks: &[Tick]) -> Vec<i8> {
269 if ticks.is_empty() {
270 return vec![];
271 }
272 let mut result = Vec::with_capacity(ticks.len());
273 result.push(0i8);
274 for w in ticks.windows(2) {
275 let prev = w[0].price.value();
276 let curr = w[1].price.value();
277 result.push(if curr > prev { 1 } else if curr < prev { -1 } else { 0 });
278 }
279 result
280 }
281
282 pub fn median_price(ticks: &[Tick]) -> Option<Decimal> {
287 if ticks.is_empty() {
288 return None;
289 }
290 let mut prices: Vec<Decimal> = ticks.iter().map(|t| t.price.value()).collect();
291 prices.sort_unstable();
292 let mid = prices.len() / 2;
293 if prices.len() % 2 == 0 {
294 Some((prices[mid - 1] + prices[mid]) / Decimal::TWO)
295 } else {
296 Some(prices[mid])
297 }
298 }
299
300 pub fn price_impact(ticks: &[Tick], ref_price: Decimal) -> Option<Decimal> {
309 if ticks.is_empty() {
310 return None;
311 }
312 let total_qty: Decimal = ticks.iter().map(|t| t.quantity.value()).sum();
313 if total_qty.is_zero() {
314 return None;
315 }
316 let weighted_dev: Decimal = ticks
317 .iter()
318 .map(|t| (t.price.value() - ref_price) * t.quantity.value())
319 .sum();
320 Some(weighted_dev / total_qty)
321 }
322
323 pub fn cluster_count(ticks: &[Tick], gap_ns: u64) -> usize {
330 if ticks.is_empty() {
331 return 0;
332 }
333 let mut clusters = 1usize;
334 for w in ticks.windows(2) {
335 let t0 = w[0].timestamp.nanos() as u64;
336 let t1 = w[1].timestamp.nanos() as u64;
337 if t1.saturating_sub(t0) > gap_ns {
338 clusters += 1;
339 }
340 }
341 clusters
342 }
343}
344
345#[derive(Clone)]
349pub struct TickFilter {
350 symbol: Option<Symbol>,
351 side: Option<Side>,
352 min_qty: Option<Quantity>,
353 max_qty: Option<Quantity>,
354 min_price: Option<Price>,
355 max_price: Option<Price>,
356 min_notional: Option<rust_decimal::Decimal>,
357 max_notional: Option<rust_decimal::Decimal>,
358 from_ts: Option<NanoTimestamp>,
359 to_ts: Option<NanoTimestamp>,
360}
361
362impl TickFilter {
363 pub fn new() -> Self {
365 Self {
366 symbol: None,
367 side: None,
368 min_qty: None,
369 max_qty: None,
370 min_price: None,
371 max_price: None,
372 min_notional: None,
373 max_notional: None,
374 from_ts: None,
375 to_ts: None,
376 }
377 }
378
379 #[must_use]
381 pub fn symbol(mut self, s: Symbol) -> Self {
382 self.symbol = Some(s);
383 self
384 }
385
386 #[must_use]
388 pub fn side(mut self, s: Side) -> Self {
389 self.side = Some(s);
390 self
391 }
392
393 #[must_use]
395 pub fn min_quantity(mut self, q: Quantity) -> Self {
396 self.min_qty = Some(q);
397 self
398 }
399
400 #[must_use]
402 pub fn max_quantity(mut self, q: Quantity) -> Self {
403 self.max_qty = Some(q);
404 self
405 }
406
407 #[must_use]
409 pub fn min_price(mut self, p: Price) -> Self {
410 self.min_price = Some(p);
411 self
412 }
413
414 #[must_use]
416 pub fn max_price(mut self, p: Price) -> Self {
417 self.max_price = Some(p);
418 self
419 }
420
421 #[must_use]
423 pub fn min_notional(mut self, n: rust_decimal::Decimal) -> Self {
424 self.min_notional = Some(n);
425 self
426 }
427
428 #[must_use]
430 pub fn max_notional(mut self, n: rust_decimal::Decimal) -> Self {
431 self.max_notional = Some(n);
432 self
433 }
434
435 #[must_use]
437 pub fn timestamp_range(mut self, from: NanoTimestamp, to: NanoTimestamp) -> Self {
438 self.from_ts = Some(from);
439 self.to_ts = Some(to);
440 self
441 }
442
443 pub fn has_symbol_filter(&self) -> bool {
445 self.symbol.is_some()
446 }
447
448 pub fn has_side_filter(&self) -> bool {
450 self.side.is_some()
451 }
452
453 pub fn has_min_qty_filter(&self) -> bool {
455 self.min_qty.is_some()
456 }
457
458 pub fn has_price_filter(&self) -> bool {
460 self.min_price.is_some() || self.max_price.is_some()
461 }
462
463 pub fn has_notional_filter(&self) -> bool {
465 self.min_notional.is_some() || self.max_notional.is_some()
466 }
467
468 pub fn clear(self) -> Self {
472 Self::new()
473 }
474
475 pub fn is_empty(&self) -> bool {
480 self.symbol.is_none()
481 && self.side.is_none()
482 && self.min_qty.is_none()
483 && self.max_qty.is_none()
484 && self.min_price.is_none()
485 && self.max_price.is_none()
486 && self.min_notional.is_none()
487 && self.max_notional.is_none()
488 && self.from_ts.is_none()
489 && self.to_ts.is_none()
490 }
491
492 pub fn matches(&self, tick: &Tick) -> bool {
494 if let Some(ref sym) = self.symbol {
495 if tick.symbol != *sym {
496 return false;
497 }
498 }
499 if let Some(ref side) = self.side {
500 if tick.side != *side {
501 return false;
502 }
503 }
504 if let Some(ref min_qty) = self.min_qty {
505 if tick.quantity < *min_qty {
506 return false;
507 }
508 }
509 if let Some(ref max_qty) = self.max_qty {
510 if tick.quantity > *max_qty {
511 return false;
512 }
513 }
514 if let Some(ref min_p) = self.min_price {
515 if tick.price < *min_p {
516 return false;
517 }
518 }
519 if let Some(ref max_p) = self.max_price {
520 if tick.price > *max_p {
521 return false;
522 }
523 }
524 if let Some(ref min_n) = self.min_notional {
525 if tick.notional() < *min_n {
526 return false;
527 }
528 }
529 if let Some(ref max_n) = self.max_notional {
530 if tick.notional() > *max_n {
531 return false;
532 }
533 }
534 if let Some(from) = self.from_ts {
535 if tick.timestamp.is_before(from) {
536 return false;
537 }
538 }
539 if let Some(to) = self.to_ts {
540 if tick.timestamp.is_after(to) {
541 return false;
542 }
543 }
544 true
545 }
546
547 pub fn count_matches(&self, ticks: &[Tick]) -> usize {
552 ticks.iter().filter(|t| self.matches(t)).count()
553 }
554}
555
556impl Default for TickFilter {
557 fn default() -> Self {
558 Self::new()
559 }
560}
561
562pub struct TickReplayer {
564 ticks: Vec<Tick>,
565 index: usize,
566}
567
568impl TickReplayer {
569 pub fn new(mut ticks: Vec<Tick>) -> Self {
571 ticks.sort_by_key(|t| t.timestamp);
572 Self { ticks, index: 0 }
573 }
574
575 pub fn next_tick(&mut self) -> Option<&Tick> {
577 let tick = self.ticks.get(self.index)?;
578 self.index += 1;
579 Some(tick)
580 }
581
582 pub fn remaining(&self) -> usize {
584 self.ticks.len().saturating_sub(self.index)
585 }
586
587 pub fn peek(&self) -> Option<&Tick> {
589 self.ticks.get(self.index)
590 }
591
592 pub fn ticks(&self) -> &[Tick] {
594 &self.ticks
595 }
596
597 pub fn reset(&mut self) {
599 self.index = 0;
600 }
601
602 pub fn count(&self) -> usize {
604 self.ticks.len()
605 }
606
607 pub fn vwap(&self) -> Option<Decimal> {
613 let total_vol: Decimal = self.ticks.iter().map(|t| t.quantity.value()).sum();
614 if total_vol.is_zero() {
615 return None;
616 }
617 let total_notional: Decimal = self.ticks.iter().map(|t| t.notional()).sum();
618 Some(total_notional / total_vol)
619 }
620
621 pub fn filter_ticks(&self, filter: &TickFilter) -> Vec<Tick> {
623 self.ticks
624 .iter()
625 .filter(|t| filter.matches(t))
626 .cloned()
627 .collect()
628 }
629
630 pub fn between(&self, from: NanoTimestamp, to: NanoTimestamp) -> Vec<Tick> {
632 self.ticks
633 .iter()
634 .filter(|t| !t.timestamp.is_before(from) && !t.timestamp.is_after(to))
635 .cloned()
636 .collect()
637 }
638
639 pub fn delta(&self) -> Decimal {
643 self.buy_volume() - self.sell_volume()
644 }
645
646 pub fn time_span_nanos(&self) -> Option<i64> {
650 if self.ticks.len() < 2 {
651 return None;
652 }
653 let first = self.ticks.first()?.timestamp;
654 let last = self.ticks.last()?.timestamp;
655 Some(last.elapsed_since(first))
656 }
657
658 pub fn total_notional(&self) -> Decimal {
660 self.ticks.iter().map(|t| t.notional()).sum()
661 }
662
663 pub fn buy_volume(&self) -> Decimal {
665 self.ticks
666 .iter()
667 .filter(|t| t.side == Side::Bid)
668 .map(|t| t.quantity.value())
669 .sum()
670 }
671
672 pub fn sell_volume(&self) -> Decimal {
674 self.ticks
675 .iter()
676 .filter(|t| t.side == Side::Ask)
677 .map(|t| t.quantity.value())
678 .sum()
679 }
680
681 pub fn first(&self) -> Option<&Tick> {
683 self.ticks.first()
684 }
685
686 pub fn last(&self) -> Option<&Tick> {
688 self.ticks.last()
689 }
690
691 pub fn vwap_by_side(&self) -> (Option<Decimal>, Option<Decimal>) {
696 let mut bid_notional = Decimal::ZERO;
697 let mut bid_vol = Decimal::ZERO;
698 let mut ask_notional = Decimal::ZERO;
699 let mut ask_vol = Decimal::ZERO;
700 for tick in &self.ticks {
701 let vol = tick.quantity.value();
702 let notional = tick.notional();
703 match tick.side {
704 Side::Bid => {
705 bid_notional += notional;
706 bid_vol += vol;
707 }
708 Side::Ask => {
709 ask_notional += notional;
710 ask_vol += vol;
711 }
712 }
713 }
714 let bid_vwap = if bid_vol.is_zero() { None } else { Some(bid_notional / bid_vol) };
715 let ask_vwap = if ask_vol.is_zero() { None } else { Some(ask_notional / ask_vol) };
716 (bid_vwap, ask_vwap)
717 }
718
719 pub fn collect_by_symbol(&self) -> std::collections::HashMap<Symbol, Vec<Tick>> {
724 let mut map: std::collections::HashMap<Symbol, Vec<Tick>> = std::collections::HashMap::new();
725 for tick in &self.ticks {
726 map.entry(tick.symbol.clone()).or_default().push(tick.clone());
727 }
728 map
729 }
730
731 pub fn price_range(&self) -> Option<Decimal> {
735 let mut max_p = self.ticks.first()?.price.value();
736 let mut min_p = max_p;
737 for t in &self.ticks {
738 let p = t.price.value();
739 if p > max_p { max_p = p; }
740 if p < min_p { min_p = p; }
741 }
742 Some(max_p - min_p)
743 }
744
745 pub fn tick_count_by_side(&self) -> (usize, usize) {
747 let bid = self.ticks.iter().filter(|t| t.side == Side::Bid).count();
748 let ask = self.ticks.iter().filter(|t| t.side == Side::Ask).count();
749 (bid, ask)
750 }
751
752 pub fn median_trade_size(&self) -> Option<Decimal> {
756 if self.ticks.is_empty() {
757 return None;
758 }
759 let mut sizes: Vec<Decimal> = self.ticks.iter().map(|t| t.quantity.value()).collect();
760 sizes.sort();
761 Some(sizes[sizes.len() / 2])
762 }
763
764 pub fn avg_trade_size(&self) -> Option<Decimal> {
768 if self.ticks.is_empty() {
769 return None;
770 }
771 let sum: Decimal = self.ticks.iter().map(|t| t.quantity.value()).sum();
772 #[allow(clippy::cast_possible_truncation)]
773 Some(sum / Decimal::from(self.ticks.len() as u64))
774 }
775
776 pub fn tick_interval_mean_nanos(&self) -> Option<i64> {
780 if self.ticks.len() < 2 {
781 return None;
782 }
783 let total = self.ticks.last()?.timestamp.elapsed_since(self.ticks.first()?.timestamp);
784 Some(total / (self.ticks.len() as i64 - 1))
785 }
786
787 #[allow(clippy::cast_possible_truncation)]
792 pub fn price_std(&self) -> Option<Decimal> {
793 if self.ticks.len() < 2 {
794 return None;
795 }
796 let prices: Vec<Decimal> = self.ticks.iter().map(|t| t.price.value()).collect();
797 let n = prices.len();
798 let mean = prices.iter().copied().sum::<Decimal>() / Decimal::from(n as u32);
799 let variance = prices
800 .iter()
801 .map(|p| { let d = *p - mean; d * d })
802 .sum::<Decimal>()
803 / Decimal::from((n - 1) as u32);
804 use rust_decimal::prelude::ToPrimitive;
805 let std = variance.to_f64()?.sqrt();
806 Decimal::try_from(std).ok()
807 }
808
809 pub fn bid_ask_imbalance(&self) -> Option<Decimal> {
814 let total: Decimal = self.ticks.iter().map(|t| t.quantity.value()).sum();
815 if total.is_zero() {
816 return None;
817 }
818 let bid_vol: Decimal = self
819 .ticks
820 .iter()
821 .filter(|t| t.side == Side::Bid)
822 .map(|t| t.quantity.value())
823 .sum();
824 let ask_vol = total - bid_vol;
825 Some((bid_vol - ask_vol) / total)
826 }
827
828 pub fn tick_velocity_per_second(&self) -> Option<f64> {
832 if self.ticks.len() < 2 {
833 return None;
834 }
835 let span_nanos = self
836 .ticks
837 .last()?
838 .timestamp
839 .elapsed_since(self.ticks.first()?.timestamp);
840 if span_nanos <= 0 {
841 return None;
842 }
843 Some(self.ticks.len() as f64 / (span_nanos as f64 / 1_000_000_000.0))
844 }
845}
846
847impl Iterator for TickReplayer {
848 type Item = Tick;
849
850 fn next(&mut self) -> Option<Self::Item> {
851 let tick = self.ticks.get(self.index)?.clone();
852 self.index += 1;
853 Some(tick)
854 }
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use rust_decimal_macros::dec;
861
862 fn make_tick(sym: &str, price: &str, qty: &str, side: Side, ts: i64) -> Tick {
863 Tick::new(
864 Symbol::new(sym).unwrap(),
865 Price::new(dec_from_str(price)).unwrap(),
866 Quantity::new(dec_from_str(qty)).unwrap(),
867 side,
868 NanoTimestamp::new(ts),
869 )
870 }
871
872 fn dec_from_str(s: &str) -> Decimal {
873 s.parse().unwrap()
874 }
875
876 #[test]
877 fn test_tick_notional_is_price_times_quantity() {
878 let t = make_tick("AAPL", "150.00", "10", Side::Ask, 0);
879 assert_eq!(t.notional(), dec!(1500.00));
880 }
881
882 #[test]
883 fn test_tick_notional_zero_quantity() {
884 let t = make_tick("AAPL", "150.00", "0", Side::Ask, 0);
885 assert_eq!(t.notional(), dec!(0));
886 }
887
888 #[test]
889 fn test_tick_filter_no_predicates_matches_all() {
890 let f = TickFilter::new();
891 let t = make_tick("AAPL", "1", "1", Side::Bid, 0);
892 assert!(f.matches(&t));
893 }
894
895 #[test]
896 fn test_tick_filter_by_symbol() {
897 let sym = Symbol::new("AAPL").unwrap();
898 let f = TickFilter::new().symbol(sym);
899 let matching = make_tick("AAPL", "1", "1", Side::Bid, 0);
900 let non_matching = make_tick("TSLA", "1", "1", Side::Bid, 0);
901 assert!(f.matches(&matching));
902 assert!(!f.matches(&non_matching));
903 }
904
905 #[test]
906 fn test_tick_filter_by_side() {
907 let f = TickFilter::new().side(Side::Ask);
908 let ask_tick = make_tick("AAPL", "1", "1", Side::Ask, 0);
909 let bid_tick = make_tick("AAPL", "1", "1", Side::Bid, 0);
910 assert!(f.matches(&ask_tick));
911 assert!(!f.matches(&bid_tick));
912 }
913
914 #[test]
915 fn test_tick_filter_by_min_quantity() {
916 let min_qty = Quantity::new(dec!(5)).unwrap();
917 let f = TickFilter::new().min_quantity(min_qty);
918 let large = make_tick("AAPL", "1", "10", Side::Bid, 0);
919 let small = make_tick("AAPL", "1", "2", Side::Bid, 0);
920 assert!(f.matches(&large));
921 assert!(!f.matches(&small));
922 }
923
924 #[test]
925 fn test_tick_filter_by_max_quantity() {
926 let max_qty = Quantity::new(dec!(5)).unwrap();
927 let f = TickFilter::new().max_quantity(max_qty);
928 let small = make_tick("AAPL", "1", "3", Side::Bid, 0);
929 let large = make_tick("AAPL", "1", "10", Side::Bid, 0);
930 assert!(f.matches(&small));
931 assert!(!f.matches(&large));
932 }
933
934 #[test]
935 fn test_tick_filter_quantity_range() {
936 let min_qty = Quantity::new(dec!(3)).unwrap();
937 let max_qty = Quantity::new(dec!(7)).unwrap();
938 let f = TickFilter::new().min_quantity(min_qty).max_quantity(max_qty);
939 assert!(f.matches(&make_tick("X", "1", "5", Side::Bid, 0)));
940 assert!(!f.matches(&make_tick("X", "1", "2", Side::Bid, 0)));
941 assert!(!f.matches(&make_tick("X", "1", "10", Side::Bid, 0)));
942 }
943
944 #[test]
945 fn test_tick_filter_by_min_price() {
946 let min_p = Price::new(dec!(100)).unwrap();
947 let f = TickFilter::new().min_price(min_p);
948 let high = make_tick("AAPL", "150", "1", Side::Bid, 0);
949 let low = make_tick("AAPL", "50", "1", Side::Bid, 0);
950 assert!(f.matches(&high));
951 assert!(!f.matches(&low));
952 }
953
954 #[test]
955 fn test_tick_filter_by_max_price() {
956 let max_p = Price::new(dec!(100)).unwrap();
957 let f = TickFilter::new().max_price(max_p);
958 let low = make_tick("AAPL", "50", "1", Side::Bid, 0);
959 let high = make_tick("AAPL", "150", "1", Side::Bid, 0);
960 assert!(f.matches(&low));
961 assert!(!f.matches(&high));
962 }
963
964 #[test]
965 fn test_tick_filter_price_range() {
966 let min_p = Price::new(dec!(90)).unwrap();
967 let max_p = Price::new(dec!(110)).unwrap();
968 let f = TickFilter::new().min_price(min_p).max_price(max_p);
969 assert!(f.matches(&make_tick("X", "100", "1", Side::Bid, 0)));
970 assert!(!f.matches(&make_tick("X", "80", "1", Side::Bid, 0)));
971 assert!(!f.matches(&make_tick("X", "120", "1", Side::Bid, 0)));
972 }
973
974 #[test]
975 fn test_tick_filter_combined_predicates() {
976 let sym = Symbol::new("AAPL").unwrap();
977 let min_qty = Quantity::new(dec!(5)).unwrap();
978 let f = TickFilter::new()
979 .symbol(sym)
980 .side(Side::Bid)
981 .min_quantity(min_qty);
982 let ok = make_tick("AAPL", "1", "10", Side::Bid, 0);
983 let wrong_sym = make_tick("TSLA", "1", "10", Side::Bid, 0);
984 let wrong_side = make_tick("AAPL", "1", "10", Side::Ask, 0);
985 let wrong_qty = make_tick("AAPL", "1", "1", Side::Bid, 0);
986 assert!(f.matches(&ok));
987 assert!(!f.matches(&wrong_sym));
988 assert!(!f.matches(&wrong_side));
989 assert!(!f.matches(&wrong_qty));
990 }
991
992 #[test]
993 fn test_tick_replayer_sorts_by_timestamp() {
994 let ticks = vec![
995 make_tick("A", "1", "1", Side::Bid, 300),
996 make_tick("A", "1", "1", Side::Bid, 100),
997 make_tick("A", "1", "1", Side::Bid, 200),
998 ];
999 let mut replayer = TickReplayer::new(ticks);
1000 let t1 = replayer.next_tick().unwrap();
1001 assert_eq!(t1.timestamp.nanos(), 100);
1002 let t2 = replayer.next_tick().unwrap();
1003 assert_eq!(t2.timestamp.nanos(), 200);
1004 let t3 = replayer.next_tick().unwrap();
1005 assert_eq!(t3.timestamp.nanos(), 300);
1006 }
1007
1008 #[test]
1009 fn test_tick_replayer_next_tick_sequential() {
1010 let ticks = vec![
1011 make_tick("A", "1", "1", Side::Bid, 1),
1012 make_tick("A", "1", "1", Side::Bid, 2),
1013 ];
1014 let mut replayer = TickReplayer::new(ticks);
1015 assert!(replayer.next_tick().is_some());
1016 assert!(replayer.next_tick().is_some());
1017 assert!(replayer.next_tick().is_none());
1018 }
1019
1020 #[test]
1021 fn test_tick_replayer_reset_restarts() {
1022 let ticks = vec![make_tick("A", "1", "1", Side::Bid, 1)];
1023 let mut replayer = TickReplayer::new(ticks);
1024 let _ = replayer.next_tick();
1025 assert!(replayer.next_tick().is_none());
1026 replayer.reset();
1027 assert!(replayer.next_tick().is_some());
1028 }
1029
1030 #[test]
1031 fn test_tick_replayer_remaining() {
1032 let ticks = vec![
1033 make_tick("A", "1", "1", Side::Bid, 1),
1034 make_tick("A", "1", "1", Side::Bid, 2),
1035 make_tick("A", "1", "1", Side::Bid, 3),
1036 ];
1037 let mut replayer = TickReplayer::new(ticks);
1038 assert_eq!(replayer.remaining(), 3);
1039 let _ = replayer.next_tick();
1040 assert_eq!(replayer.remaining(), 2);
1041 }
1042
1043 #[test]
1044 fn test_tick_replayer_iterator() {
1045 let ticks = vec![
1046 make_tick("A", "1", "1", Side::Bid, 1),
1047 make_tick("A", "2", "1", Side::Bid, 2),
1048 make_tick("A", "3", "1", Side::Bid, 3),
1049 ];
1050 let mut replayer = TickReplayer::new(ticks);
1051 let prices: Vec<_> = (&mut replayer).map(|t| t.price.value()).collect();
1052 assert_eq!(prices.len(), 3);
1053 assert_eq!(prices[0], dec!(1));
1054 assert_eq!(prices[1], dec!(2));
1055 assert_eq!(prices[2], dec!(3));
1056 }
1057
1058 #[test]
1059 fn test_tick_replayer_peek_does_not_advance() {
1060 let ticks = vec![
1061 make_tick("A", "1", "1", Side::Bid, 1),
1062 make_tick("A", "2", "1", Side::Bid, 2),
1063 ];
1064 let mut replayer = TickReplayer::new(ticks);
1065 let p1 = replayer.peek().map(|t| t.timestamp.nanos());
1066 let p2 = replayer.peek().map(|t| t.timestamp.nanos());
1067 assert_eq!(p1, p2, "peek must not advance the position");
1068 assert_eq!(replayer.remaining(), 2);
1069 let _ = replayer.next_tick();
1070 assert_eq!(replayer.remaining(), 1);
1071 }
1072
1073 #[test]
1074 fn test_tick_replayer_peek_none_when_exhausted() {
1075 let replayer = TickReplayer::new(vec![]);
1076 assert!(replayer.peek().is_none());
1077 }
1078
1079 #[test]
1080 fn test_tick_replayer_ticks_slice() {
1081 let ticks = vec![
1082 make_tick("A", "1", "1", Side::Bid, 2),
1083 make_tick("A", "2", "1", Side::Bid, 1),
1084 ];
1085 let replayer = TickReplayer::new(ticks);
1086 let slice = replayer.ticks();
1088 assert_eq!(slice.len(), 2);
1089 assert_eq!(slice[0].timestamp.nanos(), 1);
1090 assert_eq!(slice[1].timestamp.nanos(), 2);
1091 }
1092
1093 #[test]
1094 fn test_tick_filter_has_symbol_filter_false_when_unset() {
1095 let f = TickFilter::new();
1096 assert!(!f.has_symbol_filter());
1097 }
1098
1099 #[test]
1100 fn test_tick_filter_has_symbol_filter_true_when_set() {
1101 let f = TickFilter::new().symbol(Symbol::new("AAPL").unwrap());
1102 assert!(f.has_symbol_filter());
1103 }
1104
1105 #[test]
1106 fn test_tick_filter_has_side_filter_false_when_unset() {
1107 let f = TickFilter::new();
1108 assert!(!f.has_side_filter());
1109 }
1110
1111 #[test]
1112 fn test_tick_filter_has_side_filter_true_when_set() {
1113 let f = TickFilter::new().side(Side::Bid);
1114 assert!(f.has_side_filter());
1115 }
1116
1117 #[test]
1118 fn test_tick_filter_has_min_qty_filter() {
1119 let f = TickFilter::new().min_quantity(Quantity::new(dec!(1)).unwrap());
1120 assert!(f.has_min_qty_filter());
1121 }
1122
1123 #[test]
1124 fn test_tick_filter_has_price_filter_min() {
1125 let f = TickFilter::new().min_price(Price::new(dec!(10)).unwrap());
1126 assert!(f.has_price_filter());
1127 }
1128
1129 #[test]
1130 fn test_tick_filter_has_price_filter_max() {
1131 let f = TickFilter::new().max_price(Price::new(dec!(100)).unwrap());
1132 assert!(f.has_price_filter());
1133 }
1134
1135 #[test]
1136 #[cfg(feature = "serde")]
1137 fn test_tick_serde_roundtrip() {
1138 let tick = make_tick("AAPL", "150.50", "25", Side::Bid, 1_000_000_000);
1139 let json = serde_json::to_string(&tick).unwrap();
1140 let back: Tick = serde_json::from_str(&json).unwrap();
1141 assert_eq!(back.symbol, tick.symbol);
1142 assert_eq!(back.price, tick.price);
1143 assert_eq!(back.quantity, tick.quantity);
1144 assert_eq!(back.side, tick.side);
1145 assert_eq!(back.timestamp, tick.timestamp);
1146 }
1147
1148 #[test]
1149 fn test_tick_replayer_count() {
1150 let ticks = vec![
1151 make_tick("AAPL", "100", "1", Side::Bid, 1),
1152 make_tick("AAPL", "101", "1", Side::Ask, 2),
1153 make_tick("AAPL", "102", "1", Side::Bid, 3),
1154 ];
1155 let replayer = TickReplayer::new(ticks);
1156 assert_eq!(replayer.count(), 3);
1157 }
1158
1159 #[test]
1160 fn test_tick_replayer_count_empty() {
1161 let replayer = TickReplayer::new(vec![]);
1162 assert_eq!(replayer.count(), 0);
1163 }
1164
1165 #[test]
1166 fn test_tick_replayer_filter_by_side() {
1167 let ticks = vec![
1168 make_tick("AAPL", "100", "1", Side::Bid, 1),
1169 make_tick("AAPL", "101", "1", Side::Ask, 2),
1170 make_tick("AAPL", "102", "1", Side::Bid, 3),
1171 ];
1172 let replayer = TickReplayer::new(ticks);
1173 let filter = TickFilter::new().side(Side::Bid);
1174 let filtered = replayer.filter_ticks(&filter);
1175 assert_eq!(filtered.len(), 2);
1176 assert!(filtered.iter().all(|t| t.side == Side::Bid));
1177 }
1178
1179 #[test]
1180 fn test_tick_replayer_filter_no_matches() {
1181 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 1)];
1182 let replayer = TickReplayer::new(ticks);
1183 let filter = TickFilter::new().side(Side::Ask);
1184 let filtered = replayer.filter_ticks(&filter);
1185 assert!(filtered.is_empty());
1186 }
1187
1188 #[test]
1189 fn test_tick_filter_min_notional_passes_large() {
1190 let big = make_tick("AAPL", "100", "10", Side::Ask, 1); let filter = TickFilter::new().min_notional(dec_from_str("500"));
1192 assert!(filter.matches(&big));
1193 }
1194
1195 #[test]
1196 fn test_tick_filter_min_notional_rejects_small() {
1197 let small = make_tick("AAPL", "100", "1", Side::Bid, 1); let filter = TickFilter::new().min_notional(dec_from_str("500"));
1199 assert!(!filter.matches(&small));
1200 }
1201
1202 #[test]
1203 fn test_tick_filter_is_empty_when_no_predicates() {
1204 let f = TickFilter::new();
1205 assert!(f.is_empty());
1206 }
1207
1208 #[test]
1209 fn test_tick_filter_not_empty_after_symbol_set() {
1210 let f = TickFilter::new().symbol(Symbol::new("AAPL").unwrap());
1211 assert!(!f.is_empty());
1212 }
1213
1214 #[test]
1215 fn test_tick_filter_not_empty_after_side_set() {
1216 let f = TickFilter::new().side(Side::Ask);
1217 assert!(!f.is_empty());
1218 }
1219
1220 #[test]
1221 fn test_tick_notional_checked_matches_notional() {
1222 let t = make_tick("AAPL", "150.50", "10", Side::Bid, 0);
1223 assert_eq!(t.notional_checked(), Some(t.notional()));
1224 }
1225
1226 #[test]
1227 fn test_tick_notional_checked_zero_qty() {
1228 let t = make_tick("AAPL", "100", "0", Side::Bid, 0);
1229 assert_eq!(t.notional_checked(), Some(dec!(0)));
1230 }
1231
1232 #[test]
1233 fn test_tick_is_buy_bid_side() {
1234 let t = make_tick("AAPL", "100", "1", Side::Bid, 0);
1235 assert!(t.is_buy());
1236 assert!(!t.is_sell());
1237 }
1238
1239 #[test]
1240 fn test_tick_is_sell_ask_side() {
1241 let t = make_tick("AAPL", "100", "1", Side::Ask, 0);
1242 assert!(t.is_sell());
1243 assert!(!t.is_buy());
1244 }
1245
1246 #[test]
1247 fn test_tick_replayer_between_inclusive() {
1248 let ticks = vec![
1249 make_tick("AAPL", "100", "1", Side::Bid, 1),
1250 make_tick("AAPL", "101", "1", Side::Ask, 5),
1251 make_tick("AAPL", "102", "1", Side::Bid, 10),
1252 ];
1253 let replayer = TickReplayer::new(ticks);
1254 let result = replayer.between(NanoTimestamp::new(1), NanoTimestamp::new(5));
1255 assert_eq!(result.len(), 2);
1256 }
1257
1258 #[test]
1259 fn test_tick_replayer_between_no_matches() {
1260 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 100)];
1261 let replayer = TickReplayer::new(ticks);
1262 let result = replayer.between(NanoTimestamp::new(1), NanoTimestamp::new(50));
1263 assert!(result.is_empty());
1264 }
1265
1266 #[test]
1267 fn test_tick_filter_timestamp_range() {
1268 let ticks = [
1269 make_tick("AAPL", "100", "1", Side::Bid, 1),
1270 make_tick("AAPL", "101", "1", Side::Ask, 5),
1271 make_tick("AAPL", "102", "1", Side::Bid, 10),
1272 ];
1273 let filter = TickFilter::new()
1274 .timestamp_range(NanoTimestamp::new(3), NanoTimestamp::new(10));
1275 let matched: Vec<_> = ticks.iter().filter(|t| filter.matches(t)).collect();
1276 assert_eq!(matched.len(), 2);
1277 }
1278
1279 #[test]
1280 fn test_tick_replayer_first_returns_earliest() {
1281 let ticks = vec![
1282 make_tick("AAPL", "100", "1", Side::Bid, 5),
1283 make_tick("AAPL", "101", "1", Side::Ask, 1),
1284 make_tick("AAPL", "102", "1", Side::Bid, 10),
1285 ];
1286 let replayer = TickReplayer::new(ticks);
1287 let first = replayer.first().unwrap();
1288 assert_eq!(first.timestamp, NanoTimestamp::new(1));
1289 }
1290
1291 #[test]
1292 fn test_tick_replayer_last_returns_latest() {
1293 let ticks = vec![
1294 make_tick("AAPL", "100", "1", Side::Bid, 5),
1295 make_tick("AAPL", "101", "1", Side::Ask, 1),
1296 make_tick("AAPL", "102", "1", Side::Bid, 10),
1297 ];
1298 let replayer = TickReplayer::new(ticks);
1299 let last = replayer.last().unwrap();
1300 assert_eq!(last.timestamp, NanoTimestamp::new(10));
1301 }
1302
1303 #[test]
1304 fn test_tick_replayer_first_none_when_empty() {
1305 let replayer = TickReplayer::new(vec![]);
1306 assert!(replayer.first().is_none());
1307 }
1308
1309 #[test]
1310 fn test_tick_replayer_last_none_when_empty() {
1311 let replayer = TickReplayer::new(vec![]);
1312 assert!(replayer.last().is_none());
1313 }
1314
1315 #[test]
1316 fn test_tick_replayer_vwap_by_side_correct_values() {
1317 let ticks = vec![
1318 make_tick("AAPL", "100", "10", Side::Bid, 1), make_tick("AAPL", "200", "5", Side::Ask, 2), ];
1321 let replayer = TickReplayer::new(ticks);
1322 let (bid_vwap, ask_vwap) = replayer.vwap_by_side();
1323 assert_eq!(bid_vwap, Some(dec_from_str("100")));
1324 assert_eq!(ask_vwap, Some(dec_from_str("200")));
1325 }
1326
1327 #[test]
1328 fn test_tick_replayer_vwap_by_side_no_asks_returns_none_ask() {
1329 let ticks = vec![make_tick("AAPL", "100", "10", Side::Bid, 1)];
1330 let replayer = TickReplayer::new(ticks);
1331 let (bid_vwap, ask_vwap) = replayer.vwap_by_side();
1332 assert!(bid_vwap.is_some());
1333 assert!(ask_vwap.is_none());
1334 }
1335
1336 #[test]
1337 fn test_tick_replayer_vwap_by_side_empty_returns_none_both() {
1338 let replayer = TickReplayer::new(vec![]);
1339 let (bid_vwap, ask_vwap) = replayer.vwap_by_side();
1340 assert!(bid_vwap.is_none());
1341 assert!(ask_vwap.is_none());
1342 }
1343
1344 #[test]
1345 fn test_tick_filter_clear_resets_all_predicates() {
1346 let f = TickFilter::new()
1347 .symbol(Symbol::new("AAPL").unwrap())
1348 .side(Side::Bid)
1349 .min_quantity(Quantity::new(dec!(1)).unwrap());
1350 let cleared = f.clear();
1351 assert!(cleared.is_empty());
1352 }
1353
1354 #[test]
1355 fn test_tick_filter_has_notional_filter_false_when_unset() {
1356 let f = TickFilter::new();
1357 assert!(!f.has_notional_filter());
1358 }
1359
1360 #[test]
1361 fn test_tick_filter_has_notional_filter_true_with_min() {
1362 let f = TickFilter::new().min_notional(dec_from_str("100"));
1363 assert!(f.has_notional_filter());
1364 }
1365
1366 #[test]
1367 fn test_tick_filter_has_notional_filter_true_with_max() {
1368 let f = TickFilter::new().max_notional(dec_from_str("1000"));
1369 assert!(f.has_notional_filter());
1370 }
1371
1372 #[test]
1373 fn test_tick_replayer_total_notional() {
1374 let ticks = vec![
1375 make_tick("AAPL", "100", "10", Side::Bid, 1), make_tick("AAPL", "200", "5", Side::Ask, 2), ];
1378 let replayer = TickReplayer::new(ticks);
1379 assert_eq!(replayer.total_notional(), dec_from_str("2000"));
1380 }
1381
1382 #[test]
1383 fn test_tick_replayer_total_notional_empty() {
1384 let replayer = TickReplayer::new(vec![]);
1385 assert_eq!(replayer.total_notional(), dec_from_str("0"));
1386 }
1387
1388 #[test]
1389 fn test_tick_replayer_buy_volume() {
1390 let ticks = vec![
1391 make_tick("AAPL", "100", "10", Side::Bid, 1),
1392 make_tick("AAPL", "100", "5", Side::Ask, 2),
1393 ];
1394 let replayer = TickReplayer::new(ticks);
1395 assert_eq!(replayer.buy_volume(), dec_from_str("10"));
1396 }
1397
1398 #[test]
1399 fn test_tick_replayer_sell_volume() {
1400 let ticks = vec![
1401 make_tick("AAPL", "100", "10", Side::Bid, 1),
1402 make_tick("AAPL", "100", "7", Side::Ask, 2),
1403 ];
1404 let replayer = TickReplayer::new(ticks);
1405 assert_eq!(replayer.sell_volume(), dec_from_str("7"));
1406 }
1407
1408 #[test]
1409 fn test_tick_replayer_delta_positive_when_more_buys() {
1410 let ticks = vec![
1411 make_tick("AAPL", "100", "10", Side::Bid, 1),
1412 make_tick("AAPL", "100", "3", Side::Ask, 2),
1413 ];
1414 let replayer = TickReplayer::new(ticks);
1415 assert_eq!(replayer.delta(), dec_from_str("7"));
1416 }
1417
1418 #[test]
1419 fn test_tick_replayer_delta_negative_when_more_sells() {
1420 let ticks = vec![
1421 make_tick("AAPL", "100", "2", Side::Bid, 1),
1422 make_tick("AAPL", "100", "8", Side::Ask, 2),
1423 ];
1424 let replayer = TickReplayer::new(ticks);
1425 assert_eq!(replayer.delta(), dec_from_str("-6"));
1426 }
1427
1428 #[test]
1429 fn test_tick_replayer_delta_zero_when_balanced() {
1430 let ticks = vec![
1431 make_tick("AAPL", "100", "5", Side::Bid, 1),
1432 make_tick("AAPL", "100", "5", Side::Ask, 2),
1433 ];
1434 let replayer = TickReplayer::new(ticks);
1435 assert_eq!(replayer.delta(), dec_from_str("0"));
1436 }
1437
1438 #[test]
1439 fn test_tick_replayer_time_span_nanos_correct() {
1440 let ticks = vec![
1441 make_tick("AAPL", "100", "1", Side::Bid, 1_000_000),
1442 make_tick("AAPL", "100", "1", Side::Ask, 3_000_000),
1443 ];
1444 let replayer = TickReplayer::new(ticks);
1445 assert_eq!(replayer.time_span_nanos(), Some(2_000_000));
1446 }
1447
1448 #[test]
1449 fn test_tick_replayer_time_span_nanos_none_for_single_tick() {
1450 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 1_000_000)];
1451 let replayer = TickReplayer::new(ticks);
1452 assert_eq!(replayer.time_span_nanos(), None);
1453 }
1454
1455 #[test]
1456 fn test_tick_replayer_time_span_nanos_none_for_empty() {
1457 let replayer = TickReplayer::new(vec![]);
1458 assert_eq!(replayer.time_span_nanos(), None);
1459 }
1460
1461 #[test]
1462 fn test_tick_replayer_price_range_returns_spread() {
1463 let ticks = vec![
1464 make_tick("AAPL", "100", "1", Side::Bid, 1),
1465 make_tick("AAPL", "105", "1", Side::Ask, 2),
1466 make_tick("AAPL", "98", "1", Side::Bid, 3),
1467 ];
1468 let replayer = TickReplayer::new(ticks);
1469 assert_eq!(replayer.price_range(), Some(dec_from_str("7")));
1470 }
1471
1472 #[test]
1473 fn test_tick_replayer_price_range_none_for_empty() {
1474 let replayer = TickReplayer::new(vec![]);
1475 assert_eq!(replayer.price_range(), None);
1476 }
1477
1478 #[test]
1479 fn test_tick_replayer_price_range_zero_for_single_price() {
1480 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 1)];
1481 let replayer = TickReplayer::new(ticks);
1482 assert_eq!(replayer.price_range(), Some(dec_from_str("0")));
1483 }
1484
1485 #[test]
1486 fn test_tick_replayer_tick_count_by_side() {
1487 let ticks = vec![
1488 make_tick("AAPL", "100", "1", Side::Bid, 1),
1489 make_tick("AAPL", "100", "1", Side::Bid, 2),
1490 make_tick("AAPL", "100", "1", Side::Ask, 3),
1491 ];
1492 let replayer = TickReplayer::new(ticks);
1493 assert_eq!(replayer.tick_count_by_side(), (2, 1));
1494 }
1495
1496 #[test]
1497 fn test_tick_replayer_tick_count_by_side_empty() {
1498 let replayer = TickReplayer::new(vec![]);
1499 assert_eq!(replayer.tick_count_by_side(), (0, 0));
1500 }
1501
1502 #[test]
1503 fn test_tick_replayer_median_trade_size_single() {
1504 let ticks = vec![make_tick("AAPL", "100", "5", Side::Bid, 1)];
1505 let replayer = TickReplayer::new(ticks);
1506 assert_eq!(replayer.median_trade_size(), Some(dec_from_str("5")));
1507 }
1508
1509 #[test]
1510 fn test_tick_replayer_median_trade_size_odd_count() {
1511 let ticks = vec![
1512 make_tick("AAPL", "100", "1", Side::Bid, 1),
1513 make_tick("AAPL", "100", "3", Side::Bid, 2),
1514 make_tick("AAPL", "100", "5", Side::Bid, 3),
1515 ];
1516 let replayer = TickReplayer::new(ticks);
1517 assert_eq!(replayer.median_trade_size(), Some(dec_from_str("3")));
1519 }
1520
1521 #[test]
1522 fn test_tick_replayer_median_trade_size_none_for_empty() {
1523 let replayer = TickReplayer::new(vec![]);
1524 assert_eq!(replayer.median_trade_size(), None);
1525 }
1526
1527 #[test]
1528 fn test_tick_replayer_total_notional_sum_two_trades() {
1529 let ticks = vec![
1530 make_tick("X", "100", "2", Side::Bid, 1),
1531 make_tick("X", "50", "4", Side::Ask, 2),
1532 ];
1533 let replayer = TickReplayer::new(ticks);
1534 assert_eq!(replayer.total_notional(), dec_from_str("400"));
1536 }
1537
1538 #[test]
1539 fn test_tick_replayer_price_std_none_for_single_tick() {
1540 let ticks = vec![make_tick("X", "100", "1", Side::Bid, 1)];
1541 let replayer = TickReplayer::new(ticks);
1542 assert!(replayer.price_std().is_none());
1543 }
1544
1545 #[test]
1546 fn test_tick_replayer_price_std_zero_for_constant_prices() {
1547 let ticks = vec![
1548 make_tick("X", "100", "1", Side::Bid, 1),
1549 make_tick("X", "100", "2", Side::Bid, 2),
1550 make_tick("X", "100", "3", Side::Bid, 3),
1551 ];
1552 let replayer = TickReplayer::new(ticks);
1553 assert_eq!(replayer.price_std(), Some(Decimal::ZERO));
1554 }
1555
1556 #[test]
1557 fn test_tick_replayer_price_std_positive_for_varying_prices() {
1558 let ticks = vec![
1559 make_tick("X", "100", "1", Side::Bid, 1),
1560 make_tick("X", "110", "1", Side::Bid, 2),
1561 make_tick("X", "120", "1", Side::Bid, 3),
1562 ];
1563 let replayer = TickReplayer::new(ticks);
1564 let std = replayer.price_std().unwrap();
1565 assert!(std > Decimal::ZERO);
1566 }
1567}