use std::collections::VecDeque;
use std::fmt;
use std::time::Duration;
use crate::errors::Result;
use crate::indicators::AdaptiveTimeDetector;
use crate::{Next, NextBatch, Reset};
use chrono::{DateTime, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const MAX_WINDOW_SIZE: usize = 500;
const KEEP_OLDEST: usize = 10;
const KEEP_RECENT: usize = 100;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Minimum {
duration: Duration,
window: VecDeque<(DateTime<Utc>, f64)>,
#[cfg_attr(feature = "serde", serde(skip))]
window_nanos: VecDeque<i64>,
detector: AdaptiveTimeDetector,
#[cfg_attr(feature = "serde", serde(skip))]
cached_window: Option<i64>,
#[cfg_attr(feature = "serde", serde(skip))]
mono: VecDeque<(i64, f64)>,
}
impl Minimum {
pub fn get_window(&self) -> VecDeque<(DateTime<Utc>, f64)> {
self.window.clone()
}
pub fn new(duration: Duration) -> Result<Self> {
if duration.as_secs() == 0 && duration.subsec_nanos() == 0 {
return Err(crate::errors::TaError::InvalidParameter);
}
Ok(Self {
duration,
window: VecDeque::new(),
window_nanos: VecDeque::new(),
detector: AdaptiveTimeDetector::new(duration),
cached_window: None,
mono: VecDeque::new(),
})
}
#[cfg_attr(not(debug_assertions), allow(dead_code))]
fn find_min_value(&self) -> f64 {
self.window
.iter()
.map(|&(_, val)| val)
.fold(f64::INFINITY, f64::min)
}
fn rebuild_transients(&mut self) {
self.window_nanos.clear();
self.mono.clear();
let sealed_len = self.window.len().saturating_sub(1);
for (index, &(timestamp, value)) in self.window.iter().enumerate() {
let timestamp_nanos = timestamp.timestamp_nanos_opt().unwrap_or(i64::MIN);
self.window_nanos.push_back(timestamp_nanos);
if index >= sealed_len {
continue;
}
while self.mono.back().map_or(false, |&(_, bv)| bv >= value) {
self.mono.pop_back();
}
self.mono.push_back((timestamp_nanos, value));
}
}
fn remove_old(&mut self, current_nanos: i64) {
let dur_nanos = *self
.cached_window
.get_or_insert_with(|| self.duration.as_nanos() as i64);
let cutoff_nanos = current_nanos - dur_nanos;
while self
.window_nanos
.front()
.is_some_and(|×tamp_nanos| timestamp_nanos < cutoff_nanos)
{
self.window.pop_front();
self.window_nanos.pop_front();
}
while self
.mono
.front()
.is_some_and(|&(timestamp_nanos, _)| timestamp_nanos < cutoff_nanos)
{
self.mono.pop_front();
}
}
fn thin_window(&mut self) {
if self.window.len() <= MAX_WINDOW_SIZE {
return;
}
let len = self.window.len();
let middle_start = KEEP_OLDEST;
let middle_end = len.saturating_sub(KEEP_RECENT);
if middle_end <= middle_start {
return;
}
let mut new_window = VecDeque::with_capacity(MAX_WINDOW_SIZE);
for i in 0..middle_start.min(len) {
new_window.push_back(self.window[i]);
}
let mut keep = true;
for i in middle_start..middle_end {
if keep {
new_window.push_back(self.window[i]);
}
keep = !keep;
}
for i in middle_end..len {
new_window.push_back(self.window[i]);
}
self.window = new_window;
}
}
impl Next<f64> for Minimum {
type Output = f64;
fn next(&mut self, (timestamp, value): (DateTime<Utc>, f64)) -> Self::Output {
if self.window_nanos.len() != self.window.len()
|| (self.mono.is_empty() && self.window.len() > 1)
{
self.rebuild_transients();
}
let should_replace = self.detector.should_replace(timestamp);
let timestamp_nanos = timestamp.timestamp_nanos_opt().unwrap_or(i64::MIN);
self.remove_old(timestamp_nanos);
if should_replace {
if !self.window.is_empty() {
self.window.pop_back();
self.window_nanos.pop_back();
}
} else if let (Some(&(_, sealed_value)), Some(&sealed_nanos)) =
(self.window.back(), self.window_nanos.back())
{
while self
.mono
.back()
.map_or(false, |&(_, bv)| bv >= sealed_value)
{
self.mono.pop_back();
}
self.mono.push_back((sealed_nanos, sealed_value));
}
self.window.push_back((timestamp, value));
self.window_nanos.push_back(timestamp_nanos);
let len_before = self.window.len();
self.thin_window();
if self.window.len() != len_before {
self.rebuild_transients();
}
let min = match (self.mono.front(), self.window.back()) {
(Some(&(_, s)), Some(&(_, c))) => s.min(c),
(None, Some(&(_, c))) => c,
(Some(&(_, s)), None) => s,
(None, None) => f64::INFINITY,
};
debug_assert_eq!(
min,
self.find_min_value(),
"monotonic min desynced from window scan"
);
min
}
}
impl NextBatch<f64> for Minimum {}
impl Reset for Minimum {
fn reset(&mut self) {
self.window.clear();
self.window_nanos.clear();
self.mono.clear();
self.detector.reset();
}
}
impl Default for Minimum {
fn default() -> Self {
Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap()
}
}
impl fmt::Display for Minimum {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let days = self.duration.as_secs() / 86400;
write!(f, "MIN({} days)", days)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
fn datetime(s: &str) -> DateTime<Utc> {
Utc.datetime_from_str(s, "%Y-%m-%d %H:%M:%S").unwrap()
}
#[test]
fn test_new() {
assert!(Minimum::new(Duration::from_secs(0)).is_err());
assert!(Minimum::new(Duration::from_secs(86400)).is_ok()); }
#[test]
fn test_next() {
let duration = Duration::from_secs(2 * 86400); let mut min = Minimum::new(duration).unwrap();
assert_eq!(min.next((datetime("2023-01-01 00:00:00"), 4.0)), 4.0);
assert_eq!(min.next((datetime("2023-01-02 00:00:00"), 1.2)), 1.2);
assert_eq!(min.next((datetime("2023-01-03 00:00:00"), 5.0)), 1.2);
assert_eq!(min.next((datetime("2023-01-04 00:00:00"), 3.0)), 1.2);
assert_eq!(min.next((datetime("2023-01-05 00:00:00"), 4.0)), 3.0);
assert_eq!(min.next((datetime("2023-01-06 00:00:00"), 6.0)), 3.0);
assert_eq!(min.next((datetime("2023-01-07 00:00:00"), 7.0)), 4.0);
assert_eq!(min.next((datetime("2023-01-08 00:00:00"), 8.0)), 6.0);
assert_eq!(min.next((datetime("2023-01-09 00:00:00"), -9.0)), -9.0);
assert_eq!(min.next((datetime("2023-01-10 00:00:00"), 0.0)), -9.0);
}
#[test]
fn test_reset() {
let duration = Duration::from_secs(10 * 86400); let mut min = Minimum::new(duration).unwrap();
assert_eq!(min.next((datetime("2023-01-01 00:00:00"), 5.0)), 5.0);
assert_eq!(min.next((datetime("2023-01-02 00:00:00"), 7.0)), 5.0);
min.reset();
assert_eq!(min.next((datetime("2023-01-03 00:00:00"), 8.0)), 8.0);
}
#[test]
fn test_default() {
let _ = Minimum::default();
}
#[test]
fn test_display() {
let indicator = Minimum::new(Duration::from_secs(10 * 86400)).unwrap(); assert_eq!(format!("{}", indicator), "MIN(10 days)");
}
fn lcg(state: &mut u64) -> u64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*state >> 33
}
#[test]
fn monotonic_matches_scan_over_random_sequences() {
let mut state: u64 = 0x2545_F491_4F6C_DD1D;
for &secs in &[2u64, 3600, 86_400, 7 * 86_400] {
let mut min = Minimum::new(Duration::from_secs(secs)).unwrap();
let mut t = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0);
for _ in 0..1500 {
let step = (lcg(&mut state) % (secs * 2 + 1)) as i64;
t = t + chrono::Duration::seconds(step);
let v = (lcg(&mut state) % 20_000) as f64 / 100.0 - 100.0; let _ = min.next((t, v)); }
}
}
#[test]
fn monotonic_matches_scan_across_thinning() {
let mut min = Minimum::new(Duration::from_secs(1_000_000 * 86_400)).unwrap();
let start = Utc.ymd(2000, 1, 1).and_hms(0, 0, 0);
let mut true_running_min = f64::INFINITY;
for i in 0..900i64 {
let v = ((i.wrapping_mul(2_654_435_761)) % 1000) as f64;
true_running_min = true_running_min.min(v);
let got = min.next((start + chrono::Duration::seconds(i), v));
assert!(got.is_finite());
assert!(got >= true_running_min, "thinned min fell below true min");
}
}
#[cfg(feature = "serde")]
#[test]
fn transient_timestamp_cache_preserves_serialized_contract_and_resume() {
#[derive(serde::Serialize)]
struct LegacyMinimum<'a> {
duration: &'a Duration,
window: &'a VecDeque<(DateTime<Utc>, f64)>,
detector: &'a AdaptiveTimeDetector,
}
let duration = Duration::from_secs(7 * 86_400);
let start = Utc.with_ymd_and_hms(2024, 1, 2, 9, 30, 0).unwrap();
let mut uninterrupted = Minimum::new(duration).unwrap();
for (offset, value) in [(0, 100.0), (30, 95.0), (390, 101.0), (1_440, 102.0)] {
uninterrupted.next((start + chrono::Duration::minutes(offset), value));
}
let legacy_bytes = bincode::serialize(&LegacyMinimum {
duration: &uninterrupted.duration,
window: &uninterrupted.window,
detector: &uninterrupted.detector,
})
.unwrap();
assert_eq!(
bincode::serialize(&uninterrupted).unwrap(),
legacy_bytes,
"transient caches must not change persisted bytes"
);
let mut resumed: Minimum = bincode::deserialize(&legacy_bytes).unwrap();
assert!(resumed.window_nanos.is_empty());
assert!(resumed.mono.is_empty());
let next = (start + chrono::Duration::minutes(1_500), 90.0);
assert_eq!(resumed.next(next), uninterrupted.next(next));
assert_eq!(resumed.get_window(), uninterrupted.get_window());
assert_eq!(
bincode::serialize(&resumed).unwrap(),
bincode::serialize(&uninterrupted).unwrap()
);
}
}