diff --git a/src/indicators/cumulative_volume_delta.rs b/src/indicators/cumulative_volume_delta.rs new file mode 100644 index 0000000..02f1c12 --- /dev/null +++ b/src/indicators/cumulative_volume_delta.rs @@ -0,0 +1,168 @@ +use std::fmt; + +use crate::{Close, Next, Open, Reset, Volume}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Cumulative Volume Delta (CVD) indicator. +/// +/// This indicator calculates the cumulative difference between buying and selling volume +/// for each price movement. It is often used to gauge the strength of a trend by analyzing +/// whether volume is predominantly buying or selling. +/// +/// # Formula +/// For each period: +/// - If the price movement is positive (close > open), the buying volume is calculated as a fraction of the total volume. +/// - If the price movement is negative (close < open), the selling volume is calculated as a fraction of the total volume. +/// - The delta is calculated as `volume * rate` and accumulated over time. +/// +/// # Example +/// ``` +/// use ta::{Next, indicators::CumulativeVolumeDelta, DataItem}; +/// let mut cvd = CumulativeVolumeDelta::new(); +/// +/// // Assume `data` is a struct implementing `Open`, `Close`, and `Volume` traits. +/// let data = DataItem::builder().open(1.0).high(1.0).low(1.0).close(1.0).volume(1.0).build().unwrap(); +/// let delta = cvd.next(&data); +/// println!("Current CVD: {}", cvd); +/// ``` +#[doc(alias = "CVD")] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone)] +pub struct CumulativeVolumeDelta { + rate: f64, + cumulative_delta: f64, + history: Vec, +} + +impl CumulativeVolumeDelta { + /// Creates a new `CumulativeVolumeDelta` instance. + /// + /// # Returns + /// A new instance of `CumulativeVolumeDelta` with `cumulative_delta` set to 0.0 and an empty history. + pub fn new() -> Self { + Self { + rate: 0.5, + cumulative_delta: 0.0, + history: Vec::new(), + } + } + + /// The cumulative sum of volume deltas. + pub fn cumulative_delta(&self) -> f64 { + self.cumulative_delta + } + + /// Historical record of individual volume deltas. + pub fn history(&self) -> Vec { + self.history.clone() + } +} + +impl Next<&T> for CumulativeVolumeDelta { + type Output = f64; + + fn next(&mut self, input: &T) -> Self::Output { + let (open, close, volume) = (input.open(), input.close(), input.volume()); + let delta = if close > open { + // Positive price movement: assume buying pressure + volume * self.rate + } else if close < open { + // Negative price movement: assume selling pressure + -volume * self.rate + } else { + // No price movement: delta is zero + 0.0 + }; + + self.cumulative_delta += delta; + self.history.push(delta); + delta + } +} + +impl Reset for CumulativeVolumeDelta { + fn reset(&mut self) { + self.cumulative_delta = 0.0; + self.history.clear(); + } +} + +impl Default for CumulativeVolumeDelta { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for CumulativeVolumeDelta { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CVD({})", self.cumulative_delta) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helper::*; + + #[test] + fn test_new_cvd() { + let cvd = CumulativeVolumeDelta::new(); + assert_eq!(cvd.cumulative_delta, 0.0); + assert!(cvd.history.is_empty()); + } + + #[test] + fn test_next_positive_price_movement() { + let mut cvd = CumulativeVolumeDelta::new(); + let data = Bar::new().open(100.0).close(110.0).volume(1000.0); + let delta = cvd.next(&data); + assert!(delta > 0.0); + assert_eq!(cvd.history.len(), 1); + assert_eq!(cvd.cumulative_delta, delta); + } + + #[test] + fn test_next_negative_price_movement() { + let mut cvd = CumulativeVolumeDelta::new(); + let data = Bar::new().open(100.0).close(90.0).volume(1000.0); + let delta = cvd.next(&data); + assert!(delta < 0.0); + assert_eq!(cvd.history.len(), 1); + assert_eq!(cvd.cumulative_delta, delta); + } + + #[test] + fn test_next_no_price_movement() { + let mut cvd = CumulativeVolumeDelta::new(); + let data = Bar::new().open(100.0).close(100.0).volume(1000.0); + let delta = cvd.next(&data); + assert_eq!(delta, 0.0); + assert_eq!(cvd.history.len(), 1); + assert_eq!(cvd.cumulative_delta, 0.0); + } + + #[test] + fn test_cumulative_delta() { + let mut cvd = CumulativeVolumeDelta::new(); + let data1 = Bar::new().open(100.0).close(110.0).volume(1000.0); + let data2 = Bar::new().open(110.0).close(100.0).volume(1000.0); + let delta1 = cvd.next(&data1); + let delta2 = cvd.next(&data2); + assert_eq!(cvd.cumulative_delta, delta1 + delta2); + assert_eq!(cvd.history.len(), 2); + } + + #[test] + fn test_reset() { + let mut cvd = CumulativeVolumeDelta::new(); + let data = Bar::new().open(100.0).close(110.0).volume(1000.0); + cvd.next(&data); + assert_eq!(cvd.history.len(), 1); + assert_ne!(cvd.cumulative_delta, 0.0); + + cvd.reset(); + assert_eq!(cvd.cumulative_delta, 0.0); + assert!(cvd.history.is_empty()); + } +} diff --git a/src/indicators/mod.rs b/src/indicators/mod.rs index 66a08d3..28a240a 100644 --- a/src/indicators/mod.rs +++ b/src/indicators/mod.rs @@ -67,3 +67,9 @@ pub use self::money_flow_index::MoneyFlowIndex; mod on_balance_volume; pub use self::on_balance_volume::OnBalanceVolume; + +mod cumulative_volume_delta; +pub use self::cumulative_volume_delta::CumulativeVolumeDelta; + +mod parabolic_stop_and_reverse; +pub use self::parabolic_stop_and_reverse::ParabolicStopAndReverse; diff --git a/src/indicators/parabolic_stop_and_reverse.rs b/src/indicators/parabolic_stop_and_reverse.rs new file mode 100644 index 0000000..f112681 --- /dev/null +++ b/src/indicators/parabolic_stop_and_reverse.rs @@ -0,0 +1,216 @@ +use std::fmt; + +use crate::errors::Result; +use crate::{High, Low, Next, Reset}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Parabolic Stop and Reverse (Parabolic SAR) indicator. +/// +/// The Parabolic SAR is a trend-following indicator used to determine the direction of an asset's momentum and potential reversal points. +/// It is displayed as a series of dots placed either above or below the price bars, indicating potential stop and reverse levels. +/// +/// # Formula +/// The Parabolic SAR is calculated: +/// - If the trend is **up**, SAR is placed below the price and rises over time. +/// - If the trend is **down**, SAR is placed above the price and falls over time. +/// - The indicator starts with an initial acceleration factor (`step_af`) and increases it up to a maximum (`maximum_af`) as the trend extends. +/// +/// The formula for the next SAR value is: +/// `SAR = previous_SAR + acceleration_factor * (extreme_point - previous_SAR)` +/// where `extreme_point` is the highest high (for uptrend) or lowest low (for downtrend) during the trend. +/// +/// # Example +/// ``` +/// use ta::{DataItem, Next}; +/// use ta::indicators::ParabolicStopAndReverse; +/// +/// let mut sar = ParabolicStopAndReverse::new(0.2, 0.02).unwrap(); +/// let bar = DataItem::builder().open(105.0).high(105.0).low(95.0).close(95.0).volume(0.0).build().unwrap(); +/// let sar_value = sar.next(&bar); +/// println!("Parabolic SAR: {}", sar_value); +/// ``` +#[doc(alias = "ParabolicSAR")] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone)] + +pub struct ParabolicStopAndReverse { + accelerator_factor: f64, + maximum_af: f64, + step_af: f64, + extreme_point: f64, + stop_and_reverse: f64, + is_uptrend: bool, + initialized: bool, +} + +impl ParabolicStopAndReverse { + pub fn new(max_af: f64, step_af: f64) -> Result { + if max_af <= 0. || step_af <= 0. || max_af <= step_af { + return Err(crate::errors::TaError::InvalidParameter); + } + + Ok(Self { + accelerator_factor: 0.0, + maximum_af: max_af, + step_af, + extreme_point: 0.0, + stop_and_reverse: 0.0, + is_uptrend: false, + initialized: false, + }) + } + + pub fn is_uptrend(&self) -> bool { + self.is_uptrend + } +} + +impl Next<&T> for ParabolicStopAndReverse { + type Output = f64; + + fn next(&mut self, input: &T) -> Self::Output { + let (high, low) = (input.high(), input.low()); + + if !self.initialized { + self.is_uptrend = true; // uptrend by default + self.extreme_point = high; + self.stop_and_reverse = low; + self.initialized = true; + return self.stop_and_reverse; + } + + let new_sar = self.stop_and_reverse + + self.accelerator_factor * (self.extreme_point - self.stop_and_reverse); + + if self.is_uptrend && low < new_sar { + self.is_uptrend = false; + self.stop_and_reverse = self.extreme_point; + self.extreme_point = low; + self.accelerator_factor = 0.02; + return self.stop_and_reverse; + } + + if !self.is_uptrend && high > new_sar { + self.is_uptrend = true; + self.stop_and_reverse = self.extreme_point; + self.extreme_point = high; + self.accelerator_factor = 0.02; + return self.stop_and_reverse; + } + + self.stop_and_reverse = new_sar; + let old_ep = self.extreme_point; + + self.extreme_point = if self.is_uptrend { + self.extreme_point.max(high) + } else { + self.extreme_point.min(low) + }; + + if old_ep != self.extreme_point { + self.accelerator_factor = (self.accelerator_factor + self.step_af).min(self.maximum_af); + } + + self.stop_and_reverse + } +} + +impl Reset for ParabolicStopAndReverse { + fn reset(&mut self) { + self.accelerator_factor = 0.0; + self.initialized = false; + } +} + +impl Default for ParabolicStopAndReverse { + fn default() -> Self { + Self { + accelerator_factor: 0.02, + maximum_af: 0.2, + step_af: 0.02, + extreme_point: 0., + stop_and_reverse: 0., + is_uptrend: false, + initialized: false, + } + } +} + +impl fmt::Display for ParabolicStopAndReverse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SAR({})", self.stop_and_reverse) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helper::*; + + #[test] + fn test_default() { + ParabolicStopAndReverse::default(); + } + + #[test] + fn test_initialization() { + let mut sar = ParabolicStopAndReverse::default(); + let bar = Bar::new().high(15.0).low(5.0); + assert_eq!(sar.next(&bar), 5.0); + assert_eq!(sar.extreme_point, 15.0); + assert!(sar.is_uptrend); + } + + #[test] + fn test_trend_up_to_down() { + let mut sar = ParabolicStopAndReverse::new(0.2, 0.02).unwrap(); + let bar1 = Bar::new().high(15.0).low(5.0); + sar.next(&bar1); + assert!(sar.is_uptrend); + let bar2 = Bar::new().high(16.0).low(4.0); + let sar_value = sar.next(&bar2); + assert!(!sar.is_uptrend); + assert_eq!(sar_value, 15.0); + } + + #[test] + fn test_trend_down_to_up() { + let mut sar = ParabolicStopAndReverse::new(0.2, 0.02).unwrap(); + let bar1 = Bar::new().high(15.0).low(5.0); // up by default + let bar2 = Bar::new().high(16.0).low(4.0); + sar.next(&bar1); // up + sar.next(&bar2); // down + assert!(!sar.is_uptrend); + let bar3 = Bar::new().high(15.0).low(5.0); + sar.next(&bar3); // re.up + assert!(sar.is_uptrend); + } + + #[test] + fn test_acceleration_update() { + let mut sar = ParabolicStopAndReverse::new(0.2, 0.02).unwrap(); + let bar1 = Bar::new().high(15.0).low(5.0); + sar.next(&bar1); + let initial_af = sar.accelerator_factor; + + let bar2 = Bar::new().high(18.0).low(12.0); + sar.next(&bar2); + assert!(sar.accelerator_factor > initial_af); + assert!(sar.accelerator_factor <= 0.2); + } + + #[test] + fn test_display() { + let sar = ParabolicStopAndReverse::new(0.2, 0.02).unwrap(); + assert_eq!(format!("{}", sar), "SAR(0)"); + } + + #[test] + fn test_invalid_parameters() { + let result = ParabolicStopAndReverse::new(0.0, 0.02); + assert!(result.is_err()); + let result = ParabolicStopAndReverse::new(0.1, 0.2); + assert!(result.is_err()); + } +} diff --git a/src/test_helper.rs b/src/test_helper.rs index 96dbb1d..d5f57b1 100644 --- a/src/test_helper.rs +++ b/src/test_helper.rs @@ -20,10 +20,10 @@ impl Bar { } } - //pub fn open>(mut self, val :T ) -> Self { - // self.open = val.into(); - // self - //} + pub fn open>(mut self, val :T ) -> Self { + self.open = val.into(); + self + } pub fn high>(mut self, val: T) -> Self { self.high = val.into();