use crate::calendar::civil_from_timestamp;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct OvernightGap {
utc_offset_minutes: i32,
day_key: Option<(i64, u32, u32)>,
last_close: Option<f64>,
gap: Option<f64>,
}
impl OvernightGap {
pub const fn new(utc_offset_minutes: i32) -> Self {
Self {
utc_offset_minutes,
day_key: None,
last_close: None,
gap: None,
}
}
pub const fn utc_offset_minutes(&self) -> i32 {
self.utc_offset_minutes
}
pub const fn value(&self) -> Option<f64> {
self.gap
}
}
impl Indicator for OvernightGap {
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) {
if let Some(prev_close) = self.last_close {
self.gap = Some(if prev_close == 0.0 {
0.0
} else {
candle.open / prev_close - 1.0
});
}
self.day_key = Some(key);
}
self.last_close = Some(candle.close);
self.gap
}
fn reset(&mut self) {
self.day_key = None;
self.last_close = None;
self.gap = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.gap.is_some()
}
fn name(&self) -> &'static str {
"OvernightGap"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
const HOUR: i64 = 3_600_000;
fn c(open: f64, close: f64, ts: i64) -> Candle {
let high = open.max(close);
let low = open.min(close);
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn metadata_and_accessors() {
let gap = OvernightGap::new(330);
assert_eq!(gap.utc_offset_minutes(), 330);
assert_eq!(gap.name(), "OvernightGap");
assert_eq!(gap.warmup_period(), 2);
assert!(!gap.is_ready());
assert!(gap.value().is_none());
}
#[test]
fn first_session_has_no_gap() {
let mut gap = OvernightGap::new(0);
assert!(gap.update(c(99.0, 100.0, 0)).is_none());
assert!(gap.update(c(100.0, 101.0, HOUR)).is_none());
assert!(!gap.is_ready());
}
#[test]
fn computes_gap_at_day_boundary() {
let mut gap = OvernightGap::new(0);
gap.update(c(99.0, 100.0, 0)); let g = gap.update(c(105.0, 105.5, 24 * HOUR)).unwrap();
assert_relative_eq!(g, 0.05);
assert!(gap.is_ready());
let same = gap.update(c(106.0, 107.0, 25 * HOUR)).unwrap();
assert_relative_eq!(same, 0.05);
}
#[test]
fn negative_gap_down() {
let mut gap = OvernightGap::new(0);
gap.update(c(99.0, 100.0, 0));
let g = gap.update(c(90.0, 91.0, 24 * HOUR)).unwrap();
assert_relative_eq!(g, -0.1);
}
#[test]
fn zero_prev_close_yields_zero_gap() {
let mut gap = OvernightGap::new(0);
gap.update(c(0.0, 0.0, 0)); let g = gap.update(c(5.0, 6.0, 24 * HOUR)).unwrap();
assert_relative_eq!(g, 0.0);
}
#[test]
fn reset_clears_state() {
let mut gap = OvernightGap::new(0);
gap.update(c(99.0, 100.0, 0));
gap.update(c(105.0, 105.5, 24 * HOUR));
gap.reset();
assert!(!gap.is_ready());
assert!(gap.value().is_none());
assert!(gap.update(c(10.0, 11.0, 48 * HOUR)).is_none());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..50)
.map(|i| {
c(
100.0 + f64::from(i % 7),
100.0 + f64::from(i % 5),
i64::from(i) * 6 * HOUR,
)
})
.collect();
let mut a = OvernightGap::new(0);
let mut b = OvernightGap::new(0);
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}