1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct VortexOutput {
12 pub plus: f64,
14 pub minus: f64,
16}
17
18#[derive(Debug, Clone)]
51pub struct Vortex {
52 period: usize,
53 prev: Option<Candle>,
54 window: VecDeque<(f64, f64, f64)>,
56 sum_vm_plus: f64,
57 sum_vm_minus: f64,
58 sum_tr: f64,
59 last: Option<VortexOutput>,
60}
61
62impl Vortex {
63 pub fn new(period: usize) -> Result<Self> {
69 if period == 0 {
70 return Err(Error::PeriodZero);
71 }
72 if period > crate::error::MAX_PERIOD {
73 return Err(Error::InvalidPeriod {
74 message: crate::error::PERIOD_ABOVE_MAX,
75 });
76 }
77 Ok(Self {
78 period,
79 prev: None,
80 window: VecDeque::with_capacity(period),
81 sum_vm_plus: 0.0,
82 sum_vm_minus: 0.0,
83 sum_tr: 0.0,
84 last: None,
85 })
86 }
87
88 pub const fn period(&self) -> usize {
90 self.period
91 }
92
93 pub const fn value(&self) -> Option<VortexOutput> {
95 self.last
96 }
97}
98
99impl Indicator for Vortex {
100 type Input = Candle;
101 type Output = VortexOutput;
102
103 #[inline]
104 fn update(&mut self, candle: Candle) -> Option<VortexOutput> {
105 let Some(prev) = self.prev else {
106 self.prev = Some(candle);
108 return None;
109 };
110 let vm_plus = (candle.high - prev.low).abs();
111 let vm_minus = (candle.low - prev.high).abs();
112 let tr = candle.true_range(Some(prev.close));
113 self.prev = Some(candle);
114
115 if self.window.len() == self.period {
116 let (old_p, old_m, old_tr) = self.window.pop_front().expect("window is non-empty");
117 self.sum_vm_plus -= old_p;
118 self.sum_vm_minus -= old_m;
119 self.sum_tr -= old_tr;
120 }
121 self.window.push_back((vm_plus, vm_minus, tr));
122 self.sum_vm_plus += vm_plus;
123 self.sum_vm_minus += vm_minus;
124 self.sum_tr += tr;
125
126 if self.window.len() < self.period {
127 return None;
128 }
129 let out = if self.sum_tr == 0.0 {
130 VortexOutput {
132 plus: 0.0,
133 minus: 0.0,
134 }
135 } else {
136 VortexOutput {
137 plus: self.sum_vm_plus / self.sum_tr,
138 minus: self.sum_vm_minus / self.sum_tr,
139 }
140 };
141 self.last = Some(out);
142 Some(out)
143 }
144
145 fn reset(&mut self) {
146 self.prev = None;
147 self.window.clear();
148 self.sum_vm_plus = 0.0;
149 self.sum_vm_minus = 0.0;
150 self.sum_tr = 0.0;
151 self.last = None;
152 }
153
154 #[inline]
155 fn warmup_period(&self) -> usize {
156 self.period + 1
158 }
159
160 #[inline]
161 fn is_ready(&self) -> bool {
162 self.last.is_some()
163 }
164
165 #[inline]
166 fn name(&self) -> &'static str {
167 "Vortex"
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::traits::BatchExt;
175 use approx::assert_relative_eq;
176
177 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
178 Candle::new(open, high, low, close, 1.0, ts).unwrap()
179 }
180
181 #[test]
182 fn new_rejects_zero_period() {
183 assert!(matches!(Vortex::new(0), Err(Error::PeriodZero)));
184 }
185
186 #[test]
190 fn accessors_and_metadata() {
191 let mut v = Vortex::new(14).unwrap();
192 assert_eq!(v.period(), 14);
193 assert_eq!(v.name(), "Vortex");
194 assert!(v.value().is_none());
195 let warmup = i64::try_from(v.warmup_period()).unwrap();
196 let candles: Vec<Candle> = (0..warmup)
197 .map(|i| {
198 let p = 100.0 + (i as f64 * 0.3).sin() * 5.0;
199 Candle::new(p, p + 1.0, p - 1.0, p, 1.0, i).unwrap()
200 })
201 .collect();
202 for c in &candles {
203 v.update(*c);
204 }
205 assert!(v.value().is_some());
206 }
207
208 #[test]
209 fn reference_values() {
210 let candles = [
216 candle(9.0, 10.0, 8.0, 9.0, 0),
217 candle(10.0, 12.0, 9.0, 11.0, 1),
218 candle(12.0, 13.0, 11.0, 12.0, 2),
219 ];
220 let mut v = Vortex::new(2).unwrap();
221 let out = v.batch(&candles);
222 assert_eq!(v.warmup_period(), 3);
223 assert_eq!(out[0], None);
224 assert_eq!(out[1], None);
225 let o = out[2].unwrap();
226 assert_relative_eq!(o.plus, 1.6, epsilon = 1e-12);
227 assert_relative_eq!(o.minus, 0.4, epsilon = 1e-12);
228 }
229
230 #[test]
231 fn perfectly_flat_market_yields_zero() {
232 let mut v = Vortex::new(5).unwrap();
233 let candles: Vec<Candle> = (0..20).map(|i| candle(10.0, 10.0, 10.0, 10.0, i)).collect();
234 for o in v.batch(&candles).into_iter().flatten() {
235 assert_relative_eq!(o.plus, 0.0, epsilon = 1e-12);
236 assert_relative_eq!(o.minus, 0.0, epsilon = 1e-12);
237 }
238 }
239
240 #[test]
241 fn outputs_are_non_negative() {
242 let mut v = Vortex::new(14).unwrap();
243 let candles: Vec<Candle> = (0..120)
244 .map(|i| {
245 let mid = 100.0 + (i as f64 * 0.3).sin() * 10.0;
246 candle(mid, mid + 3.0, mid - 3.0, mid + 1.0, i)
247 })
248 .collect();
249 for o in v.batch(&candles).into_iter().flatten() {
250 assert!(o.plus >= 0.0 && o.minus >= 0.0, "negative VI: {o:?}");
251 }
252 }
253
254 #[test]
255 fn reset_clears_state() {
256 let mut v = Vortex::new(5).unwrap();
257 let candles: Vec<Candle> = (0..20)
258 .map(|i| candle(100.0, 102.0, 98.0, 101.0, i))
259 .collect();
260 v.batch(&candles);
261 assert!(v.is_ready());
262 v.reset();
263 assert!(!v.is_ready());
264 assert_eq!(v.update(candles[0]), None);
265 }
266
267 #[test]
268 fn batch_equals_streaming() {
269 let candles: Vec<Candle> = (0..80)
270 .map(|i| {
271 let mid = 100.0 + (i as f64 * 0.35).sin() * 9.0;
272 candle(mid, mid + 2.5, mid - 2.5, mid + 0.5, i)
273 })
274 .collect();
275 let batch = Vortex::new(14).unwrap().batch(&candles);
276 let mut b = Vortex::new(14).unwrap();
277 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
278 assert_eq!(batch, streamed);
279 }
280}