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)]
154mod tests {
155
156 use rstest::rstest;
157
158 use crate::{
159 indicator::Indicator,
160 ratio::spread_analyzer::SpreadAnalyzer,
161 stubs::{spread_analyzer_10, *},
162 };
163 #[rstest]
164 fn test_efficiency_ratio_initialized(spread_analyzer_10: SpreadAnalyzer) {
165 let display_str = format!("{spread_analyzer_10}");
166 assert_eq!(display_str, "SpreadAnalyzer(10,ETHUSDT-PERP.BINANCE)");
167 assert_eq!(spread_analyzer_10.capacity, 10);
168 assert!(!spread_analyzer_10.initialized);
169 }
170
171 #[rstest]
172 fn test_with_correct_number_of_required_inputs(mut spread_analyzer_10: SpreadAnalyzer) {
173 let bid_price: [&str; 10] = [
174 "100.50", "100.45", "100.55", "100.60", "100.52", "100.48", "100.53", "100.57",
175 "100.49", "100.51",
176 ];
177
178 let ask_price: [&str; 10] = [
179 "100.55", "100.50", "100.60", "100.65", "100.57", "100.53", "100.58", "100.62",
180 "100.54", "100.56",
181 ];
182 for i in 1..10 {
183 spread_analyzer_10.handle_quote(&stub_quote(bid_price[i], ask_price[i]));
184 }
185 assert!(!spread_analyzer_10.initialized);
186 }
187
188 #[rstest]
189 fn test_value_with_one_input(mut spread_analyzer_10: SpreadAnalyzer) {
190 spread_analyzer_10.handle_quote(&stub_quote("100.50", "100.55"));
191 assert_eq!(spread_analyzer_10.average, 0.049_999_999_999_997_16);
192 }
193
194 #[rstest]
195 fn test_value_with_all_higher_inputs_returns_expected_value(
196 mut spread_analyzer_10: SpreadAnalyzer,
197 ) {
198 let bid_price: [&str; 15] = [
199 "100.50", "100.45", "100.55", "100.60", "100.52", "100.48", "100.53", "100.57",
200 "100.49", "100.51", "100.54", "100.56", "100.58", "100.50", "100.52",
201 ];
202
203 let ask_price: [&str; 15] = [
204 "100.55", "100.50", "100.60", "100.65", "100.57", "100.53", "100.58", "100.62",
205 "100.54", "100.56", "100.59", "100.61", "100.63", "100.55", "100.57",
206 ];
207 for i in 0..10 {
208 spread_analyzer_10.handle_quote(&stub_quote(bid_price[i], ask_price[i]));
209 }
210
211 assert_eq!(spread_analyzer_10.average, 0.050_000_000_000_001_9);
212 }
213
214 #[rstest]
215 fn test_reset_successfully_returns_indicator_to_fresh_state(
216 mut spread_analyzer_10: SpreadAnalyzer,
217 ) {
218 spread_analyzer_10.handle_quote(&stub_quote("100.50", "100.55"));
219 spread_analyzer_10.reset();
220 assert!(!spread_analyzer_10.initialized());
221 assert_eq!(spread_analyzer_10.current, 0.0);
222 assert_eq!(spread_analyzer_10.average, 0.0);
223 assert!(!spread_analyzer_10.has_inputs);
224 assert!(!spread_analyzer_10.initialized);
225 }
226}