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}