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