nautilus_indicators/ratio/
spread_analyzer.rs1use std::fmt::Display;
17
18use nautilus_model::{data::QuoteTick, identifiers::InstrumentId};
19
20use crate::indicator::Indicator;
21
22#[repr(C)]
27#[derive(Debug)]
28#[cfg_attr(
29 feature = "python",
30 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
31)]
32pub struct SpreadAnalyzer {
33 pub capacity: usize,
34 pub instrument_id: InstrumentId,
35 pub current: f64,
36 pub average: f64,
37 pub initialized: bool,
38 has_inputs: bool,
39 spreads: Vec<f64>,
40}
41
42impl Display for SpreadAnalyzer {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(
45 f,
46 "{}({},{})",
47 self.name(),
48 self.capacity,
49 self.instrument_id
50 )
51 }
52}
53
54impl Indicator for SpreadAnalyzer {
55 fn name(&self) -> String {
56 stringify!(SpreadAnalyzer).to_string()
57 }
58
59 fn has_inputs(&self) -> bool {
60 self.has_inputs
61 }
62 fn initialized(&self) -> bool {
63 self.initialized
64 }
65
66 fn handle_quote(&mut self, quote: &QuoteTick) {
67 if quote.instrument_id != self.instrument_id {
68 return;
69 }
70
71 if !self.initialized {
73 self.has_inputs = true;
74 if self.spreads.len() == self.capacity {
75 self.initialized = true;
76 }
77 }
78
79 let bid: f64 = quote.bid_price.into();
80 let ask: f64 = quote.ask_price.into();
81 let spread = ask - bid;
82
83 self.current = spread;
84 self.spreads.push(spread);
85
86 self.average =
88 fast_mean_iterated(&self.spreads, spread, self.average, self.capacity, false).unwrap();
89 }
90
91 fn reset(&mut self) {
92 self.current = 0.0;
93 self.average = 0.0;
94 self.spreads.clear();
95 self.initialized = false;
96 self.has_inputs = false;
97 }
98}
99
100impl SpreadAnalyzer {
101 #[must_use]
103 pub fn new(capacity: usize, instrument_id: InstrumentId) -> Self {
104 Self {
105 capacity,
106 instrument_id,
107 current: 0.0,
108 average: 0.0,
109 initialized: false,
110 has_inputs: false,
111 spreads: Vec::with_capacity(capacity),
112 }
113 }
114}
115
116fn fast_mean_iterated(
117 values: &[f64],
118 next_value: f64,
119 current_value: f64,
120 expected_length: usize,
121 drop_left: bool,
122) -> Result<f64, &'static str> {
123 let length = values.len();
124
125 if length < expected_length {
126 return Ok(fast_mean(values));
127 }
128
129 if length != expected_length {
130 return Err("length of values must equal expected_length");
131 }
132
133 let value_to_drop = if drop_left {
134 values[0]
135 } else {
136 values[length - 1]
137 };
138
139 Ok(current_value + (next_value - value_to_drop) / length as f64)
140}
141
142fn fast_mean(values: &[f64]) -> f64 {
143 if values.is_empty() {
144 0.0
145 } else {
146 values.iter().sum::<f64>() / values.len() as f64
147 }
148}
149
150#[cfg(test)]
151mod tests {
152
153 use rstest::rstest;
154
155 use crate::{
156 indicator::Indicator,
157 ratio::spread_analyzer::SpreadAnalyzer,
158 stubs::{spread_analyzer_10, *},
159 };
160 #[rstest]
161 fn test_efficiency_ratio_initialized(spread_analyzer_10: SpreadAnalyzer) {
162 let display_str = format!("{spread_analyzer_10}");
163 assert_eq!(display_str, "SpreadAnalyzer(10,ETHUSDT-PERP.BINANCE)");
164 assert_eq!(spread_analyzer_10.capacity, 10);
165 assert!(!spread_analyzer_10.initialized);
166 }
167
168 #[rstest]
169 fn test_with_correct_number_of_required_inputs(mut spread_analyzer_10: SpreadAnalyzer) {
170 let bid_price: [&str; 10] = [
171 "100.50", "100.45", "100.55", "100.60", "100.52", "100.48", "100.53", "100.57",
172 "100.49", "100.51",
173 ];
174
175 let ask_price: [&str; 10] = [
176 "100.55", "100.50", "100.60", "100.65", "100.57", "100.53", "100.58", "100.62",
177 "100.54", "100.56",
178 ];
179 for i in 1..10 {
180 spread_analyzer_10.handle_quote(&stub_quote(bid_price[i], ask_price[i]));
181 }
182 assert!(!spread_analyzer_10.initialized);
183 }
184
185 #[rstest]
186 fn test_value_with_one_input(mut spread_analyzer_10: SpreadAnalyzer) {
187 spread_analyzer_10.handle_quote(&stub_quote("100.50", "100.55"));
188 assert_eq!(spread_analyzer_10.average, 0.049_999_999_999_997_16);
189 }
190
191 #[rstest]
192 fn test_value_with_all_higher_inputs_returns_expected_value(
193 mut spread_analyzer_10: SpreadAnalyzer,
194 ) {
195 let bid_price: [&str; 15] = [
196 "100.50", "100.45", "100.55", "100.60", "100.52", "100.48", "100.53", "100.57",
197 "100.49", "100.51", "100.54", "100.56", "100.58", "100.50", "100.52",
198 ];
199
200 let ask_price: [&str; 15] = [
201 "100.55", "100.50", "100.60", "100.65", "100.57", "100.53", "100.58", "100.62",
202 "100.54", "100.56", "100.59", "100.61", "100.63", "100.55", "100.57",
203 ];
204 for i in 0..10 {
205 spread_analyzer_10.handle_quote(&stub_quote(bid_price[i], ask_price[i]));
206 }
207
208 assert_eq!(spread_analyzer_10.average, 0.050_000_000_000_001_9);
209 }
210
211 #[rstest]
212 fn test_reset_successfully_returns_indicator_to_fresh_state(
213 mut spread_analyzer_10: SpreadAnalyzer,
214 ) {
215 spread_analyzer_10.handle_quote(&stub_quote("100.50", "100.55"));
216 spread_analyzer_10.reset();
217 assert!(!spread_analyzer_10.initialized());
218 assert_eq!(spread_analyzer_10.current, 0.0);
219 assert_eq!(spread_analyzer_10.average, 0.0);
220 assert!(!spread_analyzer_10.has_inputs);
221 assert!(!spread_analyzer_10.initialized);
222 }
223}