Skip to main content

barter/statistic/metric/drawdown/
max.rs

1use crate::statistic::metric::drawdown::Drawdown;
2use derive_more::Constructor;
3use serde::{Deserialize, Serialize};
4
5/// [`MaxDrawdown`] is the largest peak-to-trough decline of PnL (Portfolio, Strategy, Instrument),
6/// or asset balance.
7///
8/// Max Drawdown is a measure of downside risk, with larger values indicating downside movements
9/// could be volatile.
10///
11/// See documentation: <https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp>
12#[derive(Debug, Clone, PartialEq, PartialOrd, Default, Deserialize, Serialize, Constructor)]
13pub struct MaxDrawdown(pub Drawdown);
14
15/// [`MaxDrawdown`] generator.
16#[derive(Debug, Clone, PartialEq, PartialOrd, Default, Deserialize, Serialize, Constructor)]
17pub struct MaxDrawdownGenerator {
18    pub max: Option<MaxDrawdown>,
19}
20
21impl MaxDrawdownGenerator {
22    /// Initialise a [`MaxDrawdownGenerator`] from an initial [`Drawdown`].
23    pub fn init(drawdown: Drawdown) -> Self {
24        Self {
25            max: Some(MaxDrawdown(drawdown)),
26        }
27    }
28
29    /// Updates the internal [`MaxDrawdown`] using the latest next [`Drawdown`]. If the next
30    /// drawdown is larger than the current [`MaxDrawdown`], it supersedes it.
31    pub fn update(&mut self, next_drawdown: &Drawdown) {
32        let max = match self.max.take() {
33            Some(current) => {
34                if next_drawdown.value.abs() > current.0.value.abs() {
35                    MaxDrawdown(next_drawdown.clone())
36                } else {
37                    current
38                }
39            }
40            None => MaxDrawdown(next_drawdown.clone()),
41        };
42
43        self.max = Some(max);
44    }
45
46    /// Generate the current [`MaxDrawdown`], if one exists.
47    pub fn generate(&self) -> Option<MaxDrawdown> {
48        self.max.clone()
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::test_utils::time_plus_days;
56    use chrono::{DateTime, Utc};
57    use rust_decimal_macros::dec;
58
59    #[test]
60    fn test_max_drawdown_generator_update() {
61        struct TestCase {
62            input: Drawdown,
63            expected_state: MaxDrawdownGenerator,
64            expected_output: Option<MaxDrawdown>,
65        }
66
67        let base_time = DateTime::<Utc>::MIN_UTC;
68
69        let mut generator = MaxDrawdownGenerator::default();
70
71        let cases = vec![
72            // TC0: first ever drawdown
73            TestCase {
74                input: Drawdown {
75                    value: dec!(-0.227272727272727273), // -25/110
76                    time_start: base_time,
77                    time_end: time_plus_days(base_time, 2),
78                },
79                expected_state: MaxDrawdownGenerator {
80                    max: Some(MaxDrawdown::new(Drawdown {
81                        value: dec!(-0.227272727272727273),
82                        time_start: base_time,
83                        time_end: time_plus_days(base_time, 2),
84                    })),
85                },
86                expected_output: Some(MaxDrawdown::new(Drawdown {
87                    value: dec!(-0.227272727272727273),
88                    time_start: base_time,
89                    time_end: time_plus_days(base_time, 2),
90                })),
91            },
92            // TC1: larger drawdown
93            TestCase {
94                input: Drawdown {
95                    value: dec!(-0.55), // -110/200
96                    time_start: base_time,
97                    time_end: time_plus_days(base_time, 3),
98                },
99                expected_state: MaxDrawdownGenerator {
100                    max: Some(MaxDrawdown::new(Drawdown {
101                        value: dec!(-0.55),
102                        time_start: base_time,
103                        time_end: time_plus_days(base_time, 3),
104                    })),
105                },
106                expected_output: Some(MaxDrawdown::new(Drawdown {
107                    value: dec!(-0.55),
108                    time_start: base_time,
109                    time_end: time_plus_days(base_time, 3),
110                })),
111            },
112            // TC2: smaller drawdown
113            TestCase {
114                input: Drawdown {
115                    value: dec!(-0.033333333333333333), // -10/300
116                    time_start: base_time,
117                    time_end: time_plus_days(base_time, 3),
118                },
119                expected_state: MaxDrawdownGenerator {
120                    max: Some(MaxDrawdown::new(Drawdown {
121                        value: dec!(-0.55),
122                        time_start: base_time,
123                        time_end: time_plus_days(base_time, 3),
124                    })),
125                },
126                expected_output: Some(MaxDrawdown::new(Drawdown {
127                    value: dec!(-0.55),
128                    time_start: base_time,
129                    time_end: time_plus_days(base_time, 3),
130                })),
131            },
132            // TC3: largest drawdown
133            TestCase {
134                input: Drawdown {
135                    value: dec!(-0.99999), // -9999.9/10000.0
136                    time_start: base_time,
137                    time_end: time_plus_days(base_time, 3),
138                },
139                expected_state: MaxDrawdownGenerator {
140                    max: Some(MaxDrawdown::new(Drawdown {
141                        value: dec!(-0.99999),
142                        time_start: base_time,
143                        time_end: time_plus_days(base_time, 3),
144                    })),
145                },
146                expected_output: Some(MaxDrawdown::new(Drawdown {
147                    value: dec!(-0.99999),
148                    time_start: base_time,
149                    time_end: time_plus_days(base_time, 3),
150                })),
151            },
152        ];
153
154        for (index, test) in cases.into_iter().enumerate() {
155            generator.update(&test.input);
156
157            // Verify both internal state and generated output
158            assert_eq!(
159                generator, test.expected_state,
160                "TC{index} generator state failed"
161            );
162            assert_eq!(
163                generator.generate(),
164                test.expected_output,
165                "TC{index} generated output failed"
166            );
167        }
168    }
169}