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 )