nautilus_analysis/statistics/
cagr.rs1use std::collections::BTreeMap;
19
20use nautilus_core::UnixNanos;
21use nautilus_model::position::Position;
22
23use crate::statistic::PortfolioStatistic;
24
25#[repr(C)]
34#[derive(Debug, Clone)]
35#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis", from_py_object)
38)]
39#[cfg_attr(
40 feature = "python",
41 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
42)]
43pub struct CAGR {
44 pub period: usize,
46}
47
48impl CAGR {
49 #[must_use]
51 pub fn new(period: Option<usize>) -> Self {
52 Self {
53 period: period.unwrap_or(252),
54 }
55 }
56}
57
58impl PortfolioStatistic for CAGR {
59 type Item = f64;
60
61 fn name(&self) -> String {
62 format!("CAGR ({} days)", self.period)
63 }
64
65 fn calculate_from_returns(&self, returns: &BTreeMap<UnixNanos, f64>) -> Option<Self::Item> {
66 if returns.is_empty() {
67 return Some(0.0);
68 }
69
70 let daily_returns = self.downsample_to_daily_bins(returns);
72
73 let total_return: f64 = daily_returns.values().map(|&r| 1.0 + r).product::<f64>() - 1.0;
75
76 let days = daily_returns.len().max(1) as f64;
79
80 let cagr = (1.0 + total_return).powf(self.period as f64 / days) - 1.0;
82
83 if cagr.is_finite() {
84 Some(cagr)
85 } else {
86 Some(0.0)
87 }
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 let mut returns = BTreeMap::new();
106 let nanos_per_day = 86_400_000_000_000;
107 let start_time = 1_600_000_000_000_000_000;
108
109 for (i, &value) in values.iter().enumerate() {
110 let timestamp = start_time + i as u64 * nanos_per_day;
111 returns.insert(UnixNanos::from(timestamp), value);
112 }
113
114 returns
115 }
116
117 #[rstest]
118 fn test_name() {
119 let cagr = CAGR::new(Some(252));
120 assert_eq!(cagr.name(), "CAGR (252 days)");
121 }
122
123 #[rstest]
124 fn test_empty_returns() {
125 let cagr = CAGR::new(Some(252));
126 let returns = BTreeMap::new();
127 let result = cagr.calculate_from_returns(&returns);
128 assert_eq!(result, Some(0.0));
129 }
130
131 #[rstest]
132 fn test_positive_cagr() {
133 let cagr = CAGR::new(Some(252));
134 let returns = create_returns(&vec![0.001; 252]);
138 let result = cagr.calculate_from_returns(&returns).unwrap();
139
140 assert!((result - 0.288).abs() < 0.01);
143 }
144
145 #[rstest]
146 fn test_cagr_half_year() {
147 let cagr = CAGR::new(Some(252));
148 let daily_return = (1.10_f64.powf(1.0 / 126.0)) - 1.0;
150 let returns = create_returns(&vec![daily_return; 126]);
151 let result = cagr.calculate_from_returns(&returns).unwrap();
152
153 assert!((result - 0.21).abs() < 0.01);
156 }
157
158 #[rstest]
159 fn test_negative_returns() {
160 let cagr = CAGR::new(Some(252));
161 let returns = create_returns(&vec![-0.001; 252]);
163 let result = cagr.calculate_from_returns(&returns).unwrap();
164
165 assert!(result < 0.0);
167 }
168
169 #[rstest]
170 fn test_multiple_trades_per_day() {
171 let cagr = CAGR::new(Some(252));
172
173 let mut returns = BTreeMap::new();
175 let nanos_per_day = 86_400_000_000_000;
176 let start_time = 1_600_000_000_000_000_000;
177
178 for i in 0..500 {
180 let day = (i * 252) / 500; let timestamp =
182 start_time + day as u64 * nanos_per_day + (i % 3) as u64 * 1_000_000_000;
183 returns.insert(UnixNanos::from(timestamp), 0.0005);
184 }
185
186 let result = cagr.calculate_from_returns(&returns).unwrap();
187
188 assert!((result - 0.285).abs() < 0.02);
192 assert!(result > 0.2); }
194
195 #[rstest]
196 fn test_intraday_trading() {
197 let cagr = CAGR::new(Some(252));
198
199 let mut returns = BTreeMap::new();
201 let start_time = 1_600_000_000_000_000_000;
202
203 for i in 0..10 {
205 let timestamp = start_time + i as u64 * 3_600_000_000_000; returns.insert(UnixNanos::from(timestamp), 0.01);
207 }
208
209 let result = cagr.calculate_from_returns(&returns).unwrap();
210
211 assert!(result > 0.0);
216 assert!(result.is_finite());
217 }
218}