Skip to main content

quantwave_core/indicators/
ichimoku.rs

1use crate::indicators::donchian::DonchianChannels;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4
5/// Ichimoku Cloud (Ichimoku Kinko Hyo)
6/// Outputs: (Tenkan-sen, Kijun-sen, Senkou Span A, Senkou Span B)
7/// The lagging span (Chikou Span) is simply the close price.
8/// Note: Senkou Span A and B are meant to be plotted 26 periods ahead in the future.
9/// This implementation returns the values calculated at the current bar, without applying the future offset.
10#[derive(Debug, Clone)]
11pub struct IchimokuCloud {
12    tenkan_dc: DonchianChannels,
13    kijun_dc: DonchianChannels,
14    senkou_b_dc: DonchianChannels,
15}
16
17impl IchimokuCloud {
18    pub fn new(tenkan_period: usize, kijun_period: usize, senkou_b_period: usize) -> Self {
19        Self {
20            tenkan_dc: DonchianChannels::new(tenkan_period),
21            kijun_dc: DonchianChannels::new(kijun_period),
22            senkou_b_dc: DonchianChannels::new(senkou_b_period),
23        }
24    }
25}
26
27impl Next<(f64, f64)> for IchimokuCloud {
28    type Output = (f64, f64, f64, f64); // (Tenkan, Kijun, Senkou A, Senkou B)
29
30    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
31        let (_, tenkan, _) = self.tenkan_dc.next((high, low));
32        let (_, kijun, _) = self.kijun_dc.next((high, low));
33        let (_, senkou_b, _) = self.senkou_b_dc.next((high, low));
34
35        let senkou_a = (tenkan + kijun) / 2.0;
36
37        (tenkan, kijun, senkou_a, senkou_b)
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use proptest::prelude::*;
45    use serde::Deserialize;
46    use std::fs;
47    use std::path::Path;
48
49    #[derive(Debug, Deserialize)]
50    struct IchimokuCase {
51        high: Vec<f64>,
52        low: Vec<f64>,
53        expected_tenkan: Vec<f64>,
54        expected_kijun: Vec<f64>,
55        expected_senkou_a: Vec<f64>,
56        expected_senkou_b: Vec<f64>,
57    }
58
59    #[test]
60    fn test_ichimoku_gold_standard() {
61        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
62        let manifest_path = Path::new(&manifest_dir);
63        let path = manifest_path.join("tests/gold_standard/ichimoku.json");
64        let path = if path.exists() {
65            path
66        } else {
67            manifest_path
68                .parent()
69                .unwrap()
70                .join("tests/gold_standard/ichimoku.json")
71        };
72        let content = fs::read_to_string(path).unwrap();
73        let case: IchimokuCase = serde_json::from_str(&content).unwrap();
74
75        let mut ic = IchimokuCloud::new(9, 26, 52);
76        for i in 0..case.high.len() {
77            let (t, k, sa, sb) = ic.next((case.high[i], case.low[i]));
78            approx::assert_relative_eq!(t, case.expected_tenkan[i], epsilon = 1e-6);
79            approx::assert_relative_eq!(k, case.expected_kijun[i], epsilon = 1e-6);
80            approx::assert_relative_eq!(sa, case.expected_senkou_a[i], epsilon = 1e-6);
81            approx::assert_relative_eq!(sb, case.expected_senkou_b[i], epsilon = 1e-6);
82        }
83    }
84
85    fn ichimoku_batch(
86        data: Vec<(f64, f64)>,
87        p1: usize,
88        p2: usize,
89        p3: usize,
90    ) -> Vec<(f64, f64, f64, f64)> {
91        let mut ic = IchimokuCloud::new(p1, p2, p3);
92        data.into_iter().map(|x| ic.next(x)).collect()
93    }
94
95    proptest! {
96        #[test]
97        fn test_ichimoku_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0), 1..100)) {
98            let mut adj_input = Vec::with_capacity(input.len());
99            for (h, l) in input {
100                let h_f: f64 = h;
101                let l_f: f64 = l;
102                let high = h_f.max(l_f);
103                let low = l_f.min(h_f);
104                adj_input.push((high, low));
105            }
106
107            let p1 = 9;
108            let p2 = 26;
109            let p3 = 52;
110            let mut ic = IchimokuCloud::new(p1, p2, p3);
111            let mut streaming_results = Vec::with_capacity(adj_input.len());
112            for &val in &adj_input {
113                streaming_results.push(ic.next(val));
114            }
115
116            let batch_results = ichimoku_batch(adj_input, p1, p2, p3);
117
118            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
119                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
120                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
121                approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-6);
122                approx::assert_relative_eq!(s.3, b.3, epsilon = 1e-6);
123            }
124        }
125    }
126
127    #[test]
128    fn test_ichimoku_basic() {
129        let mut ic = IchimokuCloud::new(3, 5, 10);
130        let (t, k, sa, sb) = ic.next((10.0, 8.0));
131        assert_eq!(t, 9.0);
132        assert_eq!(k, 9.0);
133        assert_eq!(sa, 9.0);
134        assert_eq!(sb, 9.0);
135    }
136}
137
138pub const ICHIMOKU_METADATA: IndicatorMetadata = IndicatorMetadata {
139    name: "Ichimoku Cloud",
140    description: "Ichimoku Kinko Hyo is a comprehensive indicator that defines support and resistance, identifies trend direction, gauges momentum and provides trading signals.",
141    params: &[
142        ParamDef {
143            name: "tenkan_period",
144            default: "9",
145            description: "Tenkan-sen period",
146        },
147        ParamDef {
148            name: "kijun_period",
149            default: "26",
150            description: "Kijun-sen period",
151        },
152        ParamDef {
153            name: "senkou_span_b_period",
154            default: "52",
155            description: "Senkou Span B period",
156        },
157    ],
158    formula_source: "https://www.investopedia.com/terms/i/ichimoku-cloud.asp",
159    formula_latex: r#"
160\[
161\text{Tenkan-sen} = \frac{\text{Highest High} + \text{Lowest Low}}{2} \text{ for past 9 periods}
162\]
163"#,
164    gold_standard_file: "ichimoku.json",
165    category: "Classic",
166};