nautilus_analysis/statistics/
calmar_ratio.rs1use std::collections::BTreeMap;
19
20use nautilus_core::UnixNanos;
21use nautilus_model::position::Position;
22
23use crate::{
24 statistic::PortfolioStatistic,
25 statistics::{cagr::CAGR, max_drawdown::MaxDrawdown},
26};
27
28#[repr(C)]
42#[derive(Debug, Clone)]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.analysis", from_py_object)
46)]
47#[cfg_attr(
48 feature = "python",
49 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
50)]
51pub struct CalmarRatio {
52 pub period: usize,
54}
55
56impl CalmarRatio {
57 #[must_use]
59 pub fn new(period: Option<usize>) -> Self {
60 Self {
61 period: period.unwrap_or(252),
62 }
63 }
64}
65
66impl PortfolioStatistic for CalmarRatio {
67 type Item = f64;
68
69 fn name(&self) -> String {
70 format!("Calmar Ratio ({} days)", self.period)
71 }
72
73 fn calculate_from_returns(&self, returns: &BTreeMap<UnixNanos, f64>) -> Option<Self::Item> {
74 if returns.is_empty() {
75 return Some(f64::NAN);
76 }
77
78 let cagr_stat = CAGR::new(Some(self.period));
80 let cagr = cagr_stat.calculate_from_returns(returns)?;
81
82 let max_dd_stat = MaxDrawdown::new();
84 let max_dd = max_dd_stat.calculate_from_returns(returns)?;
85
86 if max_dd.abs() < f64::EPSILON {
90 return Some(f64::NAN);
91 }
92
93 let calmar = cagr / max_dd.abs();
94
95 if calmar.is_finite() {
96 Some(calmar)
97 } else {
98 Some(f64::NAN)
99 }
100 }
101 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
102 None
103 }
104
105 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
106 None
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use nautilus_core::approx_eq;
113 use rstest::rstest;
114
115 use super::*;
116
117 fn create_returns(values: &[f64]) -> BTreeMap<UnixNanos, f64> {
118 let mut returns = BTreeMap::new();
119 let nanos_per_day = 86_400_000_000_000;
120 let start_time = 1_600_000_000_000_000_000;
121
122 for (i, &value) in values.iter().enumerate() {
123 let timestamp = start_time + i as u64 * nanos_per_day;
124 returns.insert(UnixNanos::from(timestamp), value);
125 }
126
127 returns
128 }
129
130 #[rstest]
131 fn test_name() {
132 let ratio = CalmarRatio::new(Some(252));
133 assert_eq!(ratio.name(), "Calmar Ratio (252 days)");
134 }
135
136 #[rstest]
137 fn test_empty_returns() {
138 let ratio = CalmarRatio::new(Some(252));
139 let returns = BTreeMap::new();
140 let result = ratio.calculate_from_returns(&returns);
141 assert!(result.is_some());
142 assert!(result.unwrap().is_nan());
143 }
144
145 #[rstest]
146 fn test_no_drawdown() {
147 let ratio = CalmarRatio::new(Some(252));
148 let returns = create_returns(&vec![0.01; 252]);
150 let result = ratio.calculate_from_returns(&returns);
151
152 assert!(result.is_some());
154 assert!(result.unwrap().is_nan());
155 }
156
157 #[rstest]
158 fn test_known_value() {
159 let ratio = CalmarRatio::new(Some(5));
165 let returns = create_returns(&[0.10, -0.10, 0.50, -0.20, 0.10]);
166 let result = ratio.calculate_from_returns(&returns).unwrap();
167 assert!(approx_eq!(f64, result, 1.534, epsilon = 1e-9));
168 }
169
170 #[rstest]
171 #[case(5)]
172 #[case(252)]
173 fn test_undefined_cagr_propagates_to_calmar_ratio(#[case] days: usize) {
174 let ratio = CalmarRatio::new(Some(252));
175 let mut values = vec![0.0; days];
176 values[0] = -1.5;
177 let returns = create_returns(&values);
178
179 let result = ratio.calculate_from_returns(&returns).unwrap();
180
181 assert!(result.is_nan());
182 }
183
184 #[rstest]
185 fn test_positive_ratio() {
186 let ratio = CalmarRatio::new(Some(252));
187 let mut returns_vec = vec![0.001; 200]; returns_vec.extend(vec![-0.002; 52]); let returns = create_returns(&returns_vec);
194 let result = ratio.calculate_from_returns(&returns).unwrap();
195
196 assert!(result > 0.0);
198 }
199
200 #[rstest]
201 fn test_high_calmar_better() {
202 let ratio = CalmarRatio::new(Some(252));
203
204 let returns_a = create_returns(&vec![0.002; 252]);
206 let calmar_a = ratio.calculate_from_returns(&returns_a);
207
208 let returns_b = create_returns(&vec![0.001; 252]);
210 let calmar_b = ratio.calculate_from_returns(&returns_b);
211
212 assert!(calmar_a.is_some());
215 assert!(calmar_b.is_some());
216 }
217}