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.indicators")
31)]
32#[cfg_attr(
33 feature = "python",
34 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
35)]
36pub struct SpreadAnalyzer {
37 pub capacity: usize,
38 pub instrument_id: InstrumentId,
39 pub current: f64,
40 pub average: f64,
41 pub initialized: bool,
42 has_inputs: bool,
43 spreads: Vec<f64>,
44}
45
46impl Display for SpreadAnalyzer {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 write!(
49 f,
50 "{}({},{})",
51 self.name(),
52 self.capacity,
53 self.instrument_id
54 )
55 }
56}
57
58impl Indicator for SpreadAnalyzer {
59 fn name(&self) -> String {
60 stringify!(SpreadAnalyzer).to_string()
61 }
62
63 fn has_inputs(&self) -> bool {
64 self.has_inputs
65 }
66 fn initialized(&self) -> bool {
67 self.initialized
68 }
69
70 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
71 if quote.instrument_id != self.instrument_id {
72 return Ok(());
73 }
74
75 if !self.initialized {
77 self.has_inputs = true;
78
79 if self.spreads.len() == self.capacity {
80 self.initialized = true;
81 }
82 }
83
84 let bid: f64 = quote.bid_price.into();
85 let ask: f64 = quote.ask_price.into();
86 let spread = ask - bid;
87
88 self.current = spread;
89 self.spreads.push(spread);
90
91 if self.spreads.len() > self.capacity {
94 self.spreads.remove(0);
95 }
96
97 self.average = fast_mean(&self.spreads);
103 Ok(())
104 }
105
106 fn reset(&mut self) {
107 self.current = 0.0;
108 self.average = 0.0;
109 self.spreads.clear();
110 self.initialized = false;
111 self.has_inputs = false;
112 }
113}
114
115impl SpreadAnalyzer {
116 #[must_use]
118 pub fn new(capacity: usize, instrument_id: InstrumentId) -> Self {
119 Self {
120 capacity,
121 instrument_id,
122 current: 0.0,
123 average: 0.0,
124 initialized: false,
125 has_inputs: false,
126 spreads: Vec::with_capacity(capacity),
127 }
128 }
129}
130
131fn fast_mean(values: &[f64]) -> f64 {
132 if values.is_empty() {
133 0.0
134 } else {
135 values.iter().sum::<f64>() / values.len() as f64
136 }
137}
138
139#[cfg(test)]
140mod tests {
141
142 use rstest::rstest;
143
144 use crate::{
145 indicator::Indicator,
146 ratio::spread_analyzer::SpreadAnalyzer,
147 stubs::{spread_analyzer_10, *},
148 testing::assert_approx_equal,
149 };
150 #[rstest]
151 fn test_efficiency_ratio_initialized(spread_analyzer_10: SpreadAnalyzer) {
152 let display_str = format!("{spread_analyzer_10}");
153 assert_eq!(display_str, "SpreadAnalyzer(10,ETHUSDT-PERP.BINANCE)");
154 assert_eq!(spread_analyzer_10.capacity, 10);
155 assert!(!spread_analyzer_10.initialized);
156 }
157
158 #[rstest]
159 fn test_with_correct_number_of_required_inputs(mut spread_analyzer_10: SpreadAnalyzer) {
160 let bid_price: [&str; 10] = [
161 "100.50", "100.45", "100.55", "100.60", "100.52", "100.48", "100.53", "100.57",
162 "100.49", "100.51",
163 ];
164
165 let ask_price: [&str; 10] = [
166 "100.55", "100.50", "100.60", "100.65", "100.57", "100.53", "100.58", "100.62",
167 "100.54", "100.56",
168 ];
169
170 for i in 1..10 {
171 spread_analyzer_10
172 .handle_quote(&stub_quote(bid_price[i], ask_price[i]))
173 .unwrap();
174 }
175 assert!(!spread_analyzer_10.initialized);
176 }
177
178 #[rstest]
179 fn test_value_with_one_input(mut spread_analyzer_10: SpreadAnalyzer) {
180 spread_analyzer_10
181 .handle_quote(&stub_quote("100.50", "100.55"))
182 .unwrap();
183 assert_approx_equal(spread_analyzer_10.average, 0.05);
184 }
185
186 #[rstest]
187 fn test_value_with_all_higher_inputs_returns_expected_value(
188 mut spread_analyzer_10: SpreadAnalyzer,
189 ) {
190 let bid_price: [&str; 15] = [
191 "100.50", "100.45", "100.55", "100.60", "100.52", "100.48", "100.53", "100.57",
192 "100.49", "100.51", "100.54", "100.56", "100.58", "100.50", "100.52",
193 ];
194
195 let ask_price: [&str; 15] = [
196 "100.55", "100.50", "100.60", "100.65", "100.57", "100.53", "100.58", "100.62",
197 "100.54", "100.56", "100.59", "100.61", "100.63", "100.55", "100.57",
198 ];
199
200 for i in 0..10 {
201 spread_analyzer_10
202 .handle_quote(&stub_quote(bid_price[i], ask_price[i]))
203 .unwrap();
204 }
205
206 assert_approx_equal(spread_analyzer_10.average, 0.05);
207 }
208
209 #[rstest]
210 fn test_handles_more_inputs_than_capacity_without_panic(
211 mut spread_analyzer_10: SpreadAnalyzer,
212 ) {
213 let bid_price: [&str; 15] = [
217 "100.50", "100.45", "100.55", "100.60", "100.52", "100.48", "100.53", "100.57",
218 "100.49", "100.51", "100.54", "100.56", "100.58", "100.50", "100.52",
219 ];
220
221 let ask_price: [&str; 15] = [
222 "100.55", "100.50", "100.60", "100.65", "100.57", "100.53", "100.58", "100.62",
223 "100.54", "100.56", "100.59", "100.61", "100.63", "100.55", "100.57",
224 ];
225
226 for i in 0..15 {
227 spread_analyzer_10
228 .handle_quote(&stub_quote(bid_price[i], ask_price[i]))
229 .unwrap();
230 }
231
232 assert!(spread_analyzer_10.initialized());
233 assert_eq!(spread_analyzer_10.spreads.len(), 10);
234 assert!((spread_analyzer_10.average - 0.05).abs() < 1e-9);
235 }
236
237 #[rstest]
238 fn test_average_tracks_varying_spreads_past_capacity(mut spread_analyzer_10: SpreadAnalyzer) {
239 let bid_price: [&str; 15] = ["100.00"; 15];
245 let ask_price: [&str; 15] = [
246 "100.01", "100.02", "100.03", "100.04", "100.05", "100.06", "100.07", "100.08",
247 "100.09", "100.10", "100.11", "100.12", "100.13", "100.14", "100.15",
248 ];
249
250 for i in 0..15 {
251 spread_analyzer_10
252 .handle_quote(&stub_quote(bid_price[i], ask_price[i]))
253 .unwrap();
254 }
255
256 assert_eq!(spread_analyzer_10.spreads.len(), 10);
257 assert!((spread_analyzer_10.average - 0.105).abs() < 1e-9);
258 }
259
260 #[rstest]
261 fn test_reset_successfully_returns_indicator_to_fresh_state(
262 mut spread_analyzer_10: SpreadAnalyzer,
263 ) {
264 spread_analyzer_10
265 .handle_quote(&stub_quote("100.50", "100.55"))
266 .unwrap();
267 spread_analyzer_10.reset();
268 assert!(!spread_analyzer_10.initialized());
269 assert_eq!(spread_analyzer_10.current, 0.0);
270 assert_eq!(spread_analyzer_10.average, 0.0);
271 assert!(!spread_analyzer_10.has_inputs);
272 assert!(!spread_analyzer_10.initialized);
273 }
274}