wickra_core/indicators/
cumulative_volume_index.rs1use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
42pub struct CumulativeVolumeIndex {
43 index: f64,
44 has_emitted: bool,
45}
46
47impl CumulativeVolumeIndex {
48 #[must_use]
50 pub const fn new() -> Self {
51 Self {
52 index: 0.0,
53 has_emitted: false,
54 }
55 }
56}
57
58impl Indicator for CumulativeVolumeIndex {
59 type Input = CrossSection;
60 type Output = f64;
61
62 #[inline]
63 fn update(&mut self, section: CrossSection) -> Option<f64> {
64 let net = section.advancing_volume() - section.declining_volume();
65 let total = section.total_volume().max(f64::MIN_POSITIVE);
66 self.index += net / total;
67 self.has_emitted = true;
68 Some(self.index)
69 }
70
71 fn reset(&mut self) {
72 self.index = 0.0;
73 self.has_emitted = false;
74 }
75
76 #[inline]
77 fn warmup_period(&self) -> usize {
78 1
79 }
80
81 #[inline]
82 fn is_ready(&self) -> bool {
83 self.has_emitted
84 }
85
86 #[inline]
87 fn name(&self) -> &'static str {
88 "CumulativeVolumeIndex"
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::cross_section::Member;
96 use crate::traits::BatchExt;
97
98 fn tick(items: &[(f64, f64)]) -> CrossSection {
99 CrossSection::new(
100 items
101 .iter()
102 .map(|&(change, volume)| Member::new(change, volume, false, false))
103 .collect(),
104 0,
105 )
106 .unwrap()
107 }
108
109 #[test]
110 fn accessors_and_metadata() {
111 let cvi = CumulativeVolumeIndex::new();
112 assert_eq!(cvi.name(), "CumulativeVolumeIndex");
113 assert_eq!(cvi.warmup_period(), 1);
114 assert!(!cvi.is_ready());
115 }
116
117 #[test]
118 fn first_tick_emits_normalised_net() {
119 let mut cvi = CumulativeVolumeIndex::new();
120 assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
121 assert!(cvi.is_ready());
122 }
123
124 #[test]
125 fn index_accumulates_normalised_shares() {
126 let mut cvi = CumulativeVolumeIndex::new();
127 assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
128 assert_eq!(cvi.update(tick(&[(1.0, 60.0), (-1.0, 60.0)])), Some(0.5));
130 }
131
132 #[test]
133 fn zero_total_volume_leaves_index_unchanged() {
134 let mut cvi = CumulativeVolumeIndex::new();
135 cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
136 assert_eq!(cvi.update(tick(&[(0.0, 0.0)])), Some(0.5));
138 }
139
140 #[test]
141 fn reset_clears_state() {
142 let mut cvi = CumulativeVolumeIndex::new();
143 cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
144 assert!(cvi.is_ready());
145 cvi.reset();
146 assert!(!cvi.is_ready());
147 assert_eq!(cvi.update(tick(&[(1.0, 100.0)])), Some(1.0));
148 }
149
150 #[test]
151 fn batch_equals_streaming() {
152 let sections = vec![
153 tick(&[(1.0, 150.0), (-1.0, 50.0)]),
154 tick(&[(1.0, 60.0), (-1.0, 60.0)]),
155 tick(&[(0.0, 0.0)]),
156 ];
157 let mut a = CumulativeVolumeIndex::new();
158 let mut b = CumulativeVolumeIndex::new();
159 assert_eq!(
160 a.batch(§ions),
161 sections
162 .iter()
163 .map(|s| b.update(s.clone()))
164 .collect::<Vec<_>>()
165 );
166 }
167}