excel_lib/function/
xirr.rs1use 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
15pub 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#[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}