nautilus_analysis/statistics/
max_drawdown.rs1use std::collections::BTreeMap;
19
20use nautilus_core::UnixNanos;
21use nautilus_model::position::Position;
22
23use crate::statistic::PortfolioStatistic;
24
25#[repr(C)]
33#[derive(Debug, Clone, Default)]
34#[cfg_attr(
35 feature = "python",
36 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis", from_py_object)
37)]
38#[cfg_attr(
39 feature = "python",
40 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
41)]
42pub struct MaxDrawdown {}
43
44impl MaxDrawdown {
45 #[must_use]
47 pub fn new() -> Self {
48 Self {}
49 }
50}
51
52impl PortfolioStatistic for MaxDrawdown {
53 type Item = f64;
54
55 fn name(&self) -> String {
56 "Max Drawdown".to_string()
57 }
58
59 fn calculate_from_returns(&self, returns: &BTreeMap<UnixNanos, f64>) -> Option<Self::Item> {
60 if returns.is_empty() {
61 return Some(0.0);
62 }
63
64 let mut cumulative = 1.0;
66 let mut running_max = 1.0;
67 let mut max_drawdown = 0.0;
68
69 for &ret in returns.values() {
70 cumulative *= 1.0 + ret;
71
72 if cumulative > running_max {
74 running_max = cumulative;
75 }
76
77 let drawdown = (running_max - cumulative) / running_max;
79
80 if drawdown > max_drawdown {
82 max_drawdown = drawdown;
83 }
84 }
85
86 Some(-max_drawdown)
88 }
89 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
90 None
91 }
92
93 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
94 None
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use rstest::rstest;
101
102 use super::*;
103
104 fn create_returns(values: &[f64]) -> BTreeMap<UnixNanos, f64> {
105 values
106 .iter()
107 .copied()
108 .enumerate()
109 .map(|(i, v)| (UnixNanos::from(i as u64), v))
110 .collect()
111 }
112
113 #[rstest]
114 fn test_name() {
115 let stat = MaxDrawdown::new();
116 assert_eq!(stat.name(), "Max Drawdown");
117 }
118
119 #[rstest]
120 fn test_empty_returns() {
121 let stat = MaxDrawdown::new();
122 let returns = BTreeMap::new();
123 let result = stat.calculate_from_returns(&returns);
124 assert_eq!(result, Some(0.0));
125 }
126
127 #[rstest]
128 fn test_no_drawdown() {
129 let stat = MaxDrawdown::new();
130 let returns = create_returns(&[0.01, 0.02, 0.01, 0.015]);
132 let result = stat.calculate_from_returns(&returns).unwrap();
133 assert_eq!(result, 0.0);
134 }
135
136 #[rstest]
137 fn test_simple_drawdown() {
138 let stat = MaxDrawdown::new();
139 let returns = create_returns(&[0.10, -0.10]);
142 let result = stat.calculate_from_returns(&returns).unwrap();
143
144 assert!((result + 0.10).abs() < 0.01);
146 }
147
148 #[rstest]
149 fn test_multiple_drawdowns() {
150 let stat = MaxDrawdown::new();
151 let returns = create_returns(&[0.10, -0.10, 0.50, -0.20, 0.10]);
155 let result = stat.calculate_from_returns(&returns).unwrap();
156
157 assert!((result + 0.20).abs() < 0.01);
159 }
160
161 #[rstest]
162 fn test_initial_loss() {
163 let stat = MaxDrawdown::new();
164 let returns = create_returns(&[-0.40, -0.10]);
166 let result = stat.calculate_from_returns(&returns).unwrap();
167
168 assert!((result + 0.46).abs() < 0.01);
171 }
172}