use crate::bar_indicators::volatility::atr::Atr;
use crate::bar_indicators::average::MovingAverageType;
use crate::bar_indicators::indicator_value::IndicatorValue;
#[derive(Clone)]
pub struct VortexIndicator {
period: usize,
vi_plus_values: Vec<f64>,
vi_minus_values: Vec<f64>,
positive_vortex_sum: f64,
negative_vortex_sum: f64,
true_range_sum: f64,
positive_vortex_buffer: Vec<f64>,
negative_vortex_buffer: Vec<f64>,
true_range_buffer: Vec<f64>,
atr: Atr,
prev_high: f64,
prev_low: f64,
vi_plus: f64,
vi_minus: f64,
bars_count: usize,
is_ready: bool,
}
impl VortexIndicator {
pub fn new() -> Self {
Self::with_period(14)
}
pub fn with_period(period: usize) -> Self {
Self::with_period_and_atr_type(period, MovingAverageType::RMA)
}
pub fn with_period_and_atr_type(period: usize, atr_ma_type: MovingAverageType) -> Self {
assert!(period > 0, "Period must be greater than 0");
Self {
period,
vi_plus_values: Vec::with_capacity(period),
vi_minus_values: Vec::with_capacity(period),
positive_vortex_sum: 0.0,
negative_vortex_sum: 0.0,
true_range_sum: 0.0,
positive_vortex_buffer: Vec::with_capacity(period),
negative_vortex_buffer: Vec::with_capacity(period),
true_range_buffer: Vec::with_capacity(period),
atr: Atr::new(period, atr_ma_type),
prev_high: 0.0,
prev_low: 0.0,
vi_plus: 1.0,
vi_minus: 1.0,
bars_count: 0,
is_ready: false,
}
}
pub fn update_bar(&mut self, _open: f64, high: f64, low: f64, close: f64, _volume: f64) -> (f64, f64) {
self.bars_count += 1;
if self.bars_count == 1 {
self.prev_high = high;
self.prev_low = low;
return (self.vi_plus, self.vi_minus);
}
let true_range = self.atr.update_bar(_open, high, low, close, _volume);
let positive_vortex = (high - self.prev_low).abs();
let negative_vortex = (low - self.prev_high).abs();
if self.positive_vortex_buffer.len() >= self.period {
let old_positive = self.positive_vortex_buffer.remove(0);
let old_negative = self.negative_vortex_buffer.remove(0);
let old_tr = self.true_range_buffer.remove(0);
self.positive_vortex_sum -= old_positive;
self.negative_vortex_sum -= old_negative;
self.true_range_sum -= old_tr;
}
self.positive_vortex_buffer.push(positive_vortex);
self.negative_vortex_buffer.push(negative_vortex);
self.true_range_buffer.push(true_range);
self.positive_vortex_sum += positive_vortex;
self.negative_vortex_sum += negative_vortex;
self.true_range_sum += true_range;
if self.true_range_sum.abs() > 1e-12 {
self.vi_plus = self.positive_vortex_sum / self.true_range_sum;
self.vi_minus = self.negative_vortex_sum / self.true_range_sum;
}
if self.vi_plus_values.len() >= 512 {
self.vi_plus_values.remove(0);
}
if self.vi_minus_values.len() >= 512 {
self.vi_minus_values.remove(0);
}
self.vi_plus_values.push(self.vi_plus);
self.vi_minus_values.push(self.vi_minus);
self.prev_high = high;
self.prev_low = low;
if self.bars_count > self.period {
self.is_ready = true;
}
(self.vi_plus, self.vi_minus)
}
#[inline]
pub fn vi_plus(&self) -> f64 {
self.vi_plus
}
#[inline]
pub fn vi_minus(&self) -> f64 {
self.vi_minus
}
#[inline]
pub fn values(&self) -> (f64, f64) {
(self.vi_plus, self.vi_minus)
}
#[inline]
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Double(self.vi_plus, self.vi_minus)
}
#[inline]
pub fn is_ready(&self) -> bool {
self.is_ready
}
#[inline]
pub fn period(&self) -> usize {
self.period
}
pub fn reset(&mut self) {
self.vi_plus_values.clear();
self.vi_minus_values.clear();
self.positive_vortex_sum = 0.0;
self.negative_vortex_sum = 0.0;
self.true_range_sum = 0.0;
self.positive_vortex_buffer.clear();
self.negative_vortex_buffer.clear();
self.true_range_buffer.clear();
self.atr.reset();
self.prev_high = 0.0;
self.prev_low = 0.0;
self.vi_plus = 1.0;
self.vi_minus = 1.0;
self.bars_count = 0;
self.is_ready = false;
}
pub fn trend_condition(&self) -> &'static str {
if self.vi_plus > self.vi_minus && self.vi_plus > 1.0 {
"Strong Uptrend"
} else if self.vi_plus > self.vi_minus {
"Uptrend"
} else if self.vi_minus > self.vi_plus && self.vi_minus > 1.0 {
"Strong Downtrend"
} else if self.vi_minus > self.vi_plus {
"Downtrend"
} else {
"Sideways"
}
}
pub fn trading_signal(&self) -> i8 {
if !self.is_ready() {
return 0;
}
if self.vi_plus > self.vi_minus && self.vi_plus > 1.0 {
1 } else if self.vi_minus > self.vi_plus && self.vi_minus > 1.0 {
-1 } else {
0
}
}
pub fn advanced_signal(&self) -> i8 {
if !self.is_ready() || self.vi_plus_values.len() < 3 || self.vi_minus_values.len() < 3 {
return 0;
}
let len = self.vi_plus_values.len();
let current_plus = self.vi_plus;
let current_minus = self.vi_minus;
let prev_plus = if len >= 2 { self.vi_plus_values[len - 2] } else { 1.0 };
let prev_minus = if len >= 2 { self.vi_minus_values[len - 2] } else { 1.0 };
if prev_plus <= prev_minus && current_plus > current_minus && current_plus > 1.0 {
return 1;
}
if prev_minus <= prev_plus && current_minus > current_plus && current_minus > 1.0 {
return -1;
}
0
}
pub fn trend_strength(&self) -> f64 {
if !self.is_ready() {
return 0.0;
}
(self.vi_plus - self.vi_minus).abs()
}
pub fn trend_direction(&self) -> i8 {
if !self.is_ready() {
return 0;
}
if self.vi_plus > self.vi_minus {
1
} else if self.vi_minus > self.vi_plus {
-1
} else {
0
}
}
pub fn trend_start_signal(&self) -> i8 {
if !self.is_ready() || self.vi_plus_values.len() < 5 || self.vi_minus_values.len() < 5 {
return 0;
}
let len = self.vi_plus_values.len();
let mut bullish_count = 0;
let mut bearish_count = 0;
for i in (len - 3)..len {
let plus = self.vi_plus_values[i];
let minus = self.vi_minus_values[i];
if plus > minus && plus > 1.0 {
bullish_count += 1;
} else if minus > plus && minus > 1.0 {
bearish_count += 1;
}
}
if bullish_count >= 2 {
1
} else if bearish_count >= 2 {
-1
} else {
0
}
}
pub fn average_vi(&self, periods: usize) -> (f64, f64) {
if !self.is_ready() || self.vi_plus_values.len() < periods || self.vi_minus_values.len() < periods {
return (1.0, 1.0);
}
let start_idx = self.vi_plus_values.len() - periods;
let avg_plus: f64 = self.vi_plus_values[start_idx..].iter().sum::<f64>() / periods as f64;
let avg_minus: f64 = self.vi_minus_values[start_idx..].iter().sum::<f64>() / periods as f64;
(avg_plus, avg_minus)
}
pub fn info(&self) -> String {
format!(
"VI+: {:.3}, VI-: {:.3}, Trend: {}, Strength: {:.3}, Direction: {}",
self.vi_plus,
self.vi_minus,
self.trend_condition(),
self.trend_strength(),
match self.trend_direction() {
1 => "Up",
-1 => "Down",
_ => "Sideways"
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vortex_basic_calculation() {
let mut vi = VortexIndicator::new();
for i in 1..=20 {
let base = 100.0 + i as f64;
vi.update_bar(base, base + 1.0, base - 0.5, base + 0.5, 0.0);
}
assert!(vi.is_ready());
let (vi_plus, vi_minus) = vi.values();
assert!(vi_plus > 0.0 && vi_plus.is_finite());
assert!(vi_minus > 0.0 && vi_minus.is_finite());
}
#[test]
fn test_vortex_uptrend() {
let mut vi = VortexIndicator::new();
for i in 1..=30 {
let base = 100.0 + i as f64 * 2.0;
vi.update_bar(base, base + 2.0, base - 0.5, base + 1.5, 0.0);
}
assert!(vi.is_ready());
assert!(vi.vi_plus() > vi.vi_minus(), "VI+ should be greater than VI- in uptrend");
}
#[test]
fn test_vortex_downtrend() {
let mut vi = VortexIndicator::new();
for i in 1..=30 {
let base = 200.0 - i as f64 * 2.0;
vi.update_bar(base, base + 0.5, base - 2.0, base - 1.5, 0.0);
}
assert!(vi.is_ready());
assert!(vi.vi_minus() > vi.vi_plus(), "VI- should be greater than VI+ in downtrend");
}
#[test]
fn test_vortex_reset() {
let mut vi = VortexIndicator::new();
for i in 1..=20 {
let base = 100.0 + i as f64;
vi.update_bar(base, base + 1.0, base - 0.5, base, 0.0);
}
assert!(vi.is_ready());
vi.reset();
assert!(!vi.is_ready());
assert_eq!(vi.vi_plus(), 1.0);
assert_eq!(vi.vi_minus(), 1.0);
}
#[test]
fn test_vortex_period() {
let vi = VortexIndicator::with_period(21);
assert_eq!(vi.period(), 21);
}
#[test]
fn test_vortex_with_period_and_atr_type() {
let mut vi = VortexIndicator::with_period_and_atr_type(14, MovingAverageType::EMA);
for i in 1..=25 {
let base = 100.0 + i as f64;
vi.update_bar(base, base + 1.0, base - 0.5, base, 0.0);
}
assert!(vi.is_ready());
let (vp, vm) = vi.values();
assert!(vp.is_finite() && vm.is_finite());
}
#[test]
fn test_vortex_trend_condition() {
let mut vi = VortexIndicator::new();
for i in 1..=30 {
let base = 100.0 + i as f64;
vi.update_bar(base, base + 1.0, base - 0.5, base, 0.0);
}
assert!(vi.is_ready());
let condition = vi.trend_condition();
assert!(
condition == "Strong Uptrend"
|| condition == "Uptrend"
|| condition == "Strong Downtrend"
|| condition == "Downtrend"
|| condition == "Sideways"
);
}
#[test]
fn test_vortex_trading_signal() {
let mut vi = VortexIndicator::new();
for i in 1..=30 {
let base = 100.0 + i as f64;
vi.update_bar(base, base + 1.0, base - 0.5, base, 0.0);
}
assert!(vi.is_ready());
let signal = vi.trading_signal();
assert!(signal >= -1 && signal <= 1);
}
#[test]
fn test_vortex_trend_strength() {
let mut vi = VortexIndicator::new();
for i in 1..=30 {
let base = 100.0 + i as f64;
vi.update_bar(base, base + 1.0, base - 0.5, base, 0.0);
}
assert!(vi.is_ready());
let strength = vi.trend_strength();
assert!(strength >= 0.0);
}
#[test]
fn test_vortex_trend_direction() {
let mut vi = VortexIndicator::new();
for i in 1..=30 {
let base = 100.0 + i as f64 * 2.0;
vi.update_bar(base, base + 2.0, base - 0.5, base + 1.5, 0.0);
}
assert!(vi.is_ready());
let direction = vi.trend_direction();
assert_eq!(direction, 1, "Should indicate uptrend");
}
#[test]
fn test_vortex_average_vi() {
let mut vi = VortexIndicator::new();
for i in 1..=30 {
let base = 100.0 + i as f64;
vi.update_bar(base, base + 1.0, base - 0.5, base, 0.0);
}
assert!(vi.is_ready());
let (avg_plus, avg_minus) = vi.average_vi(5);
assert!(avg_plus.is_finite() && avg_plus > 0.0);
assert!(avg_minus.is_finite() && avg_minus > 0.0);
}
}