Skip to content

Commit 72c5f27

Browse files
committed
add: indicator cumulative volume delta
1 parent deac215 commit 72c5f27

File tree

3 files changed

+175
-4
lines changed

3 files changed

+175
-4
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use std::fmt;
2+
3+
use crate::{Close, Next, Open, Reset, Volume};
4+
#[cfg(feature = "serde")]
5+
use serde::{Deserialize, Serialize};
6+
7+
/// Cumulative Volume Delta (CVD) indicator.
8+
///
9+
/// This indicator calculates the cumulative difference between buying and selling volume
10+
/// for each price movement. It is often used to gauge the strength of a trend by analyzing
11+
/// whether volume is predominantly buying or selling.
12+
///
13+
/// # Formula
14+
/// For each period:
15+
/// - If the price movement is positive (close > open), the buying volume is calculated as a fraction of the total volume.
16+
/// - If the price movement is negative (close < open), the selling volume is calculated as a fraction of the total volume.
17+
/// - The delta is calculated as `volume * rate` and accumulated over time.
18+
///
19+
/// # Example
20+
/// ```
21+
/// use ta::{Next, indicators::CumulativeVolumeDelta, DataItem};
22+
/// let mut cvd = CumulativeVolumeDelta::new();
23+
///
24+
/// // Assume `data` is a struct implementing `Open`, `Close`, and `Volume` traits.
25+
/// let data = DataItem::builder().open(1.0).high(1.0).low(1.0).close(1.0).volume(1.0).build().unwrap();
26+
/// let delta = cvd.next(&data);
27+
/// println!("Current CVD: {}", cvd);
28+
/// ```
29+
#[doc(alias = "CVD")]
30+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31+
#[derive(Debug, Clone)]
32+
pub struct CumulativeVolumeDelta {
33+
rate: f64,
34+
cumulative_delta: f64,
35+
history: Vec<f64>,
36+
}
37+
38+
impl CumulativeVolumeDelta {
39+
/// Creates a new `CumulativeVolumeDelta` instance.
40+
///
41+
/// # Returns
42+
/// A new instance of `CumulativeVolumeDelta` with `cumulative_delta` set to 0.0 and an empty history.
43+
pub fn new() -> Self {
44+
Self {
45+
rate: 0.5,
46+
cumulative_delta: 0.0,
47+
history: Vec::new(),
48+
}
49+
}
50+
51+
/// The cumulative sum of volume deltas.
52+
pub fn cumulative_delta(&self) -> f64 {
53+
self.cumulative_delta
54+
}
55+
56+
/// Historical record of individual volume deltas.
57+
pub fn history(&self) -> Vec<f64> {
58+
self.history.clone()
59+
}
60+
}
61+
62+
impl<T: Open + Close + Volume> Next<&T> for CumulativeVolumeDelta {
63+
type Output = f64;
64+
65+
fn next(&mut self, input: &T) -> Self::Output {
66+
let (open, close, volume) = (input.open(), input.close(), input.volume());
67+
let delta = if close > open {
68+
// Positive price movement: assume buying pressure
69+
volume * self.rate
70+
} else if close < open {
71+
// Negative price movement: assume selling pressure
72+
-volume * self.rate
73+
} else {
74+
// No price movement: delta is zero
75+
0.0
76+
};
77+
78+
self.cumulative_delta += delta;
79+
self.history.push(delta);
80+
delta
81+
}
82+
}
83+
84+
impl Reset for CumulativeVolumeDelta {
85+
fn reset(&mut self) {
86+
self.cumulative_delta = 0.0;
87+
self.history.clear();
88+
}
89+
}
90+
91+
impl Default for CumulativeVolumeDelta {
92+
fn default() -> Self {
93+
Self::new()
94+
}
95+
}
96+
97+
impl fmt::Display for CumulativeVolumeDelta {
98+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99+
write!(f, "CVD({})", self.cumulative_delta)
100+
}
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use super::*;
106+
use crate::test_helper::*;
107+
108+
#[test]
109+
fn test_new_cvd() {
110+
let cvd = CumulativeVolumeDelta::new();
111+
assert_eq!(cvd.cumulative_delta, 0.0);
112+
assert!(cvd.history.is_empty());
113+
}
114+
115+
#[test]
116+
fn test_next_positive_price_movement() {
117+
let mut cvd = CumulativeVolumeDelta::new();
118+
let data = Bar::new().open(100.0).close(110.0).volume(1000.0);
119+
let delta = cvd.next(&data);
120+
assert!(delta > 0.0);
121+
assert_eq!(cvd.history.len(), 1);
122+
assert_eq!(cvd.cumulative_delta, delta);
123+
}
124+
125+
#[test]
126+
fn test_next_negative_price_movement() {
127+
let mut cvd = CumulativeVolumeDelta::new();
128+
let data = Bar::new().open(100.0).close(90.0).volume(1000.0);
129+
let delta = cvd.next(&data);
130+
assert!(delta < 0.0);
131+
assert_eq!(cvd.history.len(), 1);
132+
assert_eq!(cvd.cumulative_delta, delta);
133+
}
134+
135+
#[test]
136+
fn test_next_no_price_movement() {
137+
let mut cvd = CumulativeVolumeDelta::new();
138+
let data = Bar::new().open(100.0).close(100.0).volume(1000.0);
139+
let delta = cvd.next(&data);
140+
assert_eq!(delta, 0.0);
141+
assert_eq!(cvd.history.len(), 1);
142+
assert_eq!(cvd.cumulative_delta, 0.0);
143+
}
144+
145+
#[test]
146+
fn test_cumulative_delta() {
147+
let mut cvd = CumulativeVolumeDelta::new();
148+
let data1 = Bar::new().open(100.0).close(110.0).volume(1000.0);
149+
let data2 = Bar::new().open(110.0).close(100.0).volume(1000.0);
150+
let delta1 = cvd.next(&data1);
151+
let delta2 = cvd.next(&data2);
152+
assert_eq!(cvd.cumulative_delta, delta1 + delta2);
153+
assert_eq!(cvd.history.len(), 2);
154+
}
155+
156+
#[test]
157+
fn test_reset() {
158+
let mut cvd = CumulativeVolumeDelta::new();
159+
let data = Bar::new().open(100.0).close(110.0).volume(1000.0);
160+
cvd.next(&data);
161+
assert_eq!(cvd.history.len(), 1);
162+
assert_ne!(cvd.cumulative_delta, 0.0);
163+
164+
cvd.reset();
165+
assert_eq!(cvd.cumulative_delta, 0.0);
166+
assert!(cvd.history.is_empty());
167+
}
168+
}

src/indicators/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,8 @@ pub use self::money_flow_index::MoneyFlowIndex;
6868
mod on_balance_volume;
6969
pub use self::on_balance_volume::OnBalanceVolume;
7070

71+
mod cumulative_volume_delta;
72+
pub use self::cumulative_volume_delta::CumulativeVolumeDelta;
73+
7174
mod parabolic_stop_and_reverse;
7275
pub use self::parabolic_stop_and_reverse::ParabolicStopAndReverse;

src/test_helper.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ impl Bar {
2020
}
2121
}
2222

23-
//pub fn open<T: Into<f64>>(mut self, val :T ) -> Self {
24-
// self.open = val.into();
25-
// self
26-
//}
23+
pub fn open<T: Into<f64>>(mut self, val :T ) -> Self {
24+
self.open = val.into();
25+
self
26+
}
2727

2828
pub fn high<T: Into<f64>>(mut self, val: T) -> Self {
2929
self.high = val.into();

0 commit comments

Comments
 (0)