Skip to main content

finance_query/indicators/
ichimoku.rs

1//! Ichimoku Cloud indicator.
2
3use super::{IndicatorError, Result};
4use serde::{Deserialize, Serialize};
5
6/// Result of Ichimoku Cloud calculation
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct IchimokuResult {
9    /// Conversion Line (Tenkan-sen)
10    pub conversion_line: Vec<Option<f64>>,
11    /// Base Line (Kijun-sen)
12    pub base_line: Vec<Option<f64>>,
13    /// Leading Span A (Senkou Span A)
14    pub leading_span_a: Vec<Option<f64>>,
15    /// Leading Span B (Senkou Span B)
16    pub leading_span_b: Vec<Option<f64>>,
17    /// Lagging Span (Chikou Span)
18    pub lagging_span: Vec<Option<f64>>,
19}
20
21/// Calculate Ichimoku Cloud.
22///
23/// Returns all five Ichimoku lines. Leading Span B uses `2 * base` bars.
24///
25/// # Arguments
26///
27/// * `highs` - High prices
28/// * `lows` - Low prices
29/// * `closes` - Close prices
30/// * `conversion` - Conversion line (Tenkan-sen) period (default: 9)
31/// * `base` - Base line (Kijun-sen) period; also controls cloud displacement (default: 26)
32/// * `lagging` - Lagging span (Chikou Span) back-displacement in bars (default: 26)
33/// * `displacement` - Cloud forward displacement in bars (default: 26)
34///
35/// # Example
36///
37/// ```
38/// use finance_query::indicators::ichimoku;
39///
40/// let highs = vec![10.0; 100];
41/// let lows = vec![8.0; 100];
42/// let closes = vec![9.0; 100];
43/// let result = ichimoku(&highs, &lows, &closes, 9, 26, 26, 26).unwrap();
44/// ```
45pub fn ichimoku(
46    highs: &[f64],
47    lows: &[f64],
48    closes: &[f64],
49    conversion: usize,
50    base: usize,
51    lagging: usize,
52    displacement: usize,
53) -> Result<IchimokuResult> {
54    if conversion == 0 || base == 0 || lagging == 0 || displacement == 0 {
55        return Err(IndicatorError::InvalidPeriod(
56            "All periods must be greater than 0".to_string(),
57        ));
58    }
59    let len = highs.len();
60    if lows.len() != len || closes.len() != len {
61        return Err(IndicatorError::InvalidPeriod(
62            "Data lengths must match".to_string(),
63        ));
64    }
65    let span_b_period = 2 * base;
66    let need = span_b_period.max(lagging);
67    if len < need {
68        return Err(IndicatorError::InsufficientData { need, got: len });
69    }
70
71    let mut conversion_line = vec![None; len];
72    let mut base_line = vec![None; len];
73    let mut leading_span_a = vec![None; len];
74    let mut leading_span_b = vec![None; len];
75    let mut lagging_span = vec![None; len];
76
77    let midpoint = |h: &[f64], l: &[f64]| -> f64 {
78        let highest = h.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
79        let lowest = l.iter().fold(f64::INFINITY, |a, &b| a.min(b));
80        (highest + lowest) / 2.0
81    };
82
83    for i in 0..len {
84        if i >= conversion - 1 {
85            let start = i + 1 - conversion;
86            conversion_line[i] = Some(midpoint(&highs[start..=i], &lows[start..=i]));
87        }
88
89        if i >= base - 1 {
90            let start = i + 1 - base;
91            base_line[i] = Some(midpoint(&highs[start..=i], &lows[start..=i]));
92        }
93
94        if i >= base - 1
95            && let (Some(conv), Some(base_val)) = (conversion_line[i], base_line[i])
96        {
97            let val = (conv + base_val) / 2.0;
98            if i + displacement < len {
99                leading_span_a[i + displacement] = Some(val);
100            }
101        }
102
103        if i >= span_b_period - 1 {
104            let start = i + 1 - span_b_period;
105            let val = midpoint(&highs[start..=i], &lows[start..=i]);
106            if i + displacement < len {
107                leading_span_b[i + displacement] = Some(val);
108            }
109        }
110
111        if i >= lagging {
112            lagging_span[i - lagging] = Some(closes[i]);
113        }
114    }
115
116    Ok(IchimokuResult {
117        conversion_line,
118        base_line,
119        leading_span_a,
120        leading_span_b,
121        lagging_span,
122    })
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_ichimoku_defaults() {
131        let highs = vec![10.0; 100];
132        let lows = vec![8.0; 100];
133        let closes = vec![9.0; 100];
134        let result = ichimoku(&highs, &lows, &closes, 9, 26, 26, 26).unwrap();
135
136        assert_eq!(result.conversion_line.len(), 100);
137        assert!(result.conversion_line[8].is_some());
138        assert!(result.base_line[25].is_some());
139        assert!(result.leading_span_a[51].is_some()); // 25 + 26
140        assert!(result.leading_span_b[77].is_some()); // 51 + 26
141        assert!(result.lagging_span[0].is_some()); // 26 - 26
142    }
143
144    #[test]
145    fn test_ichimoku_custom_periods() {
146        let highs = vec![10.0; 100];
147        let lows = vec![8.0; 100];
148        let closes = vec![9.0; 100];
149        // Custom: conversion=5, base=13, lagging=13, displacement=13
150        let result = ichimoku(&highs, &lows, &closes, 5, 13, 13, 13).unwrap();
151        assert!(result.conversion_line[4].is_some());
152        assert!(result.base_line[12].is_some());
153    }
154
155    #[test]
156    fn test_ichimoku_custom_produces_different_output() {
157        let highs: Vec<f64> = (1..=100).map(|i| i as f64 + 1.0).collect();
158        let lows: Vec<f64> = (1..=100).map(|i| i as f64 - 1.0).collect();
159        let closes: Vec<f64> = (1..=100).map(|i| i as f64).collect();
160        let default = ichimoku(&highs, &lows, &closes, 9, 26, 26, 26).unwrap();
161        let custom = ichimoku(&highs, &lows, &closes, 5, 13, 13, 13).unwrap();
162        let idx = 30;
163        assert!(default.conversion_line[idx].is_some());
164        assert!(custom.conversion_line[idx].is_some());
165        assert_ne!(default.conversion_line[idx], custom.conversion_line[idx]);
166    }
167}