use crate::calendar::civil_from_timestamp;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct SessionVwap {
utc_offset_minutes: i32,
day_key: Option<(i64, u32, u32)>,
cum_pv: f64,
cum_volume: f64,
last: Option<f64>,
}
impl SessionVwap {
pub const fn new(utc_offset_minutes: i32) -> Self {
Self {
utc_offset_minutes,
day_key: None,
cum_pv: 0.0,
cum_volume: 0.0,
last: None,
}
}
pub const fn utc_offset_minutes(&self) -> i32 {
self.utc_offset_minutes
}
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for SessionVwap {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
let key = (civil.year, civil.month, civil.day);
if self.day_key != Some(key) {
self.day_key = Some(key);
self.cum_pv = 0.0;
self.cum_volume = 0.0;
}
let typical = (candle.high + candle.low + candle.close) / 3.0;
self.cum_pv += typical * candle.volume;
self.cum_volume += candle.volume;
let vwap = if self.cum_volume > 0.0 {
self.cum_pv / self.cum_volume
} else {
typical
};
self.last = Some(vwap);
Some(vwap)
}
fn reset(&mut self) {
self.day_key = None;
self.cum_pv = 0.0;
self.cum_volume = 0.0;
self.last = None;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"SessionVwap"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
const HOUR: i64 = 3_600_000;
fn c(price: f64, volume: f64, ts: i64) -> Candle {
Candle::new(price, price, price, price, volume, ts).unwrap()
}
#[test]
fn metadata_and_accessors() {
let vwap = SessionVwap::new(-480);
assert_eq!(vwap.utc_offset_minutes(), -480);
assert_eq!(vwap.name(), "SessionVwap");
assert_eq!(vwap.warmup_period(), 1);
assert!(!vwap.is_ready());
assert!(vwap.value().is_none());
}
#[test]
fn volume_weights_the_average() {
let mut vwap = SessionVwap::new(0);
let first = vwap.update(c(100.0, 10.0, 0)).unwrap();
assert_relative_eq!(first, 100.0);
assert!(vwap.is_ready());
let second = vwap.update(c(110.0, 30.0, HOUR)).unwrap();
assert_relative_eq!(second, 107.5);
}
#[test]
fn zero_volume_session_falls_back_to_typical() {
let mut vwap = SessionVwap::new(0);
let v = vwap.update(c(100.0, 0.0, 0)).unwrap();
assert_relative_eq!(v, 100.0);
let v2 = vwap.update(c(120.0, 0.0, HOUR)).unwrap();
assert_relative_eq!(v2, 120.0);
}
#[test]
fn re_anchors_on_new_day() {
let mut vwap = SessionVwap::new(0);
vwap.update(c(100.0, 10.0, 0));
vwap.update(c(110.0, 30.0, HOUR));
let next = vwap.update(c(200.0, 5.0, 24 * HOUR)).unwrap();
assert_relative_eq!(next, 200.0);
}
#[test]
fn typical_price_uses_high_low_close() {
let mut vwap = SessionVwap::new(0);
let candle = Candle::new(100.0, 120.0, 90.0, 102.0, 10.0, 0).unwrap();
let v = vwap.update(candle).unwrap();
assert_relative_eq!(v, 104.0);
}
#[test]
fn reset_clears_state() {
let mut vwap = SessionVwap::new(0);
vwap.update(c(100.0, 10.0, 0));
vwap.reset();
assert!(!vwap.is_ready());
assert!(vwap.value().is_none());
let after = vwap.update(c(50.0, 1.0, HOUR)).unwrap();
assert_relative_eq!(after, 50.0);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..30)
.map(|i| {
c(
100.0 + f64::from(i),
1.0 + f64::from(i % 4),
i64::from(i) * HOUR,
)
})
.collect();
let mut a = SessionVwap::new(0);
let mut b = SessionVwap::new(0);
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}