wickra_core/indicators/
anchored_vwap.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
43pub struct AnchoredVwap {
44 sum_pv: f64,
45 sum_v: f64,
46 has_emitted: bool,
47 pending_anchor: bool,
48}
49
50impl AnchoredVwap {
51 pub const fn new() -> Self {
53 Self {
54 sum_pv: 0.0,
55 sum_v: 0.0,
56 has_emitted: false,
57 pending_anchor: false,
58 }
59 }
60
61 pub fn set_anchor(&mut self) {
65 self.pending_anchor = true;
66 }
67
68 pub fn value(&self) -> Option<f64> {
71 if self.sum_v == 0.0 {
72 None
73 } else {
74 Some(self.sum_pv / self.sum_v)
75 }
76 }
77}
78
79impl Indicator for AnchoredVwap {
80 type Input = Candle;
81 type Output = f64;
82
83 #[inline]
84 fn update(&mut self, candle: Candle) -> Option<f64> {
85 if self.pending_anchor {
86 self.sum_pv = 0.0;
88 self.sum_v = 0.0;
89 self.has_emitted = false;
90 self.pending_anchor = false;
91 }
92 let tp = candle.typical_price();
93 self.sum_pv += tp * candle.volume;
94 self.sum_v += candle.volume;
95 if self.sum_v == 0.0 {
96 return None;
97 }
98 self.has_emitted = true;
99 Some(self.sum_pv / self.sum_v)
100 }
101
102 fn reset(&mut self) {
103 self.sum_pv = 0.0;
104 self.sum_v = 0.0;
105 self.has_emitted = false;
106 self.pending_anchor = false;
107 }
108
109 #[inline]
110 fn warmup_period(&self) -> usize {
111 1
112 }
113
114 #[inline]
115 fn is_ready(&self) -> bool {
116 self.has_emitted
117 }
118
119 #[inline]
120 fn name(&self) -> &'static str {
121 "AnchoredVWAP"
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::traits::BatchExt;
129 use approx::assert_relative_eq;
130
131 fn c(price: f64, volume: f64, ts: i64) -> Candle {
132 Candle::new(price, price, price, price, volume, ts).unwrap()
133 }
134
135 #[test]
136 fn accessors_and_metadata() {
137 let v = AnchoredVwap::new();
138 assert_eq!(v.name(), "AnchoredVWAP");
139 assert_eq!(v.warmup_period(), 1);
140 assert_eq!(v.value(), None);
141 }
142
143 #[test]
144 fn first_bar_with_zero_volume_returns_none() {
145 let mut v = AnchoredVwap::new();
146 assert_eq!(v.update(c(50.0, 0.0, 0)), None);
147 assert!(!v.is_ready());
148 assert_relative_eq!(v.update(c(10.0, 4.0, 1)).unwrap(), 10.0, epsilon = 1e-12);
150 }
151
152 #[test]
153 fn equal_volumes_yield_mean_typical_price() {
154 let mut v = AnchoredVwap::new();
156 let out = v.batch(&[c(10.0, 1.0, 0), c(20.0, 1.0, 1), c(30.0, 1.0, 2)]);
157 assert_relative_eq!(out[2].unwrap(), 20.0, epsilon = 1e-12);
158 }
159
160 #[test]
161 fn set_anchor_clears_old_window() {
162 let mut v = AnchoredVwap::new();
165 v.batch(&[c(10.0, 1.0, 0), c(10.0, 1.0, 1), c(10.0, 1.0, 2)]);
166 assert_relative_eq!(v.value().unwrap(), 10.0, epsilon = 1e-12);
167 v.set_anchor();
168 let after = v.update(c(100.0, 5.0, 3)).unwrap();
169 assert_relative_eq!(after, 100.0, epsilon = 1e-12);
170 }
171
172 #[test]
173 fn set_anchor_before_first_bar_acts_as_normal_first_bar() {
174 let mut v = AnchoredVwap::new();
177 v.set_anchor();
178 assert_relative_eq!(v.update(c(42.0, 2.0, 0)).unwrap(), 42.0, epsilon = 1e-12);
179 }
180
181 #[test]
182 fn weighted_average_reference() {
183 let mut v = AnchoredVwap::new();
185 let out = v.batch(&[c(10.0, 1.0, 0), c(20.0, 3.0, 1)]);
186 assert_relative_eq!(out[1].unwrap(), 17.5, epsilon = 1e-12);
187 }
188
189 #[test]
190 fn batch_equals_streaming() {
191 let candles: Vec<Candle> = (1..30).map(|i| c(f64::from(i), 1.0, i.into())).collect();
192 let mut a = AnchoredVwap::new();
193 let mut b = AnchoredVwap::new();
194 assert_eq!(
195 a.batch(&candles),
196 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
197 );
198 }
199
200 #[test]
201 fn reset_clears_state() {
202 let mut v = AnchoredVwap::new();
203 v.batch(&[c(10.0, 1.0, 0), c(20.0, 1.0, 1)]);
204 assert!(v.is_ready());
205 v.reset();
206 assert!(!v.is_ready());
207 assert_eq!(v.value(), None);
208 assert_relative_eq!(v.update(c(50.0, 1.0, 2)).unwrap(), 50.0, epsilon = 1e-12);
210 }
211}