rust_finprim/rate/
twr.rs

1use crate::rate::pct_change;
2use crate::{FinPrimError, FloatLike};
3
4/// Time Weighted Return (TWR)
5///
6/// This function calculates the Time Weighted Rate of Return (TWR) for a series of asset values
7/// and cash flows.
8///
9/// The TWR is a measure of the compound growth rate of an investment portfolio over time,
10/// eliminating the impact of cash flows (deposits and withdrawals) on the return. It is useful
11/// as a performance measure on the underlying investment strategy of the portfolio.
12///
13/// # Arguments
14/// * `values` - A slice of tuples containing the asset value at the end of the period and the net
15/// cash flow during the period. Cash flows are from the perspective of the asset, i.e. net contibutions
16/// are positive and withdrawals/distributions are negative. For example, at the end of the year the
17/// asset value is $1000 and there was a net contribution of $100, the tuple would be (1000, 100).
18/// * `annualization_period` (optional) - The number of annual periods to annualize the TWR.
19/// If `None`, the TWR is not annualized. For example, if you have 6 months of data, you would
20/// pass `Some(0.5)` to annualize the TWR. If you have 6 quarters of data, you would pass
21/// `Some(1.5)` to annualize the TWR.
22///
23/// # Returns
24/// * The Time Weighted Rate of Return (TWR).
25///
26/// # Example
27/// ```
28/// use rust_finprim::rate::twr;
29///
30/// let values = vec![
31///    (1000.0, 1000.0), // Initially we bought an asset for $1000
32///    (1600.0, 400.0), // In Q1'24 we had a net contribution of $400, ending value is $1600
33///    (1450.0, -200.0), // In Q2'24 we had a net withdrawal of $200, ending value is $1450
34///    (1700.0, 200.0), // In Q3'24 we had a net contribution of $200, ending value is $1700
35///    (2200.0, 300.0), // In Q4'24 we had a net contribution of $300, ending value is $2200
36///    (2500.0, 0.0), // In Q1'25 we had no cash flows, ending value is $2500
37///    (3000.0, -300.0), // In Q2'25 we had a net withdrawal of $300, ending value is $3000
38///    (1700.0, -1500.0), // In Q3'25 we had a net withdrawal of $1500, ending value is $1700
39///    (0.0, 2000.0), // In Q4'25 the asset was liquidated and we had a net withdrawal of $2000
40/// ];
41/// let twr = twr(&values, Some(2.0)); // Annualize over 2 years
42/// ```
43pub fn twr<T: FloatLike>(values: &[(T, T)], annualization_period: Option<T>) -> Result<T, FinPrimError<T>> {
44    let total_return = values.windows(2).try_fold(T::one(), |acc, window| {
45        let (start_value, _) = window[0];
46        let (end_value, end_cashflow) = window[1];
47        let adjusted_end = end_value - end_cashflow;
48        pct_change(start_value, adjusted_end)
49            .map(|pct| acc * (pct + T::one()))
50            .map_err(|_| FinPrimError::DivideByZero)
51    })?;
52    // .map(|window| {
53    //     let (start_value, _) = window[0];
54    //     let (end_value, end_cashflow) = window[1];
55    //     let adjusted_end = end_value - end_cashflow;
56    //     pct_change(start_value, adjusted_end)
57    //         .map(|pct| pct + T::one())
58    //         .map_err(|_| return FinPrimError::DivideByZero)
59    // })
60    // .product::<T>();
61
62    Ok(annualization_period
63        .map(|period| (total_return).powf(T::one() / period) - T::one())
64        .unwrap_or(total_return - T::one()))
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[cfg(not(feature = "std"))]
72    extern crate std;
73    #[cfg(not(feature = "std"))]
74    use std::vec;
75
76    #[test]
77    fn test_twr() {
78        let values = vec![
79            (1000.0, 0.0),
80            (1600.0, 400.0),
81            (1450.0, -200.0),
82            (1700.0, 200.0),
83            (2200.0, 300.0),
84        ];
85        // Assume the periods are quarterly spanning 1 year
86        let twr_qtr = twr(&values, None).unwrap();
87        let expected_qtr: f64 = 0.43078093;
88        assert!((twr_qtr - expected_qtr).abs() < 1e-5);
89        // Assume the periods are annual spanning 4 years
90        let twr_yr = twr(&values, Some(4.0)).unwrap();
91        let expected_yr = 0.093688;
92        assert!((twr_yr - expected_yr).abs() < 1e-5);
93
94        let values_bankruptcy = vec![
95            (1000.0, 0.0),
96            (1600.0, 400.0),
97            (1450.0, -200.0),
98            (1700.0, 200.0),
99            (2200.0, 300.0),
100            (2500.0, 0.0),
101            (3000.0, -300.0),
102            (1700.0, -1500.0),
103            (0.0, 0.0),
104        ];
105        let twr_bankruptcy = twr(&values_bankruptcy, Some(2.0)).unwrap();
106        let expected_bankruptcy = -1.0;
107        assert_eq!(twr_bankruptcy, expected_bankruptcy);
108
109        let values_6qtr = vec![
110            (1000.0, 0.0),
111            (1600.0, 400.0),
112            (1450.0, -200.0),
113            (1700.0, 200.0),
114            (2200.0, 300.0),
115            (2500.0, 0.0),
116            (3000.0, -300.0),
117        ];
118        let twr_6qtr = twr(&values_6qtr, Some(1.5)).unwrap();
119        let expected_6qtr = 0.663832;
120        assert!((twr_6qtr - expected_6qtr).abs() < 1e-5);
121
122        // Just 2 periods
123        let values_2 = vec![(1000.0, 0.0), (1600.0, 400.0)];
124        let twr_2 = twr(&values_2, None).unwrap();
125        assert!(twr_2 - 0.2 < 1e-5);
126    }
127}