nautilus_indicators/momentum/
stochastics.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Creates a new [`Stochastics`] instance.
81    #[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        // Initialization logic
106        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////////////////////////////////////////////////////////////////////////////////
130// Tests
131////////////////////////////////////////////////////////////////////////////////
132#[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}