Skip to content

Commit

Permalink
Add Additional Method to XIRR if Newton-Raphson Does Not Converge
Browse files Browse the repository at this point in the history
Fix PHPOffice#689. XIRR is calculated by making guesses which are hopefully better with each iteration. It is not guaranteed to succeed for Excel, PhpSpreadsheet, or any other implementation. PhpSpreadsheet uses the Newton-Raphson method for its guesses. So does Python package xirr (https://github.com/tarioch/xirr/), but, if Newton-Raphson fails to converge, Python tries Brent's method as an alternative. Two sets of non-converging data are noted in 689. For both, a solution does converge in Excel. For the first of the problems, a solution converges in Python with Newton-Raphson; but, for the second, a solution converges which requires Brent. For the Java package https://github.com/RayDeCampo/java-xirr on which Python was based, and which uses only Newton-Raphson, a solution converges for the first, and does not converge for the second.

To try to match the good results of the others, I added an alternate algorithm if Newton-Raphson fails. Brent's algorithm seems difficult to implement to me. I might have gone there regardless, but I first tried a slightly simpler alternative, bisection. This solved the problem for both of the cases in 689. Perhaps someone will one day report a problem that doesn't converge for Newton-Raphson or bisection, but does for Brent. We can review this decision then.

The new code causes 3 changes in the unit test. In all 3 tests, Excel and PhpSpreadsheet had not converged, but Python and/or Java had. I now believe that Python/Java is correct in those cases, and Excel is not. The new code aligns PhpSpreadsheet with Python/Java for those tests. It is, of course, impossible to know when Excel's implementation doesn't converge, so we aren't guaranteed to match its results in those hopefully rare situations.
  • Loading branch information
oleibman committed Dec 26, 2022
1 parent 3f9752a commit 3af6545
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ public static function rate($values, $dates, $guess = self::DEFAULT_GUESS)
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
$found = false;
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
if (!is_numeric($f1)) {
return $f1;
}
if (!is_numeric($f2)) {
return $f2;
}
$f1 = (float) $f1;
$f2 = (float) $f2;
Expand All @@ -68,11 +71,32 @@ public static function rate($values, $dates, $guess = self::DEFAULT_GUESS)
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
}
}
if (!$found) {
return ExcelError::NAN();
if ($found) {
return self::xirrPart3($values, $dates, $x1, $x2);
}

return self::xirrPart3($values, $dates, $x1, $x2);
// Newton-Raphson didn't work - try bisection
$x1 = $guess - 0.5;
$x2 = $guess + 0.5;
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$f1 = self::xnpvOrdered($x1, $values, $dates, false, true);
$f2 = self::xnpvOrdered($x2, $values, $dates, false, true);
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
}
if ($f1 * $f2 <= 0) {
$found = true;

break;
}
$x1 -= 0.5;
$x2 += 0.5;
}
if ($found) {
return self::xirrBisection($values, $dates, $x1, $x2);
}

return ExcelError::NAN();
}

/**
Expand Down Expand Up @@ -190,14 +214,53 @@ private static function xirrPart3(array $values, array $dates, float $x1, float
return $rslt;
}

/**
* @return float|string
*/
private static function xirrBisection(array $values, array $dates, float $x1, float $x2)
{
$rslt = ExcelError::NAN();
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$rslt = ExcelError::NAN();
$f1 = self::xnpvOrdered($x1, $values, $dates, false, true);
$f2 = self::xnpvOrdered($x2, $values, $dates, false, true);
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
}
$f1 = (float) $f1;
$f2 = (float) $f2;
if (abs($f1) < self::FINANCIAL_PRECISION && abs($f2) < self::FINANCIAL_PRECISION) {
break;
}
if ($f1 * $f2 > 0) {
break;
}
$rslt = ($x1 + $x2) / 2;
$f3 = self::xnpvOrdered($rslt, $values, $dates, false, true);
if (!is_float($f3)) {
break;
}
if ($f3 * $f1 < 0) {
$x2 = $rslt;
} else {
$x1 = $rslt;
}
if (abs($f3) < self::FINANCIAL_PRECISION) {
break;
}
}

return $rslt;
}

/**
* @param mixed $rate
* @param mixed $values
* @param mixed $dates
*
* @return float|string
*/
private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true)
private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true, bool $capAtNegative1 = false)
{
$rate = Functions::flattenSingleValue($rate);
$values = Functions::flattenArray($values);
Expand All @@ -206,6 +269,9 @@ private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true

try {
self::validateXnpv($rate, $values, $dates);
if ($capAtNegative1 && $rate <= -1) {
$rate = -1.0 + 1.0E-10;
}
$date0 = DateTimeExcel\Helpers::getDateValue($dates[0]);
} catch (Exception $e) {
return $e->getMessage();
Expand Down
22 changes: 16 additions & 6 deletions tests/data/Calculation/Financial/XIRR.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
['2015-04-01', '2019-06-27'],
0.1,
],
'issue 689' => [
-0.6118824173,
[-1000000.706, 947003.58],
['2018-09-05', '2018-09-26'],
],
'issue 689 updated 2022-12-25' => [
-0.935842486,
[-20972.36, 20350.545],
['2022-12-12', '2022-12-16'],
],
'XIRR calculation #1 is incorrect' => [
0.137963527441025,
[139947.43, 1893.67, 52573.25, 48849.74, 26369.16, -273029.18],
Expand Down Expand Up @@ -186,13 +196,13 @@
[-10000, 3027.25, 630.68, 2018.2, 1513.62, 1765.89, 4036.33, 4036.33, 1513.62, 1513.62, 2018.16, 1513.62, 1009.08, 1513.62, 1513.62, 1765.89, 1765.89, 22421.55],
['2000-05-24', '2000-06-05', '2001-04-09', '2004-02-24', '2005-03-18', '2006-02-15', '2007-01-10', '2007-11-14', '2008-12-17', '2010-01-15', '2011-01-14', '2012-02-03', '2013-01-18', '2014-01-24', '2015-01-30', '2016-01-22', '2017-01-20', '2017-06-05'],
],
'DeCampo issue5a, agree with Excel not DeCampo' => [
'#NUM!', //-0.7640294,
'DeCampo issue5a, agree with DeCampo not Excel' => [
-0.7640294, // '#NUM!'
[-2610, -2589, -5110, -2550, -5086, -2561, -5040, -2552, -2530, 29520],
['2001-06-22', '2001-07-03', '2001-07-05', '2001-07-06', '2001-07-09', '2001-07-10', '2001-07-12', '2001-07-13', '2001-07-16', '2001-07-17'],
],
'DeCampo issue5b, agree with Excel not DeCampo' => [
'#NUM!', //-0.8353404,
'DeCampo issue5b, agree with DeCampo not Excel' => [
-0.8353404, // '#NUM!'
[-2610, -2589, -5110, -2550, -5086, -2561, -5040, -2552, -2530, -9840, 38900],
['2001-06-22', '2001-07-03', '2001-07-05', '2001-07-06', '2001-07-09', '2001-07-10', '2001-07-12', '2001-07-13', '2001-07-16', '2001-07-17', '2001-07-18'],
],
Expand All @@ -206,8 +216,8 @@
[-2236.3994659663, -47.3417585212, -46.52619316339632, 10424.74612565936, -13.077972551952],
['2017-12-16', '2017-12-26', '2017-12-29', '2017-12-31', '2017-12-20'],
],
'Python XIRR test line 39, agree with Excel not Python' => [
'#NUM!', //-1,
'Python XIRR test line 39, agree with Python not Excel' => [
-1, // '#NUM!',
[18902, 83600, -5780, -4080, -56780, -2210, -2380, 33975, 23067.98, -1619.57],
['2016-04-06', '2016-05-04', '2016-05-12', '2017-05-08', '2017-07-03', '2018-05-07', '2019-05-06', '2019-10-01', '2020-03-13', '2020-05-07'],
],
Expand Down

0 comments on commit 3af6545

Please sign in to comment.