use crate::Timed;
use chrono::{DateTime, TimeDelta, Utc};
use derive_more::Constructor;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
pub mod max;
pub mod mean;
#[derive(Debug, Clone, PartialEq, PartialOrd, Default, Deserialize, Serialize, Constructor)]
pub struct Drawdown {
pub value: Decimal,
pub time_start: DateTime<Utc>,
pub time_end: DateTime<Utc>,
}
impl Drawdown {
pub fn duration(&self) -> TimeDelta {
self.time_end.signed_duration_since(self.time_start)
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Default, Deserialize, Serialize, Constructor)]
pub struct DrawdownGenerator {
pub peak: Option<Decimal>,
pub drawdown_max: Decimal,
pub time_peak: Option<DateTime<Utc>>,
pub time_now: DateTime<Utc>,
}
impl DrawdownGenerator {
pub fn init(point: Timed<Decimal>) -> Self {
Self {
peak: Some(point.value),
drawdown_max: Decimal::ZERO,
time_peak: Some(point.time),
time_now: point.time,
}
}
pub fn update(&mut self, point: Timed<Decimal>) -> Option<Drawdown> {
self.time_now = point.time;
let Some(peak) = self.peak else {
self.peak = Some(point.value);
self.time_peak = Some(point.time);
return None;
};
if point.value > peak {
let ended_drawdown = self.generate();
self.peak = Some(point.value);
self.time_peak = Some(point.time);
self.drawdown_max = Decimal::ZERO;
ended_drawdown
} else {
let drawdown_current = (peak - point.value).checked_div(peak);
if let Some(drawdown_current) = drawdown_current {
if drawdown_current > self.drawdown_max {
self.drawdown_max = drawdown_current;
}
}
None
}
}
pub fn generate(&mut self) -> Option<Drawdown> {
let time_peak = self.time_peak?;
(self.drawdown_max != Decimal::ZERO).then_some(Drawdown {
value: self.drawdown_max,
time_start: time_peak,
time_end: self.time_now,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::time_plus_days;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::str::FromStr;
#[test]
fn test_drawdown_generate_update() {
struct TestCase {
input: Timed<Decimal>,
expected_state: DrawdownGenerator,
expected_output: Option<Drawdown>,
}
let time_base = DateTime::<Utc>::MIN_UTC;
let mut generator = DrawdownGenerator::default();
let cases = vec![
TestCase {
input: Timed::new(dec!(100.0), time_base),
expected_state: DrawdownGenerator {
peak: Some(dec!(100.0)),
drawdown_max: dec!(0.0),
time_peak: Some(time_base),
time_now: time_base,
},
expected_output: None,
},
TestCase {
input: Timed::new(dec!(110.0), time_plus_days(time_base, 1)),
expected_state: DrawdownGenerator {
peak: Some(dec!(110.0)),
drawdown_max: dec!(0.0),
time_peak: Some(time_plus_days(time_base, 1)),
time_now: time_plus_days(time_base, 1),
},
expected_output: None,
},
TestCase {
input: Timed::new(dec!(99.0), time_plus_days(time_base, 2)),
expected_state: DrawdownGenerator {
peak: Some(dec!(110.0)),
drawdown_max: dec!(0.1), time_peak: Some(time_plus_days(time_base, 1)),
time_now: time_plus_days(time_base, 2),
},
expected_output: None,
},
TestCase {
input: Timed::new(dec!(88.0), time_plus_days(time_base, 3)),
expected_state: DrawdownGenerator {
peak: Some(dec!(110.0)),
drawdown_max: dec!(0.2), time_peak: Some(time_plus_days(time_base, 1)),
time_now: time_plus_days(time_base, 3),
},
expected_output: None,
},
TestCase {
input: Timed::new(dec!(95.0), time_plus_days(time_base, 4)),
expected_state: DrawdownGenerator {
peak: Some(dec!(110.0)),
drawdown_max: dec!(0.2), time_peak: Some(time_plus_days(time_base, 1)),
time_now: time_plus_days(time_base, 4),
},
expected_output: None,
},
TestCase {
input: Timed::new(dec!(115.0), time_plus_days(time_base, 5)),
expected_state: DrawdownGenerator {
peak: Some(dec!(115.0)),
drawdown_max: dec!(0.0), time_peak: Some(time_plus_days(time_base, 5)),
time_now: time_plus_days(time_base, 5),
},
expected_output: Some(Drawdown {
value: dec!(0.2), time_start: time_plus_days(time_base, 1),
time_end: time_plus_days(time_base, 5),
}),
},
TestCase {
input: Timed::new(dec!(115.0), time_plus_days(time_base, 6)),
expected_state: DrawdownGenerator {
peak: Some(dec!(115.0)),
drawdown_max: dec!(0.0),
time_peak: Some(time_plus_days(time_base, 5)),
time_now: time_plus_days(time_base, 6),
},
expected_output: None,
},
TestCase {
input: Timed::new(
Decimal::from_str("114.99999").unwrap(),
time_plus_days(time_base, 7),
),
expected_state: DrawdownGenerator {
peak: Some(dec!(115.0)),
drawdown_max: Decimal::from_str("0.0000000869565217391304347826").unwrap(), time_peak: Some(time_plus_days(time_base, 5)),
time_now: time_plus_days(time_base, 7),
},
expected_output: None,
},
TestCase {
input: Timed::new(dec!(200.0), time_plus_days(time_base, 8)),
expected_state: DrawdownGenerator {
peak: Some(dec!(200.0)),
drawdown_max: dec!(0.0),
time_peak: Some(time_plus_days(time_base, 8)),
time_now: time_plus_days(time_base, 8),
},
expected_output: Some(Drawdown {
value: Decimal::from_str("0.0000000869565217391304347826").unwrap(), time_start: time_plus_days(time_base, 5),
time_end: time_plus_days(time_base, 8),
}),
},
];
for (index, test) in cases.into_iter().enumerate() {
let output = generator.update(test.input);
assert_eq!(generator, test.expected_state, "TC{index} failed");
assert_eq!(output, test.expected_output, "TC{index} failed");
}
}
}