wickra_core/indicators/
cumulative_volume_index.rs1use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
35pub struct CumulativeVolumeIndex {
36 index: f64,
37 has_emitted: bool,
38}
39
40impl CumulativeVolumeIndex {
41 #[must_use]
43 pub const fn new() -> Self {
44 Self {
45 index: 0.0,
46 has_emitted: false,
47 }
48 }
49}
50
51impl Indicator for CumulativeVolumeIndex {
52 type Input = CrossSection;
53 type Output = f64;
54
55 #[inline]
56 fn update(&mut self, section: CrossSection) -> Option<f64> {
57 let net = section.advancing_volume() - section.declining_volume();
58 self.index += net;
59 self.has_emitted = true;
60 Some(self.index)
61 }
62
63 fn reset(&mut self) {
64 self.index = 0.0;
65 self.has_emitted = false;
66 }
67
68 #[inline]
69 fn warmup_period(&self) -> usize {
70 1
71 }
72
73 #[inline]
74 fn is_ready(&self) -> bool {
75 self.has_emitted
76 }
77
78 #[inline]
79 fn name(&self) -> &'static str {
80 "CumulativeVolumeIndex"
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::cross_section::Member;
88 use crate::traits::BatchExt;
89
90 fn tick(items: &[(f64, f64)]) -> CrossSection {
91 CrossSection::new(
92 items
93 .iter()
94 .map(|&(change, volume)| Member::new(change, volume, false, false))
95 .collect(),
96 0,
97 )
98 .unwrap()
99 }
100
101 #[test]
102 fn accessors_and_metadata() {
103 let cvi = CumulativeVolumeIndex::new();
104 assert_eq!(cvi.name(), "CumulativeVolumeIndex");
105 assert_eq!(cvi.warmup_period(), 1);
106 assert!(!cvi.is_ready());
107 }
108
109 #[test]
110 fn first_tick_emits_net_volume() {
111 let mut cvi = CumulativeVolumeIndex::new();
112 assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(100.0));
113 assert!(cvi.is_ready());
114 }
115
116 #[test]
117 fn index_accumulates_net_volume() {
118 let mut cvi = CumulativeVolumeIndex::new();
119 assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(100.0));
120 assert_eq!(cvi.update(tick(&[(1.0, 60.0), (-1.0, 60.0)])), Some(100.0));
122 }
123
124 #[test]
125 fn zero_volume_leaves_index_unchanged() {
126 let mut cvi = CumulativeVolumeIndex::new();
127 cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
128 assert_eq!(cvi.update(tick(&[(0.0, 0.0)])), Some(100.0));
130 }
131
132 #[test]
133 fn reset_clears_state() {
134 let mut cvi = CumulativeVolumeIndex::new();
135 cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
136 assert!(cvi.is_ready());
137 cvi.reset();
138 assert!(!cvi.is_ready());
139 assert_eq!(cvi.update(tick(&[(1.0, 100.0)])), Some(100.0));
140 }
141
142 #[test]
143 fn batch_equals_streaming() {
144 let sections = vec![
145 tick(&[(1.0, 150.0), (-1.0, 50.0)]),
146 tick(&[(1.0, 60.0), (-1.0, 60.0)]),
147 tick(&[(0.0, 0.0)]),
148 ];
149 let mut a = CumulativeVolumeIndex::new();
150 let mut b = CumulativeVolumeIndex::new();
151 assert_eq!(
152 a.batch(§ions),
153 sections
154 .iter()
155 .map(|s| b.update(s.clone()))
156 .collect::<Vec<_>>()
157 );
158 }
159
160 #[test]
161 fn warmup_first_value_at_index_zero() {
162 let mut cvi = CumulativeVolumeIndex::new();
163 assert_eq!(cvi.warmup_period(), 1);
164 assert!(cvi.update(tick(&[(1.0, 10.0)])).is_some());
166 }
167
168 #[test]
169 fn hand_computed_multi_member_series_ignores_unchanged() {
170 let mut cvi = CumulativeVolumeIndex::new();
171 assert_eq!(
173 cvi.update(tick(&[
174 (2.0, 100.0),
175 (0.5, 20.0),
176 (-1.0, 30.0),
177 (0.0, 500.0)
178 ])),
179 Some(90.0)
180 );
181 assert_eq!(
183 cvi.update(tick(&[(1.0, 10.0), (-0.5, 70.0), (-3.0, 40.0)])),
184 Some(-10.0)
185 );
186 assert_eq!(cvi.update(tick(&[(0.0, 1_000.0)])), Some(-10.0));
188 assert_eq!(cvi.update(tick(&[(1.0, 25.0)])), Some(15.0));
190 }
191
192 #[test]
193 fn reset_replays_identically_to_fresh_instance() {
194 let sections = vec![
195 tick(&[(1.0, 150.0), (-1.0, 50.0)]),
196 tick(&[(-1.0, 80.0), (0.0, 5.0)]),
197 tick(&[(1.0, 7.5), (-2.0, 2.5)]),
198 ];
199 let mut cvi = CumulativeVolumeIndex::new();
200 let first = cvi.batch(§ions);
201 cvi.reset();
202 let second = cvi.batch(§ions);
203 let fresh = CumulativeVolumeIndex::default().batch(§ions);
204 assert_eq!(first, second);
205 assert_eq!(second, fresh);
206 }
207}