use std::collections::BTreeMap;
use nautilus_core::UnixNanos;
use crate::{
statistic::PortfolioStatistic,
statistics::{cagr::CAGR, max_drawdown::MaxDrawdown},
};
#[repr(C)]
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
)]
pub struct CalmarRatio {
pub period: usize,
}
impl CalmarRatio {
#[must_use]
pub fn new(period: Option<usize>) -> Self {
Self {
period: period.unwrap_or(252),
}
}
}
impl PortfolioStatistic for CalmarRatio {
type Item = f64;
fn name(&self) -> String {
format!("Calmar Ratio ({} days)", self.period)
}
fn calculate_from_returns(&self, returns: &BTreeMap<UnixNanos, f64>) -> Option<Self::Item> {
if returns.is_empty() {
return Some(f64::NAN);
}
let cagr_stat = CAGR::new(Some(self.period));
let cagr = cagr_stat.calculate_from_returns(returns)?;
let max_dd_stat = MaxDrawdown::new();
let max_dd = max_dd_stat.calculate_from_returns(returns)?;
if max_dd.abs() < f64::EPSILON {
return Some(f64::NAN);
}
let calmar = cagr / max_dd.abs();
if calmar.is_finite() {
Some(calmar)
} else {
Some(f64::NAN)
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn create_returns(values: &[f64]) -> BTreeMap<UnixNanos, f64> {
let mut returns = BTreeMap::new();
let nanos_per_day = 86_400_000_000_000;
let start_time = 1_600_000_000_000_000_000;
for (i, &value) in values.iter().enumerate() {
let timestamp = start_time + i as u64 * nanos_per_day;
returns.insert(UnixNanos::from(timestamp), value);
}
returns
}
#[rstest]
fn test_name() {
let ratio = CalmarRatio::new(Some(252));
assert_eq!(ratio.name(), "Calmar Ratio (252 days)");
}
#[rstest]
fn test_empty_returns() {
let ratio = CalmarRatio::new(Some(252));
let returns = BTreeMap::new();
let result = ratio.calculate_from_returns(&returns);
assert!(result.is_some());
assert!(result.unwrap().is_nan());
}
#[rstest]
fn test_no_drawdown() {
let ratio = CalmarRatio::new(Some(252));
let returns = create_returns(&vec![0.01; 252]);
let result = ratio.calculate_from_returns(&returns);
assert!(result.is_some());
assert!(result.unwrap().is_nan());
}
#[rstest]
fn test_positive_ratio() {
let ratio = CalmarRatio::new(Some(252));
let mut returns_vec = vec![0.001; 200]; returns_vec.extend(vec![-0.002; 52]);
let returns = create_returns(&returns_vec);
let result = ratio.calculate_from_returns(&returns).unwrap();
assert!(result > 0.0);
}
#[rstest]
fn test_high_calmar_better() {
let ratio = CalmarRatio::new(Some(252));
let returns_a = create_returns(&vec![0.002; 252]);
let calmar_a = ratio.calculate_from_returns(&returns_a);
let returns_b = create_returns(&vec![0.001; 252]);
let calmar_b = ratio.calculate_from_returns(&returns_b);
assert!(calmar_a.is_some());
assert!(calmar_b.is_some());
}
}