quantwave_core/indicators/
vortex.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
13pub struct VortexIndicator {
14 period: usize,
15 vm_plus: VecDeque<f64>,
16 vm_minus: VecDeque<f64>,
17 tr: VecDeque<f64>,
18 sum_vm_plus: f64,
19 sum_vm_minus: f64,
20 sum_tr: f64,
21 prev_high: Option<f64>,
22 prev_low: Option<f64>,
23 prev_close: Option<f64>,
24}
25
26impl VortexIndicator {
27 pub fn new(period: usize) -> Self {
28 Self {
29 period,
30 vm_plus: VecDeque::with_capacity(period),
31 vm_minus: VecDeque::with_capacity(period),
32 tr: VecDeque::with_capacity(period),
33 sum_vm_plus: 0.0,
34 sum_vm_minus: 0.0,
35 sum_tr: 0.0,
36 prev_high: None,
37 prev_low: None,
38 prev_close: None,
39 }
40 }
41}
42
43impl Next<(f64, f64, f64)> for VortexIndicator {
44 type Output = (f64, f64);
45
46 fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
47 let (vmp, vmm, tr) = match (self.prev_high, self.prev_low, self.prev_close) {
48 (Some(ph), Some(pl), Some(pc)) => {
49 let vmp = (high - pl).abs();
50 let vmm = (low - ph).abs();
51 let tr = (high - low).max((high - pc).abs()).max((low - pc).abs());
52 (vmp, vmm, tr)
53 }
54 _ => (0.0, 0.0, 0.0), };
56
57 self.vm_plus.push_back(vmp);
58 self.vm_minus.push_back(vmm);
59 self.tr.push_back(tr);
60 self.sum_vm_plus += vmp;
61 self.sum_vm_minus += vmm;
62 self.sum_tr += tr;
63
64 if self.vm_plus.len() > self.period {
65 if let Some(old_vmp) = self.vm_plus.pop_front() {
66 self.sum_vm_plus -= old_vmp;
67 }
68 if let Some(old_vmm) = self.vm_minus.pop_front() {
69 self.sum_vm_minus -= old_vmm;
70 }
71 if let Some(old_tr) = self.tr.pop_front() {
72 self.sum_tr -= old_tr;
73 }
74 }
75
76 self.prev_high = Some(high);
77 self.prev_low = Some(low);
78 self.prev_close = Some(close);
79
80 if self.sum_tr == 0.0 {
81 (1.0, 1.0)
82 } else {
83 (
84 self.sum_vm_plus / self.sum_tr,
85 self.sum_vm_minus / self.sum_tr,
86 )
87 }
88 }
89}
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use proptest::prelude::*;
94 use serde::Deserialize;
95 use std::fs;
96 use std::path::Path;
97
98 #[derive(Debug, Deserialize)]
99 struct VortexCase {
100 high: Vec<f64>,
101 low: Vec<f64>,
102 close: Vec<f64>,
103 expected_plus: Vec<f64>,
104 expected_minus: Vec<f64>,
105 }
106
107 #[test]
108 fn test_vortex_gold_standard() {
109 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
110 let manifest_path = Path::new(&manifest_dir);
111 let path = manifest_path.join("tests/gold_standard/vortex_14.json");
112 let path = if path.exists() {
113 path
114 } else {
115 manifest_path
116 .parent()
117 .unwrap()
118 .join("tests/gold_standard/vortex_14.json")
119 };
120 let content = fs::read_to_string(path).unwrap();
121 let case: VortexCase = serde_json::from_str(&content).unwrap();
122
123 let mut vi = VortexIndicator::new(14);
124 for i in 0..case.high.len() {
125 let (plus, minus) = vi.next((case.high[i], case.low[i], case.close[i]));
126 approx::assert_relative_eq!(plus, case.expected_plus[i], epsilon = 1e-6);
127 approx::assert_relative_eq!(minus, case.expected_minus[i], epsilon = 1e-6);
128 }
129 }
130
131 fn vortex_batch(data: Vec<(f64, f64, f64)>, period: usize) -> Vec<(f64, f64)> {
132 let mut vi = VortexIndicator::new(period);
133 data.into_iter().map(|x| vi.next(x)).collect()
134 }
135
136 proptest! {
137 #[test]
138 fn test_vortex_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
139 let mut adj_input = Vec::with_capacity(input.len());
140 for (h, l, c) in input {
141 let h_f: f64 = h;
142 let l_f: f64 = l;
143 let c_f: f64 = c;
144 let high = h_f.max(l_f).max(c_f);
145 let low = l_f.min(h_f).min(c_f);
146 adj_input.push((high, low, c_f));
147 }
148
149 let period = 14;
150 let mut vi = VortexIndicator::new(period);
151 let mut streaming_results = Vec::with_capacity(adj_input.len());
152 for &val in &adj_input {
153 streaming_results.push(vi.next(val));
154 }
155
156 let batch_results = vortex_batch(adj_input, period);
157
158 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
159 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
160 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
161 }
162 }
163 }
164
165 #[test]
166 fn test_vortex_basic() {
167 let mut vi = VortexIndicator::new(3);
168 let (p0, m0) = vi.next((10.0, 8.0, 9.0));
174 assert_eq!(p0, 1.0);
175 assert_eq!(m0, 1.0);
176
177 let (p1, m1) = vi.next((12.0, 10.0, 11.0));
178 approx::assert_relative_eq!(p1, 1.3333333333, epsilon = 1e-6);
179 assert_eq!(m1, 0.0);
180 }
181}
182
183pub const VORTEX_METADATA: IndicatorMetadata = IndicatorMetadata {
184 name: "Vortex Indicator",
185 description: "The Vortex Indicator helps identify the start of a new trend or the continuation of an existing one.",
186 usage: "Use to detect the start of new trends. A Vortex Indicator crossover (VI+ crossing above VI-) signals the beginning of an uptrend; the reverse signals a downtrend.",
187 keywords: &["trend", "momentum", "classic", "breakout"],
188 ehlers_summary: "The Vortex Indicator, developed by Etienne Botes and Douglas Siepman (2010), is inspired by the vortex flow of water discovered by Viktor Schauberger. VI+ measures upward movement relative to the prior bar low; VI- measures downward movement relative to the prior bar high. Normalized by ATR, they produce two oscillating lines whose crossovers signal trend changes. — Technical Analysis of Stocks and Commodities, 2010",
189 params: &[ParamDef {
190 name: "period",
191 default: "14",
192 description: "Period",
193 }],
194 formula_source: "https://www.investopedia.com/terms/v/vortex-indicator-vi.asp",
195 formula_latex: r#"
196\[
197VI+ = \frac{\sum VM+}{\sum TR} \\ VI- = \frac{\sum VM-}{\sum TR}
198\]
199"#,
200 gold_standard_file: "vortex.json",
201 category: "Classic",
202};