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