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