rust_finprim/rate/
mirr.rs

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