use crate::calendar::civil_from_timestamp;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SessionRangeOutput {
pub asia: f64,
pub eu: f64,
pub us: f64,
}
#[derive(Debug, Clone, Copy)]
struct Extent {
high: f64,
low: f64,
}
impl Extent {
const EMPTY: Self = Self {
high: f64::NEG_INFINITY,
low: f64::INFINITY,
};
fn add(&mut self, candle: Candle) {
if candle.high > self.high {
self.high = candle.high;
}
if candle.low < self.low {
self.low = candle.low;
}
}
fn range(self) -> f64 {
if self.high >= self.low {
self.high - self.low
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct SessionRange {
utc_offset_minutes: i32,
day_key: Option<(i64, u32, u32)>,
sessions: [Extent; 3],
last: Option<SessionRangeOutput>,
}
impl SessionRange {
pub const fn new(utc_offset_minutes: i32) -> Self {
Self {
utc_offset_minutes,
day_key: None,
sessions: [Extent::EMPTY; 3],
last: None,
}
}
pub const fn utc_offset_minutes(&self) -> i32 {
self.utc_offset_minutes
}
pub const fn value(&self) -> Option<SessionRangeOutput> {
self.last
}
fn snapshot(&self) -> SessionRangeOutput {
SessionRangeOutput {
asia: self.sessions[0].range(),
eu: self.sessions[1].range(),
us: self.sessions[2].range(),
}
}
}
impl Indicator for SessionRange {
type Input = Candle;
type Output = SessionRangeOutput;
fn update(&mut self, candle: Candle) -> Option<SessionRangeOutput> {
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.sessions = [Extent::EMPTY; 3];
}
let session = (civil.hour / 8) as usize; self.sessions[session].add(candle);
let out = self.snapshot();
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.day_key = None;
self.sessions = [Extent::EMPTY; 3];
self.last = None;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"SessionRange"
}
}
#[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 sr = SessionRange::new(60);
assert_eq!(sr.utc_offset_minutes(), 60);
assert_eq!(sr.name(), "SessionRange");
assert_eq!(sr.warmup_period(), 1);
assert!(!sr.is_ready());
assert!(sr.value().is_none());
}
#[test]
fn assigns_bars_to_sessions() {
let mut sr = SessionRange::new(0);
let asia = sr.update(c(104.0, 98.0, 2 * HOUR)).unwrap();
assert_relative_eq!(asia.asia, 6.0);
assert_relative_eq!(asia.eu, 0.0);
assert_relative_eq!(asia.us, 0.0);
assert!(sr.is_ready());
let eu = sr.update(c(110.0, 100.0, 10 * HOUR)).unwrap();
assert_relative_eq!(eu.eu, 10.0);
let us = sr.update(c(120.0, 118.0, 20 * HOUR)).unwrap();
assert_relative_eq!(us.us, 2.0);
assert_relative_eq!(us.asia, 6.0);
}
#[test]
fn widens_within_one_session() {
let mut sr = SessionRange::new(0);
sr.update(c(104.0, 98.0, HOUR));
let wider = sr.update(c(106.0, 95.0, 3 * HOUR)).unwrap();
assert_relative_eq!(wider.asia, 11.0);
}
#[test]
fn resets_sessions_on_new_day() {
let mut sr = SessionRange::new(0);
sr.update(c(104.0, 98.0, 2 * HOUR));
sr.update(c(110.0, 100.0, 10 * HOUR));
let next = sr.update(c(101.0, 99.0, (24 + 2) * HOUR)).unwrap();
assert_relative_eq!(next.asia, 2.0);
assert_relative_eq!(next.eu, 0.0);
}
#[test]
fn utc_offset_moves_bar_between_sessions() {
let mut utc = SessionRange::new(0);
let a = utc.update(c(104.0, 98.0, 7 * HOUR)).unwrap();
assert_relative_eq!(a.asia, 6.0);
assert_relative_eq!(a.eu, 0.0);
let mut shifted = SessionRange::new(120);
let e = shifted.update(c(104.0, 98.0, 7 * HOUR)).unwrap();
assert_relative_eq!(e.asia, 0.0);
assert_relative_eq!(e.eu, 6.0);
}
#[test]
fn reset_clears_state() {
let mut sr = SessionRange::new(0);
sr.update(c(104.0, 98.0, 2 * HOUR));
sr.reset();
assert!(!sr.is_ready());
assert!(sr.value().is_none());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
c(
100.0 + f64::from(i % 5),
95.0 - f64::from(i % 3),
i64::from(i) * HOUR,
)
})
.collect();
let mut a = SessionRange::new(0);
let mut b = SessionRange::new(0);
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}