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}