1use crate::error::FinError;
18use rust_decimal::prelude::ToPrimitive;
19use rust_decimal::Decimal;
20use std::collections::VecDeque;
21
22#[derive(Debug)]
48pub struct BidAskSpread {
49 window: usize,
50 buf: VecDeque<f64>,
52}
53
54impl BidAskSpread {
55 pub fn new(window: usize) -> Result<Self, FinError> {
60 if window == 0 {
61 return Err(FinError::InvalidPeriod(window));
62 }
63 Ok(Self { window, buf: VecDeque::with_capacity(window) })
64 }
65
66 pub fn update(&mut self, bid: Decimal, ask: Decimal) -> Result<(), FinError> {
71 if bid <= Decimal::ZERO {
72 return Err(FinError::InvalidInput(format!("bid must be positive, got {bid}")));
73 }
74 if ask <= bid {
75 return Err(FinError::InvalidInput(format!(
76 "ask ({ask}) must be greater than bid ({bid})"
77 )));
78 }
79 let mid = (bid + ask) / Decimal::from(2u32);
80 let spread = ask - bid;
81 let mid_f = mid.to_f64().unwrap_or(0.0);
82 let spread_f = spread.to_f64().unwrap_or(0.0);
83 if mid_f <= 0.0 {
84 return Err(FinError::InvalidInput("mid price must be positive".to_owned()));
85 }
86 let bps = spread_f / mid_f * 10_000.0;
87 self.buf.push_back(bps);
88 if self.buf.len() > self.window {
89 self.buf.pop_front();
90 }
91 Ok(())
92 }
93
94 pub fn average_spread_bps(&self) -> Option<f64> {
96 if self.buf.len() < self.window {
97 return None;
98 }
99 let sum: f64 = self.buf.iter().sum();
100 Some(sum / self.buf.len() as f64)
101 }
102
103 pub fn is_ready(&self) -> bool {
105 self.buf.len() >= self.window
106 }
107
108 pub fn window(&self) -> usize {
110 self.window
111 }
112
113 pub fn sample_count(&self) -> usize {
115 self.buf.len()
116 }
117
118 pub fn reset(&mut self) {
120 self.buf.clear();
121 }
122}
123
124#[derive(Debug)]
147pub struct AmihudIlliquidity {
148 window: usize,
149 buf: VecDeque<f64>,
150}
151
152impl AmihudIlliquidity {
153 pub fn new(window: usize) -> Result<Self, FinError> {
158 if window == 0 {
159 return Err(FinError::InvalidPeriod(window));
160 }
161 Ok(Self { window, buf: VecDeque::with_capacity(window) })
162 }
163
164 pub fn update(
173 &mut self,
174 prev_close: Decimal,
175 close: Decimal,
176 volume: Decimal,
177 ) -> Result<(), FinError> {
178 if prev_close <= Decimal::ZERO {
179 return Err(FinError::InvalidInput("prev_close must be positive".to_owned()));
180 }
181 if close <= Decimal::ZERO {
182 return Err(FinError::InvalidInput("close must be positive".to_owned()));
183 }
184 if volume <= Decimal::ZERO {
185 return Err(FinError::InvalidInput("volume must be positive".to_owned()));
186 }
187 let pc = prev_close.to_f64().unwrap_or(1.0);
188 let c = close.to_f64().unwrap_or(pc);
189 let v = volume.to_f64().unwrap_or(1.0);
190 let ret = ((c / pc).ln()).abs();
191 let ratio = ret / v;
192 self.buf.push_back(ratio);
193 if self.buf.len() > self.window {
194 self.buf.pop_front();
195 }
196 Ok(())
197 }
198
199 pub fn get(&self) -> Option<f64> {
201 if self.buf.len() < self.window {
202 return None;
203 }
204 let sum: f64 = self.buf.iter().sum();
205 Some(sum / self.buf.len() as f64)
206 }
207
208 pub fn is_ready(&self) -> bool {
210 self.buf.len() >= self.window
211 }
212
213 pub fn window(&self) -> usize {
215 self.window
216 }
217
218 pub fn sample_count(&self) -> usize {
220 self.buf.len()
221 }
222
223 pub fn reset(&mut self) {
225 self.buf.clear();
226 }
227}
228
229#[derive(Debug)]
257pub struct KyleLambda {
258 window: usize,
259 buf: VecDeque<(f64, f64)>,
261}
262
263impl KyleLambda {
264 pub fn new(window: usize) -> Result<Self, FinError> {
269 if window < 2 {
270 return Err(FinError::InvalidPeriod(window));
271 }
272 Ok(Self { window, buf: VecDeque::with_capacity(window) })
273 }
274
275 pub fn update(&mut self, price_change: Decimal, signed_volume: Decimal) -> Result<(), FinError> {
283 let dp = price_change.to_f64().ok_or_else(|| {
284 FinError::InvalidInput("price_change is not representable as f64".to_owned())
285 })?;
286 let dq = signed_volume.to_f64().ok_or_else(|| {
287 FinError::InvalidInput("signed_volume is not representable as f64".to_owned())
288 })?;
289 if !dp.is_finite() || !dq.is_finite() {
290 return Err(FinError::InvalidInput(
291 "price_change and signed_volume must be finite".to_owned(),
292 ));
293 }
294 self.buf.push_back((dp, dq));
295 if self.buf.len() > self.window {
296 self.buf.pop_front();
297 }
298 Ok(())
299 }
300
301 pub fn get(&self) -> Option<f64> {
303 if self.buf.len() < self.window {
304 return None;
305 }
306 let n = self.buf.len() as f64;
307 let mean_dp = self.buf.iter().map(|(dp, _)| dp).sum::<f64>() / n;
308 let mean_dq = self.buf.iter().map(|(_, dq)| dq).sum::<f64>() / n;
309 let cov: f64 = self.buf.iter().map(|(dp, dq)| (dp - mean_dp) * (dq - mean_dq)).sum::<f64>();
310 let var_dq: f64 = self.buf.iter().map(|(_, dq)| (dq - mean_dq).powi(2)).sum::<f64>();
311 if var_dq == 0.0 {
312 return None;
313 }
314 Some(cov / var_dq)
315 }
316
317 pub fn is_ready(&self) -> bool {
319 self.buf.len() >= self.window
320 }
321
322 pub fn window(&self) -> usize {
324 self.window
325 }
326
327 pub fn sample_count(&self) -> usize {
329 self.buf.len()
330 }
331
332 pub fn reset(&mut self) {
334 self.buf.clear();
335 }
336}
337
338#[derive(Debug)]
367pub struct RollImpliedSpread {
368 window: usize,
369 changes: VecDeque<f64>,
371}
372
373impl RollImpliedSpread {
374 pub fn new(window: usize) -> Result<Self, FinError> {
379 if window < 2 {
380 return Err(FinError::InvalidPeriod(window));
381 }
382 Ok(Self {
383 window,
384 changes: VecDeque::with_capacity(window + 1),
385 })
386 }
387
388 pub fn update(&mut self, price_change: Decimal) -> Result<(), FinError> {
393 let dp = price_change.to_f64().ok_or_else(|| {
394 FinError::InvalidInput("price_change is not representable as f64".to_owned())
395 })?;
396 if !dp.is_finite() {
397 return Err(FinError::InvalidInput("price_change must be finite".to_owned()));
398 }
399 self.changes.push_back(dp);
400 if self.changes.len() > self.window + 1 {
401 self.changes.pop_front();
402 }
403 Ok(())
404 }
405
406 pub fn get(&self) -> Option<f64> {
410 if self.changes.len() < self.window + 1 {
411 return None;
412 }
413 let n = self.changes.len();
414 let mean = self.changes.iter().sum::<f64>() / n as f64;
416 let cov: f64 = self
417 .changes
418 .iter()
419 .zip(self.changes.iter().skip(1))
420 .map(|(a, b)| (a - mean) * (b - mean))
421 .sum::<f64>()
422 / (n - 1) as f64;
423
424 if cov >= 0.0 {
425 Some(0.0)
426 } else {
427 let spread = 2.0 * (-cov).sqrt();
428 Some(spread)
429 }
430 }
431
432 pub fn is_ready(&self) -> bool {
434 self.changes.len() > self.window
435 }
436
437 pub fn window(&self) -> usize {
439 self.window
440 }
441
442 pub fn sample_count(&self) -> usize {
444 self.changes.len()
445 }
446
447 pub fn reset(&mut self) {
449 self.changes.clear();
450 }
451}
452
453#[derive(Debug)]
477pub struct OrderImbalance {
478 window: usize,
479 buf: VecDeque<f64>,
481}
482
483impl OrderImbalance {
484 pub fn new(window: usize) -> Result<Self, FinError> {
489 if window == 0 {
490 return Err(FinError::InvalidPeriod(window));
491 }
492 Ok(Self { window, buf: VecDeque::with_capacity(window) })
493 }
494
495 pub fn update(&mut self, buy_volume: Decimal, sell_volume: Decimal) -> Result<(), FinError> {
503 if buy_volume < Decimal::ZERO {
504 return Err(FinError::InvalidInput(
505 "buy_volume must be non-negative".to_owned(),
506 ));
507 }
508 if sell_volume < Decimal::ZERO {
509 return Err(FinError::InvalidInput(
510 "sell_volume must be non-negative".to_owned(),
511 ));
512 }
513 let total = buy_volume + sell_volume;
514 if total == Decimal::ZERO {
515 return Err(FinError::InvalidInput(
516 "buy_volume + sell_volume must be positive".to_owned(),
517 ));
518 }
519 let bv = buy_volume.to_f64().unwrap_or(0.0);
520 let sv = sell_volume.to_f64().unwrap_or(0.0);
521 let tot = bv + sv;
522 let oir = (bv - sv) / tot;
523 self.buf.push_back(oir);
524 if self.buf.len() > self.window {
525 self.buf.pop_front();
526 }
527 Ok(())
528 }
529
530 pub fn get(&self) -> Option<f64> {
532 if self.buf.len() < self.window {
533 return None;
534 }
535 let sum: f64 = self.buf.iter().sum();
536 Some(sum / self.buf.len() as f64)
537 }
538
539 pub fn is_ready(&self) -> bool {
541 self.buf.len() >= self.window
542 }
543
544 pub fn window(&self) -> usize {
546 self.window
547 }
548
549 pub fn sample_count(&self) -> usize {
551 self.buf.len()
552 }
553
554 pub fn reset(&mut self) {
556 self.buf.clear();
557 }
558}
559
560#[derive(Debug, Clone, Default)]
569pub struct MicrostructureSnapshot {
570 pub avg_spread_bps: Option<f64>,
572 pub order_imbalance: Option<f64>,
574 pub kyle_lambda: Option<f64>,
576 pub amihud_illiquidity: Option<f64>,
578 pub roll_spread: Option<f64>,
580}
581
582pub struct MicrostructureMetrics {
602 spread: BidAskSpread,
603 imbalance: OrderImbalance,
604 kyle: KyleLambda,
605 amihud: AmihudIlliquidity,
606 roll: RollImpliedSpread,
607}
608
609impl MicrostructureMetrics {
610 pub fn new(window: usize) -> Result<Self, FinError> {
617 if window < 2 {
618 return Err(FinError::InvalidPeriod(window));
619 }
620 Ok(Self {
621 spread: BidAskSpread::new(window)?,
622 imbalance: OrderImbalance::new(window)?,
623 kyle: KyleLambda::new(window)?,
624 amihud: AmihudIlliquidity::new(window)?,
625 roll: RollImpliedSpread::new(window)?,
626 })
627 }
628
629 pub fn update_spread(&mut self, bid: Decimal, ask: Decimal) -> Result<(), FinError> {
634 self.spread.update(bid, ask)
635 }
636
637 pub fn update_volume_imbalance(
642 &mut self,
643 buy_volume: Decimal,
644 sell_volume: Decimal,
645 ) -> Result<(), FinError> {
646 self.imbalance.update(buy_volume, sell_volume)
647 }
648
649 pub fn update_price_impact(
654 &mut self,
655 price_change: Decimal,
656 signed_volume: Decimal,
657 ) -> Result<(), FinError> {
658 self.kyle.update(price_change, signed_volume)
659 }
660
661 pub fn update_amihud(
666 &mut self,
667 prev_close: Decimal,
668 close: Decimal,
669 volume: Decimal,
670 ) -> Result<(), FinError> {
671 self.amihud.update(prev_close, close, volume)
672 }
673
674 pub fn update_roll(&mut self, price_change: Decimal) -> Result<(), FinError> {
679 self.roll.update(price_change)
680 }
681
682 pub fn snapshot(&self) -> MicrostructureSnapshot {
686 MicrostructureSnapshot {
687 avg_spread_bps: self.spread.average_spread_bps(),
688 order_imbalance: self.imbalance.get(),
689 kyle_lambda: self.kyle.get(),
690 amihud_illiquidity: self.amihud.get(),
691 roll_spread: self.roll.get(),
692 }
693 }
694
695 pub fn reset(&mut self) {
697 self.spread.reset();
698 self.imbalance.reset();
699 self.kyle.reset();
700 self.amihud.reset();
701 self.roll.reset();
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use rust_decimal_macros::dec;
709
710 #[test]
713 fn test_bid_ask_spread_zero_window_fails() {
714 assert!(BidAskSpread::new(0).is_err());
715 }
716
717 #[test]
718 fn test_bid_ask_spread_not_ready_before_window() {
719 let mut t = BidAskSpread::new(3).unwrap();
720 t.update(dec!(99.9), dec!(100.1)).unwrap();
721 t.update(dec!(99.9), dec!(100.1)).unwrap();
722 assert!(!t.is_ready());
723 assert!(t.average_spread_bps().is_none());
724 }
725
726 #[test]
727 fn test_bid_ask_spread_correct_bps() {
728 let mut t = BidAskSpread::new(3).unwrap();
729 for _ in 0..3 {
731 t.update(dec!(99.90), dec!(100.10)).unwrap();
732 }
733 let bps = t.average_spread_bps().unwrap();
734 assert!((bps - 20.0).abs() < 0.01, "bps={bps}");
735 }
736
737 #[test]
738 fn test_bid_ask_spread_inverted_fails() {
739 let mut t = BidAskSpread::new(3).unwrap();
740 assert!(t.update(dec!(101), dec!(100)).is_err());
741 }
742
743 #[test]
744 fn test_bid_ask_spread_negative_bid_fails() {
745 let mut t = BidAskSpread::new(3).unwrap();
746 assert!(t.update(dec!(-1), dec!(100)).is_err());
747 }
748
749 #[test]
750 fn test_bid_ask_spread_reset() {
751 let mut t = BidAskSpread::new(2).unwrap();
752 t.update(dec!(99), dec!(101)).unwrap();
753 t.update(dec!(99), dec!(101)).unwrap();
754 assert!(t.is_ready());
755 t.reset();
756 assert!(!t.is_ready());
757 }
758
759 #[test]
762 fn test_amihud_zero_window_fails() {
763 assert!(AmihudIlliquidity::new(0).is_err());
764 }
765
766 #[test]
767 fn test_amihud_not_ready_before_window() {
768 let mut ai = AmihudIlliquidity::new(3).unwrap();
769 ai.update(dec!(100), dec!(102), dec!(1000)).unwrap();
770 assert!(!ai.is_ready());
771 assert!(ai.get().is_none());
772 }
773
774 #[test]
775 fn test_amihud_positive_for_price_moves() {
776 let mut ai = AmihudIlliquidity::new(3).unwrap();
777 ai.update(dec!(100), dec!(105), dec!(1000)).unwrap();
778 ai.update(dec!(105), dec!(103), dec!(800)).unwrap();
779 ai.update(dec!(103), dec!(107), dec!(1200)).unwrap();
780 let illiq = ai.get().unwrap();
781 assert!(illiq > 0.0, "illiquidity should be positive: {illiq}");
782 }
783
784 #[test]
785 fn test_amihud_zero_volume_fails() {
786 let mut ai = AmihudIlliquidity::new(3).unwrap();
787 assert!(ai.update(dec!(100), dec!(105), dec!(0)).is_err());
788 }
789
790 #[test]
791 fn test_amihud_reset() {
792 let mut ai = AmihudIlliquidity::new(2).unwrap();
793 ai.update(dec!(100), dec!(102), dec!(500)).unwrap();
794 ai.update(dec!(102), dec!(101), dec!(600)).unwrap();
795 assert!(ai.is_ready());
796 ai.reset();
797 assert!(!ai.is_ready());
798 }
799
800 #[test]
803 fn test_kyle_period_1_fails() {
804 assert!(KyleLambda::new(1).is_err());
805 }
806
807 #[test]
808 fn test_kyle_not_ready_before_window() {
809 let mut kl = KyleLambda::new(4).unwrap();
810 kl.update(dec!(0.1), dec!(200)).unwrap();
811 assert!(!kl.is_ready());
812 assert!(kl.get().is_none());
813 }
814
815 #[test]
816 fn test_kyle_positive_lambda_for_aligned_signals() {
817 let mut kl = KyleLambda::new(4).unwrap();
818 kl.update(dec!(0.10), dec!(100)).unwrap();
820 kl.update(dec!(0.20), dec!(200)).unwrap();
821 kl.update(dec!(0.15), dec!(150)).unwrap();
822 kl.update(dec!(0.25), dec!(250)).unwrap();
823 let lambda = kl.get().unwrap();
824 assert!(lambda > 0.0, "lambda should be positive: {lambda}");
825 }
826
827 #[test]
828 fn test_kyle_zero_volume_variance_returns_none() {
829 let mut kl = KyleLambda::new(3).unwrap();
830 kl.update(dec!(0.1), dec!(100)).unwrap();
832 kl.update(dec!(0.2), dec!(100)).unwrap();
833 kl.update(dec!(0.3), dec!(100)).unwrap();
834 assert!(kl.get().is_none());
835 }
836
837 #[test]
838 fn test_kyle_reset() {
839 let mut kl = KyleLambda::new(2).unwrap();
840 kl.update(dec!(0.1), dec!(100)).unwrap();
841 kl.update(dec!(0.2), dec!(200)).unwrap();
842 assert!(kl.is_ready());
843 kl.reset();
844 assert!(!kl.is_ready());
845 }
846
847 #[test]
850 fn test_roll_period_1_fails() {
851 assert!(RollImpliedSpread::new(1).is_err());
852 }
853
854 #[test]
855 fn test_roll_not_ready_before_window() {
856 let mut r = RollImpliedSpread::new(5).unwrap();
857 r.update(dec!(0.05)).unwrap();
858 assert!(!r.is_ready());
859 assert!(r.get().is_none());
860 }
861
862 #[test]
863 fn test_roll_positive_spread_for_alternating_returns() {
864 let mut r = RollImpliedSpread::new(10).unwrap();
865 for i in 0..11 {
866 let ret = if i % 2 == 0 { dec!(0.05) } else { dec!(-0.05) };
867 r.update(ret).unwrap();
868 }
869 let spread = r.get().unwrap();
870 assert!(spread > 0.0, "alternating returns should give positive Roll spread: {spread}");
871 }
872
873 #[test]
874 fn test_roll_zero_spread_for_trending_returns() {
875 let mut r = RollImpliedSpread::new(5).unwrap();
877 for _ in 0..6 {
878 r.update(dec!(0.10)).unwrap();
879 }
880 let spread = r.get().unwrap();
881 assert_eq!(spread, 0.0);
883 }
884
885 #[test]
886 fn test_roll_reset() {
887 let mut r = RollImpliedSpread::new(3).unwrap();
888 for _ in 0..4 {
889 r.update(dec!(0.01)).unwrap();
890 }
891 assert!(r.is_ready());
892 r.reset();
893 assert!(!r.is_ready());
894 }
895
896 #[test]
899 fn test_order_imbalance_zero_window_fails() {
900 assert!(OrderImbalance::new(0).is_err());
901 }
902
903 #[test]
904 fn test_order_imbalance_not_ready_before_window() {
905 let mut oi = OrderImbalance::new(3).unwrap();
906 oi.update(dec!(600), dec!(400)).unwrap();
907 assert!(!oi.is_ready());
908 assert!(oi.get().is_none());
909 }
910
911 #[test]
912 fn test_order_imbalance_positive_for_buy_heavy() {
913 let mut oi = OrderImbalance::new(3).unwrap();
914 oi.update(dec!(800), dec!(200)).unwrap();
915 oi.update(dec!(700), dec!(300)).unwrap();
916 oi.update(dec!(900), dec!(100)).unwrap();
917 let imbalance = oi.get().unwrap();
918 assert!(imbalance > 0.0, "expected positive imbalance: {imbalance}");
919 }
920
921 #[test]
922 fn test_order_imbalance_negative_for_sell_heavy() {
923 let mut oi = OrderImbalance::new(3).unwrap();
924 oi.update(dec!(200), dec!(800)).unwrap();
925 oi.update(dec!(300), dec!(700)).unwrap();
926 oi.update(dec!(100), dec!(900)).unwrap();
927 let imbalance = oi.get().unwrap();
928 assert!(imbalance < 0.0, "expected negative imbalance: {imbalance}");
929 }
930
931 #[test]
932 fn test_order_imbalance_zero_total_fails() {
933 let mut oi = OrderImbalance::new(3).unwrap();
934 assert!(oi.update(dec!(0), dec!(0)).is_err());
935 }
936
937 #[test]
938 fn test_order_imbalance_negative_volume_fails() {
939 let mut oi = OrderImbalance::new(3).unwrap();
940 assert!(oi.update(dec!(-100), dec!(100)).is_err());
941 }
942
943 #[test]
944 fn test_order_imbalance_reset() {
945 let mut oi = OrderImbalance::new(2).unwrap();
946 oi.update(dec!(500), dec!(500)).unwrap();
947 oi.update(dec!(500), dec!(500)).unwrap();
948 assert!(oi.is_ready());
949 oi.reset();
950 assert!(!oi.is_ready());
951 }
952
953 #[test]
956 fn test_microstructure_metrics_window_too_small_fails() {
957 assert!(MicrostructureMetrics::new(1).is_err());
958 assert!(MicrostructureMetrics::new(0).is_err());
959 }
960
961 #[test]
962 fn test_microstructure_metrics_snapshot_none_before_warm() {
963 let mut m = MicrostructureMetrics::new(5).unwrap();
964 m.update_spread(dec!(99.9), dec!(100.1)).unwrap();
965 let snap = m.snapshot();
966 assert!(snap.avg_spread_bps.is_none());
967 assert!(snap.order_imbalance.is_none());
968 assert!(snap.kyle_lambda.is_none());
969 assert!(snap.amihud_illiquidity.is_none());
970 assert!(snap.roll_spread.is_none());
971 }
972
973 #[test]
974 fn test_microstructure_metrics_snapshot_some_after_warm() {
975 let mut m = MicrostructureMetrics::new(3).unwrap();
976 for i in 0..3 {
977 m.update_spread(dec!(99.90), dec!(100.10)).unwrap();
978 m.update_volume_imbalance(dec!(600), dec!(400)).unwrap();
979 m.update_price_impact(
980 rust_decimal::prelude::FromPrimitive::from_f64(0.05 + i as f64 * 0.01).unwrap_or(dec!(0.05)),
981 rust_decimal::prelude::FromPrimitive::from_f64(100.0 + i as f64 * 50.0).unwrap_or(dec!(100)),
982 ).unwrap();
983 m.update_amihud(dec!(100), dec!(102), dec!(1000)).unwrap();
984 m.update_roll(if i % 2 == 0 { dec!(0.05) } else { dec!(-0.05) }).unwrap();
985 }
986 let snap = m.snapshot();
987 assert!(snap.avg_spread_bps.is_some());
988 assert!(snap.order_imbalance.is_some());
989 assert!(snap.amihud_illiquidity.is_some());
990 let _ = snap.roll_spread; }
993
994 #[test]
995 fn test_microstructure_metrics_reset() {
996 let mut m = MicrostructureMetrics::new(2).unwrap();
997 for _ in 0..2 {
998 m.update_spread(dec!(99.9), dec!(100.1)).unwrap();
999 m.update_volume_imbalance(dec!(500), dec!(500)).unwrap();
1000 m.update_price_impact(dec!(0.05), dec!(100)).unwrap();
1001 m.update_amihud(dec!(100), dec!(102), dec!(1000)).unwrap();
1002 m.update_roll(dec!(0.05)).unwrap();
1003 }
1004 m.reset();
1005 let snap = m.snapshot();
1006 assert!(snap.avg_spread_bps.is_none());
1007 assert!(snap.order_imbalance.is_none());
1008 }
1009
1010 #[test]
1013 fn test_trade_sign_classify_buy() {
1014 assert_eq!(TradeSign::classify(100.5, 100.0), TradeSign::Buy);
1015 }
1016
1017 #[test]
1018 fn test_trade_sign_classify_sell() {
1019 assert_eq!(TradeSign::classify(99.5, 100.0), TradeSign::Sell);
1020 }
1021
1022 #[test]
1023 fn test_trade_sign_classify_unknown_equal() {
1024 assert_eq!(TradeSign::classify(100.0, 100.0), TradeSign::Unknown);
1025 }
1026
1027 #[test]
1030 fn test_kyle_lambda_estimate_positive() {
1031 let changes = vec![0.1, 0.2, 0.15, 0.25, -0.05];
1032 let volumes = vec![100.0, 200.0, 150.0, 250.0, -50.0];
1033 let est = KyleLambdaEstimate::estimate(&changes, &volumes);
1034 assert!(est.lambda > 0.0, "lambda={}", est.lambda);
1035 assert!((0.0..=1.0).contains(&est.r_squared), "r2={}", est.r_squared);
1036 }
1037
1038 #[test]
1039 fn test_kyle_lambda_estimate_empty() {
1040 let est = KyleLambdaEstimate::estimate(&[], &[]);
1041 assert_eq!(est.lambda, 0.0);
1042 assert_eq!(est.r_squared, 0.0);
1043 }
1044
1045 #[test]
1046 fn test_kyle_lambda_estimate_zero_variance() {
1047 let changes = vec![0.1, 0.1, 0.1];
1048 let volumes = vec![100.0, 100.0, 100.0];
1049 let est = KyleLambdaEstimate::estimate(&changes, &volumes);
1050 assert_eq!(est.lambda, 0.0);
1051 assert_eq!(est.r_squared, 0.0);
1052 }
1053
1054 #[test]
1057 fn test_hasbrouck_share_equal_variance() {
1058 let a = vec![0.1, -0.1, 0.2, -0.2, 0.1];
1059 let b = vec![0.1, -0.1, 0.2, -0.2, 0.1];
1060 let hs = HasbrouckShare::estimate(&a, &b);
1061 assert!((hs.security_a_share - 0.5).abs() < 0.01, "a={}", hs.security_a_share);
1063 assert!((hs.security_b_share - 0.5).abs() < 0.01, "b={}", hs.security_b_share);
1064 }
1065
1066 #[test]
1067 fn test_hasbrouck_share_sums_to_one() {
1068 let a = vec![0.1, 0.2, -0.1, 0.3];
1069 let b = vec![0.05, 0.1, -0.2, 0.15];
1070 let hs = HasbrouckShare::estimate(&a, &b);
1071 let total = hs.security_a_share + hs.security_b_share;
1072 assert!((total - 1.0).abs() < 1e-10, "total={total}");
1073 }
1074
1075 #[test]
1076 fn test_hasbrouck_share_empty() {
1077 let hs = HasbrouckShare::estimate(&[], &[]);
1078 assert_eq!(hs.security_a_share, 0.5);
1079 assert_eq!(hs.security_b_share, 0.5);
1080 }
1081
1082 #[test]
1085 fn test_pin_probability_basic() {
1086 let buys = vec![80u64, 90, 85, 70, 95];
1087 let sells = vec![20u64, 10, 15, 30, 5];
1088 let pin = PinEstimate::from_trade_counts(&buys, &sells);
1089 let p = pin.pin_probability();
1090 assert!((0.0..=1.0).contains(&p), "pin={p}");
1091 }
1092
1093 #[test]
1094 fn test_pin_probability_zero_denominator() {
1095 let pin = PinEstimate { alpha: 0.0, mu: 0.0, epsilon_b: 0.0, epsilon_s: 0.0 };
1096 assert_eq!(pin.pin_probability(), 0.0);
1097 }
1098
1099 #[test]
1100 fn test_pin_from_empty_counts() {
1101 let pin = PinEstimate::from_trade_counts(&[], &[]);
1102 assert_eq!(pin.pin_probability(), 0.0);
1103 }
1104}
1105
1106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1126pub enum TradeSign {
1127 Buy,
1129 Sell,
1131 Unknown,
1133}
1134
1135impl TradeSign {
1136 pub fn classify(price: f64, prev_price: f64) -> Self {
1141 if price > prev_price {
1142 Self::Buy
1143 } else if price < prev_price {
1144 Self::Sell
1145 } else {
1146 Self::Unknown
1147 }
1148 }
1149}
1150
1151#[derive(Debug, Clone, Copy)]
1171pub struct KyleLambdaEstimate {
1172 pub lambda: f64,
1174 pub r_squared: f64,
1176}
1177
1178impl KyleLambdaEstimate {
1179 pub fn estimate(price_changes: &[f64], signed_volumes: &[f64]) -> Self {
1187 let n = price_changes.len().min(signed_volumes.len());
1188 if n < 2 {
1189 return Self { lambda: 0.0, r_squared: 0.0 };
1190 }
1191 let nf = n as f64;
1192 let mean_dp = price_changes[..n].iter().sum::<f64>() / nf;
1193 let mean_dq = signed_volumes[..n].iter().sum::<f64>() / nf;
1194
1195 let mut cov_pq = 0.0_f64;
1196 let mut var_q = 0.0_f64;
1197 let mut var_p = 0.0_f64;
1198 for i in 0..n {
1199 let dp = price_changes[i] - mean_dp;
1200 let dq = signed_volumes[i] - mean_dq;
1201 cov_pq += dp * dq;
1202 var_q += dq * dq;
1203 var_p += dp * dp;
1204 }
1205
1206 if var_q == 0.0 {
1207 return Self { lambda: 0.0, r_squared: 0.0 };
1208 }
1209
1210 let lambda = cov_pq / var_q;
1211 let r_squared = if var_p == 0.0 {
1212 0.0
1213 } else {
1214 let cor = cov_pq / (var_q.sqrt() * var_p.sqrt());
1215 (cor * cor).min(1.0).max(0.0)
1216 };
1217
1218 Self { lambda, r_squared }
1219 }
1220}
1221
1222#[derive(Debug, Clone, Copy)]
1244pub struct HasbrouckShare {
1245 pub security_a_share: f64,
1247 pub security_b_share: f64,
1249}
1250
1251impl HasbrouckShare {
1252 pub fn estimate(returns_a: &[f64], returns_b: &[f64]) -> Self {
1257 let n = returns_a.len().min(returns_b.len());
1258 if n == 0 {
1259 return Self { security_a_share: 0.5, security_b_share: 0.5 };
1260 }
1261 let nf = n as f64;
1262 let mean_a = returns_a[..n].iter().sum::<f64>() / nf;
1263 let mean_b = returns_b[..n].iter().sum::<f64>() / nf;
1264 let var_a: f64 = returns_a[..n].iter().map(|x| (x - mean_a).powi(2)).sum::<f64>() / nf;
1265 let var_b: f64 = returns_b[..n].iter().map(|x| (x - mean_b).powi(2)).sum::<f64>() / nf;
1266 let total = var_a + var_b;
1267 if total == 0.0 {
1268 return Self { security_a_share: 0.5, security_b_share: 0.5 };
1269 }
1270 Self {
1271 security_a_share: var_a / total,
1272 security_b_share: var_b / total,
1273 }
1274 }
1275}
1276
1277#[derive(Debug, Clone, Copy)]
1300pub struct PinEstimate {
1301 pub alpha: f64,
1303 pub mu: f64,
1305 pub epsilon_b: f64,
1307 pub epsilon_s: f64,
1309}
1310
1311impl PinEstimate {
1312 pub fn from_trade_counts(buy_days: &[u64], sell_days: &[u64]) -> Self {
1322 let n = buy_days.len().min(sell_days.len());
1323 if n == 0 {
1324 return Self { alpha: 0.0, mu: 0.0, epsilon_b: 0.0, epsilon_s: 0.0 };
1325 }
1326 let nf = n as f64;
1327 let mut sum_min = 0.0_f64;
1328 let mut sum_excess = 0.0_f64;
1329 let mut event_days = 0u64;
1330
1331 for i in 0..n {
1332 let b = buy_days[i] as f64;
1333 let s = sell_days[i] as f64;
1334 let mn = b.min(s);
1335 let excess = (b - s).abs();
1336 sum_min += mn;
1337 sum_excess += excess;
1338 if (b - s).abs() > 1e-9 {
1339 event_days += 1;
1340 }
1341 }
1342
1343 let epsilon_b = sum_min / nf;
1344 let epsilon_s = epsilon_b;
1345 let mu = sum_excess / nf;
1346 let alpha = event_days as f64 / nf;
1347
1348 Self { alpha, mu, epsilon_b, epsilon_s }
1349 }
1350
1351 pub fn pin_probability(&self) -> f64 {
1357 let numerator = self.alpha * self.mu;
1358 let denominator = numerator + self.epsilon_b + self.epsilon_s;
1359 if denominator == 0.0 {
1360 0.0
1361 } else {
1362 numerator / denominator
1363 }
1364 }
1365}