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}