Skip to main content

quant_metrics/
returns.rs

1//! Return calculations.
2
3use rust_decimal::Decimal;
4
5use crate::MetricsError;
6
7/// Calculate total return as a percentage.
8///
9/// Formula: (end - start) / start * 100
10///
11/// # Arguments
12/// * `equity` - Equity curve (NAV or portfolio value over time)
13///
14/// # Returns
15/// Total percentage return, or error if insufficient data.
16///
17/// # Example
18/// ```
19/// use quant_metrics::total_return;
20/// use rust_decimal_macros::dec;
21///
22/// let equity = vec![dec!(10000), dec!(10500)];
23/// assert_eq!(total_return(&equity).unwrap(), dec!(5)); // 5%
24/// ```
25pub fn total_return(equity: &[Decimal]) -> Result<Decimal, MetricsError> {
26    if equity.len() < 2 {
27        return Err(MetricsError::InsufficientData {
28            required: 2,
29            actual: equity.len(),
30        });
31    }
32
33    let start = equity[0];
34    let end = equity[equity.len() - 1];
35
36    if start == Decimal::ZERO {
37        return Err(MetricsError::DivisionByZero {
38            context: "starting equity is zero",
39        });
40    }
41
42    Ok(((end - start) / start) * Decimal::from(100))
43}
44
45/// Calculate Compound Annual Growth Rate (CAGR).
46///
47/// Formula: ((end / start) ^ (1/years) - 1) * 100
48///
49/// # Arguments
50/// * `equity` - Equity curve
51/// * `years` - Number of years in the period
52///
53/// # Note
54/// Uses Newton-Raphson approximation for nth root since Decimal
55/// doesn't have native power functions.
56pub fn cagr(equity: &[Decimal], years: Decimal) -> Result<Decimal, MetricsError> {
57    if equity.len() < 2 {
58        return Err(MetricsError::InsufficientData {
59            required: 2,
60            actual: equity.len(),
61        });
62    }
63
64    if years <= Decimal::ZERO {
65        return Err(MetricsError::InvalidParameter(
66            "years must be positive".into(),
67        ));
68    }
69
70    let start = equity[0];
71    let end = equity[equity.len() - 1];
72
73    if start == Decimal::ZERO {
74        return Err(MetricsError::DivisionByZero {
75            context: "starting equity is zero",
76        });
77    }
78
79    let ratio = end / start;
80
81    // For CAGR we need: ratio^(1/years) - 1
82    // Using natural log approximation: e^(ln(ratio)/years) - 1
83    // Approximated via Taylor series for small growth rates
84    let ln_ratio = ln_approx(ratio);
85    let exponent = ln_ratio / years;
86    let growth_factor = exp_approx(exponent);
87
88    Ok((growth_factor - Decimal::ONE) * Decimal::from(100))
89}
90
91/// Calculate annualized return from period return.
92///
93/// Formula: ((1 + period_return/100) ^ (periods_per_year / periods) - 1) * 100
94///
95/// # Arguments
96/// * `equity` - Equity curve
97/// * `periods_per_year` - Number of periods in a year (252 for daily, 12 for monthly)
98pub fn annualized_return(
99    equity: &[Decimal],
100    periods_per_year: u32,
101) -> Result<Decimal, MetricsError> {
102    if equity.len() < 2 {
103        return Err(MetricsError::InsufficientData {
104            required: 2,
105            actual: equity.len(),
106        });
107    }
108
109    let periods = Decimal::from(equity.len() - 1);
110    let years = periods / Decimal::from(periods_per_year);
111
112    cagr(equity, years)
113}
114
115/// Natural logarithm approximation using Taylor series.
116/// Works best for values close to 1.
117fn ln_approx(x: Decimal) -> Decimal {
118    if x <= Decimal::ZERO {
119        return Decimal::ZERO;
120    }
121
122    // For x close to 1: ln(x) ≈ (x-1) - (x-1)^2/2 + (x-1)^3/3 - ...
123    // For larger x, we use: ln(x) = ln(x/e^n) + n for some n
124    let one = Decimal::ONE;
125    let y = x - one;
126
127    if y.abs() < Decimal::from(1) {
128        // Taylor series around 1 (10 terms for better accuracy)
129        let y2 = y * y;
130        let y3 = y2 * y;
131        let y4 = y3 * y;
132        let y5 = y4 * y;
133        let y6 = y5 * y;
134        let y7 = y6 * y;
135        let y8 = y7 * y;
136        let y9 = y8 * y;
137        let y10 = y9 * y;
138
139        y - y2 / Decimal::from(2) + y3 / Decimal::from(3) - y4 / Decimal::from(4)
140            + y5 / Decimal::from(5)
141            - y6 / Decimal::from(6)
142            + y7 / Decimal::from(7)
143            - y8 / Decimal::from(8)
144            + y9 / Decimal::from(9)
145            - y10 / Decimal::from(10)
146    } else {
147        // For larger values, use iterative reduction
148        // ln(x) = 2 * ln(sqrt(x)) if x > 2
149        let mut val = x;
150        let mut multiplier = Decimal::ONE;
151
152        while val > Decimal::from(2) {
153            val = decimal_sqrt(val);
154            multiplier *= Decimal::from(2);
155        }
156
157        multiplier * ln_approx(val)
158    }
159}
160
161/// Exponential approximation using Taylor series (12 terms).
162fn exp_approx(x: Decimal) -> Decimal {
163    // e^x ≈ 1 + x + x^2/2! + x^3/3! + ...
164    // 12 terms gives <0.01% error for |x| < 3
165    let x2 = x * x;
166    let x3 = x2 * x;
167    let x4 = x3 * x;
168    let x5 = x4 * x;
169    let x6 = x5 * x;
170    let x7 = x6 * x;
171    let x8 = x7 * x;
172    let x9 = x8 * x;
173    let x10 = x9 * x;
174    let x11 = x10 * x;
175    let x12 = x11 * x;
176
177    Decimal::ONE
178        + x
179        + x2 / Decimal::from(2)
180        + x3 / Decimal::from(6)
181        + x4 / Decimal::from(24)
182        + x5 / Decimal::from(120)
183        + x6 / Decimal::from(720)
184        + x7 / Decimal::from(5040)
185        + x8 / Decimal::from(40320)
186        + x9 / Decimal::from(362880)
187        + x10 / Decimal::from(3628800)
188        + x11 / Decimal::from(39916800)
189        + x12 / Decimal::from(479001600)
190}
191
192use crate::math::decimal_sqrt;
193
194#[cfg(test)]
195#[path = "returns_tests.rs"]
196mod tests;