nautilus_indicators/average/
zscore.rs1use std::{collections::VecDeque, fmt::Display};
17
18use nautilus_core::correctness::FAILED;
19use nautilus_model::{
20 data::{Bar, QuoteTick, TradeTick},
21 enums::PriceType,
22};
23
24use crate::{
25 indicator::Indicator,
26 support::{MAX_PERIOD, ShiftedMoments},
27};
28
29#[repr(C)]
42#[derive(Debug)]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.indicators")
46)]
47#[cfg_attr(
48 feature = "python",
49 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
50)]
51pub struct ZScore {
52 pub period: usize,
53 pub price_type: PriceType,
54 pub value: f64,
55 pub mean: f64,
56 pub std: f64,
57 pub count: usize,
58 pub initialized: bool,
59 has_inputs: bool,
60 window: VecDeque<f64>,
61 moments: ShiftedMoments,
62}
63
64impl Display for ZScore {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}({})", self.name(), self.period)
67 }
68}
69
70impl Indicator for ZScore {
71 fn name(&self) -> String {
72 stringify!(ZScore).to_string()
73 }
74
75 fn has_inputs(&self) -> bool {
76 self.has_inputs
77 }
78
79 fn initialized(&self) -> bool {
80 self.initialized
81 }
82
83 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
84 self.update_raw(quote.extract_price(self.price_type)?.into());
85 Ok(())
86 }
87
88 fn handle_trade(&mut self, trade: &TradeTick) {
89 self.update_raw((&trade.price).into());
90 }
91
92 fn handle_bar(&mut self, bar: &Bar) {
93 self.update_raw((&bar.close).into());
94 }
95
96 fn reset(&mut self) {
97 self.window.clear();
98 self.moments.reset();
99 self.value = 0.0;
100 self.mean = 0.0;
101 self.std = 0.0;
102 self.count = 0;
103 self.has_inputs = false;
104 self.initialized = false;
105 }
106}
107
108impl ZScore {
109 #[must_use]
116 pub fn new(period: usize, price_type: Option<PriceType>) -> Self {
117 Self::new_checked(period, price_type).expect(FAILED)
118 }
119
120 pub fn new_checked(period: usize, price_type: Option<PriceType>) -> anyhow::Result<Self> {
126 anyhow::ensure!(period <= MAX_PERIOD, "period cannot exceed {MAX_PERIOD}");
127 anyhow::ensure!(period > 0, "ZScore: period must be > 0 (received {period})");
128 Ok(Self {
129 period,
130 price_type: price_type.unwrap_or(PriceType::Last),
131 value: 0.0,
132 mean: 0.0,
133 std: 0.0,
134 count: 0,
135 has_inputs: false,
136 initialized: false,
137 window: VecDeque::with_capacity(period),
138 moments: ShiftedMoments::new(),
139 })
140 }
141
142 pub fn update_raw(&mut self, value: f64) {
144 if !value.is_finite() {
145 return;
146 }
147 self.count += 1;
148 self.has_inputs = true;
149
150 if self.window.len() == self.period
151 && let Some(old) = self.window.pop_front()
152 {
153 self.moments.evict(old);
154 }
155 self.window.push_back(value);
156 self.moments.push(value);
157 if self.moments.needs_reseed(self.period) {
158 self.moments.reseed(&self.window);
159 }
160
161 if self.window.len() < self.period {
162 return;
163 }
164 self.mean = self.moments.mean(self.period);
165 self.std = self.moments.std_dev(self.period);
166 self.value = if self.std == 0.0 {
168 0.0
169 } else {
170 (value - self.mean) / self.std
171 };
172 self.initialized = true;
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use rstest::rstest;
179
180 use super::*;
181 use crate::indicator::Indicator;
182
183 #[rstest]
184 fn test_name_and_display() {
185 let indicator = ZScore::new(20, None);
186 assert_eq!(indicator.name(), "ZScore");
187 assert_eq!(format!("{indicator}"), "ZScore(20)");
188 assert_eq!(indicator.period, 20);
189 assert!(!indicator.initialized());
190 assert!(!indicator.has_inputs());
191 }
192
193 #[rstest]
194 #[should_panic(expected = "period must be > 0")]
195 fn test_zero_period_panics() {
196 let _ = ZScore::new(0, None);
197 }
198
199 #[rstest]
200 fn test_first_value_on_period_th_input() {
201 let mut indicator = ZScore::new(5, None);
202 for i in 1..5 {
203 indicator.update_raw(f64::from(i));
204 assert!(!indicator.initialized(), "initialized early at input {i}");
205 }
206 indicator.update_raw(5.0);
207 assert!(indicator.initialized());
208 }
209
210 #[rstest]
211 fn test_reference_value() {
212 let mut indicator = ZScore::new(2, None);
215 indicator.update_raw(1.0);
216 assert!(!indicator.initialized());
217 indicator.update_raw(3.0);
218 assert!(indicator.initialized());
219 assert_eq!(indicator.value, 1.0);
220 }
221
222 #[rstest]
223 fn test_constant_series_yields_zero() {
224 let mut indicator = ZScore::new(10, None);
225 for _ in 0..30 {
226 indicator.update_raw(42.0);
227 }
228 assert_eq!(indicator.value, 0.0);
229 }
230
231 #[rstest]
232 fn test_tiny_spread_far_from_previous_offset_keeps_its_deviation() {
233 let tiny = 2.0_f64.powi(-24);
237 let mut indicator = ZScore::new(2, None);
238 for value in [110.0, 100.0, 100.0 + tiny] {
239 indicator.update_raw(value);
240 }
241 assert_eq!(indicator.mean, 100.0 + tiny / 2.0);
242 assert_eq!(indicator.std, tiny / 2.0);
243 assert_eq!(indicator.value, 1.0);
244 }
245
246 #[rstest]
247 fn test_matches_naive_definition() {
248 fn naive(window: &[f64]) -> f64 {
250 let n = window.len() as f64;
251 let mean = window.iter().sum::<f64>() / n;
252 let var = window.iter().map(|y| (y - mean) * (y - mean)).sum::<f64>() / n;
253 (window[window.len() - 1] - mean) / var.sqrt()
254 }
255
256 let prices: Vec<f64> = (0..60)
257 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 10.0)
258 .collect();
259 let period = 20;
260 let mut indicator = ZScore::new(period, None);
261 let mut compared = 0_usize;
262
263 for (i, &p) in prices.iter().enumerate() {
264 indicator.update_raw(p);
265
266 if i + 1 < period {
267 continue;
268 }
269 let want = naive(&prices[i + 1 - period..=i]);
270 assert!(
271 (indicator.value - want).abs() <= 1e-12 * want.abs().max(1.0),
272 "bar {i}: got {} want {want}",
273 indicator.value
274 );
275 compared += 1;
276 }
277 assert_eq!(compared, prices.len() - period + 1);
278 }
279
280 #[rstest]
281 fn test_reset() {
282 let mut indicator = ZScore::new(5, None);
283 for i in 0..20 {
284 indicator.update_raw(f64::from(i));
285 }
286 indicator.reset();
287 assert!(!indicator.initialized());
288 assert!(!indicator.has_inputs());
289 assert_eq!(indicator.value, 0.0);
290 assert_eq!(indicator.count, 0);
291 }
292}