1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Cumulative Volume Delta — running sum of signed trade volume.
use crate::microstructure::Trade;
use crate::traits::Indicator;
/// Cumulative Volume Delta (CVD) — the running sum of [signed volume].
///
/// ```text
/// CVDₜ = CVDₜ₋₁ + sizeₜ · (+1 if buy, −1 if sell)
/// ```
///
/// CVD is an unbounded running total: a rising line signals net buying pressure
/// over the session, a falling line net selling. Divergence between CVD and
/// price is a classic absorption / exhaustion signal. Call [`reset`] at the
/// start of each session to re-anchor the cumulative total at zero.
///
/// `Input = Trade`, `Output = f64`. Ready after the first trade.
///
/// [signed volume]: crate::SignedVolume
/// [`reset`]: crate::Indicator::reset
///
/// # Example
///
/// ```
/// use wickra_core::{CumulativeVolumeDelta, Indicator, Side, Trade};
///
/// let mut cvd = CumulativeVolumeDelta::new();
/// assert_eq!(cvd.update(Trade::new(100.0, 5.0, Side::Buy, 0).unwrap()), Some(5.0));
/// assert_eq!(cvd.update(Trade::new(100.0, 2.0, Side::Sell, 1).unwrap()), Some(3.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct CumulativeVolumeDelta {
cumulative: f64,
has_emitted: bool,
}
impl CumulativeVolumeDelta {
/// Construct a new CVD indicator with a zero running total.
pub const fn new() -> Self {
Self {
cumulative: 0.0,
has_emitted: false,
}
}
}
impl Indicator for CumulativeVolumeDelta {
type Input = Trade;
type Output = f64;
#[inline]
fn update(&mut self, trade: Trade) -> Option<f64> {
self.has_emitted = true;
self.cumulative += trade.size * trade.side.sign();
Some(self.cumulative)
}
fn reset(&mut self) {
self.cumulative = 0.0;
self.has_emitted = false;
}
#[inline]
fn warmup_period(&self) -> usize {
1
}
#[inline]
fn is_ready(&self) -> bool {
self.has_emitted
}
#[inline]
fn name(&self) -> &'static str {
"CumulativeVolumeDelta"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::microstructure::Side;
use crate::traits::BatchExt;
fn trade(size: f64, side: Side, ts: i64) -> Trade {
Trade::new(100.0, size, side, ts).unwrap()
}
#[test]
fn accessors_and_metadata() {
let cvd = CumulativeVolumeDelta::new();
assert_eq!(cvd.name(), "CumulativeVolumeDelta");
assert_eq!(cvd.warmup_period(), 1);
assert!(!cvd.is_ready());
}
#[test]
fn accumulates_signed_volume() {
let mut cvd = CumulativeVolumeDelta::new();
assert_eq!(cvd.update(trade(5.0, Side::Buy, 0)), Some(5.0));
assert_eq!(cvd.update(trade(2.0, Side::Sell, 1)), Some(3.0));
assert_eq!(cvd.update(trade(4.0, Side::Sell, 2)), Some(-1.0));
assert!(cvd.is_ready());
}
#[test]
fn batch_equals_streaming() {
let trades: Vec<Trade> = (0..20)
.map(|i| {
let side = if i % 3 == 0 { Side::Sell } else { Side::Buy };
trade(1.0 + (i % 4) as f64, side, i)
})
.collect();
let mut a = CumulativeVolumeDelta::new();
let mut b = CumulativeVolumeDelta::new();
assert_eq!(
a.batch(&trades),
trades.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_re_anchors_at_zero() {
let mut cvd = CumulativeVolumeDelta::new();
cvd.update(trade(5.0, Side::Buy, 0));
cvd.reset();
assert!(!cvd.is_ready());
// After reset the running total starts again from zero.
assert_eq!(cvd.update(trade(2.0, Side::Buy, 1)), Some(2.0));
}
}