wickra_core/indicators/
effective_spread.rs1use crate::microstructure::TradeQuote;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
45pub struct EffectiveSpread {
46 has_emitted: bool,
47}
48
49impl EffectiveSpread {
50 pub const fn new() -> Self {
52 Self { has_emitted: false }
53 }
54}
55
56impl Indicator for EffectiveSpread {
57 type Input = TradeQuote;
58 type Output = f64;
59
60 #[inline]
61 fn update(&mut self, quote: TradeQuote) -> Option<f64> {
62 self.has_emitted = true;
63 let sign = quote.trade.side.sign();
64 Some(2.0 * sign * (quote.trade.price - quote.mid) / quote.mid * 10_000.0)
65 }
66
67 fn reset(&mut self) {
68 self.has_emitted = false;
69 }
70
71 #[inline]
72 fn warmup_period(&self) -> usize {
73 1
74 }
75
76 #[inline]
77 fn is_ready(&self) -> bool {
78 self.has_emitted
79 }
80
81 #[inline]
82 fn name(&self) -> &'static str {
83 "EffectiveSpread"
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::microstructure::{Side, Trade};
91 use crate::traits::BatchExt;
92
93 fn quote(price: f64, side: Side, mid: f64) -> TradeQuote {
94 TradeQuote::new(Trade::new(price, 1.0, side, 0).unwrap(), mid).unwrap()
95 }
96
97 #[test]
98 fn accessors_and_metadata() {
99 let es = EffectiveSpread::new();
100 assert_eq!(es.name(), "EffectiveSpread");
101 assert_eq!(es.warmup_period(), 1);
102 assert!(!es.is_ready());
103 }
104
105 #[test]
106 fn buy_above_mid_is_positive() {
107 let mut es = EffectiveSpread::new();
108 let out = es.update(quote(100.05, Side::Buy, 100.0)).unwrap();
110 assert!((out - 10.0).abs() < 1e-9);
111 assert!(es.is_ready());
112 }
113
114 #[test]
115 fn sell_below_mid_is_positive() {
116 let mut es = EffectiveSpread::new();
117 let out = es.update(quote(99.95, Side::Sell, 100.0)).unwrap();
119 assert!((out - 10.0).abs() < 1e-9);
120 }
121
122 #[test]
123 fn price_improvement_reads_negative() {
124 let mut es = EffectiveSpread::new();
125 let out = es.update(quote(99.95, Side::Buy, 100.0)).unwrap();
127 assert!(out < 0.0);
128 }
129
130 #[test]
131 fn trade_at_mid_is_zero() {
132 let mut es = EffectiveSpread::new();
133 assert_eq!(es.update(quote(100.0, Side::Buy, 100.0)), Some(0.0));
134 }
135
136 #[test]
137 fn batch_equals_streaming() {
138 let quotes: Vec<TradeQuote> = (0..20)
139 .map(|i| {
140 let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
141 let price = 100.0 + f64::from(i % 4) * 0.01;
142 quote(price, side, 100.0)
143 })
144 .collect();
145 let mut a = EffectiveSpread::new();
146 let mut b = EffectiveSpread::new();
147 assert_eq!(
148 a.batch("es),
149 quotes.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
150 );
151 }
152
153 #[test]
154 fn reset_clears_state() {
155 let mut es = EffectiveSpread::new();
156 es.update(quote(100.05, Side::Buy, 100.0));
157 assert!(es.is_ready());
158 es.reset();
159 assert!(!es.is_ready());
160 }
161}