finance_query/indicators/
zigzag.rs1use super::{IndicatorError, Result};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9#[non_exhaustive]
10pub struct ZigZagPoint {
11 pub index: usize,
13 pub price: f64,
15 pub is_high: bool,
17}
18
19pub fn zigzag(highs: &[f64], lows: &[f64], deviation_pct: f64) -> Result<Vec<ZigZagPoint>> {
48 if deviation_pct <= 0.0 {
49 return Err(IndicatorError::InvalidPeriod(
50 "deviation_pct must be greater than 0".to_string(),
51 ));
52 }
53 if highs.len() != lows.len() {
54 return Err(IndicatorError::InvalidPeriod(
55 "highs and lows must have the same length".to_string(),
56 ));
57 }
58 if highs.is_empty() {
59 return Err(IndicatorError::InsufficientData {
60 need: 2,
61 got: highs.len(),
62 });
63 }
64
65 let threshold = deviation_pct / 100.0;
66 let mut pivots = Vec::new();
67
68 let start_price = (highs[0] + lows[0]) / 2.0;
69 let mut trend: Option<bool> = None; let mut extreme_idx = 0usize;
71 let mut extreme_price = start_price;
72
73 for i in 1..highs.len() {
74 match trend {
75 None => {
76 if highs[i] >= start_price * (1.0 + threshold) {
77 trend = Some(true);
78 extreme_idx = i;
79 extreme_price = highs[i];
80 } else if lows[i] <= start_price * (1.0 - threshold) {
81 trend = Some(false);
82 extreme_idx = i;
83 extreme_price = lows[i];
84 }
85 }
86 Some(true) => {
87 if highs[i] > extreme_price {
88 extreme_price = highs[i];
89 extreme_idx = i;
90 } else if lows[i] <= extreme_price * (1.0 - threshold) {
91 pivots.push(ZigZagPoint {
92 index: extreme_idx,
93 price: extreme_price,
94 is_high: true,
95 });
96 trend = Some(false);
97 extreme_price = lows[i];
98 extreme_idx = i;
99 }
100 }
101 Some(false) => {
102 if lows[i] < extreme_price {
103 extreme_price = lows[i];
104 extreme_idx = i;
105 } else if highs[i] >= extreme_price * (1.0 + threshold) {
106 pivots.push(ZigZagPoint {
107 index: extreme_idx,
108 price: extreme_price,
109 is_high: false,
110 });
111 trend = Some(true);
112 extreme_price = highs[i];
113 extreme_idx = i;
114 }
115 }
116 }
117 }
118
119 if let Some(is_high) = trend {
120 pivots.push(ZigZagPoint {
121 index: extreme_idx,
122 price: extreme_price,
123 is_high,
124 });
125 }
126
127 Ok(pivots)
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_zigzag_basic() {
136 let highs = vec![100.0, 110.0, 90.0, 120.0, 80.0];
137 let lows = vec![100.0, 110.0, 90.0, 120.0, 80.0];
138 let pivots = zigzag(&highs, &lows, 5.0).unwrap();
139
140 assert_eq!(pivots.len(), 4);
141 assert_eq!(
142 pivots[0],
143 ZigZagPoint {
144 index: 1,
145 price: 110.0,
146 is_high: true
147 }
148 );
149 assert_eq!(
150 pivots[1],
151 ZigZagPoint {
152 index: 2,
153 price: 90.0,
154 is_high: false
155 }
156 );
157 assert_eq!(
158 pivots[2],
159 ZigZagPoint {
160 index: 3,
161 price: 120.0,
162 is_high: true
163 }
164 );
165 assert_eq!(
166 pivots[3],
167 ZigZagPoint {
168 index: 4,
169 price: 80.0,
170 is_high: false
171 }
172 );
173 }
174
175 #[test]
176 fn test_zigzag_alternates() {
177 let highs = vec![100.0, 110.0, 90.0, 120.0, 80.0];
178 let lows = vec![100.0, 110.0, 90.0, 120.0, 80.0];
179 let pivots = zigzag(&highs, &lows, 5.0).unwrap();
180 for w in pivots.windows(2) {
181 assert_ne!(w[0].is_high, w[1].is_high, "pivots must alternate");
182 }
183 }
184
185 #[test]
186 fn test_zigzag_small_moves_filtered() {
187 let highs = vec![100.0, 101.0, 100.5, 101.5, 130.0];
189 let lows = vec![100.0, 100.5, 100.0, 101.0, 129.0];
190 let pivots = zigzag(&highs, &lows, 10.0).unwrap();
191 assert!(pivots.len() <= 2);
193 }
194
195 #[test]
196 fn test_zigzag_invalid_deviation() {
197 assert!(zigzag(&[1.0, 2.0], &[1.0, 2.0], 0.0).is_err());
198 assert!(zigzag(&[1.0, 2.0], &[1.0, 2.0], -1.0).is_err());
199 }
200
201 #[test]
202 fn test_zigzag_mismatched_lengths() {
203 assert!(zigzag(&[1.0, 2.0], &[1.0], 5.0).is_err());
204 }
205
206 #[test]
207 fn test_zigzag_empty() {
208 assert!(zigzag(&[], &[], 5.0).is_err());
209 }
210}