rust_finprim/rate/irr/
irr.rs

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