#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
pub mod prelude {
pub use crate::{Drawdown, DrawdownError, DrawdownPoint, DrawdownWindow};
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Drawdown {
value: f64,
}
impl Drawdown {
pub fn new(value: f64) -> Result<Self, DrawdownError> {
if !value.is_finite() {
return Err(DrawdownError::NonFinite);
}
if value > 0.0 {
return Err(DrawdownError::Positive);
}
Ok(Self { value })
}
pub fn from_peak_current(peak: f64, current: f64) -> Result<Self, DrawdownError> {
validate_peak(peak)?;
validate_current(current)?;
if current >= peak {
Self::new(0.0)
} else {
Self::new((current / peak) - 1.0)
}
}
pub fn maximum_from_values(values: &[f64]) -> Result<Self, DrawdownError> {
let Some((&first, rest)) = values.split_first() else {
return Err(DrawdownError::EmptySeries);
};
validate_peak(first)?;
let mut peak = first;
let mut maximum = Self::new(0.0)?;
for &value in rest {
validate_current(value)?;
if value > peak {
peak = value;
}
let drawdown = Self::from_peak_current(peak, value)?;
if drawdown.value() < maximum.value() {
maximum = drawdown;
}
}
Ok(maximum)
}
#[must_use]
pub const fn value(self) -> f64 {
self.value
}
}
impl fmt::Display for Drawdown {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.value.fmt(formatter)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DrawdownPoint {
label: Option<String>,
drawdown: Drawdown,
}
impl DrawdownPoint {
#[must_use]
pub const fn new(drawdown: Drawdown) -> Self {
Self {
label: None,
drawdown,
}
}
pub fn with_label(mut self, label: impl AsRef<str>) -> Result<Self, DrawdownError> {
let trimmed = label.as_ref().trim();
if trimmed.is_empty() {
return Err(DrawdownError::EmptyLabel);
}
self.label = Some(trimmed.to_string());
Ok(self)
}
#[must_use]
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
#[must_use]
pub const fn drawdown(&self) -> Drawdown {
self.drawdown
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DrawdownWindow {
length: usize,
}
impl DrawdownWindow {
pub const fn new(length: usize) -> Result<Self, DrawdownError> {
if length == 0 {
Err(DrawdownError::ZeroWindow)
} else {
Ok(Self { length })
}
}
#[must_use]
pub const fn length(self) -> usize {
self.length
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DrawdownError {
NonFinite,
Positive,
InvalidPeak,
NegativeValue,
EmptySeries,
ZeroWindow,
EmptyLabel,
}
impl fmt::Display for DrawdownError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonFinite => formatter.write_str("drawdown values must be finite"),
Self::Positive => {
formatter.write_str("drawdown cannot be positive with this convention")
},
Self::InvalidPeak => formatter.write_str("drawdown peak must be finite and positive"),
Self::NegativeValue => formatter.write_str("drawdown current value cannot be negative"),
Self::EmptySeries => {
formatter.write_str("maximum drawdown requires at least one value")
},
Self::ZeroWindow => formatter.write_str("drawdown window length must be non-zero"),
Self::EmptyLabel => formatter.write_str("drawdown point label cannot be empty"),
}
}
}
impl Error for DrawdownError {}
fn validate_peak(value: f64) -> Result<(), DrawdownError> {
if !value.is_finite() || value <= 0.0 {
Err(DrawdownError::InvalidPeak)
} else {
Ok(())
}
}
fn validate_current(value: f64) -> Result<(), DrawdownError> {
if !value.is_finite() {
return Err(DrawdownError::NonFinite);
}
if value < 0.0 {
return Err(DrawdownError::NegativeValue);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{Drawdown, DrawdownError};
#[test]
fn computes_drawdown_from_peak_current() {
let drawdown = Drawdown::from_peak_current(120.0, 90.0).expect("drawdown should compute");
assert!((drawdown.value() + 0.25).abs() < f64::EPSILON);
}
#[test]
fn returns_zero_drawdown_at_new_high() {
let drawdown = Drawdown::from_peak_current(120.0, 130.0).expect("drawdown should compute");
assert!(drawdown.value().abs() < f64::EPSILON);
}
#[test]
fn rejects_invalid_peak() {
assert_eq!(
Drawdown::from_peak_current(0.0, 90.0),
Err(DrawdownError::InvalidPeak)
);
}
#[test]
fn computes_maximum_drawdown() {
let drawdown = Drawdown::maximum_from_values(&[100.0, 120.0, 90.0, 80.0, 130.0])
.expect("drawdown should compute");
assert!((drawdown.value() - (-1.0 / 3.0)).abs() < 1.0e-12);
}
}