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)]
41#[derive(Debug, Clone, Default)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.analysis", from_py_object)
45)]
46#[cfg_attr(
47 feature = "python",
48 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
49)]
50pub struct MaxDrawdown {}
51
52impl MaxDrawdown {
53 #[must_use]
55 pub fn new() -> Self {
56 Self {}
57 }
58}
59
60impl PortfolioStatistic for MaxDrawdown {
61 type Item = f64;
62
63 fn name(&self) -> String {
64 "Max Drawdown".to_string()
65 }
66
67 fn calculate_from_returns(&self, returns: &BTreeMap<UnixNanos, f64>) -> Option<Self::Item> {
68 if returns.is_empty() {
69 return Some(0.0);
70 }
71
72 let mut cumulative = 1.0;
74 let mut running_max = 1.0;
75 let mut max_drawdown = 0.0;
76
77 for &ret in returns.values() {
78 cumulative *= 1.0 + ret;
79
80 if cumulative > running_max {
82 running_max = cumulative;
83 }
84
85 let drawdown = (running_max - cumulative) / running_max;
87
88 if drawdown > max_drawdown {
90 max_drawdown = drawdown;
91 }
92 }
93
94 Some(-max_drawdown)
96 }
97 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
98 None
99 }
100
101 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
102 None
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use nautilus_core::approx_eq;
109 use rstest::rstest;
110
111 use super::*;
112
113 fn create_returns(values: &[f64]) -> BTreeMap<UnixNanos, f64> {
114 values
115 .iter()
116 .copied()
117 .enumerate()
118 .map(|(i, v)| (UnixNanos::from(i as u64), v))
119 .collect()
120 }
121
122 #[rstest]
123 fn test_name() {
124 let stat = MaxDrawdown::new();
125 assert_eq!(stat.name(), "Max Drawdown");
126 }
127
128 #[rstest]
129 fn test_empty_returns() {
130 let stat = MaxDrawdown::new();
131 let returns = BTreeMap::new();
132 let result = stat.calculate_from_returns(&returns);
133 assert_eq!(result, Some(0.0));
134 }
135
136 #[rstest]
137 fn test_no_drawdown() {
138 let stat = MaxDrawdown::new();
139 let returns = create_returns(&[0.01, 0.02, 0.01, 0.015]);
141 let result = stat.calculate_from_returns(&returns).unwrap();
142 assert_eq!(result, 0.0);
143 }
144
145 #[rstest]
146 fn test_simple_drawdown() {
147 let stat = MaxDrawdown::new();
148 let returns = create_returns(&[0.10, -0.10]);
151 let result = stat.calculate_from_returns(&returns).unwrap();
152
153 assert!(approx_eq!(f64, result, -0.10, epsilon = 1e-12));
154 }
155
156 #[rstest]
157 fn test_multiple_drawdowns() {
158 let stat = MaxDrawdown::new();
159 let returns = create_returns(&[0.10, -0.10, 0.50, -0.20, 0.10]);
163 let result = stat.calculate_from_returns(&returns).unwrap();
164
165 assert!(approx_eq!(f64, result, -0.20, epsilon = 1e-12));
167 }
168
169 #[rstest]
170 fn test_initial_loss() {
171 let stat = MaxDrawdown::new();
172 let returns = create_returns(&[-0.40, -0.10]);
174 let result = stat.calculate_from_returns(&returns).unwrap();
175
176 assert!(approx_eq!(f64, result, -0.46, epsilon = 1e-12));
179 }
180}