1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7pub mod prelude {
9 pub use crate::{Drawdown, DrawdownError, DrawdownPoint, DrawdownWindow};
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
14pub struct Drawdown {
15 value: f64,
16}
17
18impl Drawdown {
19 pub fn new(value: f64) -> Result<Self, DrawdownError> {
26 if !value.is_finite() {
27 return Err(DrawdownError::NonFinite);
28 }
29
30 if value > 0.0 {
31 return Err(DrawdownError::Positive);
32 }
33
34 Ok(Self { value })
35 }
36
37 pub fn from_peak_current(peak: f64, current: f64) -> Result<Self, DrawdownError> {
44 validate_peak(peak)?;
45 validate_current(current)?;
46
47 if current >= peak {
48 Self::new(0.0)
49 } else {
50 Self::new((current / peak) - 1.0)
51 }
52 }
53
54 pub fn maximum_from_values(values: &[f64]) -> Result<Self, DrawdownError> {
60 let Some((&first, rest)) = values.split_first() else {
61 return Err(DrawdownError::EmptySeries);
62 };
63
64 validate_peak(first)?;
65 let mut peak = first;
66 let mut maximum = Self::new(0.0)?;
67
68 for &value in rest {
69 validate_current(value)?;
70 if value > peak {
71 peak = value;
72 }
73
74 let drawdown = Self::from_peak_current(peak, value)?;
75 if drawdown.value() < maximum.value() {
76 maximum = drawdown;
77 }
78 }
79
80 Ok(maximum)
81 }
82
83 #[must_use]
85 pub const fn value(self) -> f64 {
86 self.value
87 }
88}
89
90impl fmt::Display for Drawdown {
91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92 self.value.fmt(formatter)
93 }
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub struct DrawdownPoint {
99 label: Option<String>,
100 drawdown: Drawdown,
101}
102
103impl DrawdownPoint {
104 #[must_use]
106 pub const fn new(drawdown: Drawdown) -> Self {
107 Self {
108 label: None,
109 drawdown,
110 }
111 }
112
113 pub fn with_label(mut self, label: impl AsRef<str>) -> Result<Self, DrawdownError> {
119 let trimmed = label.as_ref().trim();
120 if trimmed.is_empty() {
121 return Err(DrawdownError::EmptyLabel);
122 }
123
124 self.label = Some(trimmed.to_string());
125 Ok(self)
126 }
127
128 #[must_use]
130 pub fn label(&self) -> Option<&str> {
131 self.label.as_deref()
132 }
133
134 #[must_use]
136 pub const fn drawdown(&self) -> Drawdown {
137 self.drawdown
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
143pub struct DrawdownWindow {
144 length: usize,
145}
146
147impl DrawdownWindow {
148 pub const fn new(length: usize) -> Result<Self, DrawdownError> {
154 if length == 0 {
155 Err(DrawdownError::ZeroWindow)
156 } else {
157 Ok(Self { length })
158 }
159 }
160
161 #[must_use]
163 pub const fn length(self) -> usize {
164 self.length
165 }
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub enum DrawdownError {
171 NonFinite,
173 Positive,
175 InvalidPeak,
177 NegativeValue,
179 EmptySeries,
181 ZeroWindow,
183 EmptyLabel,
185}
186
187impl fmt::Display for DrawdownError {
188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 match self {
190 Self::NonFinite => formatter.write_str("drawdown values must be finite"),
191 Self::Positive => {
192 formatter.write_str("drawdown cannot be positive with this convention")
193 },
194 Self::InvalidPeak => formatter.write_str("drawdown peak must be finite and positive"),
195 Self::NegativeValue => formatter.write_str("drawdown current value cannot be negative"),
196 Self::EmptySeries => {
197 formatter.write_str("maximum drawdown requires at least one value")
198 },
199 Self::ZeroWindow => formatter.write_str("drawdown window length must be non-zero"),
200 Self::EmptyLabel => formatter.write_str("drawdown point label cannot be empty"),
201 }
202 }
203}
204
205impl Error for DrawdownError {}
206
207fn validate_peak(value: f64) -> Result<(), DrawdownError> {
208 if !value.is_finite() || value <= 0.0 {
209 Err(DrawdownError::InvalidPeak)
210 } else {
211 Ok(())
212 }
213}
214
215fn validate_current(value: f64) -> Result<(), DrawdownError> {
216 if !value.is_finite() {
217 return Err(DrawdownError::NonFinite);
218 }
219
220 if value < 0.0 {
221 return Err(DrawdownError::NegativeValue);
222 }
223
224 Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229 use super::{Drawdown, DrawdownError};
230
231 #[test]
232 fn computes_drawdown_from_peak_current() {
233 let drawdown = Drawdown::from_peak_current(120.0, 90.0).expect("drawdown should compute");
234
235 assert!((drawdown.value() + 0.25).abs() < f64::EPSILON);
236 }
237
238 #[test]
239 fn returns_zero_drawdown_at_new_high() {
240 let drawdown = Drawdown::from_peak_current(120.0, 130.0).expect("drawdown should compute");
241
242 assert!(drawdown.value().abs() < f64::EPSILON);
243 }
244
245 #[test]
246 fn rejects_invalid_peak() {
247 assert_eq!(
248 Drawdown::from_peak_current(0.0, 90.0),
249 Err(DrawdownError::InvalidPeak)
250 );
251 }
252
253 #[test]
254 fn computes_maximum_drawdown() {
255 let drawdown = Drawdown::maximum_from_values(&[100.0, 120.0, 90.0, 80.0, 130.0])
256 .expect("drawdown should compute");
257
258 assert!((drawdown.value() - (-1.0 / 3.0)).abs() < 1.0e-12);
259 }
260}