Finance-Math-IRR
view release on metacpan or search on metacpan
lib/Finance/Math/IRR.pm view on Meta::CPAN
# 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__
=head1 NAME
Finance::Math::IRR - Calculate the internal rate of return of a cash flow
=head1 SYNOPSIS
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);
( run in 0.572 second using v1.01-cache-2.11-cpan-71847e10f99 )