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