use crate::calendar::civil_from_timestamp;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SessionHighLowOutput {
pub high: f64,
pub low: f64,
}
#[derive(Debug, Clone)]
pub struct SessionHighLow {
utc_offset_minutes: i32,
day_key: Option<(i64, u32, u32)>,
high: f64,
low: f64,
last: Option<SessionHighLowOutput>,
}
impl SessionHighLow {
pub const fn new(utc_offset_minutes: i32) -> Self {
Self {
utc_offset_minutes,
day_key: None,
high: f64::NEG_INFINITY,
low: f64::INFINITY,
last: None,
}
}
pub const fn utc_offset_minutes(&self) -> i32 {
self.utc_offset_minutes
}
pub const fn value(&self) -> Option<SessionHighLowOutput> {
self.last
}
}
impl Indicator for SessionHighLow {
type Input = Candle;
type Output = SessionHighLowOutput;
fn update(&mut self, candle: Candle) -> Option<SessionHighLowOutput> {
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) {
if candle.high > self.high {
self.high = candle.high;
}
if candle.low < self.low {
self.low = candle.low;
}
} else {
self.day_key = Some(key);
self.high = candle.high;
self.low = candle.low;
}
let out = SessionHighLowOutput {
high: self.high,
low: self.low,
};
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.day_key = None;
self.high = f64::NEG_INFINITY;
self.low = f64::INFINITY;
self.last = None;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"SessionHighLow"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
const HOUR: i64 = 3_600_000;
fn c(high: f64, low: f64, ts: i64) -> Candle {
let mid = f64::midpoint(high, low);
Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
}
#[test]
fn metadata_and_accessors() {
let shl = SessionHighLow::new(-300);
assert_eq!(shl.utc_offset_minutes(), -300);
assert_eq!(shl.name(), "SessionHighLow");
assert_eq!(shl.warmup_period(), 1);
assert!(!shl.is_ready());
assert!(shl.value().is_none());
}
#[test]
fn tracks_high_low_within_day() {
let mut shl = SessionHighLow::new(0);
let first = shl.update(c(105.0, 99.0, 0)).unwrap();
assert_relative_eq!(first.high, 105.0);
assert_relative_eq!(first.low, 99.0);
assert!(shl.is_ready());
let second = shl.update(c(108.0, 100.0, HOUR)).unwrap();
assert_relative_eq!(second.high, 108.0);
assert_relative_eq!(second.low, 99.0);
let third = shl.update(c(106.0, 101.0, 2 * HOUR)).unwrap();
assert_relative_eq!(third.high, 108.0);
assert_relative_eq!(third.low, 99.0);
let fourth = shl.update(c(107.0, 95.0, 3 * HOUR)).unwrap();
assert_relative_eq!(fourth.high, 108.0);
assert_relative_eq!(fourth.low, 95.0);
}
#[test]
fn re_anchors_on_new_day() {
let mut shl = SessionHighLow::new(0);
shl.update(c(105.0, 99.0, 0));
shl.update(c(108.0, 100.0, HOUR));
let next = shl.update(c(51.0, 49.0, 24 * HOUR)).unwrap();
assert_relative_eq!(next.high, 51.0);
assert_relative_eq!(next.low, 49.0);
}
#[test]
fn utc_offset_shifts_day_boundary() {
let pre = 23 * HOUR; let post = 24 * HOUR; let mut utc = SessionHighLow::new(0);
utc.update(c(105.0, 99.0, pre));
let rolled = utc.update(c(108.0, 100.0, post)).unwrap();
assert_relative_eq!(rolled.high, 108.0);
assert_relative_eq!(rolled.low, 100.0);
let mut shifted = SessionHighLow::new(120);
shifted.update(c(105.0, 99.0, pre));
let same = shifted.update(c(108.0, 100.0, post)).unwrap();
assert_relative_eq!(same.high, 108.0);
assert_relative_eq!(same.low, 99.0); }
#[test]
fn reset_clears_state() {
let mut shl = SessionHighLow::new(0);
shl.update(c(105.0, 99.0, 0));
shl.reset();
assert!(!shl.is_ready());
assert!(shl.value().is_none());
let after = shl.update(c(60.0, 50.0, HOUR)).unwrap();
assert_relative_eq!(after.high, 60.0);
assert_relative_eq!(after.low, 50.0);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..30)
.map(|i| {
c(
100.0 + f64::from(i),
90.0 + f64::from(i) * 0.5,
i64::from(i) * HOUR,
)
})
.collect();
let mut a = SessionHighLow::new(0);
let mut b = SessionHighLow::new(0);
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}