| Finance-Math-IRR documentation | Contained in the Finance-Math-IRR distribution. |
Finance::Math::IRR - Calculate the internal rate of return of a cash flow
use Finance::Math::IRR;
# we provide a cash flow
my %cashflow = (
'2001-01-01' => 100,
'2001-03-15' => 250.45,
'2001-03-20' => -50,
'2001-06-23' => -763.12, # the last transaction should always be <= 0
);
# and get the internal rate of return for this cashflow
# we want a precision of 0.1%
my $irr = xirr(%cashflow, precision => 0.001);
# or simply: my $irr = xirr(%cashflow);
if (!defined $irr) {
die "ERROR: xirr() failed to calculate the IRR of this cashflow\n";
}
The internal rate of return (IRR) is a powerfull tool when evaluating the behaviour of a cashflow. It is typically used to assess whether an investment will yield profit. But since you are reading those lines, I assume you already know what an IRR is about.
In this module, the internal rate of return is calculated in a similar way as in the function XIRR present in both Excell and Gnumeric. This means that cash flows where transactions come at irregular intervals are well supported, and the rate is a yearly rate.
An IRR is obtained by finding the root of a polynomial where each coefficient is the amount of one transaction in the cash flow, and the power of the corresponding coefficient is the number of days between that transaction and the first transaction divided by 365 (one year). Note that it isn't a polynomial in the traditional meaning since its powers may have decimals or be less than 1.
There is no universal way to solve this equation analytically. Instead, we have to find the polynomial's root with various root finding algorithms. That's where the fun starts...
The approach of Finance::Math::IRR is to try to approximate one of the polynomial's roots with the secant method. If it fails, Brent's method is tried. However, Brent's method requires to know of an interval such that the polynomial is positive on one end of the interval and negative on the other. Finance::Math::IRR searches for such an interval by trying systematically a sequence of points. But it may fail to find such an interval and therefore fail to approximate the cashflow's IRR:
Calculates an approximation of the internal rate of return (IRR) of the provided cashflow. The returned IRR will be within $float of the exact IRR. The cashflow is a hash with the following structure:
my %cashflow = (
# date => transaction_amount
'2006-01-01' => 15,
'2006-01-15' => -5,
'2006-03-15' => -8,
);
To get the IRR in percent, multiply xirr's result by 100.
If precision is omitted, it defaults to 0.001, yielding 0.1% precision on the resulting IRR.
xirr may fail to find the IRR, in which case it returns undef.
xirr will croak if you feed it with junk.
xirr removes all transactions with amount 0 from the cashflow. If the resulting cashflow is empty, an irr of 0% is returned. If the resulting cashflow contains only one non 0 transaction, undef is returned.
Finding the right strategy to solve the IRR equation is tricky. Finance::Math::IRR uses a slightly different technique than the corresponding XIRR function in Gnumeric.
Gnumeric uses first Newton's method to approximate the IRR. If it fails, it evaluates the polynomial on a sequence of points ( '-1 + 10/(i+9)' and 'i' with i from 1 to 1024), hoping to find 2 points where the polynomial is respectively positive and negative. If it finds 2 such points, gnumeric's XIRR then uses the bisection method on their interval.
Finance::Math::IRR has a slightly different strategy. It uses the secant method instead of Newton's, and Brent's method instead of the bisection. Both methods are believed to be superior to their Gnumeric counterparts. Finance::Math::IRR performs additional controls to guaranty the validity of the result, such as controlling that the root candidate returned by Secant and Brent really are roots.
To display debug information, set in your code:
local $Finance::Math::IRR::DEBUG = 1;
This module has been used in recquiring production environments and thoroughly tested. It is therefore believed to be robust.
Yet, the method used in xirr may fail to find the IRR even
on cashflows that do have an IRR. If you happen to find
such an example, please email it to the author at
<erwan@cpan.org>.
The source of Finance::Math::IRR is hosted at sourceforge as part of the xirr4perl project. You can access it at https://sourceforge.net/projects/xirr4perl/.
See Math::Polynom, Math::Function::Roots.
$Id: IRR.pm,v 1.5 2007/07/12 12:35:46 erwan_lemonnier Exp $
Kind thanks to Gautam Satpathy (gautam@satpathy.in) who provided me with his port of
Gnumeric's XIRR to Java. Its source can be found at http://www.satpathy.in/jxirr/index.html.
Thanks to the team of Gnumeric for releasing their implementation of XIRR in open source. For the curious, the code for XIRR is available in the sources of Gnumeric in the file 'plugins/fn-financial/functions.c' (as of Gnumeric 1.6.3).
More thanks to Nicholas Caratzas for his efficient help and sharp financial and mathematical insight!
Erwan Lemonnier <erwan@cpan.org>,
as part of the Pluto developer group at the Swedish Premium Pension Authority.
This code was developed at the Swedish Premium Pension Authority as part of the Authority's software development activities. This code is distributed under the same terms as Perl itself. We encourage you to help us improving this code by sending feedback and bug reports to the author(s).
This code comes with no warranty. The Swedish Premium Pension Authority and the author(s) decline any responsibility regarding the possible use of this code or any consequence of its use.
| Finance-Math-IRR documentation | Contained in the Finance-Math-IRR distribution. |
################################################################# # # Finance::Math::IRR - Calculate the internal rate of return of a cash flow # # $Id: IRR.pm,v 1.5 2007/07/12 12:35:46 erwan_lemonnier Exp $ # # 061215 erwan Started implementation # 061218 erwan Differentiate bugs from failures when calling secant() and brent() # 061218 erwan Handle precision correctly # 061218 erwan Support cashflows with only 0 amounts # 070220 erwan Support when secant converges toward a non root value # 070404 erwan Cleanup cashflow from transactions of amount 0 # 070404 erwan Error if last transaction is a positive amount. Added $DEBUG # 070411 erwan Return undef when cashflow has only 1 non zero transaction # 070418 erwan Update license # 070711 erwan Removed the restriction requiring the last transaction to be negative # package Finance::Math::IRR; use 5.006; use strict; use warnings; use Carp qw(confess croak); use Data::Dumper; use Math::Polynom; use Date::Calc qw(Delta_Days); use Scalar::Util qw(looks_like_number); use base qw(Exporter); our @EXPORT = qw(xirr); our $VERSION = '0.10'; our $DEBUG = 0; #---------------------------------------------------------------- # # parameters for secant and brent methods # my %ARGS_SECANT = ( p0 => 0.5, p1 => 1, max_depth => 100, ); my %ARGS_BRENT = ( max_depth => 50 ); # how many couple of points to search for positive and negative values my $MAX_POS_NEG_POINTS = 1024; #---------------------------------------------------------------- # # _crash - die with a usable error description # sub _crash { my($method,$poly,$args,$err) = @_; croak "BUG: something went wrong while calling Math::Polynom::$method with the arguments:\n". Dumper($args)."on the polynomial:\n". Dumper($poly)."the error was: [$err]\n". "Please email all this output to erwan\@cpan.org\n"; } #---------------------------------------------------------------- # # _debug # sub _debug { my $msg = shift; print STDOUT "Finance::Math::IRR: $msg\n" if ($DEBUG); } #---------------------------------------------------------------- # # xirr - calculate the internal rate of return of a cash flow # sub xirr { my $precision = 0.001; # default precision seeked on irr, ie 0.1% my $guess = 0.1; my %cashflow; my $root; # # Parse input arguments and build the cashflow's polynomial # croak("ERROR: xirr() got an odd number of arguments. this can not be correct") if (!scalar(@_) || scalar(@_) % 2); %cashflow = @_; _debug("xirr() called with arguments:\n".Dumper(\%cashflow)); # parse arguments if (exists $cashflow{precision}) { $precision = $cashflow{precision}; delete $cashflow{precision}; } if (!defined $precision || !looks_like_number($precision)) { croak "ERROR: precision is not a valid number"; } # remove intermediary transactions with 0 amount from cashflow my @sorted_dates = sort keys %cashflow; croak "ERROR: you provided an empty cash flow" if (scalar @sorted_dates == 0); my $date_end = $sorted_dates[-1]; foreach my $date (@sorted_dates) { my $amount = $cashflow{$date}; croak "ERROR: the provided cashflow contains undefined values" if (!defined $date || !defined $amount); croak "ERROR: invalid date in the provided cashflow [$date]" if ($date !~ /^\d\d\d\d-\d\d-\d\d$/); croak "ERROR: invalid amount in the provided cashflow at date [$date]" if (!looks_like_number($amount)); # remove transaction from cashflow if it has a 0 amount if ($amount == 0 && $date ne $date_end) { delete $cashflow{$date}; } } if ($cashflow{$date_end} == 0) { # the last value is 0: we may be able to handle it # was the whole cashflow made of transactions with amount 0? if (scalar keys %cashflow == 1) { _debug("all transactions in the cashflow have 0 in amount. IRR=0."); return 0; } } if (scalar keys %cashflow < 2) { # we got a cashflow with only 1 entry and can't calculate an irr on it return undef; } # TODO: what if all transactions have the same sign? # we want $precision on the irr, but can only steer the precision of 1/(1+irr), hence this ratio, that # should insure us the given precision even on the irr for irrs up to 1000% $precision = $precision / 1000; # build the polynomial whose solution is x=1/(1+IRR) @sorted_dates = sort keys %cashflow; my @date_start = split(/-/,$sorted_dates[0]); croak "BUG: expected 3 arguments after splitting [".$sorted_dates[0]."]" if (scalar @date_start != 3); my %coeffs; while (my($date,$amount) = each %cashflow) { my $ddays = Delta_Days(@date_start, split(/-/,$date)); $coeffs{$ddays/365} = $amount; } my $poly = Math::Polynom->new(%coeffs); # # Find a real root of the polynomial # $ARGS_SECANT{precision} = $precision; _debug("trying secant method on interval [".$ARGS_SECANT{p0}."-".$ARGS_SECANT{p1}."] with precision ". $ARGS_SECANT{precision}." and max ".$ARGS_SECANT{max_depth}." iterations"); # try finding the IRR with the secant metho eval { $root = $poly->secant(%ARGS_SECANT); }; if ($@) { # secant failed. let's make sure it was not a bug my $error = $poly->error; if ( grep( /^$error$/, Math::Polynom::ERROR_NAN, Math::Polynom::ERROR_DIVIDE_BY_ZERO, Math::Polynom::ERROR_MAX_DEPTH, Math::Polynom::ERROR_NOT_A_ROOT ) ) { _debug("secant failed on with error code $error"); } else { # ok, the method did not fail, something else did _crash("secant", $poly, \%ARGS_SECANT, $@); } # let's find two points where the polynomial is positive respectively negative my $i = 1; while ( (!defined $poly->xneg || !defined $poly->xpos) && $i <= $MAX_POS_NEG_POINTS ) { $poly->eval( $i ); $poly->eval( -1+10/($i+9) ); $i++; } # if we did not find 2 points where the polynomial is >0 and <0, we can't use Brent's method (nor the bisection) if ( !defined $poly->xneg || !defined $poly->xpos ) { _debug("failed to find an interval on which polynomial is >0 and <0 at the boundaries"); return undef; } # try finding the IRR with Brent's method $ARGS_BRENT{precision} = $precision; $ARGS_BRENT{a} = $poly->xneg; $ARGS_BRENT{b} = $poly->xpos; _debug("trying Brent's method on interval [".$ARGS_BRENT{a}."-".$ARGS_BRENT{b}."] with precision ". $ARGS_BRENT{precision}." and max ".$ARGS_BRENT{max_depth}." iterations"); eval { $root = $poly->brent(%ARGS_BRENT); }; if ($@) { # Brent's method failed $error = $poly->error; if ( grep( /^$error$/, Math::Polynom::ERROR_NAN, Math::Polynom::ERROR_MAX_DEPTH, Math::Polynom::ERROR_NOT_A_ROOT )) { # Brent's method was unable to approximate the root _debug("brent failed with error code: $error"); return undef; } else { # looks like a bug, either in Math::Polynom's implementation of Brent of in the arguments we sent to it _crash("brent", $poly, \%ARGS_BRENT, $@); } } } if ($root == 0) { # that would mean IRR = infinity, which is kind of not plausible _debug("got 0 as the root, meaning infinite IRR. impossible."); return undef; } # TODO: verify IRR against cashflow # TODO: is the IRR impossibly large? # TODO: try secant with other intervals # TODO: calculate the number of real roots of the polynomial, find them all and choose the most relevant? or die if more than 1? return -1 + 1/$root; } 1; __END__