rust_finprim/rate/
irr.rs

1use crate::derivatives::pv_prime_r;
2use crate::tvm::{npv, xnpv};
3use rust_decimal::prelude::*;
4use rust_decimal_macros::*;
5
6/// IRR - Internal Rate of Return
7///
8/// The internal rate of return (IRR) is a metric used in capital budgeting to estimate the profitability of potential investments.
9/// The IRR is the interest rate (discount rate) that makes the net present value (NPV) of all cash flows from a particular project equal to zero.
10/// IRR calculations rely on the same formula as NPV does, but in this case, the NPV is set to zero and the discount rate is the unknown variable.
11/// Similar behavior and usage to the `IRR` function in Excel.
12///
13/// # Arguments
14/// * `cash_flows` - A vector of Decimal values representing the cash flows of the investment
15/// * `guess` (optional) - A guess for the IRR, defaults to 0.1. Providing a guess can help the function converge faster
16/// * `tolerance` (optional) - The tolerance/maximum error bound for the IRR calculation, defaults to 1e-5 i.e. 0.00001
17///
18/// # Returns
19/// * Result of the IRR calculation
20/// * If the calculation fails, it returns a tuple of the last estimated rate and the NPV at that rate
21/// * If the NPV is close to zero, you may consider lowering the tolerance or providing a guess at
22/// the last estimated rate. Otherwise, there may be no IRR.
23///
24/// # Example
25/// * Cash flows of $-100, $50, $40, $30, $20
26/// ```
27/// use rust_finprim::rate::irr;
28/// use rust_decimal_macros::*;
29///
30/// let cash_flows = vec![dec!(-100), dec!(50), dec!(40), dec!(30), dec!(20)];
31/// irr(&cash_flows, None, None);
32/// ```
33///
34/// # Formula
35/// The IRR is calculated by finding the discount rate that makes the net present value (NPV) of all cash flows equal to zero.
36/// The formula is:
37/// $$NPV = \sum_{t=0}^{n} \frac{CF_t}{(1+IRR)^t} = 0$$
38///
39/// Where:
40/// * \\(CF_t\\) = cash flow at time \\(t\\)
41/// * \\(IRR\\) = internal rate of return
42///
43/// This function uses the Newton-Raphson method to find the root of the NPV formula, maxing out
44/// at 20 iterations.
45pub fn irr(
46    cash_flows: &[Decimal],
47    guess: Option<Decimal>,
48    tolerance: Option<Decimal>,
49) -> Result<Decimal, (Decimal, Decimal)> {
50    const MAX_ITER: u8 = 20;
51    let tolerance = tolerance.unwrap_or(dec!(1e-5));
52
53    // Newton-Raphson method
54    let mut rate = guess.unwrap_or(dec!(0.1));
55    for _ in 0..MAX_ITER {
56        let npv_value = npv(rate, cash_flows);
57        if npv_value.abs() < tolerance {
58            return Ok(rate);
59        }
60        let drate: Decimal = cash_flows
61            .iter()
62            .enumerate()
63            .map(|(i, &cf)| pv_prime_r(rate, i.into(), cf))
64            .sum();
65        if drate.is_zero() {
66            // Avoid division by zero, return the current rate
67            return Err((rate, npv_value));
68        }
69        rate -= npv_value / drate;
70    }
71    Err((rate, npv(rate, cash_flows)))
72}
73
74/// XIRR - Internal Rate of Return for Irregular Cash Flows
75///
76/// The XIRR function calculates the internal rate of return for a schedule of cash flows that is not necessarily periodic.
77///
78/// # Arguments
79/// * `flow_table` - A slice of tuples representing the cash flows and dates for each period `(cash_flow, date)`
80/// where `date` represents the number of days from an arbitrary epoch. The first cash flow
81/// is assumed to be the initial investment date, the order of subsequent cash flows does
82/// not matter.
83/// * `guess` (optional) - A guess for the IRR, defaults to 0.1. Providing a guess can help the function converge faster
84/// * `tolerance` (optional) - The tolerance/maximum error bound for the IRR calculation, defaults to 1e-5 i.e. 0.00001
85///
86/// Most time libraries will provide a method for the number of days from an epoch. For example, in the `chrono` library
87/// you can use the `num_days_from_ce` method to get the number of days from the Common Era (CE) epoch, simply convert
88/// your date types to an integer representing the number of days from any epoch. Alternatively, you can calculate the
89/// time delta in days from an arbitrary epoch, such as the initial investment date.
90///
91/// Cash flows are discounted assuming a 365-day year.
92///
93/// # Returns
94/// * Result of the IRR calculation
95/// * If the calculation fails, it returns a tuple of the last estimated rate and the NPV at that rate
96/// * If the NPV is close to zero, you may consider lowering the tolerance or providing a guess at
97/// the last estimated rate. Otherwise, there may be no IRR.
98///
99/// # Example
100/// * Cash flows of $-100, $50, $40, $30, $20
101/// ```
102/// use rust_finprim::rate::xirr;
103/// use rust_decimal_macros::*;
104///
105/// let flow_table = vec![
106///    (dec!(-100), 0),
107///    (dec!(50), 359),
108///    (dec!(40), 400),
109///    (dec!(30), 1000),
110///    (dec!(20), 2000),
111/// ];
112/// xirr(&flow_table, None, None);
113pub fn xirr(
114    flow_table: &[(Decimal, i32)],
115    guess: Option<Decimal>,
116    tolerance: Option<Decimal>,
117) -> Result<Decimal, (Decimal, Decimal)> {
118    let tolerance = tolerance.unwrap_or(dec!(1e-5));
119    const MAX_ITER: u8 = 20;
120    // First date should be 0 (initial investment) and the rest should be difference from the initial date
121    let init_date = flow_table.first().unwrap().1;
122
123    let mut rate = guess.unwrap_or(dec!(0.1));
124    for _ in 0..MAX_ITER {
125        let npv_value = xnpv(rate, &flow_table);
126        if npv_value.abs() < tolerance {
127            return Ok(rate);
128        }
129        let drate: Decimal = flow_table
130            .iter()
131            .map(|&(cf, date)| pv_prime_r(rate, Decimal::from_i32(date - init_date).unwrap() / dec!(365), cf))
132            .sum();
133        if drate.is_zero() {
134            // Avoid division by zero, return the current rate
135            return Err((rate, npv_value));
136        }
137        rate -= npv_value / drate;
138    }
139    Err((rate, xnpv(rate, &flow_table)))
140}
141
142#[cfg(test)]
143mod tests {
144    #[cfg(not(feature = "std"))]
145    extern crate std;
146    use super::*;
147    #[cfg(not(feature = "std"))]
148    use std::prelude::v1::*;
149    #[cfg(not(feature = "std"))]
150    use std::{assert, vec};
151
152    #[test]
153    fn test_irr() {
154        let cash_flows = vec![dec!(-100), dec!(50), dec!(40), dec!(30), dec!(1000)];
155        let result = irr(&cash_flows, None, Some(dec!(1e-20)));
156        if let Err((rate, npv)) = result {
157            assert!(
158                (npv).abs() < dec!(1e-20),
159                "Failed to converge at 1e-20 precision. Last rate: {}, NPV: {}",
160                rate,
161                npv
162            );
163        } else {
164            assert!(true);
165        }
166    }
167
168    #[test]
169    fn test_xirr() {
170        let flow_table = vec![
171            (dec!(-100), 0),
172            (dec!(50), 359),
173            (dec!(40), 400),
174            (dec!(30), 1000),
175            (dec!(20), 2000),
176        ];
177        let xirr = xirr(&flow_table, None, Some(dec!(1e-20)));
178        if let Err((rate, npv)) = xirr {
179            assert!(
180                (npv).abs() < dec!(1e-20),
181                "Failed to converge at 1e-20 precision. Last rate: {}, NPV: {}",
182                rate,
183                npv
184            );
185        } else {
186            let expected = dec!(0.20084);
187            assert!(
188                (xirr.unwrap() - expected).abs() < dec!(1e-5),
189                "Failed on case: {}. Expected: {}, Result: {}",
190                "Cash flows of -100, 50, 40, 30, 20",
191                expected,
192                xirr.unwrap()
193            );
194        }
195    }
196}