nautilus_indicators/momentum/
stochastics.rs1use std::{
17 collections::VecDeque,
18 fmt::{Debug, Display},
19};
20
21use nautilus_model::data::Bar;
22
23use crate::indicator::Indicator;
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28 feature = "python",
29 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
30)]
31pub struct Stochastics {
32 pub period_k: usize,
33 pub period_d: usize,
34 pub value_k: f64,
35 pub value_d: f64,
36 pub initialized: bool,
37 has_inputs: bool,
38 highs: VecDeque<f64>,
39 lows: VecDeque<f64>,
40 c_sub_1: VecDeque<f64>,
41 h_sub_l: VecDeque<f64>,
42}
43
44impl Display for Stochastics {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "{}({},{})", self.name(), self.period_k, self.period_d,)
47 }
48}
49
50impl Indicator for Stochastics {
51 fn name(&self) -> String {
52 stringify!(Stochastics).to_string()
53 }
54
55 fn has_inputs(&self) -> bool {
56 self.has_inputs
57 }
58
59 fn initialized(&self) -> bool {
60 self.initialized
61 }
62
63 fn handle_bar(&mut self, bar: &Bar) {
64 self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
65 }
66
67 fn reset(&mut self) {
68 self.highs.clear();
69 self.lows.clear();
70 self.c_sub_1.clear();
71 self.h_sub_l.clear();
72 self.value_k = 0.0;
73 self.value_d = 0.0;
74 self.has_inputs = false;
75 self.initialized = false;
76 }
77}
78
79impl Stochastics {
80 #[must_use]
82 pub fn new(period_k: usize, period_d: usize) -> Self {
83 Self {
84 period_k,
85 period_d,
86 has_inputs: false,
87 initialized: false,
88 value_k: 0.0,
89 value_d: 0.0,
90 highs: VecDeque::with_capacity(period_k),
91 lows: VecDeque::with_capacity(period_k),
92 h_sub_l: VecDeque::with_capacity(period_d),
93 c_sub_1: VecDeque::with_capacity(period_d),
94 }
95 }
96
97 pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
98 if !self.has_inputs {
99 self.has_inputs = true;
100 }
101
102 self.highs.push_back(high);
103 self.lows.push_back(low);
104
105 if !self.initialized
107 && self.highs.len() == self.period_k
108 && self.lows.len() == self.period_k
109 {
110 self.initialized = true;
111 }
112
113 let k_max_high = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
114 let k_min_low = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
115
116 self.c_sub_1.push_back(close - k_min_low);
117 self.h_sub_l.push_back(k_max_high - k_min_low);
118
119 if k_max_high == k_min_low {
120 return;
121 }
122
123 self.value_k = 100.0 * ((close - k_min_low) / (k_max_high - k_min_low));
124 self.value_d =
125 100.0 * (self.c_sub_1.iter().sum::<f64>() / self.h_sub_l.iter().sum::<f64>());
126 }
127}
128
129#[cfg(test)]
133mod tests {
134 use nautilus_model::data::Bar;
135 use rstest::rstest;
136
137 use crate::{
138 indicator::Indicator,
139 momentum::stochastics::Stochastics,
140 stubs::{bar_ethusdt_binance_minute_bid, stochastics_10},
141 };
142
143 #[rstest]
144 fn test_stochastics_initialized(stochastics_10: Stochastics) {
145 let display_str = format!("{stochastics_10}");
146 assert_eq!(display_str, "Stochastics(10,10)");
147 assert_eq!(stochastics_10.period_d, 10);
148 assert_eq!(stochastics_10.period_k, 10);
149 assert!(!stochastics_10.initialized);
150 assert!(!stochastics_10.has_inputs);
151 }
152
153 #[rstest]
154 fn test_value_with_one_input(mut stochastics_10: Stochastics) {
155 stochastics_10.update_raw(1.0, 1.0, 1.0);
156 assert_eq!(stochastics_10.value_d, 0.0);
157 assert_eq!(stochastics_10.value_k, 0.0);
158 }
159
160 #[rstest]
161 fn test_value_with_three_inputs(mut stochastics_10: Stochastics) {
162 stochastics_10.update_raw(1.0, 1.0, 1.0);
163 stochastics_10.update_raw(2.0, 2.0, 2.0);
164 stochastics_10.update_raw(3.0, 3.0, 3.0);
165 assert_eq!(stochastics_10.value_d, 100.0);
166 assert_eq!(stochastics_10.value_k, 100.0);
167 }
168
169 #[rstest]
170 fn test_value_with_ten_inputs(mut stochastics_10: Stochastics) {
171 let high_values = [
172 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
173 ];
174 let low_values = [
175 0.9, 1.9, 2.9, 3.9, 4.9, 5.9, 6.9, 7.9, 8.9, 9.9, 10.1, 10.2, 10.3, 11.1, 11.4,
176 ];
177 let close_values = [
178 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
179 ];
180
181 for i in 0..15 {
182 stochastics_10.update_raw(high_values[i], low_values[i], close_values[i]);
183 }
184
185 assert!(stochastics_10.initialized());
186 assert_eq!(stochastics_10.value_d, 100.0);
187 assert_eq!(stochastics_10.value_k, 100.0);
188 }
189
190 #[rstest]
191 fn test_initialized_with_required_input(mut stochastics_10: Stochastics) {
192 for i in 1..10 {
193 stochastics_10.update_raw(f64::from(i), f64::from(i), f64::from(i));
194 }
195 assert!(!stochastics_10.initialized);
196 stochastics_10.update_raw(10.0, 12.0, 14.0);
197 assert!(stochastics_10.initialized);
198 }
199
200 #[rstest]
201 fn test_handle_bar(mut stochastics_10: Stochastics, bar_ethusdt_binance_minute_bid: Bar) {
202 stochastics_10.handle_bar(&bar_ethusdt_binance_minute_bid);
203 assert_eq!(stochastics_10.value_d, 49.090_909_090_909_09);
204 assert_eq!(stochastics_10.value_k, 49.090_909_090_909_09);
205 assert!(stochastics_10.has_inputs);
206 assert!(!stochastics_10.initialized);
207 }
208
209 #[rstest]
210 fn test_reset(mut stochastics_10: Stochastics) {
211 stochastics_10.update_raw(1.0, 1.0, 1.0);
212 assert_eq!(stochastics_10.c_sub_1.len(), 1);
213 assert_eq!(stochastics_10.h_sub_l.len(), 1);
214
215 stochastics_10.reset();
216 assert_eq!(stochastics_10.value_d, 0.0);
217 assert_eq!(stochastics_10.value_k, 0.0);
218 assert_eq!(stochastics_10.h_sub_l.len(), 0);
219 assert_eq!(stochastics_10.c_sub_1.len(), 0);
220 assert!(!stochastics_10.has_inputs);
221 assert!(!stochastics_10.initialized);
222 }
223}