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.
24///
25/// # Arguments
26///
27/// * `highs` - High prices
28/// * `lows` - Low prices
29/// * `closes` - Close prices
30///
31/// # Example
32///
33/// ```
34/// use finance_query::indicators::ichimoku;
35///
36/// let highs = vec![10.0; 100];
37/// let lows = vec![8.0; 100];
38/// let closes = vec![9.0; 100];
39/// let result = ichimoku(&highs, &lows, &closes).unwrap();
40/// ```
41pub fn ichimoku(highs: &[f64], lows: &[f64], closes: &[f64]) -> Result<IchimokuResult> {
42    let len = highs.len();
43    if lows.len() != len || closes.len() != len {
44        return Err(IndicatorError::InvalidPeriod(
45            "Data lengths must match".to_string(),
46        ));
47    }
48    if len < 52 {
49        return Err(IndicatorError::InsufficientData { need: 52, got: len });
50    }
51
52    let mut conversion_line = vec![None; len];
53    let mut base_line = vec![None; len];
54    let mut leading_span_a = vec![None; len];
55    let mut leading_span_b = vec![None; len];
56    let mut lagging_span = vec![None; len];
57
58    let midpoint = |h: &[f64], l: &[f64]| -> f64 {
59        let highest = h.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
60        let lowest = l.iter().fold(f64::INFINITY, |a, &b| a.min(b));
61        (highest + lowest) / 2.0
62    };
63
64    for i in 0..len {
65        if i >= 8 {
66            let start = i - 8;
67            conversion_line[i] = Some(midpoint(&highs[start..=i], &lows[start..=i]));
68        }
69
70        if i >= 25 {
71            let start = i - 25;
72            base_line[i] = Some(midpoint(&highs[start..=i], &lows[start..=i]));
73        }
74
75        if i >= 25
76            && let (Some(conv), Some(base)) = (conversion_line[i], base_line[i])
77        {
78            let val = (conv + base) / 2.0;
79            if i + 26 < len {
80                leading_span_a[i + 26] = Some(val);
81            }
82        }
83
84        if i >= 51 {
85            let start = i - 51;
86            let val = midpoint(&highs[start..=i], &lows[start..=i]);
87            if i + 26 < len {
88                leading_span_b[i + 26] = Some(val);
89            }
90        }
91
92        if i >= 26 {
93            lagging_span[i - 26] = Some(closes[i]);
94        }
95    }
96
97    Ok(IchimokuResult {
98        conversion_line,
99        base_line,
100        leading_span_a,
101        leading_span_b,
102        lagging_span,
103    })
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_ichimoku() {
112        let highs = vec![10.0; 100];
113        let lows = vec![8.0; 100];
114        let closes = vec![9.0; 100];
115        let result = ichimoku(&highs, &lows, &closes).unwrap();
116
117        assert_eq!(result.conversion_line.len(), 100);
118        assert!(result.conversion_line[8].is_some());
119        assert!(result.base_line[25].is_some());
120        assert!(result.leading_span_a[51].is_some()); // 25 + 26
121        assert!(result.leading_span_b[77].is_some()); // 51 + 26
122        assert!(result.lagging_span[0].is_some()); // 26 - 26
123    }
124}