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