Skip to content

Commit

Permalink
IRR/XIRR analytics solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Anexen committed Jun 18, 2024
1 parent 1866855 commit c18e209
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 133 deletions.
File renamed without changes.
135 changes: 3 additions & 132 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ndarray = "0.15"

[dev-dependencies]
assert_approx_eq = "1.1"
rstest = "0.18.2"
rstest = { version = "0.18.2", default-features = false }
pyo3 = { version = "0.20", features = ["auto-initialize"]}

[features]
Expand Down
63 changes: 63 additions & 0 deletions src/core/periodic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ pub fn irr(values: &[f64], guess: Option<f64>) -> Result<f64, InvalidPaymentsErr
// must contain at least one positive and one negative value
validate(values, None)?;

if values.len() == 2 {
return Ok(irr_analytical_2(values));
}

if values.len() == 3 {
return Ok(irr_analytical_3(values));
}

let f = |rate| {
if rate <= -1.0 {
// bound newton_raphson
Expand Down Expand Up @@ -476,6 +484,61 @@ pub fn irr(values: &[f64], guess: Option<f64>) -> Result<f64, InvalidPaymentsErr
Ok(rate.unwrap_or(f64::NAN))
}

fn irr_analytical_2(values: &[f64]) -> f64 {
// cf[0]/(1+r)^0 + cf[1]/(1+r)^1 = 0 => multiply by (1 + r)
// cf[0]*(1+r) + cf[1] = 0 => divide by cf[0] and move tho the right
// lets x = 1+r, a = cf[0], b = cf[1]
// solve a*x + b = 0
// x = -b/a, r = x - 1
-values[1] / values[0] - 1.0
}

fn irr_analytical_3(values: &[f64]) -> f64 {
// cf[0]/(1+r)^0 + cf[1]/(1+r)^1 + cf[2]/(1+r)^2 = 0 => multiply by (1+r)^2
// cf[0]*(1+r)^2 + cf[1]*(1+r) + cf[2] = 0 => quadratic equation
// lets x = 1+r, a = cf[0], b = cf[1], c = cf[2]
// solve a*x^2 + b*x + c = 0
// x = (-b ± sqrt(b^2-4ac))/2a, a != 0

let (a, b, c) = (values[0], values[1], values[2]);

let x = if a == 0. {
// 0*x^2 + bx + c = 0 =>
// x = -c/b
-c / b
} else {
let d = b.powf(2.) - 4. * a * c; // discriminant
if d < 0.0 {
// no real solutions
f64::NAN
} else if d == 0.0 {
// exactly one solution
-b / (2. * a)
} else {
let mut x1 = (-b + d.sqrt()) / (2. * a);
let mut x2 = (-b - d.sqrt()) / (2. * a);
// since x = 1 + r => r = x - 1,
// negative x doesn't make sense (rate will be < -1)
// use the first non negative value to be conservative
if x1 > x2 {
// make x2 always > x1
std::mem::swap(&mut x1, &mut x2);
}
if x1 > 0.0 {
x1
} else if x2 > 0.0 {
x2
} else if x1 == 0.0 || x2 == 0.0 {
0.0
} else {
f64::NAN
}
}
};
// x = 1 + r => r = x - 1
x - 1.
}

pub fn mirr(
values: &[f64],
finance_rate: f64,
Expand Down
12 changes: 12 additions & 0 deletions src/core/scheduled/xirr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ pub fn xirr(

let deltas = &day_count_factor(dates, day_count);

if amounts.len() == 2 {
// solve analytically:
// cf[0]/(1+r)^d[0] + cf[1]/(1+r)^d[1] = 0 =>
// cf[1]/(1+r)^d[1] = -cf[0]/(1+r)^d[0] => rearrange
// cf[1]/cf[0] = -(1+r)^d[1]/(1+r)^d[0] => simplify
// cf[1]/cf[0] = -(1+r)^(d[1] - d[0]) => take the root
// (cf[1]/cf[0])^(1/(d[1] - d[0])) = -(1 + r) => multiply by -1 and subtract 1
// r = -(cf[1]/cf[0])^(1/(d[1] - d[0])) - 1
let rate = (-amounts[1] / amounts[0]).powf(1. / (deltas[1] - deltas[0])) - 1.0;
return Ok(rate);
}

let f = |rate| xnpv_result(amounts, deltas, rate);
let fd = |rate| xnpv_result_with_deriv(amounts, deltas, rate);

Expand Down

0 comments on commit c18e209

Please sign in to comment.