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}