Skip to main content

finance_query/indicators/
ichimoku.rs

1//! Ichimoku Cloud indicator.
2
3use std::collections::VecDeque;
4
5use super::{IndicatorError, Result};
6use serde::{Deserialize, Serialize};
7
8/// Result of Ichimoku Cloud calculation
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct IchimokuResult {
11    /// Conversion Line (Tenkan-sen)
12    pub conversion_line: Vec<Option<f64>>,
13    /// Base Line (Kijun-sen)
14    pub base_line: Vec<Option<f64>>,
15    /// Leading Span A (Senkou Span A)
16    pub leading_span_a: Vec<Option<f64>>,
17    /// Leading Span B (Senkou Span B)
18    pub leading_span_b: Vec<Option<f64>>,
19    /// Lagging Span (Chikou Span)
20    pub lagging_span: Vec<Option<f64>>,
21}
22
23/// Calculate Ichimoku Cloud.
24///
25/// Returns all five Ichimoku lines. Leading Span B uses `2 * base` bars.
26///
27/// # Arguments
28///
29/// * `highs` - High prices
30/// * `lows` - Low prices
31/// * `closes` - Close prices
32/// * `conversion` - Conversion line (Tenkan-sen) period (default: 9)
33/// * `base` - Base line (Kijun-sen) period; also controls cloud displacement (default: 26)
34/// * `lagging` - Lagging span (Chikou Span) back-displacement in bars (default: 26)
35/// * `displacement` - Cloud forward displacement in bars (default: 26)
36///
37/// # Example
38///
39/// ```
40/// use finance_query::indicators::ichimoku;
41///
42/// let highs = vec![10.0; 100];
43/// let lows = vec![8.0; 100];
44/// let closes = vec![9.0; 100];
45/// let result = ichimoku(&highs, &lows, &closes, 9, 26, 26, 26).unwrap();
46/// ```
47pub fn ichimoku(
48    highs: &[f64],
49    lows: &[f64],
50    closes: &[f64],
51    conversion: usize,
52    base: usize,
53    lagging: usize,
54    displacement: usize,
55) -> Result<IchimokuResult> {
56    if conversion == 0 || base == 0 || lagging == 0 || displacement == 0 {
57        return Err(IndicatorError::InvalidPeriod(
58            "All periods must be greater than 0".to_string(),
59        ));
60    }
61    let len = highs.len();
62    if lows.len() != len || closes.len() != len {
63        return Err(IndicatorError::InvalidPeriod(
64            "Data lengths must match".to_string(),
65        ));
66    }
67    let span_b_period = 2 * base;
68    let need = span_b_period.max(lagging);
69    if len < need {
70        return Err(IndicatorError::InsufficientData { need, got: len });
71    }
72
73    let mut conversion_line = vec![None; len];
74    let mut base_line = vec![None; len];
75    let mut leading_span_a = vec![None; len];
76    let mut leading_span_b = vec![None; len];
77    let mut lagging_span = vec![None; len];
78
79    // Single pass with 6 inline deques — loads highs[i]/lows[i] once per iteration.
80    let conv_off = conversion - 1;
81    let base_off = base - 1;
82    let span_b_off = span_b_period - 1;
83    let mut conv_max: VecDeque<usize> = VecDeque::new();
84    let mut conv_min: VecDeque<usize> = VecDeque::new();
85    let mut base_max: VecDeque<usize> = VecDeque::new();
86    let mut base_min: VecDeque<usize> = VecDeque::new();
87    let mut sb_max: VecDeque<usize> = VecDeque::new();
88    let mut sb_min: VecDeque<usize> = VecDeque::new();
89
90    for i in 0..len {
91        let hi = highs[i];
92        let lo = lows[i];
93
94        // Evict fronts
95        while conv_max.front().is_some_and(|&j| j + conversion <= i) {
96            conv_max.pop_front();
97        }
98        while conv_min.front().is_some_and(|&j| j + conversion <= i) {
99            conv_min.pop_front();
100        }
101        while base_max.front().is_some_and(|&j| j + base <= i) {
102            base_max.pop_front();
103        }
104        while base_min.front().is_some_and(|&j| j + base <= i) {
105            base_min.pop_front();
106        }
107        while sb_max.front().is_some_and(|&j| j + span_b_period <= i) {
108            sb_max.pop_front();
109        }
110        while sb_min.front().is_some_and(|&j| j + span_b_period <= i) {
111            sb_min.pop_front();
112        }
113
114        // Maintain monotonic backs
115        while conv_max.back().is_some_and(|&j| highs[j] <= hi) {
116            conv_max.pop_back();
117        }
118        while conv_min.back().is_some_and(|&j| lows[j] >= lo) {
119            conv_min.pop_back();
120        }
121        while base_max.back().is_some_and(|&j| highs[j] <= hi) {
122            base_max.pop_back();
123        }
124        while base_min.back().is_some_and(|&j| lows[j] >= lo) {
125            base_min.pop_back();
126        }
127        while sb_max.back().is_some_and(|&j| highs[j] <= hi) {
128            sb_max.pop_back();
129        }
130        while sb_min.back().is_some_and(|&j| lows[j] >= lo) {
131            sb_min.pop_back();
132        }
133
134        conv_max.push_back(i);
135        conv_min.push_back(i);
136        base_max.push_back(i);
137        base_min.push_back(i);
138        sb_max.push_back(i);
139        sb_min.push_back(i);
140
141        let conv_val = if i >= conv_off {
142            let cv = (highs[*conv_max.front().unwrap()] + lows[*conv_min.front().unwrap()]) / 2.0;
143            conversion_line[i] = Some(cv);
144            Some(cv)
145        } else {
146            None
147        };
148
149        if i >= base_off {
150            let bv = (highs[*base_max.front().unwrap()] + lows[*base_min.front().unwrap()]) / 2.0;
151            base_line[i] = Some(bv);
152            if let Some(cv) = conv_val
153                && i + displacement < len
154            {
155                leading_span_a[i + displacement] = Some((cv + bv) / 2.0);
156            }
157        }
158
159        if i >= span_b_off && i + displacement < len {
160            let bv = (highs[*sb_max.front().unwrap()] + lows[*sb_min.front().unwrap()]) / 2.0;
161            leading_span_b[i + displacement] = Some(bv);
162        }
163
164        if i >= lagging {
165            lagging_span[i - lagging] = Some(closes[i]);
166        }
167    }
168
169    Ok(IchimokuResult {
170        conversion_line,
171        base_line,
172        leading_span_a,
173        leading_span_b,
174        lagging_span,
175    })
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_ichimoku_defaults() {
184        let highs = vec![10.0; 100];
185        let lows = vec![8.0; 100];
186        let closes = vec![9.0; 100];
187        let result = ichimoku(&highs, &lows, &closes, 9, 26, 26, 26).unwrap();
188
189        assert_eq!(result.conversion_line.len(), 100);
190        assert!(result.conversion_line[8].is_some());
191        assert!(result.base_line[25].is_some());
192        assert!(result.leading_span_a[51].is_some()); // 25 + 26
193        assert!(result.leading_span_b[77].is_some()); // 51 + 26
194        assert!(result.lagging_span[0].is_some()); // 26 - 26
195    }
196
197    #[test]
198    fn test_ichimoku_custom_periods() {
199        let highs = vec![10.0; 100];
200        let lows = vec![8.0; 100];
201        let closes = vec![9.0; 100];
202        // Custom: conversion=5, base=13, lagging=13, displacement=13
203        let result = ichimoku(&highs, &lows, &closes, 5, 13, 13, 13).unwrap();
204        assert!(result.conversion_line[4].is_some());
205        assert!(result.base_line[12].is_some());
206    }
207
208    #[test]
209    fn test_ichimoku_custom_produces_different_output() {
210        let highs: Vec<f64> = (1..=100).map(|i| i as f64 + 1.0).collect();
211        let lows: Vec<f64> = (1..=100).map(|i| i as f64 - 1.0).collect();
212        let closes: Vec<f64> = (1..=100).map(|i| i as f64).collect();
213        let default = ichimoku(&highs, &lows, &closes, 9, 26, 26, 26).unwrap();
214        let custom = ichimoku(&highs, &lows, &closes, 5, 13, 13, 13).unwrap();
215        let idx = 30;
216        assert!(default.conversion_line[idx].is_some());
217        assert!(custom.conversion_line[idx].is_some());
218        assert_ne!(default.conversion_line[idx], custom.conversion_line[idx]);
219    }
220}