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