finance_query/indicators/
ichimoku.rs1use super::{IndicatorError, Result};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct IchimokuResult {
9 pub conversion_line: Vec<Option<f64>>,
11 pub base_line: Vec<Option<f64>>,
13 pub leading_span_a: Vec<Option<f64>>,
15 pub leading_span_b: Vec<Option<f64>>,
17 pub lagging_span: Vec<Option<f64>>,
19}
20
21pub 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()); assert!(result.leading_span_b[77].is_some()); assert!(result.lagging_span[0].is_some()); }
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 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}