velora_ta/trend/
vortex.rs

1//! Vortex Indicator
2//!
3//! Identifies the start of trends and their direction using two oscillating lines.
4
5use chrono::{DateTime, Utc};
6
7use crate::{
8    traits::{Indicator, MultiIndicator},
9    types::{MultiIndicatorValue, OhlcBar},
10    utils::CircularBuffer,
11    IndicatorError, IndicatorResult,
12};
13
14#[derive(Debug, Clone, Copy, Default)]
15struct VortexPoint {
16    high: f64,
17    low: f64,
18    close: f64,
19}
20
21/// Vortex indicator for trend identification.
22#[derive(Debug, Clone)]
23pub struct Vortex {
24    period: usize,
25    buffer: CircularBuffer<VortexPoint>,
26    name: String,
27}
28
29impl Vortex {
30    /// Creates a new Vortex indicator.
31    pub fn new(period: usize) -> IndicatorResult<Self> {
32        if period == 0 {
33            return Err(IndicatorError::InvalidParameter(
34                "Period must be > 0".to_string(),
35            ));
36        }
37
38        Ok(Vortex {
39            period,
40            buffer: CircularBuffer::new(period + 1),
41            name: format!("Vortex({period})"),
42        })
43    }
44
45    /// Update Vortex with OHLC bar.
46    pub fn update_ohlc(
47        &mut self,
48        bar: &OhlcBar,
49        _timestamp: DateTime<Utc>,
50    ) -> IndicatorResult<Option<Vec<f64>>> {
51        self.buffer.push(VortexPoint {
52            high: bar.high,
53            low: bar.low,
54            close: bar.close,
55        });
56
57        if !self.is_ready() {
58            return Ok(None);
59        }
60
61        let mut vm_plus = 0.0;
62        let mut vm_minus = 0.0;
63        let mut tr_sum = 0.0;
64
65        for i in 1..self.buffer.len() {
66            let curr = self.buffer.get(i).unwrap();
67            let prev = self.buffer.get(i - 1).unwrap();
68
69            vm_plus += (curr.high - prev.low).abs();
70            vm_minus += (curr.low - prev.high).abs();
71            tr_sum += (curr.high - curr.low)
72                .max((curr.high - prev.close).abs())
73                .max((curr.low - prev.close).abs());
74        }
75
76        if tr_sum == 0.0 {
77            return Ok(Some(vec![0.0, 0.0]));
78        }
79
80        let vi_plus = vm_plus / tr_sum;
81        let vi_minus = vm_minus / tr_sum;
82
83        Ok(Some(vec![vi_plus, vi_minus]))
84    }
85}
86
87impl Indicator for Vortex {
88    fn name(&self) -> &str {
89        &self.name
90    }
91
92    fn warmup_period(&self) -> usize {
93        self.period + 1
94    }
95
96    fn is_ready(&self) -> bool {
97        self.buffer.is_full()
98    }
99
100    fn reset(&mut self) {
101        self.buffer.clear();
102    }
103}
104
105impl MultiIndicator for Vortex {
106    fn output_count(&self) -> usize {
107        2
108    }
109
110    fn output_names(&self) -> Vec<&str> {
111        vec!["VI+", "VI-"]
112    }
113
114    fn update(
115        &mut self,
116        _price: f64,
117        _timestamp: DateTime<Utc>,
118    ) -> IndicatorResult<Option<Vec<f64>>> {
119        Err(IndicatorError::NotInitialized(
120            "Vortex requires OHLC data".to_string(),
121        ))
122    }
123
124    fn current(&self) -> Option<Vec<f64>> {
125        None
126    }
127
128    fn calculate(&self, _prices: &[f64]) -> IndicatorResult<Vec<Option<MultiIndicatorValue>>> {
129        Err(IndicatorError::NotInitialized(
130            "Vortex requires OHLC data".to_string(),
131        ))
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_vortex_creation() {
141        let vortex = Vortex::new(14).unwrap();
142        assert_eq!(vortex.output_count(), 2);
143    }
144}