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