#[derive(Debug, Clone)]
pub struct DrawdownF64 {
peak: f64,
current: f64,
max_drawdown: f64,
count: u64,
}
impl DrawdownF64 {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
peak: 0.0,
current: 0.0,
max_drawdown: 0.0,
count: 0,
}
}
#[inline]
pub fn update(&mut self, sample: f64) -> Result<f64, crate::DataError> {
check_finite!(sample);
self.count += 1;
self.current = sample;
if self.count == 1 || sample > self.peak {
self.peak = sample;
}
let dd = self.peak - self.current;
if dd > self.max_drawdown {
self.max_drawdown = dd;
}
Ok(dd)
}
#[inline]
#[must_use]
pub fn peak(&self) -> Option<f64> {
if self.count == 0 {
None
} else {
Some(self.peak)
}
}
#[inline]
#[must_use]
pub fn drawdown(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.peak - self.current
}
}
#[inline]
#[must_use]
pub fn max_drawdown(&self) -> f64 {
self.max_drawdown
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.count > 0
}
#[inline]
pub fn reset(&mut self) {
self.peak = 0.0;
self.current = 0.0;
self.max_drawdown = 0.0;
self.count = 0;
}
}
impl Default for DrawdownF64 {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DrawdownI64 {
peak: i64,
current: i64,
max_drawdown: i64,
count: u64,
}
impl DrawdownI64 {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
peak: 0,
current: 0,
max_drawdown: 0,
count: 0,
}
}
#[inline]
#[must_use]
pub fn update(&mut self, sample: i64) -> i64 {
self.count += 1;
self.current = sample;
if self.count == 1 || sample > self.peak {
self.peak = sample;
}
let dd = self.peak - self.current;
if dd > self.max_drawdown {
self.max_drawdown = dd;
}
dd
}
#[inline]
#[must_use]
pub fn peak(&self) -> Option<i64> {
if self.count == 0 {
None
} else {
Some(self.peak)
}
}
#[inline]
#[must_use]
pub fn drawdown(&self) -> i64 {
if self.count == 0 {
0
} else {
self.peak - self.current
}
}
#[inline]
#[must_use]
pub fn max_drawdown(&self) -> i64 {
self.max_drawdown
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.count > 0
}
#[inline]
pub fn reset(&mut self) {
self.peak = 0;
self.current = 0;
self.max_drawdown = 0;
self.count = 0;
}
}
impl Default for DrawdownI64 {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn empty_state() {
let dd = DrawdownF64::new();
assert_eq!(dd.count(), 0);
assert!(!dd.is_primed());
assert!(dd.peak().is_none());
assert_eq!(dd.drawdown(), 0.0);
assert_eq!(dd.max_drawdown(), 0.0);
}
#[test]
#[allow(clippy::float_cmp)]
fn first_sample_sets_peak() {
let mut dd = DrawdownF64::new();
let result = dd.update(100.0).unwrap();
assert_eq!(result, 0.0); assert_eq!(dd.peak(), Some(100.0));
}
#[test]
#[allow(clippy::float_cmp)]
fn drawdown_from_peak() {
let mut dd = DrawdownF64::new();
let _ = dd.update(100.0).unwrap();
let result = dd.update(90.0).unwrap();
assert_eq!(result, 10.0);
assert_eq!(dd.drawdown(), 10.0);
}
#[test]
#[allow(clippy::float_cmp)]
fn new_peak_resets_drawdown() {
let mut dd = DrawdownF64::new();
let _ = dd.update(100.0).unwrap();
let _ = dd.update(90.0).unwrap();
assert_eq!(dd.drawdown(), 10.0);
let result = dd.update(110.0).unwrap(); assert_eq!(result, 0.0);
assert_eq!(dd.peak(), Some(110.0));
}
#[test]
#[allow(clippy::float_cmp)]
fn max_drawdown_tracks_worst() {
let mut dd = DrawdownF64::new();
let _ = dd.update(100.0).unwrap();
let _ = dd.update(80.0).unwrap(); let _ = dd.update(110.0).unwrap(); let _ = dd.update(100.0).unwrap();
assert_eq!(dd.max_drawdown(), 20.0); }
#[test]
#[allow(clippy::float_cmp)]
fn reset_clears_all() {
let mut dd = DrawdownF64::new();
let _ = dd.update(100.0).unwrap();
let _ = dd.update(80.0).unwrap();
dd.reset();
assert_eq!(dd.count(), 0);
assert!(dd.peak().is_none());
assert_eq!(dd.max_drawdown(), 0.0);
}
#[test]
fn default_is_empty() {
let dd = DrawdownF64::default();
assert_eq!(dd.count(), 0);
}
#[test]
fn i64_basic() {
let mut dd = DrawdownI64::new();
let _ = dd.update(1000);
let _ = dd.update(800);
assert_eq!(dd.drawdown(), 200);
assert_eq!(dd.max_drawdown(), 200);
}
#[test]
#[allow(clippy::float_cmp)]
fn monotonic_increasing_no_drawdown() {
let mut dd = DrawdownF64::new();
for i in 0..100 {
let result = dd.update(i as f64).unwrap();
assert_eq!(result, 0.0);
}
assert_eq!(dd.max_drawdown(), 0.0);
}
#[test]
fn rejects_nan_and_inf() {
let mut dd = DrawdownF64::new();
assert!(matches!(
dd.update(f64::NAN),
Err(crate::DataError::NotANumber)
));
assert!(matches!(
dd.update(f64::INFINITY),
Err(crate::DataError::Infinite)
));
assert!(matches!(
dd.update(f64::NEG_INFINITY),
Err(crate::DataError::Infinite)
));
}
}