excel_lib/function/
xirr.rs

1use chrono::NaiveDate; 
2use std::error::Error;
3use std::fmt;
4use std::fmt::{Formatter, Display};
5
6const MAX_ERROR: f64 = 1e-10;
7const MAX_COMPUTE_WITH_GUESS_ITERATIONS: u32 = 50;
8
9#[derive(Copy, Clone)]
10pub struct Payment {
11    pub amount: f64,
12    pub date: NaiveDate,
13}
14
15/// Calculates the internal rate of return of a series of irregular payments.
16///
17/// It tries to identify the rate of return using Newton's method with an initial guess of 0.1.
18/// If that does not provide a solution, it attempts with guesses from -0.99 to 0.99
19/// in increments of 0.01 and returns NaN if that fails too.
20///
21/// # Errors
22///
23/// This function will return [`InvalidPaymentsError`](struct.InvalidPaymentsError.html)
24/// if both positive and negative payments are not provided.
25pub fn compute(payments: &[Payment]) -> Result<f64, InvalidPaymentsError> {
26    validate(payments)?;
27
28    let mut sorted: Vec<_> = payments.iter().collect();
29    sorted.sort_by_key(|p| p.date);
30
31    let mut rate = compute_with_guess(&sorted, 0.1);
32    let mut guess = -0.99;
33
34    while guess < 1.0 && (rate.is_nan() || rate.is_infinite()) {
35        rate = compute_with_guess(&sorted, guess);
36        guess += 0.01;
37    }
38
39    Ok(rate)
40}
41
42/// An error returned when the payments provided to [`compute`](fn.compute.html) do not contain
43/// both negative and positive payments.
44#[derive(Debug)]
45pub struct InvalidPaymentsError;
46
47impl Display for InvalidPaymentsError {
48    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
49        "negative and positive payments are required".fmt(f)
50    }
51}
52
53impl Error for InvalidPaymentsError {}
54
55fn compute_with_guess(payments: &[&Payment], guess: f64) -> f64 {
56    let mut r = guess;
57    let mut e = 1.0;
58
59    for _ in 0..MAX_COMPUTE_WITH_GUESS_ITERATIONS {
60        if e <= MAX_ERROR {
61            return r;
62        }
63
64        let r1 = r - xirr(payments, r) / dxirr(payments, r);
65        e = (r1 - r).abs();
66        r = r1;
67    }
68
69    f64::NAN
70}
71
72pub fn xirr(payments: &[&Payment], rate: f64) -> f64 {
73    let mut result = 0.0;
74    for p in payments {
75        let exp = get_exp(p, payments[0]);
76        result += p.amount / (1.0 + rate).powf(exp)
77    }
78    result
79}
80
81fn dxirr(payments: &[&Payment], rate: f64) -> f64 {
82    let mut result = 0.0;
83    for p in payments {
84        let exp = get_exp(p, payments[0]);
85        result -= p.amount * exp / (1.0 + rate).powf(exp + 1.0)
86    }
87    result
88}
89
90fn validate(payments: &[Payment]) -> Result<(), InvalidPaymentsError> {
91    let positive = payments.iter().any(|p| p.amount > 0.0);
92    let negative = payments.iter().any(|p| p.amount < 0.0);
93
94    if positive && negative {
95        Ok(())
96    } else {
97        Err(InvalidPaymentsError)
98    }
99}
100
101fn get_exp(p: &Payment, p0: &Payment) -> f64 {
102    NaiveDate::signed_duration_since(p.date, p0.date).num_days() as f64 / 365.0
103}