1use crate::types::{NanoTimestamp, Price, Quantity, Side, Symbol};
18use rust_decimal::Decimal;
19
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22pub struct Tick {
23 pub symbol: Symbol,
25 pub price: Price,
27 pub quantity: Quantity,
29 pub side: Side,
31 pub timestamp: NanoTimestamp,
33}
34
35impl Tick {
36 pub fn new(
38 symbol: Symbol,
39 price: Price,
40 quantity: Quantity,
41 side: Side,
42 timestamp: NanoTimestamp,
43 ) -> Self {
44 Self {
45 symbol,
46 price,
47 quantity,
48 side,
49 timestamp,
50 }
51 }
52
53 pub fn notional(&self) -> Decimal {
55 self.price.value() * self.quantity.value()
56 }
57
58 pub fn notional_checked(&self) -> Option<Decimal> {
60 self.price.checked_mul(self.quantity)
61 }
62
63 pub fn is_buy_aggressor(&self) -> bool {
65 self.side == Side::Bid
66 }
67
68 pub fn is_sell_aggressor(&self) -> bool {
70 self.side == Side::Ask
71 }
72
73 pub fn is_buy(&self) -> bool {
75 self.side == Side::Bid
76 }
77
78 pub fn is_sell(&self) -> bool {
80 self.side == Side::Ask
81 }
82
83 pub fn is_uptick(&self, prev: &Tick) -> bool {
85 self.price.value() > prev.price.value()
86 }
87
88 pub fn is_downtick(&self, prev: &Tick) -> bool {
90 self.price.value() < prev.price.value()
91 }
92
93 pub fn delta(ticks: &[Tick]) -> Decimal {
98 ticks.iter().map(|t| {
99 match t.side {
100 Side::Bid => t.quantity.value(),
101 Side::Ask => -t.quantity.value(),
102 }
103 }).sum()
104 }
105
106 pub fn cumulative_delta(ticks: &[Tick]) -> Vec<Decimal> {
112 let mut running = Decimal::ZERO;
113 ticks
114 .iter()
115 .map(|t| {
116 match t.side {
117 Side::Bid => running += t.quantity.value(),
118 Side::Ask => running -= t.quantity.value(),
119 }
120 running
121 })
122 .collect()
123 }
124
125 pub fn average_price(ticks: &[Tick]) -> Option<Decimal> {
129 if ticks.is_empty() {
130 return None;
131 }
132 #[allow(clippy::cast_possible_truncation)]
133 let sum: Decimal = ticks.iter().map(|t| t.price.value()).sum();
134 Some(sum / Decimal::from(ticks.len() as u32))
135 }
136
137 pub fn buy_volume(ticks: &[Tick]) -> Decimal {
141 ticks
142 .iter()
143 .filter(|t| t.side == Side::Bid)
144 .map(|t| t.quantity.value())
145 .sum()
146 }
147
148 pub fn sell_volume(ticks: &[Tick]) -> Decimal {
152 ticks
153 .iter()
154 .filter(|t| t.side == Side::Ask)
155 .map(|t| t.quantity.value())
156 .sum()
157 }
158
159 pub fn vwap_from_slice(ticks: &[Tick]) -> Option<Decimal> {
165 let total_qty: Decimal = ticks.iter().map(|t| t.quantity.value()).sum();
166 if total_qty.is_zero() {
167 return None;
168 }
169 let weighted: Decimal = ticks.iter().map(|t| t.price.value() * t.quantity.value()).sum();
170 Some(weighted / total_qty)
171 }
172
173 pub fn max_price(ticks: &[Tick]) -> Option<Price> {
175 ticks.iter().map(|t| t.price).max_by(|a, b| a.value().cmp(&b.value()))
176 }
177
178 pub fn min_price(ticks: &[Tick]) -> Option<Price> {
180 ticks.iter().map(|t| t.price).min_by(|a, b| a.value().cmp(&b.value()))
181 }
182
183 pub fn time_weighted_avg_price(ticks: &[Tick]) -> Option<Decimal> {
189 if ticks.len() < 2 {
190 return None;
191 }
192 let mut total_weight = 0u128;
193 let mut weighted_sum = Decimal::ZERO;
194 for i in 1..ticks.len() {
195 let elapsed = ticks[i].timestamp.nanos()
196 .saturating_sub(ticks[i - 1].timestamp.nanos())
197 .max(0) as u128;
198 total_weight = total_weight.saturating_add(elapsed);
199 #[allow(clippy::cast_possible_truncation)]
200 let w = Decimal::from(elapsed as u64);
201 weighted_sum += ticks[i].price.value() * w;
202 }
203 if total_weight == 0 {
204 return None;
205 }
206 #[allow(clippy::cast_possible_truncation)]
207 Some(weighted_sum / Decimal::from(total_weight as u64))
208 }
209
210 pub fn largest_trade(ticks: &[Tick]) -> Option<&Tick> {
214 ticks.iter().max_by(|a, b| {
215 let na = a.price.value() * a.quantity.value();
216 let nb = b.price.value() * b.quantity.value();
217 na.cmp(&nb)
218 })
219 }
220
221 pub fn classify_aggressor(&self) -> &'static str {
228 match self.side {
229 Side::Bid => "market_buy",
230 Side::Ask => "market_sell",
231 }
232 }
233
234 pub fn imbalance_ratio(ticks: &[Tick]) -> Option<Decimal> {
239 let total: Decimal = ticks.iter().map(|t| t.quantity.value()).sum();
240 if total.is_zero() {
241 return None;
242 }
243 let buy_vol = Self::buy_volume(ticks);
244 Some(buy_vol / total)
245 }
246
247 pub fn count_by_side(ticks: &[Tick]) -> (usize, usize) {
251 let buy = ticks.iter().filter(|t| t.side == Side::Bid).count();
252 let sell = ticks.len() - buy;
253 (buy, sell)
254 }
255
256 pub fn notional_volume(ticks: &[Tick]) -> Decimal {
260 ticks.iter().map(|t| t.price.value() * t.quantity.value()).sum()
261 }
262
263 pub fn tick_direction_series(ticks: &[Tick]) -> Vec<i8> {
268 if ticks.is_empty() {
269 return vec![];
270 }
271 let mut result = Vec::with_capacity(ticks.len());
272 result.push(0i8);
273 for w in ticks.windows(2) {
274 let prev = w[0].price.value();
275 let curr = w[1].price.value();
276 result.push(if curr > prev { 1 } else if curr < prev { -1 } else { 0 });
277 }
278 result
279 }
280
281 pub fn median_price(ticks: &[Tick]) -> Option<Decimal> {
286 if ticks.is_empty() {
287 return None;
288 }
289 let mut prices: Vec<Decimal> = ticks.iter().map(|t| t.price.value()).collect();
290 prices.sort_unstable_by(|a, b| a.cmp(b));
291 let mid = prices.len() / 2;
292 if prices.len() % 2 == 0 {
293 Some((prices[mid - 1] + prices[mid]) / Decimal::TWO)
294 } else {
295 Some(prices[mid])
296 }
297 }
298
299 pub fn price_impact(ticks: &[Tick], ref_price: Decimal) -> Option<Decimal> {
308 if ticks.is_empty() {
309 return None;
310 }
311 let total_qty: Decimal = ticks.iter().map(|t| t.quantity.value()).sum();
312 if total_qty.is_zero() {
313 return None;
314 }
315 let weighted_dev: Decimal = ticks
316 .iter()
317 .map(|t| (t.price.value() - ref_price) * t.quantity.value())
318 .sum();
319 Some(weighted_dev / total_qty)
320 }
321
322 pub fn cluster_count(ticks: &[Tick], gap_ns: u64) -> usize {
329 if ticks.is_empty() {
330 return 0;
331 }
332 let mut clusters = 1usize;
333 for w in ticks.windows(2) {
334 let t0 = w[0].timestamp.nanos() as u64;
335 let t1 = w[1].timestamp.nanos() as u64;
336 if t1.saturating_sub(t0) > gap_ns {
337 clusters += 1;
338 }
339 }
340 clusters
341 }
342}
343
344#[derive(Clone)]
348pub struct TickFilter {
349 symbol: Option<Symbol>,
350 side: Option<Side>,
351 min_qty: Option<Quantity>,
352 max_qty: Option<Quantity>,
353 min_price: Option<Price>,
354 max_price: Option<Price>,
355 min_notional: Option<rust_decimal::Decimal>,
356 max_notional: Option<rust_decimal::Decimal>,
357 from_ts: Option<NanoTimestamp>,
358 to_ts: Option<NanoTimestamp>,
359}
360
361impl TickFilter {
362 pub fn new() -> Self {
364 Self {
365 symbol: None,
366 side: None,
367 min_qty: None,
368 max_qty: None,
369 min_price: None,
370 max_price: None,
371 min_notional: None,
372 max_notional: None,
373 from_ts: None,
374 to_ts: None,
375 }
376 }
377
378 #[must_use]
380 pub fn symbol(mut self, s: Symbol) -> Self {
381 self.symbol = Some(s);
382 self
383 }
384
385 #[must_use]
387 pub fn side(mut self, s: Side) -> Self {
388 self.side = Some(s);
389 self
390 }
391
392 #[must_use]
394 pub fn min_quantity(mut self, q: Quantity) -> Self {
395 self.min_qty = Some(q);
396 self
397 }
398
399 #[must_use]
401 pub fn max_quantity(mut self, q: Quantity) -> Self {
402 self.max_qty = Some(q);
403 self
404 }
405
406 #[must_use]
408 pub fn min_price(mut self, p: Price) -> Self {
409 self.min_price = Some(p);
410 self
411 }
412
413 #[must_use]
415 pub fn max_price(mut self, p: Price) -> Self {
416 self.max_price = Some(p);
417 self
418 }
419
420 #[must_use]
422 pub fn min_notional(mut self, n: rust_decimal::Decimal) -> Self {
423 self.min_notional = Some(n);
424 self
425 }
426
427 #[must_use]
429 pub fn max_notional(mut self, n: rust_decimal::Decimal) -> Self {
430 self.max_notional = Some(n);
431 self
432 }
433
434 #[must_use]
436 pub fn timestamp_range(mut self, from: NanoTimestamp, to: NanoTimestamp) -> Self {
437 self.from_ts = Some(from);
438 self.to_ts = Some(to);
439 self
440 }
441
442 pub fn has_symbol_filter(&self) -> bool {
444 self.symbol.is_some()
445 }
446
447 pub fn has_side_filter(&self) -> bool {
449 self.side.is_some()
450 }
451
452 pub fn has_min_qty_filter(&self) -> bool {
454 self.min_qty.is_some()
455 }
456
457 pub fn has_price_filter(&self) -> bool {
459 self.min_price.is_some() || self.max_price.is_some()
460 }
461
462 pub fn has_notional_filter(&self) -> bool {
464 self.min_notional.is_some() || self.max_notional.is_some()
465 }
466
467 pub fn clear(self) -> Self {
471 Self::new()
472 }
473
474 pub fn is_empty(&self) -> bool {
479 self.symbol.is_none()
480 && self.side.is_none()
481 && self.min_qty.is_none()
482 && self.max_qty.is_none()
483 && self.min_price.is_none()
484 && self.max_price.is_none()
485 && self.min_notional.is_none()
486 && self.max_notional.is_none()
487 && self.from_ts.is_none()
488 && self.to_ts.is_none()
489 }
490
491 pub fn matches(&self, tick: &Tick) -> bool {
493 if let Some(ref sym) = self.symbol {
494 if tick.symbol != *sym {
495 return false;
496 }
497 }
498 if let Some(ref side) = self.side {
499 if tick.side != *side {
500 return false;
501 }
502 }
503 if let Some(ref min_qty) = self.min_qty {
504 if tick.quantity < *min_qty {
505 return false;
506 }
507 }
508 if let Some(ref max_qty) = self.max_qty {
509 if tick.quantity > *max_qty {
510 return false;
511 }
512 }
513 if let Some(ref min_p) = self.min_price {
514 if tick.price < *min_p {
515 return false;
516 }
517 }
518 if let Some(ref max_p) = self.max_price {
519 if tick.price > *max_p {
520 return false;
521 }
522 }
523 if let Some(ref min_n) = self.min_notional {
524 if tick.notional() < *min_n {
525 return false;
526 }
527 }
528 if let Some(ref max_n) = self.max_notional {
529 if tick.notional() > *max_n {
530 return false;
531 }
532 }
533 if let Some(from) = self.from_ts {
534 if tick.timestamp.is_before(from) {
535 return false;
536 }
537 }
538 if let Some(to) = self.to_ts {
539 if tick.timestamp.is_after(to) {
540 return false;
541 }
542 }
543 true
544 }
545
546 pub fn count_matches(&self, ticks: &[Tick]) -> usize {
551 ticks.iter().filter(|t| self.matches(t)).count()
552 }
553}
554
555impl Default for TickFilter {
556 fn default() -> Self {
557 Self::new()
558 }
559}
560
561pub struct TickReplayer {
563 ticks: Vec<Tick>,
564 index: usize,
565}
566
567impl TickReplayer {
568 pub fn new(mut ticks: Vec<Tick>) -> Self {
570 ticks.sort_by_key(|t| t.timestamp);
571 Self { ticks, index: 0 }
572 }
573
574 pub fn next_tick(&mut self) -> Option<&Tick> {
576 let tick = self.ticks.get(self.index)?;
577 self.index += 1;
578 Some(tick)
579 }
580
581 pub fn remaining(&self) -> usize {
583 self.ticks.len().saturating_sub(self.index)
584 }
585
586 pub fn peek(&self) -> Option<&Tick> {
588 self.ticks.get(self.index)
589 }
590
591 pub fn ticks(&self) -> &[Tick] {
593 &self.ticks
594 }
595
596 pub fn reset(&mut self) {
598 self.index = 0;
599 }
600
601 pub fn count(&self) -> usize {
603 self.ticks.len()
604 }
605
606 pub fn vwap(&self) -> Option<Decimal> {
612 let total_vol: Decimal = self.ticks.iter().map(|t| t.quantity.value()).sum();
613 if total_vol.is_zero() {
614 return None;
615 }
616 let total_notional: Decimal = self.ticks.iter().map(|t| t.notional()).sum();
617 Some(total_notional / total_vol)
618 }
619
620 pub fn filter_ticks(&self, filter: &TickFilter) -> Vec<Tick> {
622 self.ticks
623 .iter()
624 .filter(|t| filter.matches(t))
625 .cloned()
626 .collect()
627 }
628
629 pub fn between(&self, from: NanoTimestamp, to: NanoTimestamp) -> Vec<Tick> {
631 self.ticks
632 .iter()
633 .filter(|t| !t.timestamp.is_before(from) && !t.timestamp.is_after(to))
634 .cloned()
635 .collect()
636 }
637
638 pub fn delta(&self) -> Decimal {
642 self.buy_volume() - self.sell_volume()
643 }
644
645 pub fn time_span_nanos(&self) -> Option<i64> {
649 if self.ticks.len() < 2 {
650 return None;
651 }
652 let first = self.ticks.first().unwrap().timestamp;
653 let last = self.ticks.last().unwrap().timestamp;
654 Some(last.elapsed_since(first))
655 }
656
657 pub fn total_notional(&self) -> Decimal {
659 self.ticks.iter().map(|t| t.notional()).sum()
660 }
661
662 pub fn buy_volume(&self) -> Decimal {
664 self.ticks
665 .iter()
666 .filter(|t| t.side == Side::Bid)
667 .map(|t| t.quantity.value())
668 .sum()
669 }
670
671 pub fn sell_volume(&self) -> Decimal {
673 self.ticks
674 .iter()
675 .filter(|t| t.side == Side::Ask)
676 .map(|t| t.quantity.value())
677 .sum()
678 }
679
680 pub fn first(&self) -> Option<&Tick> {
682 self.ticks.first()
683 }
684
685 pub fn last(&self) -> Option<&Tick> {
687 self.ticks.last()
688 }
689
690 pub fn vwap_by_side(&self) -> (Option<Decimal>, Option<Decimal>) {
695 let mut bid_notional = Decimal::ZERO;
696 let mut bid_vol = Decimal::ZERO;
697 let mut ask_notional = Decimal::ZERO;
698 let mut ask_vol = Decimal::ZERO;
699 for tick in &self.ticks {
700 let vol = tick.quantity.value();
701 let notional = tick.notional();
702 match tick.side {
703 Side::Bid => {
704 bid_notional += notional;
705 bid_vol += vol;
706 }
707 Side::Ask => {
708 ask_notional += notional;
709 ask_vol += vol;
710 }
711 }
712 }
713 let bid_vwap = if bid_vol.is_zero() { None } else { Some(bid_notional / bid_vol) };
714 let ask_vwap = if ask_vol.is_zero() { None } else { Some(ask_notional / ask_vol) };
715 (bid_vwap, ask_vwap)
716 }
717
718 pub fn collect_by_symbol(&self) -> std::collections::HashMap<Symbol, Vec<Tick>> {
723 let mut map: std::collections::HashMap<Symbol, Vec<Tick>> = std::collections::HashMap::new();
724 for tick in &self.ticks {
725 map.entry(tick.symbol.clone()).or_default().push(tick.clone());
726 }
727 map
728 }
729
730 pub fn price_range(&self) -> Option<Decimal> {
734 let mut max_p = self.ticks.first()?.price.value();
735 let mut min_p = max_p;
736 for t in &self.ticks {
737 let p = t.price.value();
738 if p > max_p { max_p = p; }
739 if p < min_p { min_p = p; }
740 }
741 Some(max_p - min_p)
742 }
743
744 pub fn tick_count_by_side(&self) -> (usize, usize) {
746 let bid = self.ticks.iter().filter(|t| t.side == Side::Bid).count();
747 let ask = self.ticks.iter().filter(|t| t.side == Side::Ask).count();
748 (bid, ask)
749 }
750
751 pub fn median_trade_size(&self) -> Option<Decimal> {
755 if self.ticks.is_empty() {
756 return None;
757 }
758 let mut sizes: Vec<Decimal> = self.ticks.iter().map(|t| t.quantity.value()).collect();
759 sizes.sort();
760 Some(sizes[sizes.len() / 2])
761 }
762
763 pub fn avg_trade_size(&self) -> Option<Decimal> {
767 if self.ticks.is_empty() {
768 return None;
769 }
770 let sum: Decimal = self.ticks.iter().map(|t| t.quantity.value()).sum();
771 #[allow(clippy::cast_possible_truncation)]
772 Some(sum / Decimal::from(self.ticks.len() as u64))
773 }
774
775 pub fn tick_interval_mean_nanos(&self) -> Option<i64> {
779 if self.ticks.len() < 2 {
780 return None;
781 }
782 let total = self.ticks.last().unwrap().timestamp.elapsed_since(self.ticks.first().unwrap().timestamp);
783 Some(total / (self.ticks.len() as i64 - 1))
784 }
785
786 #[allow(clippy::cast_possible_truncation)]
791 pub fn price_std(&self) -> Option<Decimal> {
792 if self.ticks.len() < 2 {
793 return None;
794 }
795 let prices: Vec<Decimal> = self.ticks.iter().map(|t| t.price.value()).collect();
796 let n = prices.len();
797 let mean = prices.iter().copied().sum::<Decimal>() / Decimal::from(n as u32);
798 let variance = prices
799 .iter()
800 .map(|p| { let d = *p - mean; d * d })
801 .sum::<Decimal>()
802 / Decimal::from((n - 1) as u32);
803 use rust_decimal::prelude::ToPrimitive;
804 let std = variance.to_f64()?.sqrt();
805 Decimal::try_from(std).ok()
806 }
807
808 pub fn bid_ask_imbalance(&self) -> Option<Decimal> {
813 let total: Decimal = self.ticks.iter().map(|t| t.quantity.value()).sum();
814 if total.is_zero() {
815 return None;
816 }
817 let bid_vol: Decimal = self
818 .ticks
819 .iter()
820 .filter(|t| t.side == Side::Bid)
821 .map(|t| t.quantity.value())
822 .sum();
823 let ask_vol = total - bid_vol;
824 Some((bid_vol - ask_vol) / total)
825 }
826
827 pub fn tick_velocity_per_second(&self) -> Option<f64> {
831 if self.ticks.len() < 2 {
832 return None;
833 }
834 let span_nanos = self
835 .ticks
836 .last()
837 .unwrap()
838 .timestamp
839 .elapsed_since(self.ticks.first().unwrap().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 fn test_tick_serde_roundtrip() {
1137 let tick = make_tick("AAPL", "150.50", "25", Side::Bid, 1_000_000_000);
1138 let json = serde_json::to_string(&tick).unwrap();
1139 let back: Tick = serde_json::from_str(&json).unwrap();
1140 assert_eq!(back.symbol, tick.symbol);
1141 assert_eq!(back.price, tick.price);
1142 assert_eq!(back.quantity, tick.quantity);
1143 assert_eq!(back.side, tick.side);
1144 assert_eq!(back.timestamp, tick.timestamp);
1145 }
1146
1147 #[test]
1148 fn test_tick_replayer_count() {
1149 let ticks = vec![
1150 make_tick("AAPL", "100", "1", Side::Bid, 1),
1151 make_tick("AAPL", "101", "1", Side::Ask, 2),
1152 make_tick("AAPL", "102", "1", Side::Bid, 3),
1153 ];
1154 let replayer = TickReplayer::new(ticks);
1155 assert_eq!(replayer.count(), 3);
1156 }
1157
1158 #[test]
1159 fn test_tick_replayer_count_empty() {
1160 let replayer = TickReplayer::new(vec![]);
1161 assert_eq!(replayer.count(), 0);
1162 }
1163
1164 #[test]
1165 fn test_tick_replayer_filter_by_side() {
1166 let ticks = vec![
1167 make_tick("AAPL", "100", "1", Side::Bid, 1),
1168 make_tick("AAPL", "101", "1", Side::Ask, 2),
1169 make_tick("AAPL", "102", "1", Side::Bid, 3),
1170 ];
1171 let replayer = TickReplayer::new(ticks);
1172 let filter = TickFilter::new().side(Side::Bid);
1173 let filtered = replayer.filter_ticks(&filter);
1174 assert_eq!(filtered.len(), 2);
1175 assert!(filtered.iter().all(|t| t.side == Side::Bid));
1176 }
1177
1178 #[test]
1179 fn test_tick_replayer_filter_no_matches() {
1180 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 1)];
1181 let replayer = TickReplayer::new(ticks);
1182 let filter = TickFilter::new().side(Side::Ask);
1183 let filtered = replayer.filter_ticks(&filter);
1184 assert!(filtered.is_empty());
1185 }
1186
1187 #[test]
1188 fn test_tick_filter_min_notional_passes_large() {
1189 let big = make_tick("AAPL", "100", "10", Side::Ask, 1); let filter = TickFilter::new().min_notional(dec_from_str("500"));
1191 assert!(filter.matches(&big));
1192 }
1193
1194 #[test]
1195 fn test_tick_filter_min_notional_rejects_small() {
1196 let small = make_tick("AAPL", "100", "1", Side::Bid, 1); let filter = TickFilter::new().min_notional(dec_from_str("500"));
1198 assert!(!filter.matches(&small));
1199 }
1200
1201 #[test]
1202 fn test_tick_filter_is_empty_when_no_predicates() {
1203 let f = TickFilter::new();
1204 assert!(f.is_empty());
1205 }
1206
1207 #[test]
1208 fn test_tick_filter_not_empty_after_symbol_set() {
1209 let f = TickFilter::new().symbol(Symbol::new("AAPL").unwrap());
1210 assert!(!f.is_empty());
1211 }
1212
1213 #[test]
1214 fn test_tick_filter_not_empty_after_side_set() {
1215 let f = TickFilter::new().side(Side::Ask);
1216 assert!(!f.is_empty());
1217 }
1218
1219 #[test]
1220 fn test_tick_notional_checked_matches_notional() {
1221 let t = make_tick("AAPL", "150.50", "10", Side::Bid, 0);
1222 assert_eq!(t.notional_checked(), Some(t.notional()));
1223 }
1224
1225 #[test]
1226 fn test_tick_notional_checked_zero_qty() {
1227 let t = make_tick("AAPL", "100", "0", Side::Bid, 0);
1228 assert_eq!(t.notional_checked(), Some(dec!(0)));
1229 }
1230
1231 #[test]
1232 fn test_tick_is_buy_bid_side() {
1233 let t = make_tick("AAPL", "100", "1", Side::Bid, 0);
1234 assert!(t.is_buy());
1235 assert!(!t.is_sell());
1236 }
1237
1238 #[test]
1239 fn test_tick_is_sell_ask_side() {
1240 let t = make_tick("AAPL", "100", "1", Side::Ask, 0);
1241 assert!(t.is_sell());
1242 assert!(!t.is_buy());
1243 }
1244
1245 #[test]
1246 fn test_tick_replayer_between_inclusive() {
1247 let ticks = vec![
1248 make_tick("AAPL", "100", "1", Side::Bid, 1),
1249 make_tick("AAPL", "101", "1", Side::Ask, 5),
1250 make_tick("AAPL", "102", "1", Side::Bid, 10),
1251 ];
1252 let replayer = TickReplayer::new(ticks);
1253 let result = replayer.between(NanoTimestamp::new(1), NanoTimestamp::new(5));
1254 assert_eq!(result.len(), 2);
1255 }
1256
1257 #[test]
1258 fn test_tick_replayer_between_no_matches() {
1259 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 100)];
1260 let replayer = TickReplayer::new(ticks);
1261 let result = replayer.between(NanoTimestamp::new(1), NanoTimestamp::new(50));
1262 assert!(result.is_empty());
1263 }
1264
1265 #[test]
1266 fn test_tick_filter_timestamp_range() {
1267 let ticks = vec![
1268 make_tick("AAPL", "100", "1", Side::Bid, 1),
1269 make_tick("AAPL", "101", "1", Side::Ask, 5),
1270 make_tick("AAPL", "102", "1", Side::Bid, 10),
1271 ];
1272 let filter = TickFilter::new()
1273 .timestamp_range(NanoTimestamp::new(3), NanoTimestamp::new(10));
1274 let matched: Vec<_> = ticks.iter().filter(|t| filter.matches(t)).collect();
1275 assert_eq!(matched.len(), 2);
1276 }
1277
1278 #[test]
1279 fn test_tick_replayer_first_returns_earliest() {
1280 let ticks = vec![
1281 make_tick("AAPL", "100", "1", Side::Bid, 5),
1282 make_tick("AAPL", "101", "1", Side::Ask, 1),
1283 make_tick("AAPL", "102", "1", Side::Bid, 10),
1284 ];
1285 let replayer = TickReplayer::new(ticks);
1286 let first = replayer.first().unwrap();
1287 assert_eq!(first.timestamp, NanoTimestamp::new(1));
1288 }
1289
1290 #[test]
1291 fn test_tick_replayer_last_returns_latest() {
1292 let ticks = vec![
1293 make_tick("AAPL", "100", "1", Side::Bid, 5),
1294 make_tick("AAPL", "101", "1", Side::Ask, 1),
1295 make_tick("AAPL", "102", "1", Side::Bid, 10),
1296 ];
1297 let replayer = TickReplayer::new(ticks);
1298 let last = replayer.last().unwrap();
1299 assert_eq!(last.timestamp, NanoTimestamp::new(10));
1300 }
1301
1302 #[test]
1303 fn test_tick_replayer_first_none_when_empty() {
1304 let replayer = TickReplayer::new(vec![]);
1305 assert!(replayer.first().is_none());
1306 }
1307
1308 #[test]
1309 fn test_tick_replayer_last_none_when_empty() {
1310 let replayer = TickReplayer::new(vec![]);
1311 assert!(replayer.last().is_none());
1312 }
1313
1314 #[test]
1315 fn test_tick_replayer_vwap_by_side_correct_values() {
1316 let ticks = vec![
1317 make_tick("AAPL", "100", "10", Side::Bid, 1), make_tick("AAPL", "200", "5", Side::Ask, 2), ];
1320 let replayer = TickReplayer::new(ticks);
1321 let (bid_vwap, ask_vwap) = replayer.vwap_by_side();
1322 assert_eq!(bid_vwap, Some(dec_from_str("100")));
1323 assert_eq!(ask_vwap, Some(dec_from_str("200")));
1324 }
1325
1326 #[test]
1327 fn test_tick_replayer_vwap_by_side_no_asks_returns_none_ask() {
1328 let ticks = vec![make_tick("AAPL", "100", "10", Side::Bid, 1)];
1329 let replayer = TickReplayer::new(ticks);
1330 let (bid_vwap, ask_vwap) = replayer.vwap_by_side();
1331 assert!(bid_vwap.is_some());
1332 assert!(ask_vwap.is_none());
1333 }
1334
1335 #[test]
1336 fn test_tick_replayer_vwap_by_side_empty_returns_none_both() {
1337 let replayer = TickReplayer::new(vec![]);
1338 let (bid_vwap, ask_vwap) = replayer.vwap_by_side();
1339 assert!(bid_vwap.is_none());
1340 assert!(ask_vwap.is_none());
1341 }
1342
1343 #[test]
1344 fn test_tick_filter_clear_resets_all_predicates() {
1345 let f = TickFilter::new()
1346 .symbol(Symbol::new("AAPL").unwrap())
1347 .side(Side::Bid)
1348 .min_quantity(Quantity::new(dec!(1)).unwrap());
1349 let cleared = f.clear();
1350 assert!(cleared.is_empty());
1351 }
1352
1353 #[test]
1354 fn test_tick_filter_has_notional_filter_false_when_unset() {
1355 let f = TickFilter::new();
1356 assert!(!f.has_notional_filter());
1357 }
1358
1359 #[test]
1360 fn test_tick_filter_has_notional_filter_true_with_min() {
1361 let f = TickFilter::new().min_notional(dec_from_str("100"));
1362 assert!(f.has_notional_filter());
1363 }
1364
1365 #[test]
1366 fn test_tick_filter_has_notional_filter_true_with_max() {
1367 let f = TickFilter::new().max_notional(dec_from_str("1000"));
1368 assert!(f.has_notional_filter());
1369 }
1370
1371 #[test]
1372 fn test_tick_replayer_total_notional() {
1373 let ticks = vec![
1374 make_tick("AAPL", "100", "10", Side::Bid, 1), make_tick("AAPL", "200", "5", Side::Ask, 2), ];
1377 let replayer = TickReplayer::new(ticks);
1378 assert_eq!(replayer.total_notional(), dec_from_str("2000"));
1379 }
1380
1381 #[test]
1382 fn test_tick_replayer_total_notional_empty() {
1383 let replayer = TickReplayer::new(vec![]);
1384 assert_eq!(replayer.total_notional(), dec_from_str("0"));
1385 }
1386
1387 #[test]
1388 fn test_tick_replayer_buy_volume() {
1389 let ticks = vec![
1390 make_tick("AAPL", "100", "10", Side::Bid, 1),
1391 make_tick("AAPL", "100", "5", Side::Ask, 2),
1392 ];
1393 let replayer = TickReplayer::new(ticks);
1394 assert_eq!(replayer.buy_volume(), dec_from_str("10"));
1395 }
1396
1397 #[test]
1398 fn test_tick_replayer_sell_volume() {
1399 let ticks = vec![
1400 make_tick("AAPL", "100", "10", Side::Bid, 1),
1401 make_tick("AAPL", "100", "7", Side::Ask, 2),
1402 ];
1403 let replayer = TickReplayer::new(ticks);
1404 assert_eq!(replayer.sell_volume(), dec_from_str("7"));
1405 }
1406
1407 #[test]
1408 fn test_tick_replayer_delta_positive_when_more_buys() {
1409 let ticks = vec![
1410 make_tick("AAPL", "100", "10", Side::Bid, 1),
1411 make_tick("AAPL", "100", "3", Side::Ask, 2),
1412 ];
1413 let replayer = TickReplayer::new(ticks);
1414 assert_eq!(replayer.delta(), dec_from_str("7"));
1415 }
1416
1417 #[test]
1418 fn test_tick_replayer_delta_negative_when_more_sells() {
1419 let ticks = vec![
1420 make_tick("AAPL", "100", "2", Side::Bid, 1),
1421 make_tick("AAPL", "100", "8", Side::Ask, 2),
1422 ];
1423 let replayer = TickReplayer::new(ticks);
1424 assert_eq!(replayer.delta(), dec_from_str("-6"));
1425 }
1426
1427 #[test]
1428 fn test_tick_replayer_delta_zero_when_balanced() {
1429 let ticks = vec![
1430 make_tick("AAPL", "100", "5", Side::Bid, 1),
1431 make_tick("AAPL", "100", "5", Side::Ask, 2),
1432 ];
1433 let replayer = TickReplayer::new(ticks);
1434 assert_eq!(replayer.delta(), dec_from_str("0"));
1435 }
1436
1437 #[test]
1438 fn test_tick_replayer_time_span_nanos_correct() {
1439 let ticks = vec![
1440 make_tick("AAPL", "100", "1", Side::Bid, 1_000_000),
1441 make_tick("AAPL", "100", "1", Side::Ask, 3_000_000),
1442 ];
1443 let replayer = TickReplayer::new(ticks);
1444 assert_eq!(replayer.time_span_nanos(), Some(2_000_000));
1445 }
1446
1447 #[test]
1448 fn test_tick_replayer_time_span_nanos_none_for_single_tick() {
1449 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 1_000_000)];
1450 let replayer = TickReplayer::new(ticks);
1451 assert_eq!(replayer.time_span_nanos(), None);
1452 }
1453
1454 #[test]
1455 fn test_tick_replayer_time_span_nanos_none_for_empty() {
1456 let replayer = TickReplayer::new(vec![]);
1457 assert_eq!(replayer.time_span_nanos(), None);
1458 }
1459
1460 #[test]
1461 fn test_tick_replayer_price_range_returns_spread() {
1462 let ticks = vec![
1463 make_tick("AAPL", "100", "1", Side::Bid, 1),
1464 make_tick("AAPL", "105", "1", Side::Ask, 2),
1465 make_tick("AAPL", "98", "1", Side::Bid, 3),
1466 ];
1467 let replayer = TickReplayer::new(ticks);
1468 assert_eq!(replayer.price_range(), Some(dec_from_str("7")));
1469 }
1470
1471 #[test]
1472 fn test_tick_replayer_price_range_none_for_empty() {
1473 let replayer = TickReplayer::new(vec![]);
1474 assert_eq!(replayer.price_range(), None);
1475 }
1476
1477 #[test]
1478 fn test_tick_replayer_price_range_zero_for_single_price() {
1479 let ticks = vec![make_tick("AAPL", "100", "1", Side::Bid, 1)];
1480 let replayer = TickReplayer::new(ticks);
1481 assert_eq!(replayer.price_range(), Some(dec_from_str("0")));
1482 }
1483
1484 #[test]
1485 fn test_tick_replayer_tick_count_by_side() {
1486 let ticks = vec![
1487 make_tick("AAPL", "100", "1", Side::Bid, 1),
1488 make_tick("AAPL", "100", "1", Side::Bid, 2),
1489 make_tick("AAPL", "100", "1", Side::Ask, 3),
1490 ];
1491 let replayer = TickReplayer::new(ticks);
1492 assert_eq!(replayer.tick_count_by_side(), (2, 1));
1493 }
1494
1495 #[test]
1496 fn test_tick_replayer_tick_count_by_side_empty() {
1497 let replayer = TickReplayer::new(vec![]);
1498 assert_eq!(replayer.tick_count_by_side(), (0, 0));
1499 }
1500
1501 #[test]
1502 fn test_tick_replayer_median_trade_size_single() {
1503 let ticks = vec![make_tick("AAPL", "100", "5", Side::Bid, 1)];
1504 let replayer = TickReplayer::new(ticks);
1505 assert_eq!(replayer.median_trade_size(), Some(dec_from_str("5")));
1506 }
1507
1508 #[test]
1509 fn test_tick_replayer_median_trade_size_odd_count() {
1510 let ticks = vec![
1511 make_tick("AAPL", "100", "1", Side::Bid, 1),
1512 make_tick("AAPL", "100", "3", Side::Bid, 2),
1513 make_tick("AAPL", "100", "5", Side::Bid, 3),
1514 ];
1515 let replayer = TickReplayer::new(ticks);
1516 assert_eq!(replayer.median_trade_size(), Some(dec_from_str("3")));
1518 }
1519
1520 #[test]
1521 fn test_tick_replayer_median_trade_size_none_for_empty() {
1522 let replayer = TickReplayer::new(vec![]);
1523 assert_eq!(replayer.median_trade_size(), None);
1524 }
1525
1526 #[test]
1527 fn test_tick_replayer_total_notional_sum_two_trades() {
1528 let ticks = vec![
1529 make_tick("X", "100", "2", Side::Bid, 1),
1530 make_tick("X", "50", "4", Side::Ask, 2),
1531 ];
1532 let replayer = TickReplayer::new(ticks);
1533 assert_eq!(replayer.total_notional(), dec_from_str("400"));
1535 }
1536
1537 #[test]
1538 fn test_tick_replayer_price_std_none_for_single_tick() {
1539 let ticks = vec![make_tick("X", "100", "1", Side::Bid, 1)];
1540 let replayer = TickReplayer::new(ticks);
1541 assert!(replayer.price_std().is_none());
1542 }
1543
1544 #[test]
1545 fn test_tick_replayer_price_std_zero_for_constant_prices() {
1546 let ticks = vec![
1547 make_tick("X", "100", "1", Side::Bid, 1),
1548 make_tick("X", "100", "2", Side::Bid, 2),
1549 make_tick("X", "100", "3", Side::Bid, 3),
1550 ];
1551 let replayer = TickReplayer::new(ticks);
1552 assert_eq!(replayer.price_std(), Some(Decimal::ZERO));
1553 }
1554
1555 #[test]
1556 fn test_tick_replayer_price_std_positive_for_varying_prices() {
1557 let ticks = vec![
1558 make_tick("X", "100", "1", Side::Bid, 1),
1559 make_tick("X", "110", "1", Side::Bid, 2),
1560 make_tick("X", "120", "1", Side::Bid, 3),
1561 ];
1562 let replayer = TickReplayer::new(ticks);
1563 let std = replayer.price_std().unwrap();
1564 assert!(std > Decimal::ZERO);
1565 }
1566}