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