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 fn update(&mut self, section: CrossSection) -> Option<f64> {
63 let net = section.advancing_volume() - section.declining_volume();
64 let total = section.total_volume().max(f64::MIN_POSITIVE);
65 self.index += net / total;
66 self.has_emitted = true;
67 Some(self.index)
68 }
69
70 fn reset(&mut self) {
71 self.index = 0.0;
72 self.has_emitted = false;
73 }
74
75 fn warmup_period(&self) -> usize {
76 1
77 }
78
79 fn is_ready(&self) -> bool {
80 self.has_emitted
81 }
82
83 fn name(&self) -> &'static str {
84 "CumulativeVolumeIndex"
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::cross_section::Member;
92 use crate::traits::BatchExt;
93
94 fn tick(items: &[(f64, f64)]) -> CrossSection {
95 CrossSection::new(
96 items
97 .iter()
98 .map(|&(change, volume)| Member::new(change, volume, false, false))
99 .collect(),
100 0,
101 )
102 .unwrap()
103 }
104
105 #[test]
106 fn accessors_and_metadata() {
107 let cvi = CumulativeVolumeIndex::new();
108 assert_eq!(cvi.name(), "CumulativeVolumeIndex");
109 assert_eq!(cvi.warmup_period(), 1);
110 assert!(!cvi.is_ready());
111 }
112
113 #[test]
114 fn first_tick_emits_normalised_net() {
115 let mut cvi = CumulativeVolumeIndex::new();
116 assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
117 assert!(cvi.is_ready());
118 }
119
120 #[test]
121 fn index_accumulates_normalised_shares() {
122 let mut cvi = CumulativeVolumeIndex::new();
123 assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
124 assert_eq!(cvi.update(tick(&[(1.0, 60.0), (-1.0, 60.0)])), Some(0.5));
126 }
127
128 #[test]
129 fn zero_total_volume_leaves_index_unchanged() {
130 let mut cvi = CumulativeVolumeIndex::new();
131 cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
132 assert_eq!(cvi.update(tick(&[(0.0, 0.0)])), Some(0.5));
134 }
135
136 #[test]
137 fn reset_clears_state() {
138 let mut cvi = CumulativeVolumeIndex::new();
139 cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
140 assert!(cvi.is_ready());
141 cvi.reset();
142 assert!(!cvi.is_ready());
143 assert_eq!(cvi.update(tick(&[(1.0, 100.0)])), Some(1.0));
144 }
145
146 #[test]
147 fn batch_equals_streaming() {
148 let sections = vec![
149 tick(&[(1.0, 150.0), (-1.0, 50.0)]),
150 tick(&[(1.0, 60.0), (-1.0, 60.0)]),
151 tick(&[(0.0, 0.0)]),
152 ];
153 let mut a = CumulativeVolumeIndex::new();
154 let mut b = CumulativeVolumeIndex::new();
155 assert_eq!(
156 a.batch(§ions),
157 sections
158 .iter()
159 .map(|s| b.update(s.clone()))
160 .collect::<Vec<_>>()
161 );
162 }
163}