fin_primitives/signals/indicators/
vortex.rs1use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6use std::collections::VecDeque;
7
8pub struct Vortex {
32 name: String,
33 period: usize,
34 prev_high: Option<Decimal>,
35 prev_low: Option<Decimal>,
36 prev_close: Option<Decimal>,
37 vm_plus: VecDeque<Decimal>,
38 vm_minus: VecDeque<Decimal>,
39 trs: VecDeque<Decimal>,
40 last_vi_minus: Option<Decimal>,
41}
42
43impl Vortex {
44 pub fn new(name: impl Into<String>, period: usize) -> Result<Self, FinError> {
49 if period == 0 {
50 return Err(FinError::InvalidPeriod(period));
51 }
52 Ok(Self {
53 name: name.into(),
54 period,
55 prev_high: None,
56 prev_low: None,
57 prev_close: None,
58 vm_plus: VecDeque::with_capacity(period),
59 vm_minus: VecDeque::with_capacity(period),
60 trs: VecDeque::with_capacity(period),
61 last_vi_minus: None,
62 })
63 }
64
65 pub fn vi_minus(&self) -> Option<Decimal> {
67 self.last_vi_minus
68 }
69}
70
71impl Signal for Vortex {
72 fn name(&self) -> &str {
73 &self.name
74 }
75
76 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
77 let (prev_high, prev_low, prev_close) = match (self.prev_high, self.prev_low, self.prev_close) {
78 (Some(h), Some(l), Some(c)) => (h, l, c),
79 _ => {
80 self.prev_high = Some(bar.high);
81 self.prev_low = Some(bar.low);
82 self.prev_close = Some(bar.close);
83 return Ok(SignalValue::Unavailable);
84 }
85 };
86
87 let vm_p = (bar.high - prev_low).abs();
88 let vm_m = (bar.low - prev_high).abs();
89 let true_high = bar.high.max(prev_close);
90 let true_low = bar.low.min(prev_close);
91 let tr = true_high - true_low;
92
93 self.vm_plus.push_back(vm_p);
94 self.vm_minus.push_back(vm_m);
95 self.trs.push_back(tr);
96 if self.vm_plus.len() > self.period {
97 self.vm_plus.pop_front();
98 self.vm_minus.pop_front();
99 self.trs.pop_front();
100 }
101
102 self.prev_high = Some(bar.high);
103 self.prev_low = Some(bar.low);
104 self.prev_close = Some(bar.close);
105
106 if self.vm_plus.len() < self.period {
107 return Ok(SignalValue::Unavailable);
108 }
109
110 let sum_vm_p: Decimal = self.vm_plus.iter().sum();
111 let sum_vm_m: Decimal = self.vm_minus.iter().sum();
112 let sum_tr: Decimal = self.trs.iter().sum();
113
114 if sum_tr.is_zero() {
115 return Ok(SignalValue::Unavailable);
116 }
117
118 let vi_plus = sum_vm_p / sum_tr;
119 let vi_minus = sum_vm_m / sum_tr;
120 self.last_vi_minus = Some(vi_minus);
121 Ok(SignalValue::Scalar(vi_plus))
122 }
123
124 fn is_ready(&self) -> bool {
125 self.last_vi_minus.is_some()
126 }
127
128 fn period(&self) -> usize {
129 self.period
130 }
131
132 fn reset(&mut self) {
133 self.prev_high = None;
134 self.prev_low = None;
135 self.prev_close = None;
136 self.vm_plus.clear();
137 self.vm_minus.clear();
138 self.trs.clear();
139 self.last_vi_minus = None;
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::ohlcv::OhlcvBar;
147 use crate::signals::Signal;
148 use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
149
150 fn bar(h: &str, l: &str, c: &str) -> OhlcvBar {
151 let hi = Price::new(h.parse().unwrap()).unwrap();
152 let lo = Price::new(l.parse().unwrap()).unwrap();
153 let cl = Price::new(c.parse().unwrap()).unwrap();
154 OhlcvBar {
155 symbol: Symbol::new("X").unwrap(),
156 open: cl, high: hi, low: lo, close: cl,
157 volume: Quantity::zero(),
158 ts_open: NanoTimestamp::new(0),
159 ts_close: NanoTimestamp::new(1),
160 tick_count: 1,
161 }
162 }
163
164 #[test]
165 fn test_vortex_period_zero_fails() {
166 assert!(Vortex::new("vx", 0).is_err());
167 }
168
169 #[test]
170 fn test_vortex_unavailable_before_period() {
171 let mut vx = Vortex::new("vx", 3).unwrap();
172 assert_eq!(vx.update_bar(&bar("110", "90", "100")).unwrap(), SignalValue::Unavailable);
173 assert_eq!(vx.update_bar(&bar("115", "95", "105")).unwrap(), SignalValue::Unavailable);
174 assert!(!vx.is_ready());
175 }
176
177 #[test]
178 fn test_vortex_ready_after_period() {
179 let mut vx = Vortex::new("vx", 3).unwrap();
180 for _ in 0..4 {
181 vx.update_bar(&bar("110", "90", "100")).unwrap();
182 }
183 assert!(vx.is_ready());
184 assert!(vx.vi_minus().is_some());
185 }
186
187 #[test]
188 fn test_vortex_reset() {
189 let mut vx = Vortex::new("vx", 3).unwrap();
190 for _ in 0..5 { vx.update_bar(&bar("110", "90", "100")).unwrap(); }
191 assert!(vx.is_ready());
192 vx.reset();
193 assert!(!vx.is_ready());
194 assert!(vx.vi_minus().is_none());
195 }
196}