wickra_core/indicators/
wad.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
44pub struct Wad {
45 prev_close: Option<f64>,
46 line: f64,
47 last: Option<f64>,
48}
49
50impl Wad {
51 #[must_use]
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub const fn value(&self) -> Option<f64> {
59 self.last
60 }
61}
62
63impl Indicator for Wad {
64 type Input = Candle;
65 type Output = f64;
66
67 fn update(&mut self, candle: Candle) -> Option<f64> {
68 let Some(prev_close) = self.prev_close else {
69 self.prev_close = Some(candle.close);
70 return None;
71 };
72 let ad = if candle.close > prev_close {
73 candle.close - candle.low.min(prev_close)
74 } else if candle.close < prev_close {
75 candle.close - candle.high.max(prev_close)
76 } else {
77 0.0
78 };
79 self.line += ad;
80 self.prev_close = Some(candle.close);
81 self.last = Some(self.line);
82 Some(self.line)
83 }
84
85 fn reset(&mut self) {
86 self.prev_close = None;
87 self.line = 0.0;
88 self.last = None;
89 }
90
91 fn warmup_period(&self) -> usize {
92 2
95 }
96
97 fn is_ready(&self) -> bool {
98 self.last.is_some()
99 }
100
101 fn name(&self) -> &'static str {
102 "Wad"
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::BatchExt;
110 use approx::assert_relative_eq;
111
112 fn candle(high: f64, low: f64, close: f64) -> Candle {
113 Candle::new_unchecked(low, high, low, close, 1_000.0, 0)
114 }
115
116 #[test]
117 fn accessors_and_metadata() {
118 let wad = Wad::new();
119 assert_eq!(wad.warmup_period(), 2);
120 assert_eq!(wad.name(), "Wad");
121 assert!(!wad.is_ready());
122 assert_eq!(wad.value(), None);
123 }
124
125 #[test]
126 fn first_bar_seeds_without_output() {
127 let mut wad = Wad::new();
128 assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
129 assert!(wad.update(candle(102.0, 100.0, 101.0)).is_some());
130 }
131
132 #[test]
133 fn up_close_accumulates() {
134 let mut wad = Wad::new();
137 wad.update(candle(101.0, 99.0, 100.0));
138 let v = wad.update(candle(102.0, 100.0, 101.0)).unwrap();
139 assert_relative_eq!(v, 1.0, epsilon = 1e-9);
140 }
141
142 #[test]
143 fn down_close_distributes() {
144 let mut wad = Wad::new();
147 wad.update(candle(102.0, 100.0, 100.0));
148 let v = wad.update(candle(101.0, 98.0, 99.0)).unwrap();
149 assert_relative_eq!(v, -2.0, epsilon = 1e-9);
150 }
151
152 #[test]
153 fn unchanged_close_adds_nothing() {
154 let mut wad = Wad::new();
155 wad.update(candle(101.0, 99.0, 100.0));
156 let v = wad.update(candle(105.0, 95.0, 100.0)).unwrap();
157 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
158 }
159
160 #[test]
161 fn pure_uptrend_is_monotone() {
162 let mut wad = Wad::new();
163 let candles: Vec<Candle> = (0..30)
164 .map(|i| {
165 let base = 100.0 + f64::from(i);
166 candle(base + 1.0, base - 1.0, base)
167 })
168 .collect();
169 let mut prev = f64::NEG_INFINITY;
170 for v in wad.batch(&candles).into_iter().flatten() {
171 assert!(v >= prev, "WAD must rise in an uptrend");
172 prev = v;
173 }
174 }
175
176 #[test]
177 fn reset_clears_state() {
178 let mut wad = Wad::new();
179 let candles: Vec<Candle> = (0..10)
180 .map(|i| {
181 let base = 100.0 + f64::from(i);
182 candle(base + 1.0, base - 1.0, base)
183 })
184 .collect();
185 wad.batch(&candles);
186 assert!(wad.is_ready());
187 wad.reset();
188 assert!(!wad.is_ready());
189 assert_eq!(wad.value(), None);
190 assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
191 }
192
193 #[test]
194 fn batch_equals_streaming() {
195 let candles: Vec<Candle> = (0..80)
196 .map(|i| {
197 let base = 100.0 + (f64::from(i) * 0.3).sin() * 8.0;
198 candle(base + 2.0, base - 2.0, base + 0.5)
199 })
200 .collect();
201 let batch = Wad::new().batch(&candles);
202 let mut b = Wad::new();
203 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
204 assert_eq!(batch, streamed);
205 }
206}