nautilus_indicators/momentum/
ichimoku.rs1use 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 #[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 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 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); ich.update_raw(8.0, 3.0, 6.0);
318 assert_eq!(ich.tenkan_sen, (14.0 + 3.0) / 2.0); ich.update_raw(20.0, 4.0, 12.0);
322 assert_eq!(ich.tenkan_sen, (20.0 + 3.0) / 2.0); }
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}