rust_finprim/rate/
twr.rs

1use crate::{rate::pct_change, ONE};
2use rust_decimal::prelude::*;
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) as a `Decimal`.
25///
26/// # Example
27/// ```
28/// use rust_decimal_macros::dec;
29/// use rust_finprim::rate::twr;
30///
31/// let values = vec![
32///    (dec!(1000), dec!(1000)), // Initially we bought an asset for $1000
33///    (dec!(1600), dec!(400)), // In Q1'24 we had a net contribution of $400, ending value is $1600
34///    (dec!(1450), dec!(-200)), // In Q2'24 we had a net withdrawal of $200, ending value is $1450
35///    (dec!(1700), dec!(200)), // In Q3'24 we had a net contribution of $200, ending value is $1700
36///    (dec!(2200), dec!(300)), // In Q4'24 we had a net contribution of $300, ending value is $2200
37///    (dec!(2500), dec!(0)), // In Q1'25 we had no cash flows, ending value is $2500
38///    (dec!(3000), dec!(-300)), // In Q2'25 we had a net withdrawal of $300, ending value is $3000
39///    (dec!(1700), dec!(-1500)), // In Q3'25 we had a net withdrawal of $1500, ending value is $1700
40///    (dec!(0), dec!(2000)), // In Q4'25 the asset was liquidated and we had a net withdrawal of $2000
41/// ];
42/// let twr = twr(&values, Some(dec!(2))); // Annualize over 2 years
43/// ```
44pub fn twr(values: &[(Decimal, Decimal)], annualization_period: Option<Decimal>) -> Decimal {
45    let total_return = values
46        .windows(2)
47        .map(|window| {
48            let (start_value, _) = window[0];
49            let (end_value, end_cashflow) = window[1];
50            let adjusted_end = end_value - end_cashflow;
51            pct_change(start_value, adjusted_end)
52                .map(|pct| ONE + pct)
53                .expect("TWR: pct_change failed, (ending value - cash flow) should not be zero")
54        })
55        .product::<Decimal>();
56
57    annualization_period
58        .map(|period| (total_return).powd(ONE / period) - ONE)
59        .unwrap_or(total_return - ONE)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use rust_decimal_macros::*;
66    #[cfg(not(feature = "std"))]
67    extern crate std;
68    #[cfg(not(feature = "std"))]
69    use std::assert;
70    #[cfg(not(feature = "std"))]
71    use std::prelude::v1::*;
72
73    #[test]
74    fn test_twr() {
75        let values = vec![
76            (dec!(1000), dec!(0)),
77            (dec!(1600), dec!(400)),
78            (dec!(1450), dec!(-200)),
79            (dec!(1700), dec!(200)),
80            (dec!(2200), dec!(300)),
81        ];
82        // Assume the periods are quarterly spanning 1 year
83        let twr_qtr = twr(&values, None);
84        let expected_qtr = dec!(0.43078093);
85        assert!((twr_qtr - expected_qtr).abs() < dec!(1e-5));
86        // Assume the periods are annual spanning 4 years
87        let twr_yr = twr(&values, Some(dec!(4)));
88        let expected_yr = dec!(0.093688);
89        assert!((twr_yr - expected_yr).abs() < dec!(1e-5));
90
91        let values_bankruptcy = vec![
92            (dec!(1000), dec!(0)),
93            (dec!(1600), dec!(400)),
94            (dec!(1450), dec!(-200)),
95            (dec!(1700), dec!(200)),
96            (dec!(2200), dec!(300)),
97            (dec!(2500), dec!(0)),
98            (dec!(3000), dec!(-300)),
99            (dec!(1700), dec!(-1500)),
100            (dec!(0), dec!(0)),
101        ];
102        let twr_bankruptcy = twr(&values_bankruptcy, Some(dec!(2)));
103        println!("TWR with bankruptcy: {}", twr_bankruptcy);
104        let expected_bankruptcy = dec!(-1);
105        assert_eq!(twr_bankruptcy, expected_bankruptcy);
106
107        let values_6qtr = vec![
108            (dec!(1000), dec!(0)),
109            (dec!(1600), dec!(400)),
110            (dec!(1450), dec!(-200)),
111            (dec!(1700), dec!(200)),
112            (dec!(2200), dec!(300)),
113            (dec!(2500), dec!(0)),
114            (dec!(3000), dec!(-300)),
115        ];
116        let twr_6qtr = twr(&values_6qtr, Some(dec!(1.5)));
117        let expected_6qtr = dec!(0.663832);
118        assert!((twr_6qtr - expected_6qtr).abs() < dec!(1e-5));
119
120        // Just 2 periods
121        let values_2 = vec![(dec!(1000), dec!(0)), (dec!(1600), dec!(400))];
122        let twr_2 = twr(&values_2, None);
123        assert_eq!(twr_2, dec!(0.2));
124    }
125}