use crate::bar_indicators::average::moving_average::{MovingAverageProvider, MovingAverageType};
use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::ohlcv_field::OhlcvField;
#[derive(Clone)]
pub struct VolumePriceTrend {
signal_period: usize,
price_source: OhlcvField,
vpt_values: Vec<f64>,
signal_ma: MovingAverageProvider,
prev_close: f64,
vpt_value: f64,
signal_value: f64,
bars_count: usize,
is_ready: bool,
}
impl VolumePriceTrend {
pub fn new() -> Self {
Self::with_signal_period(21)
}
pub fn with_signal_period(signal_period: usize) -> Self {
Self::with_source(signal_period, OhlcvField::Close)
}
pub fn with_source(signal_period: usize, price_source: OhlcvField) -> Self {
assert!(signal_period > 0, "Signal period must be greater than 0");
Self {
signal_period,
price_source,
vpt_values: Vec::with_capacity(512),
signal_ma: MovingAverageProvider::new(MovingAverageType::SMA, signal_period),
prev_close: 0.0,
vpt_value: 0.0,
signal_value: 0.0,
bars_count: 0,
is_ready: false,
}
}
pub fn update_bar(&mut self, open: f64, high: f64, low: f64, close: f64, volume: f64) -> f64 {
let price = self.price_source.extract(open, high, low, close, volume);
self.bars_count += 1;
if self.bars_count == 1 {
self.prev_close = price;
self.vpt_value = 0.0;
return self.vpt_value;
}
let price_change_pct = if self.prev_close.abs() > 1e-12 {
(price - self.prev_close) / self.prev_close
} else {
0.0
};
self.vpt_value += volume * price_change_pct;
if self.vpt_values.len() >= 512 {
self.vpt_values.remove(0);
}
self.vpt_values.push(self.vpt_value);
self.signal_value = self.signal_ma.update_bar(self.vpt_value, self.vpt_value, self.vpt_value, self.vpt_value, 1.0);
self.prev_close = price;
if self.bars_count >= self.signal_period + 5 {
self.is_ready = true;
}
self.vpt_value
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Single(self.vpt_value)
}
pub fn signal_value(&self) -> f64 {
self.signal_value
}
pub fn histogram(&self) -> f64 {
self.vpt_value - self.signal_value
}
pub fn is_ready(&self) -> bool {
self.is_ready
}
pub fn signal_period(&self) -> usize {
self.signal_period
}
pub fn reset(&mut self) {
self.vpt_values.clear();
self.signal_ma.reset();
self.prev_close = 0.0;
self.vpt_value = 0.0;
self.signal_value = 0.0;
self.bars_count = 0;
self.is_ready = false;
}
pub fn trend_condition(&self) -> &'static str {
if self.vpt_value > self.signal_value && self.vpt_value > 0.0 {
"Strong Bullish"
} else if self.vpt_value > self.signal_value {
"Bullish"
} else if self.vpt_value < self.signal_value && self.vpt_value < 0.0 {
"Strong Bearish"
} else if self.vpt_value < self.signal_value {
"Bearish"
} else {
"Neutral"
}
}
pub fn trading_signal(&self) -> i8 {
if !self.is_ready() {
return 0;
}
if self.vpt_value > self.signal_value {
1 } else if self.vpt_value < self.signal_value {
-1 } else {
0 }
}
pub fn advanced_signal(&self) -> i8 {
if !self.is_ready() || self.vpt_values.len() < 3 {
return 0;
}
let len = self.vpt_values.len();
let current = self.vpt_value;
let prev_1 = if len >= 2 { self.vpt_values[len - 2] } else { 0.0 };
let _prev_2 = if len >= 3 { self.vpt_values[len - 3] } else { 0.0 };
if prev_1 <= self.signal_value && current > self.signal_value && current > prev_1 {
return 1;
}
if prev_1 >= self.signal_value && current < self.signal_value && current < prev_1 {
return -1;
}
0
}
pub fn check_divergence(&self, price_history: &[f64], lookback: usize) -> i8 {
if !self.is_ready() || price_history.len() < lookback + 1 || self.vpt_values.len() < lookback + 1 {
return 0;
}
let current_price = price_history[price_history.len() - 1];
let past_price = price_history[price_history.len() - lookback - 1];
let current_vpt = self.vpt_value;
let past_vpt = self.vpt_values[self.vpt_values.len() - lookback - 1];
let price_change = current_price - past_price;
let vpt_change = current_vpt - past_vpt;
if price_change < 0.0 && vpt_change > 0.0 {
return 1;
}
if price_change > 0.0 && vpt_change < 0.0 {
return -1;
}
0
}
pub fn trend_strength(&self) -> f64 {
if !self.is_ready() {
return 0.0;
}
self.vpt_value.abs()
}
pub fn rate_of_change(&self, periods: usize) -> f64 {
if !self.is_ready() || self.vpt_values.len() < periods + 1 {
return 0.0;
}
let current = self.vpt_value;
let past = self.vpt_values[self.vpt_values.len() - periods - 1];
if past.abs() < 1e-12 {
0.0
} else {
(current - past) / past * 100.0
}
}
pub fn trend_direction(&self, lookback: usize) -> i8 {
if !self.is_ready() || self.vpt_values.len() < lookback + 1 {
return 0;
}
let current = self.vpt_value;
let past = self.vpt_values[self.vpt_values.len() - lookback - 1];
if current > past {
1 } else if current < past {
-1 } else {
0 }
}
pub fn volatility(&self, periods: usize) -> f64 {
if !self.is_ready() || self.vpt_values.len() < periods {
return 0.0;
}
let start_idx = self.vpt_values.len() - periods;
let slice = &self.vpt_values[start_idx..];
let mean = slice.iter().sum::<f64>() / slice.len() as f64;
let variance = slice.iter()
.map(|&x| (x - mean).powi(2))
.sum::<f64>() / slice.len() as f64;
variance.sqrt()
}
pub fn average_value(&self, periods: usize) -> f64 {
if !self.is_ready() || self.vpt_values.len() < periods {
return 0.0;
}
let start_idx = self.vpt_values.len() - periods;
let slice = &self.vpt_values[start_idx..];
slice.iter().sum::<f64>() / slice.len() as f64
}
pub fn extremes(&self, periods: usize) -> (f64, f64) {
if !self.is_ready() || self.vpt_values.len() < periods {
return (0.0, 0.0);
}
let start_idx = self.vpt_values.len() - periods;
let slice = &self.vpt_values[start_idx..];
let max_val = slice.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let min_val = slice.iter().fold(f64::INFINITY, |a, &b| a.min(b));
(min_val, max_val)
}
pub fn normalized_value(&self, periods: usize) -> f64 {
if !self.is_ready() {
return 0.5;
}
let (min_val, max_val) = self.extremes(periods);
let range = max_val - min_val;
if range.abs() < 1e-12 {
0.5
} else {
(self.vpt_value - min_val) / range
}
}
pub fn info(&self) -> String {
let strength = self.trend_strength();
let roc = self.rate_of_change(5);
let direction = match self.trend_direction(5) {
1 => "Up",
-1 => "Down",
_ => "Sideways"
};
format!(
"VPT: {:.2}, Signal: {:.2}, Histogram: {:.2}, Trend: {}, Strength: {:.2}, ROC: {:.2}%, Direction: {}",
self.vpt_value,
self.signal_value,
self.histogram(),
self.trend_condition(),
strength,
roc,
direction
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vpt_creation() {
let vpt = VolumePriceTrend::new();
assert!(!vpt.is_ready());
assert_eq!(vpt.value().main(), 0.0);
}
#[test]
fn test_vpt_with_signal_period() {
let vpt = VolumePriceTrend::with_signal_period(10);
assert!(!vpt.is_ready());
assert_eq!(vpt.signal_period(), 10);
}
#[test]
fn test_vpt_warmup() {
let mut vpt = VolumePriceTrend::with_signal_period(10);
for i in 0..20 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
vpt.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
assert!(vpt.is_ready());
}
#[test]
fn test_vpt_values_finite() {
let mut vpt = VolumePriceTrend::new();
for i in 0..40 {
let price = 100.0 + i as f64;
let value = vpt.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
assert!(value.is_finite());
}
}
#[test]
fn test_vpt_reset() {
let mut vpt = VolumePriceTrend::new();
for i in 0..30 {
vpt.update_bar(100.0 + i as f64, 105.0, 95.0, 101.0, 1000.0);
}
vpt.reset();
assert!(!vpt.is_ready());
assert_eq!(vpt.value().main(), 0.0);
}
}