rust_finprim/rate/
mirr.rs

1use crate::rate::cagr;
2use crate::tvm::{fv, pv};
3use crate::FloatLike;
4
5/// MIRR - Modified Internal Rate of Return
6///
7/// The modified internal rate of return (MIRR) is a financial metric that adjusts the
8/// internal rate of return (IRR) to account for a different cost of capital and reinvestment rate.
9/// Similar behavior and usage to the `MIRR` function in Excel.
10///
11/// The MIRR assumes that positive cash flows are reinvested at a reinvestment rate, and
12/// any negative cash flows are financed at the cost of capital.
13///
14/// # Arguments
15/// * `cash_flows` - A slice of values representing the cash flows of the investment
16/// * `finance_rate` - The cost of capital (interest rate) for financing
17/// * `reinvest_rate` - The reinvestment rate for positive cash flows
18///
19/// # Returns
20/// * The modified internal rate of return (MIRR)
21///
22/// # Example
23/// * Cash flows of $-100, $50, $40, $30, $20, finance rate of 0.1, reinvestment rate of 0.05
24/// ```
25/// use rust_finprim::rate::mirr;
26///
27/// let cash_flows = vec![-100.0, 50.0, 40.0, 30.0, 20.0];
28/// let finance_rate = 0.1;
29/// let reinvest_rate = 0.05;
30/// mirr(&cash_flows, finance_rate, reinvest_rate);
31/// ```
32pub fn mirr<T: FloatLike>(cash_flows: &[T], finance_rate: T, reinvest_rate: T) -> T {
33    // Num of compounding perids does not include the final period
34    let n = cash_flows.len() - 1;
35
36    let mut npv_neg = T::zero();
37    let mut fv_pos = T::zero();
38    for (i, &cf) in cash_flows.iter().enumerate() {
39        if cf < T::zero() {
40            // Calculate the present value of negative cash flows
41            npv_neg += pv(finance_rate, T::from_usize(i), T::zero(), Some(cf), None);
42        } else {
43            // Calculate the future value of positive cash flows
44            fv_pos -= fv(reinvest_rate, T::from_usize(n - i), T::zero(), Some(cf), None);
45        }
46    }
47    npv_neg = npv_neg.abs(); // Ensure npv_neg is positive for the calculation
48    cagr(
49        // Calculate the CAGR using the future value of positive cash flows and the present value of negative cash flows
50        npv_neg,
51        fv_pos,
52        T::from_usize(n),
53    )
54}
55
56/// XMIRR - Modified Internal Rate of Return for Irregular Cash Flows
57///
58/// The XMIRR function calculates the modified internal rate of return for a schedule of cash flows that is not necessarily periodic.
59///
60/// # Arguments
61/// * `flow_table` - A slice of tuples representing the cash flows and dates for each period `(cash_flow, date)`
62/// where `date` represents the number of days from an arbitrary epoch. The first cash flow is assumed to be the initial investment date
63/// at time 0, the order of subsequent cash flows does not matter.
64/// * `finance_rate` - The cost of capital (interest rate) for financing
65/// * `reinvest_rate` - The reinvestment rate for positive cash flows
66///
67/// Most time libraries will provide a method for the number of days from an epoch. For example, in the `chrono` library
68/// you can use the `num_days_from_ce` method to get the number of days from the Common Era (CE) epoch, simply convert
69/// your date types to an integer representing the number of days from any epoch. Alternatively, you can calculate the
70/// time delta in days from an arbitrary epoch, such as the initial investment date.
71///
72/// Cash flows are discounted assuming a 365-day year.
73///
74/// # Returns
75/// * The modified internal rate of return (MIRR)
76///
77/// # Example
78/// * Cash flows of $-100, $-20, $20, $20, $20, finance rate of 0.1, reinvestment rate of 0.05
79/// ```
80/// use rust_finprim::rate::xmirr;
81///
82/// let flow_table = vec![
83///   (-100.0, 0),
84///   (-20.0, 359),
85///   (20.0, 400),
86///   (20.0, 1000),
87///   (20.0, 2000),
88/// ];
89/// let finance_rate = 0.1;
90/// let reinvest_rate = 0.05;
91/// xmirr(&flow_table, finance_rate, reinvest_rate);
92pub fn xmirr<T: FloatLike>(flow_table: &[(T, i32)], finance_rate: T, reinvest_rate: T) -> T {
93    let init_date = flow_table.first().unwrap().1;
94
95    let n = T::from_i32(flow_table.last().unwrap().1);
96    let mut npv_neg = T::zero();
97    let mut fv_pos = T::zero();
98    // Calculate the NPV of negative cash flows and the FV of positive cash Flows
99    for &(cf, date) in flow_table {
100        // For negative cash flows, calculate the present value
101        // For positive cash flows, calculate the future value
102        if cf < T::zero() {
103            npv_neg += pv(
104                finance_rate,
105                T::from_i32(date - init_date) / T::from_u16(365),
106                T::zero(),
107                Some(cf),
108                None,
109            );
110        } else {
111            fv_pos -= fv(
112                reinvest_rate,
113                (n - T::from_i32(date)) / T::from_u16(365),
114                T::zero(),
115                Some(cf),
116                None,
117            );
118        }
119    }
120    npv_neg.abs(); // Ensure npv_neg is positive for the calculation
121    cagr(
122        // Calculate the CAGR using the future value of positive cash flows and the present value of negative cash flows
123        npv_neg,
124        fv_pos,
125        n / T::from_u16(365), // Convert to years by dividing by 365
126    )
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[cfg(not(feature = "std"))]
134    extern crate std;
135    #[cfg(not(feature = "std"))]
136    use std::{assert, vec};
137
138    #[test]
139    fn test_mirr() {
140        let cash_flows = vec![-100.0, -20.0, 20.0, 20.0, 20.0];
141        let finance_rate = 0.1;
142        let reinvest_rate = 0.05;
143        let result = mirr(&cash_flows, finance_rate, reinvest_rate);
144        let expected: f64 = -0.14536;
145        assert!(
146            (result - expected).abs() < 1e-5,
147            "Failed on case: {}. Expected: {}, Result: {}",
148            "Cash flows of -100, -20, 20, 20, 20, finance rate of 0.1, reinvestment rate of 0.05",
149            expected,
150            result
151        );
152    }
153
154    #[test]
155    fn test_xmirr() {
156        let finance_rate = 0.1;
157        let reinvest_rate = 0.05;
158
159        // Simtle 1 year case
160        let flow_table = vec![(-100.0, 0), (-20.0, 365), (20.0, 730), (20.0, 1095), (20.0, 1460)];
161        let result = xmirr(&flow_table, finance_rate, reinvest_rate);
162        let expected: f64 = -0.14536;
163        assert!(
164            (result - expected).abs() < 1e-5,
165            "Failed on case: {}. Expected: {}, Result: {}",
166            "Cash flows of -100, -20, 20, 20, 20, finance rate of 0.1, reinvestment rate of 0.05",
167            expected,
168            result
169        );
170
171        // More complex case
172        let flow_table = vec![(-100.0, 0), (-20.0, 359), (20.0, 400), (20.0, 1000), (20.0, 2000)];
173        let result = xmirr(&flow_table, finance_rate, reinvest_rate);
174        let expected: f64 = -0.09689;
175        assert!(
176            (result - expected).abs() < 1e-5,
177            "Failed on case: {}. Expected: {}, Result: {}",
178            "Cash flows of -100, -20, 20, 20, 20, at 0, 359, 400, 1000, 2000 days,
179            finance rate of 0.1, reinvestment rate of 0.05",
180            expected,
181            result
182        );
183    }
184}