nautilus_indicators/momentum/
stochastics.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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::fmt::Display;
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::data::Bar;
20
21use crate::indicator::Indicator;
22
23const MAX_PERIOD: usize = 1_024;
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28    feature = "python",
29    pyo3::pyclass(module = "posei_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: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
39    lows: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
40    c_sub_1: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
41    h_sub_l: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
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    ///
82    /// # Panics
83    ///
84    /// This function panics if:
85    /// - `period_k` or `period_d` is less than 1 or greater than `MAX_PERIOD`.
86    #[must_use]
87    pub fn new(period_k: usize, period_d: usize) -> Self {
88        assert!(
89            period_k > 0 && period_k <= MAX_PERIOD,
90            "Stochastics: period_k {period_k} exceeds bounds (1..={MAX_PERIOD})"
91        );
92        assert!(
93            period_d > 0 && period_d <= MAX_PERIOD,
94            "Stochastics: period_d {period_d} exceeds bounds (1..={MAX_PERIOD})"
95        );
96
97        Self {
98            period_k,
99            period_d,
100            has_inputs: false,
101            initialized: false,
102            value_k: 0.0,
103            value_d: 0.0,
104            highs: ArrayDeque::new(),
105            lows: ArrayDeque::new(),
106            h_sub_l: ArrayDeque::new(),
107            c_sub_1: ArrayDeque::new(),
108        }
109    }
110
111    pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
112        if !self.has_inputs {
113            self.has_inputs = true;
114        }
115
116        if self.highs.len() == self.period_k {
117            self.highs.pop_front();
118            self.lows.pop_front();
119        }
120        let _ = self.highs.push_back(high);
121        let _ = self.lows.push_back(low);
122
123        if !self.initialized
124            && self.highs.len() == self.period_k
125            && self.lows.len() == self.period_k
126        {
127            self.initialized = true;
128        }
129
130        let k_max_high = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
131        let k_min_low = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
132
133        if self.c_sub_1.len() == self.period_d {
134            self.c_sub_1.pop_front();
135            self.h_sub_l.pop_front();
136        }
137        let _ = self.c_sub_1.push_back(close - k_min_low);
138        let _ = self.h_sub_l.push_back(k_max_high - k_min_low);
139
140        if k_max_high == k_min_low {
141            return;
142        }
143
144        self.value_k = 100.0 * ((close - k_min_low) / (k_max_high - k_min_low));
145        self.value_d =
146            100.0 * (self.c_sub_1.iter().sum::<f64>() / self.h_sub_l.iter().sum::<f64>());
147    }
148}
149
150////////////////////////////////////////////////////////////////////////////////
151// Tests
152////////////////////////////////////////////////////////////////////////////////
153#[cfg(test)]
154mod tests {
155    use nautilus_model::data::Bar;
156    use rstest::rstest;
157
158    use crate::{
159        indicator::Indicator,
160        momentum::stochastics::Stochastics,
161        stubs::{bar_ethusdt_binance_minute_bid, stochastics_10},
162    };
163
164    #[rstest]
165    fn test_stochastics_initialized(stochastics_10: Stochastics) {
166        let display_str = format!("{stochastics_10}");
167        assert_eq!(display_str, "Stochastics(10,10)");
168        assert_eq!(stochastics_10.period_d, 10);
169        assert_eq!(stochastics_10.period_k, 10);
170        assert!(!stochastics_10.initialized);
171        assert!(!stochastics_10.has_inputs);
172    }
173
174    #[rstest]
175    fn test_value_with_one_input(mut stochastics_10: Stochastics) {
176        stochastics_10.update_raw(1.0, 1.0, 1.0);
177        assert_eq!(stochastics_10.value_d, 0.0);
178        assert_eq!(stochastics_10.value_k, 0.0);
179    }
180
181    #[rstest]
182    fn test_value_with_three_inputs(mut stochastics_10: Stochastics) {
183        stochastics_10.update_raw(1.0, 1.0, 1.0);
184        stochastics_10.update_raw(2.0, 2.0, 2.0);
185        stochastics_10.update_raw(3.0, 3.0, 3.0);
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_value_with_ten_inputs(mut stochastics_10: Stochastics) {
192        let high_values = [
193            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,
194        ];
195        let low_values = [
196            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,
197        ];
198        let close_values = [
199            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,
200        ];
201
202        for i in 0..15 {
203            stochastics_10.update_raw(high_values[i], low_values[i], close_values[i]);
204        }
205
206        assert!(stochastics_10.initialized());
207        assert_eq!(stochastics_10.value_d, 100.0);
208        assert_eq!(stochastics_10.value_k, 100.0);
209    }
210
211    #[rstest]
212    fn test_initialized_with_required_input(mut stochastics_10: Stochastics) {
213        for i in 1..10 {
214            stochastics_10.update_raw(f64::from(i), f64::from(i), f64::from(i));
215        }
216        assert!(!stochastics_10.initialized);
217        stochastics_10.update_raw(10.0, 12.0, 14.0);
218        assert!(stochastics_10.initialized);
219    }
220
221    #[rstest]
222    fn test_handle_bar(mut stochastics_10: Stochastics, bar_ethusdt_binance_minute_bid: Bar) {
223        stochastics_10.handle_bar(&bar_ethusdt_binance_minute_bid);
224        assert_eq!(stochastics_10.value_d, 49.090_909_090_909_09);
225        assert_eq!(stochastics_10.value_k, 49.090_909_090_909_09);
226        assert!(stochastics_10.has_inputs);
227        assert!(!stochastics_10.initialized);
228    }
229
230    #[rstest]
231    fn test_reset(mut stochastics_10: Stochastics) {
232        stochastics_10.update_raw(1.0, 1.0, 1.0);
233        assert_eq!(stochastics_10.c_sub_1.len(), 1);
234        assert_eq!(stochastics_10.h_sub_l.len(), 1);
235
236        stochastics_10.reset();
237        assert_eq!(stochastics_10.value_d, 0.0);
238        assert_eq!(stochastics_10.value_k, 0.0);
239        assert_eq!(stochastics_10.h_sub_l.len(), 0);
240        assert_eq!(stochastics_10.c_sub_1.len(), 0);
241        assert!(!stochastics_10.has_inputs);
242        assert!(!stochastics_10.initialized);
243    }
244}