use crate::errors::{Result, TaError};
use crate::indicators::window_aggregate::{DrawdownAgg, MonoidWindow, WindowAggregate};
use crate::indicators::AdaptiveTimeDetector;
use crate::{Next, NextBatch, Reset};
use chrono::{DateTime, Utc}; #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::fmt;
use std::time::Duration;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct MaxDrawdown {
duration: Duration, window: VecDeque<(DateTime<Utc>, f64)>,
detector: AdaptiveTimeDetector,
#[cfg_attr(feature = "serde", serde(skip))]
cached_window: Option<i64>,
#[cfg_attr(feature = "serde", serde(skip))]
swag: MonoidWindow<DrawdownAgg>,
#[cfg_attr(feature = "serde", serde(skip))]
swag_built: bool,
}
impl MaxDrawdown {
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 {
Err(TaError::InvalidParameter)
} else {
Ok(Self {
duration,
window: VecDeque::new(),
detector: AdaptiveTimeDetector::new(duration),
cached_window: None,
swag: MonoidWindow::default(),
swag_built: false,
})
}
}
fn ensure_built(&mut self) {
if self.swag_built {
return;
}
self.swag.clear();
let committed = self.window.len().saturating_sub(1);
for &(_, value) in self.window.iter().take(committed) {
self.swag.push_back(value);
}
self.swag_built = true;
}
fn remove_old_data(&mut self, current_time: DateTime<Utc>) {
let dur_nanos = *self
.cached_window
.get_or_insert_with(|| self.duration.as_nanos() as i64);
let cutoff_nanos = current_time.timestamp_nanos_opt().unwrap_or(i64::MIN) - dur_nanos;
while self.window.front().map_or(false, |(time, _)| {
time.timestamp_nanos_opt().unwrap_or(i64::MIN) < cutoff_nanos
}) {
if self.window.len() > 1 {
self.swag.pop_front();
}
self.window.pop_front();
}
}
fn current_max_drawdown(&self, tail: f64) -> f64 {
let agg = match self.swag.aggregate() {
Some(prefix) => prefix.combine(&DrawdownAgg::leaf(tail)),
None => DrawdownAgg::leaf(tail),
};
100.0 * agg.ratio()
}
}
impl Next<f64> for MaxDrawdown {
type Output = f64;
fn next(&mut self, (timestamp, value): (DateTime<Utc>, f64)) -> Self::Output {
self.ensure_built();
let should_replace = self.detector.should_replace(timestamp);
self.remove_old_data(timestamp);
if should_replace && !self.window.is_empty() {
self.window.pop_back();
} else if let Some(&(_, prev_tail)) = self.window.back() {
self.swag.push_back(prev_tail);
}
self.window.push_back((timestamp, value));
self.current_max_drawdown(value)
}
}
impl NextBatch<f64> for MaxDrawdown {}
impl Reset for MaxDrawdown {
fn reset(&mut self) {
self.window.clear();
self.detector.reset();
self.swag.clear();
self.swag_built = true;
}
}
impl fmt::Display for MaxDrawdown {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MaxDrawdown({}s)", self.duration.as_secs())
}
}
impl Default for MaxDrawdown {
fn default() -> Self {
Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() }
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_new() {
assert!(MaxDrawdown::new(Duration::from_secs(0)).is_err());
assert!(MaxDrawdown::new(Duration::from_secs(1)).is_ok());
}
#[test]
fn test_next() {
let duration = Duration::from_secs(2);
let mut max = MaxDrawdown::new(duration).unwrap();
let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0);
assert_eq!(max.next((start_time, 4.0)), 0.0);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(1), 2.0)),
50.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(2), 1.0)),
75.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(3), 3.0)),
50.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(4), 4.0)),
0.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(5), 0.0)),
100.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(6), 2.0)),
100.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(7), 3.0)),
0.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(8), 1.5)),
50.0
);
}
#[test]
fn test_reset() {
let duration = Duration::from_secs(100);
let mut max = MaxDrawdown::new(duration).unwrap();
let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0);
assert_eq!(max.next((start_time, 4.0)), 0.0);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(50), 10.0)),
0.0
);
assert_eq!(
max.next((start_time + chrono::Duration::seconds(100), 2.0)),
80.0
);
max.reset();
assert_eq!(
max.next((start_time + chrono::Duration::seconds(150), 4.0)),
0.0
);
}
#[test]
fn test_display() {
let indicator = MaxDrawdown::new(Duration::from_secs(7)).unwrap();
assert_eq!(format!("{}", indicator), "MaxDrawdown(7s)");
}
fn naive(window: &VecDeque<(DateTime<Utc>, f64)>) -> f64 {
let mut peak = f64::MIN;
let mut max_drawdown = 0.0;
for &(_, value) in window {
if value > peak {
peak = value;
}
let drawdown = (peak - value) / peak;
if drawdown > max_drawdown {
max_drawdown = drawdown;
}
}
100.0 * max_drawdown
}
#[test]
fn equivalence_with_naive_under_appends_replaces_and_evictions() {
let mut state: u64 = 0x9e3779b97f4a7c15;
let mut rng = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let start_time = Utc.ymd(2021, 6, 1).and_hms(0, 0, 0);
for _ in 0..300 {
let duration = Duration::from_secs(1 + rng() % 30);
let mut indicator = MaxDrawdown::new(duration).unwrap();
let mut offset_ms: i64 = 0;
for _ in 0..400 {
offset_ms += (rng() % 1500) as i64;
let ts = start_time + chrono::Duration::milliseconds(offset_ms);
let value = 1.0 + (rng() % 1_000_000) as f64 / 1000.0;
let got = indicator.next((ts, value));
let expected = naive(&indicator.get_window());
assert_eq!(
got, expected,
"incremental {got} != naive {expected} (dur={duration:?})"
);
}
}
}
}