Skip to main content

nautilus_indicators/momentum/
ichimoku.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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
16//! Ichimoku Cloud (Kinko Hyo) indicator.
17
18use std::fmt::Display;
19
20use arraydeque::{ArrayDeque, Wrapping};
21use nautilus_model::data::Bar;
22
23use crate::indicator::Indicator;
24
25const MAX_PERIOD: usize = 128;
26const MAX_DISPLACEMENT: usize = 64;
27
28#[repr(C)]
29#[derive(Debug)]
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
33)]
34#[cfg_attr(
35    feature = "python",
36    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
37)]
38pub struct IchimokuCloud {
39    pub tenkan_period: usize,
40    pub kijun_period: usize,
41    pub senkou_period: usize,
42    pub displacement: usize,
43    pub tenkan_sen: f64,
44    pub kijun_sen: f64,
45    pub senkou_span_a: f64,
46    pub senkou_span_b: f64,
47    pub chikou_span: f64,
48    pub initialized: bool,
49    has_inputs: bool,
50    highs: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
51    lows: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
52    senkou_a: ArrayDeque<f64, MAX_DISPLACEMENT, Wrapping>,
53    senkou_b: ArrayDeque<f64, MAX_DISPLACEMENT, Wrapping>,
54    chikou: ArrayDeque<f64, MAX_DISPLACEMENT, Wrapping>,
55}
56
57impl Display for IchimokuCloud {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(
60            f,
61            "{}({},{},{},{})",
62            self.name(),
63            self.tenkan_period,
64            self.kijun_period,
65            self.senkou_period,
66            self.displacement,
67        )
68    }
69}
70
71impl Indicator for IchimokuCloud {
72    fn name(&self) -> String {
73        stringify!(IchimokuCloud).to_string()
74    }
75
76    fn has_inputs(&self) -> bool {
77        self.has_inputs
78    }
79
80    fn initialized(&self) -> bool {
81        self.initialized
82    }
83
84    fn handle_bar(&mut self, bar: &Bar) {
85        self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
86    }
87
88    fn reset(&mut self) {
89        self.highs.clear();
90        self.lows.clear();
91        self.senkou_a.clear();
92        self.senkou_b.clear();
93        self.chikou.clear();
94        self.tenkan_sen = 0.0;
95        self.kijun_sen = 0.0;
96        self.senkou_span_a = 0.0;
97        self.senkou_span_b = 0.0;
98        self.chikou_span = 0.0;
99        self.has_inputs = false;
100        self.initialized = false;
101    }
102}
103
104impl IchimokuCloud {
105    /// Creates a new [`IchimokuCloud`] instance.
106    ///
107    /// The indicator becomes `initialized` after `senkou_period` bars,
108    /// at which point `tenkan_sen` and `kijun_sen` are valid. The displaced
109    /// outputs (`senkou_span_a`, `senkou_span_b`, `chikou_span`) require an
110    /// additional `displacement` bars before they become non-zero.
111    ///
112    /// # Panics
113    ///
114    /// Panics if periods are invalid: `tenkan_period` and others must be positive,
115    /// `kijun_period >= tenkan_period`, `senkou_period >= kijun_period`,
116    /// and all within allowed maximums.
117    #[must_use]
118    pub fn new(
119        tenkan_period: usize,
120        kijun_period: usize,
121        senkou_period: usize,
122        displacement: usize,
123    ) -> Self {
124        assert!(
125            tenkan_period > 0 && tenkan_period <= MAX_PERIOD,
126            "IchimokuCloud: tenkan_period must be in 1..={MAX_PERIOD}"
127        );
128        assert!(
129            kijun_period > 0 && kijun_period <= MAX_PERIOD,
130            "IchimokuCloud: kijun_period must be in 1..={MAX_PERIOD}"
131        );
132        assert!(
133            senkou_period > 0 && senkou_period <= MAX_PERIOD,
134            "IchimokuCloud: senkou_period must be in 1..={MAX_PERIOD}"
135        );
136        assert!(
137            displacement > 0 && displacement <= MAX_DISPLACEMENT,
138            "IchimokuCloud: displacement must be in 1..={MAX_DISPLACEMENT}"
139        );
140        assert!(
141            kijun_period >= tenkan_period,
142            "IchimokuCloud: kijun_period must be >= tenkan_period"
143        );
144        assert!(
145            senkou_period >= kijun_period,
146            "IchimokuCloud: senkou_period must be >= kijun_period"
147        );
148
149        Self {
150            tenkan_period,
151            kijun_period,
152            senkou_period,
153            displacement,
154            tenkan_sen: 0.0,
155            kijun_sen: 0.0,
156            senkou_span_a: 0.0,
157            senkou_span_b: 0.0,
158            chikou_span: 0.0,
159            initialized: false,
160            has_inputs: false,
161            highs: ArrayDeque::new(),
162            lows: ArrayDeque::new(),
163            senkou_a: ArrayDeque::new(),
164            senkou_b: ArrayDeque::new(),
165            chikou: ArrayDeque::new(),
166        }
167    }
168
169    /// Updates the indicator with OHLC values.
170    pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
171        let _ = self.highs.push_back(high);
172        let _ = self.lows.push_back(low);
173
174        if !self.initialized {
175            self.has_inputs = true;
176            let n = self.highs.len();
177            if n >= self.tenkan_period && n >= self.kijun_period && n >= self.senkou_period {
178                self.initialized = true;
179            }
180        }
181
182        self.tenkan_sen = Self::midpoint_over(&self.highs, &self.lows, self.tenkan_period);
183        self.kijun_sen = Self::midpoint_over(&self.highs, &self.lows, self.kijun_period);
184        let mid52 = Self::midpoint_over(&self.highs, &self.lows, self.senkou_period);
185
186        if self.initialized {
187            if self.senkou_a.len() == self.displacement {
188                self.senkou_span_a = self.senkou_a.pop_front().unwrap_or(0.0);
189            }
190            let _ = self
191                .senkou_a
192                .push_back((self.tenkan_sen + self.kijun_sen) / 2.0);
193
194            if self.senkou_b.len() == self.displacement {
195                self.senkou_span_b = self.senkou_b.pop_front().unwrap_or(0.0);
196            }
197            let _ = self.senkou_b.push_back(mid52);
198
199            if self.chikou.len() == self.displacement {
200                self.chikou_span = self.chikou.pop_front().unwrap_or(0.0);
201            }
202            let _ = self.chikou.push_back(close);
203        }
204    }
205
206    fn midpoint_over(
207        highs: &ArrayDeque<f64, MAX_PERIOD, Wrapping>,
208        lows: &ArrayDeque<f64, MAX_PERIOD, Wrapping>,
209        period: usize,
210    ) -> f64 {
211        if highs.len() < period || lows.len() < period {
212            return 0.0;
213        }
214        let high_max = highs
215            .iter()
216            .rev()
217            .take(period)
218            .copied()
219            .fold(f64::NEG_INFINITY, f64::max);
220        let low_min = lows
221            .iter()
222            .rev()
223            .take(period)
224            .copied()
225            .fold(f64::INFINITY, f64::min);
226        (high_max + low_min) / 2.0
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use rstest::{fixture, rstest};
233
234    use super::*;
235    use crate::indicator::Indicator;
236
237    #[fixture]
238    fn ich_default() -> IchimokuCloud {
239        IchimokuCloud::new(9, 26, 52, 26)
240    }
241
242    #[rstest]
243    fn test_name(ich_default: IchimokuCloud) {
244        assert_eq!(ich_default.name(), "IchimokuCloud");
245    }
246
247    #[rstest]
248    fn test_display(ich_default: IchimokuCloud) {
249        assert_eq!(format!("{ich_default}"), "IchimokuCloud(9,26,52,26)");
250    }
251
252    #[rstest]
253    fn test_initialized_without_inputs(ich_default: IchimokuCloud) {
254        assert!(!ich_default.initialized());
255        assert!(!ich_default.has_inputs());
256    }
257
258    #[rstest]
259    fn test_tenkan_after_nine_bars(mut ich_default: IchimokuCloud) {
260        for _ in 0..9 {
261            ich_default.update_raw(12.0, 8.0, 10.0);
262        }
263        assert_eq!(ich_default.tenkan_sen, 10.0);
264    }
265
266    #[rstest]
267    fn test_kijun_after_twenty_six_bars(mut ich_default: IchimokuCloud) {
268        for _ in 0..26 {
269            ich_default.update_raw(12.0, 8.0, 10.0);
270        }
271        assert_eq!(ich_default.kijun_sen, 10.0);
272    }
273
274    #[rstest]
275    fn test_initialized_after_fifty_two_bars(mut ich_default: IchimokuCloud) {
276        for _ in 0..52 {
277            ich_default.update_raw(10.0, 8.0, 9.0);
278        }
279        assert!(ich_default.initialized());
280    }
281
282    #[rstest]
283    fn test_senkou_chikou_after_displacement_bars(mut ich_default: IchimokuCloud) {
284        for _ in 0..(52 + 26) {
285            ich_default.update_raw(12.0, 8.0, 10.0);
286        }
287        assert_eq!(ich_default.senkou_span_a, 10.0);
288        assert_eq!(ich_default.senkou_span_b, 10.0);
289        assert_eq!(ich_default.chikou_span, 10.0);
290    }
291
292    #[rstest]
293    fn test_reset(mut ich_default: IchimokuCloud) {
294        for _ in 0..20 {
295            ich_default.update_raw(10.0, 8.0, 9.0);
296        }
297        ich_default.reset();
298        assert!(!ich_default.initialized());
299        assert_eq!(ich_default.tenkan_sen, 0.0);
300        assert_eq!(ich_default.kijun_sen, 0.0);
301        assert_eq!(ich_default.senkou_span_a, 0.0);
302        assert_eq!(ich_default.senkou_span_b, 0.0);
303        assert_eq!(ich_default.chikou_span, 0.0);
304    }
305
306    #[rstest]
307    fn test_tenkan_sen_updates_with_varying_data() {
308        let mut ich = IchimokuCloud::new(3, 3, 3, 2);
309
310        // Fill the window: highs=[10, 12, 14], lows=[5, 6, 7]
311        ich.update_raw(10.0, 5.0, 8.0);
312        ich.update_raw(12.0, 6.0, 9.0);
313        ich.update_raw(14.0, 7.0, 10.0);
314        assert_eq!(ich.tenkan_sen, (14.0 + 5.0) / 2.0); // 9.5
315
316        // Push a new bar that evicts the (10, 5) pair: highs=[12, 14, 8], lows=[6, 7, 3]
317        ich.update_raw(8.0, 3.0, 6.0);
318        assert_eq!(ich.tenkan_sen, (14.0 + 3.0) / 2.0); // 8.5
319
320        // Push another bar that evicts the (12, 6) pair: highs=[14, 8, 20], lows=[7, 3, 4]
321        ich.update_raw(20.0, 4.0, 12.0);
322        assert_eq!(ich.tenkan_sen, (20.0 + 3.0) / 2.0); // 11.5
323    }
324
325    #[rstest]
326    #[should_panic(expected = "kijun_period must be >= tenkan_period")]
327    fn test_new_panics_invalid_kijun() {
328        let _ = IchimokuCloud::new(9, 5, 52, 26);
329    }
330
331    #[rstest]
332    #[should_panic(expected = "senkou_period must be >= kijun_period")]
333    fn test_new_panics_invalid_senkou() {
334        let _ = IchimokuCloud::new(9, 26, 20, 26);
335    }
336
337    #[rstest]
338    #[should_panic(expected = "displacement must be in 1..=")]
339    fn test_new_panics_invalid_displacement() {
340        let _ = IchimokuCloud::new(9, 26, 52, 0);
341    }
342
343    #[rstest]
344    fn test_custom_periods_initialization() {
345        let mut ich = IchimokuCloud::new(5, 10, 20, 10);
346        assert_eq!(ich.tenkan_period, 5);
347        assert_eq!(ich.kijun_period, 10);
348        assert_eq!(ich.senkou_period, 20);
349        assert_eq!(ich.displacement, 10);
350        for _ in 0..20 {
351            ich.update_raw(1.0, 1.0, 1.0);
352        }
353        assert!(ich.initialized());
354        assert_eq!(ich.tenkan_sen, 1.0);
355        assert_eq!(ich.kijun_sen, 1.0);
356    }
357}