wickra_core/indicators/
session_vwap.rs1use crate::calendar::civil_from_timestamp;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
35pub struct SessionVwap {
36 utc_offset_minutes: i32,
37 day_key: Option<(i64, u32, u32)>,
38 cum_pv: f64,
39 cum_volume: f64,
40 last: Option<f64>,
41}
42
43impl SessionVwap {
44 pub const fn new(utc_offset_minutes: i32) -> Self {
53 Self {
54 utc_offset_minutes,
55 day_key: None,
56 cum_pv: 0.0,
57 cum_volume: 0.0,
58 last: None,
59 }
60 }
61
62 pub const fn utc_offset_minutes(&self) -> i32 {
64 self.utc_offset_minutes
65 }
66
67 pub const fn value(&self) -> Option<f64> {
69 self.last
70 }
71}
72
73impl Indicator for SessionVwap {
74 type Input = Candle;
75 type Output = f64;
76
77 #[inline]
78 fn update(&mut self, candle: Candle) -> Option<f64> {
79 let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
80 let key = (civil.year, civil.month, civil.day);
81 if self.day_key != Some(key) {
82 self.day_key = Some(key);
83 self.cum_pv = 0.0;
84 self.cum_volume = 0.0;
85 }
86 let typical = (candle.high + candle.low + candle.close) / 3.0;
87 self.cum_pv += typical * candle.volume;
88 self.cum_volume += candle.volume;
89 let vwap = if self.cum_volume > 0.0 {
90 self.cum_pv / self.cum_volume
91 } else {
92 typical
93 };
94 self.last = Some(vwap);
95 Some(vwap)
96 }
97
98 fn reset(&mut self) {
99 self.day_key = None;
100 self.cum_pv = 0.0;
101 self.cum_volume = 0.0;
102 self.last = None;
103 }
104
105 #[inline]
106 fn warmup_period(&self) -> usize {
107 1
108 }
109
110 #[inline]
111 fn is_ready(&self) -> bool {
112 self.last.is_some()
113 }
114
115 #[inline]
116 fn name(&self) -> &'static str {
117 "SessionVwap"
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::traits::BatchExt;
125 use approx::assert_relative_eq;
126
127 const HOUR: i64 = 3_600_000;
128
129 fn c(price: f64, volume: f64, ts: i64) -> Candle {
130 Candle::new(price, price, price, price, volume, ts).unwrap()
131 }
132
133 #[test]
134 fn metadata_and_accessors() {
135 let vwap = SessionVwap::new(-480);
136 assert_eq!(vwap.utc_offset_minutes(), -480);
137 assert_eq!(vwap.name(), "SessionVwap");
138 assert_eq!(vwap.warmup_period(), 1);
139 assert!(!vwap.is_ready());
140 assert!(vwap.value().is_none());
141 }
142
143 #[test]
144 fn volume_weights_the_average() {
145 let mut vwap = SessionVwap::new(0);
146 let first = vwap.update(c(100.0, 10.0, 0)).unwrap();
147 assert_relative_eq!(first, 100.0);
148 assert!(vwap.is_ready());
149 let second = vwap.update(c(110.0, 30.0, HOUR)).unwrap();
150 assert_relative_eq!(second, 107.5);
151 }
152
153 #[test]
154 fn zero_volume_session_falls_back_to_typical() {
155 let mut vwap = SessionVwap::new(0);
156 let v = vwap.update(c(100.0, 0.0, 0)).unwrap();
157 assert_relative_eq!(v, 100.0);
158 let v2 = vwap.update(c(120.0, 0.0, HOUR)).unwrap();
159 assert_relative_eq!(v2, 120.0);
160 }
161
162 #[test]
163 fn re_anchors_on_new_day() {
164 let mut vwap = SessionVwap::new(0);
165 vwap.update(c(100.0, 10.0, 0));
166 vwap.update(c(110.0, 30.0, HOUR));
167 let next = vwap.update(c(200.0, 5.0, 24 * HOUR)).unwrap();
169 assert_relative_eq!(next, 200.0);
170 }
171
172 #[test]
173 fn typical_price_uses_high_low_close() {
174 let mut vwap = SessionVwap::new(0);
175 let candle = Candle::new(100.0, 120.0, 90.0, 102.0, 10.0, 0).unwrap();
177 let v = vwap.update(candle).unwrap();
178 assert_relative_eq!(v, 104.0);
179 }
180
181 #[test]
182 fn reset_clears_state() {
183 let mut vwap = SessionVwap::new(0);
184 vwap.update(c(100.0, 10.0, 0));
185 vwap.reset();
186 assert!(!vwap.is_ready());
187 assert!(vwap.value().is_none());
188 let after = vwap.update(c(50.0, 1.0, HOUR)).unwrap();
189 assert_relative_eq!(after, 50.0);
190 }
191
192 #[test]
193 fn batch_equals_streaming() {
194 let candles: Vec<Candle> = (0..30)
195 .map(|i| {
196 c(
197 100.0 + f64::from(i),
198 1.0 + f64::from(i % 4),
199 i64::from(i) * HOUR,
200 )
201 })
202 .collect();
203 let mut a = SessionVwap::new(0);
204 let mut b = SessionVwap::new(0);
205 assert_eq!(
206 a.batch(&candles),
207 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
208 );
209 }
210}