line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
################################################################# |
2
|
|
|
|
|
|
|
# |
3
|
|
|
|
|
|
|
# Finance::Math::IRR - Calculate the internal rate of return of a cash flow |
4
|
|
|
|
|
|
|
# |
5
|
|
|
|
|
|
|
# $Id: IRR.pm,v 1.5 2007/07/12 12:35:46 erwan_lemonnier Exp $ |
6
|
|
|
|
|
|
|
# |
7
|
|
|
|
|
|
|
# 061215 erwan Started implementation |
8
|
|
|
|
|
|
|
# 061218 erwan Differentiate bugs from failures when calling secant() and brent() |
9
|
|
|
|
|
|
|
# 061218 erwan Handle precision correctly |
10
|
|
|
|
|
|
|
# 061218 erwan Support cashflows with only 0 amounts |
11
|
|
|
|
|
|
|
# 070220 erwan Support when secant converges toward a non root value |
12
|
|
|
|
|
|
|
# 070404 erwan Cleanup cashflow from transactions of amount 0 |
13
|
|
|
|
|
|
|
# 070404 erwan Error if last transaction is a positive amount. Added $DEBUG |
14
|
|
|
|
|
|
|
# 070411 erwan Return undef when cashflow has only 1 non zero transaction |
15
|
|
|
|
|
|
|
# 070418 erwan Update license |
16
|
|
|
|
|
|
|
# 070711 erwan Removed the restriction requiring the last transaction to be negative |
17
|
|
|
|
|
|
|
# |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
package Finance::Math::IRR; |
20
|
|
|
|
|
|
|
|
21
|
4
|
|
|
4
|
|
33685
|
use 5.006; |
|
4
|
|
|
|
|
16
|
|
|
4
|
|
|
|
|
169
|
|
22
|
4
|
|
|
4
|
|
31
|
use strict; |
|
4
|
|
|
|
|
8
|
|
|
4
|
|
|
|
|
139
|
|
23
|
4
|
|
|
4
|
|
32
|
use warnings; |
|
4
|
|
|
|
|
8
|
|
|
4
|
|
|
|
|
408
|
|
24
|
4
|
|
|
4
|
|
24
|
use Carp qw(confess croak); |
|
4
|
|
|
|
|
7
|
|
|
4
|
|
|
|
|
441
|
|
25
|
4
|
|
|
4
|
|
8042
|
use Data::Dumper; |
|
4
|
|
|
|
|
39884
|
|
|
4
|
|
|
|
|
326
|
|
26
|
4
|
|
|
4
|
|
3986
|
use Math::Polynom; |
|
4
|
|
|
|
|
81051
|
|
|
4
|
|
|
|
|
260
|
|
27
|
4
|
|
|
4
|
|
3627
|
use Date::Calc qw(Delta_Days); |
|
4
|
|
|
|
|
151501
|
|
|
4
|
|
|
|
|
3783
|
|
28
|
4
|
|
|
4
|
|
220
|
use Scalar::Util qw(looks_like_number); |
|
4
|
|
|
|
|
8
|
|
|
4
|
|
|
|
|
1977
|
|
29
|
4
|
|
|
4
|
|
215
|
use base qw(Exporter); |
|
4
|
|
|
|
|
10
|
|
|
4
|
|
|
|
|
5768
|
|
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
our @EXPORT = qw(xirr); |
32
|
|
|
|
|
|
|
our $VERSION = '0.10'; |
33
|
|
|
|
|
|
|
our $DEBUG = 0; |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
#---------------------------------------------------------------- |
36
|
|
|
|
|
|
|
# |
37
|
|
|
|
|
|
|
# parameters for secant and brent methods |
38
|
|
|
|
|
|
|
# |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
my %ARGS_SECANT = ( p0 => 0.5, |
41
|
|
|
|
|
|
|
p1 => 1, |
42
|
|
|
|
|
|
|
max_depth => 100, |
43
|
|
|
|
|
|
|
); |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
my %ARGS_BRENT = ( max_depth => 50 ); |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
# how many couple of points to search for positive and negative values |
48
|
|
|
|
|
|
|
my $MAX_POS_NEG_POINTS = 1024; |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
#---------------------------------------------------------------- |
51
|
|
|
|
|
|
|
# |
52
|
|
|
|
|
|
|
# _crash - die with a usable error description |
53
|
|
|
|
|
|
|
# |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
sub _crash { |
56
|
0
|
|
|
0
|
|
0
|
my($method,$poly,$args,$err) = @_; |
57
|
|
|
|
|
|
|
|
58
|
0
|
|
|
|
|
0
|
croak "BUG: something went wrong while calling Math::Polynom::$method with the arguments:\n". |
59
|
|
|
|
|
|
|
Dumper($args)."on the polynomial:\n". |
60
|
|
|
|
|
|
|
Dumper($poly)."the error was: [$err]\n". |
61
|
|
|
|
|
|
|
"Please email all this output to erwan\@cpan.org\n"; |
62
|
|
|
|
|
|
|
} |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
#---------------------------------------------------------------- |
65
|
|
|
|
|
|
|
# |
66
|
|
|
|
|
|
|
# _debug |
67
|
|
|
|
|
|
|
# |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
sub _debug { |
70
|
386
|
|
|
386
|
|
102071
|
my $msg = shift; |
71
|
386
|
50
|
|
|
|
1421
|
print STDOUT "Finance::Math::IRR: $msg\n" if ($DEBUG); |
72
|
|
|
|
|
|
|
} |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
#---------------------------------------------------------------- |
75
|
|
|
|
|
|
|
# |
76
|
|
|
|
|
|
|
# xirr - calculate the internal rate of return of a cash flow |
77
|
|
|
|
|
|
|
# |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
sub xirr { |
80
|
195
|
|
|
195
|
1
|
205607
|
my $precision = 0.001; # default precision seeked on irr, ie 0.1% |
81
|
195
|
|
|
|
|
5858
|
my $guess = 0.1; |
82
|
195
|
|
|
|
|
296
|
my %cashflow; |
83
|
|
|
|
|
|
|
my $root; |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
# |
86
|
|
|
|
|
|
|
# Parse input arguments and build the cashflow's polynomial |
87
|
|
|
|
|
|
|
# |
88
|
|
|
|
|
|
|
|
89
|
195
|
100
|
100
|
|
|
1938
|
croak("ERROR: xirr() got an odd number of arguments. this can not be correct") if (!scalar(@_) || scalar(@_) % 2); |
90
|
|
|
|
|
|
|
|
91
|
193
|
|
|
|
|
6647
|
%cashflow = @_; |
92
|
|
|
|
|
|
|
|
93
|
193
|
|
|
|
|
1143
|
_debug("xirr() called with arguments:\n".Dumper(\%cashflow)); |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
# parse arguments |
96
|
193
|
100
|
|
|
|
6572
|
if (exists $cashflow{precision}) { |
97
|
192
|
|
|
|
|
443
|
$precision = $cashflow{precision}; |
98
|
192
|
|
|
|
|
534
|
delete $cashflow{precision}; |
99
|
|
|
|
|
|
|
} |
100
|
|
|
|
|
|
|
|
101
|
193
|
100
|
66
|
|
|
1221
|
if (!defined $precision || !looks_like_number($precision)) { |
102
|
1
|
|
|
|
|
131
|
croak "ERROR: precision is not a valid number"; |
103
|
|
|
|
|
|
|
} |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
# remove intermediary transactions with 0 amount from cashflow |
106
|
192
|
|
|
|
|
7472
|
my @sorted_dates = sort keys %cashflow; |
107
|
192
|
100
|
|
|
|
1387
|
croak "ERROR: you provided an empty cash flow" if (scalar @sorted_dates == 0); |
108
|
|
|
|
|
|
|
|
109
|
191
|
|
|
|
|
383
|
my $date_end = $sorted_dates[-1]; |
110
|
|
|
|
|
|
|
|
111
|
191
|
|
|
|
|
451
|
foreach my $date (@sorted_dates) { |
112
|
11576
|
|
|
|
|
17188
|
my $amount = $cashflow{$date}; |
113
|
11576
|
100
|
33
|
|
|
32413
|
croak "ERROR: the provided cashflow contains undefined values" if (!defined $date || !defined $amount); |
114
|
11575
|
100
|
|
|
|
39999
|
croak "ERROR: invalid date in the provided cashflow [$date]" if ($date !~ /^\d\d\d\d-\d\d-\d\d$/); |
115
|
11574
|
100
|
|
|
|
31073
|
croak "ERROR: invalid amount in the provided cashflow at date [$date]" if (!looks_like_number($amount)); |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
# remove transaction from cashflow if it has a 0 amount |
118
|
11573
|
100
|
100
|
|
|
32427
|
if ($amount == 0 && $date ne $date_end) { |
119
|
19
|
|
|
|
|
30
|
delete $cashflow{$date}; |
120
|
|
|
|
|
|
|
} |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
|
123
|
188
|
100
|
|
|
|
662
|
if ($cashflow{$date_end} == 0) { |
124
|
|
|
|
|
|
|
# the last value is 0: we may be able to handle it |
125
|
|
|
|
|
|
|
# was the whole cashflow made of transactions with amount 0? |
126
|
4
|
100
|
|
|
|
14
|
if (scalar keys %cashflow == 1) { |
127
|
3
|
|
|
|
|
24
|
_debug("all transactions in the cashflow have 0 in amount. IRR=0."); |
128
|
3
|
|
|
|
|
13
|
return 0; |
129
|
|
|
|
|
|
|
} |
130
|
|
|
|
|
|
|
} |
131
|
|
|
|
|
|
|
|
132
|
185
|
100
|
|
|
|
547
|
if (scalar keys %cashflow < 2) { |
133
|
|
|
|
|
|
|
# we got a cashflow with only 1 entry and can't calculate an irr on it |
134
|
1
|
|
|
|
|
5
|
return undef; |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
# TODO: what if all transactions have the same sign? |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
# we want $precision on the irr, but can only steer the precision of 1/(1+irr), hence this ratio, that |
140
|
|
|
|
|
|
|
# should insure us the given precision even on the irr for irrs up to 1000% |
141
|
184
|
|
|
|
|
331
|
$precision = $precision / 1000; |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
# build the polynomial whose solution is x=1/(1+IRR) |
144
|
184
|
|
|
|
|
8293
|
@sorted_dates = sort keys %cashflow; |
145
|
184
|
|
|
|
|
1905
|
my @date_start = split(/-/,$sorted_dates[0]); |
146
|
184
|
50
|
|
|
|
721
|
croak "BUG: expected 3 arguments after splitting [".$sorted_dates[0]."]" if (scalar @date_start != 3); |
147
|
|
|
|
|
|
|
|
148
|
184
|
|
|
|
|
440
|
my %coeffs; |
149
|
|
|
|
|
|
|
|
150
|
184
|
|
|
|
|
735
|
while (my($date,$amount) = each %cashflow) { |
151
|
11550
|
|
|
|
|
43042
|
my $ddays = Delta_Days(@date_start, split(/-/,$date)); |
152
|
11550
|
|
|
|
|
729450
|
$coeffs{$ddays/365} = $amount; |
153
|
|
|
|
|
|
|
} |
154
|
|
|
|
|
|
|
|
155
|
184
|
|
|
|
|
3414
|
my $poly = Math::Polynom->new(%coeffs); |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
# |
158
|
|
|
|
|
|
|
# Find a real root of the polynomial |
159
|
|
|
|
|
|
|
# |
160
|
|
|
|
|
|
|
|
161
|
184
|
|
|
|
|
74210
|
$ARGS_SECANT{precision} = $precision; |
162
|
|
|
|
|
|
|
|
163
|
184
|
|
|
|
|
2442
|
_debug("trying secant method on interval [".$ARGS_SECANT{p0}."-".$ARGS_SECANT{p1}."] with precision ". |
164
|
|
|
|
|
|
|
$ARGS_SECANT{precision}." and max ".$ARGS_SECANT{max_depth}." iterations"); |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
# try finding the IRR with the secant metho |
167
|
184
|
|
|
|
|
324
|
eval { |
168
|
184
|
|
|
|
|
2949
|
$root = $poly->secant(%ARGS_SECANT); |
169
|
|
|
|
|
|
|
}; |
170
|
|
|
|
|
|
|
|
171
|
184
|
100
|
|
|
|
1251763
|
if ($@) { |
172
|
|
|
|
|
|
|
# secant failed. let's make sure it was not a bug |
173
|
3
|
|
|
|
|
15
|
my $error = $poly->error; |
174
|
3
|
50
|
|
|
|
102
|
if ( grep( /^$error$/, |
175
|
|
|
|
|
|
|
Math::Polynom::ERROR_NAN, |
176
|
|
|
|
|
|
|
Math::Polynom::ERROR_DIVIDE_BY_ZERO, |
177
|
|
|
|
|
|
|
Math::Polynom::ERROR_MAX_DEPTH, |
178
|
|
|
|
|
|
|
Math::Polynom::ERROR_NOT_A_ROOT ) ) { |
179
|
3
|
|
|
|
|
12
|
_debug("secant failed on with error code $error"); |
180
|
|
|
|
|
|
|
} else { |
181
|
|
|
|
|
|
|
# ok, the method did not fail, something else did |
182
|
0
|
|
|
|
|
0
|
_crash("secant", $poly, \%ARGS_SECANT, $@); |
183
|
|
|
|
|
|
|
} |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
# let's find two points where the polynomial is positive respectively negative |
186
|
3
|
|
|
|
|
5
|
my $i = 1; |
187
|
3
|
|
66
|
|
|
12
|
while ( (!defined $poly->xneg || !defined $poly->xpos) && $i <= $MAX_POS_NEG_POINTS ) { |
|
|
|
100
|
|
|
|
|
188
|
1026
|
|
|
|
|
9087
|
$poly->eval( $i ); |
189
|
1026
|
|
|
|
|
50938
|
$poly->eval( -1+10/($i+9) ); |
190
|
1026
|
|
|
|
|
46807
|
$i++; |
191
|
|
|
|
|
|
|
} |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
# if we did not find 2 points where the polynomial is >0 and <0, we can't use Brent's method (nor the bisection) |
194
|
3
|
100
|
66
|
|
|
39
|
if ( !defined $poly->xneg || !defined $poly->xpos ) { |
195
|
1
|
|
|
|
|
11
|
_debug("failed to find an interval on which polynomial is >0 and <0 at the boundaries"); |
196
|
1
|
|
|
|
|
18
|
return undef; |
197
|
|
|
|
|
|
|
} |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
# try finding the IRR with Brent's method |
200
|
2
|
|
|
|
|
22
|
$ARGS_BRENT{precision} = $precision; |
201
|
2
|
|
|
|
|
5
|
$ARGS_BRENT{a} = $poly->xneg; |
202
|
2
|
|
|
|
|
21
|
$ARGS_BRENT{b} = $poly->xpos; |
203
|
|
|
|
|
|
|
|
204
|
2
|
|
|
|
|
26
|
_debug("trying Brent's method on interval [".$ARGS_BRENT{a}."-".$ARGS_BRENT{b}."] with precision ". |
205
|
|
|
|
|
|
|
$ARGS_BRENT{precision}." and max ".$ARGS_BRENT{max_depth}." iterations"); |
206
|
|
|
|
|
|
|
|
207
|
2
|
|
|
|
|
4
|
eval { |
208
|
2
|
|
|
|
|
9
|
$root = $poly->brent(%ARGS_BRENT); |
209
|
|
|
|
|
|
|
}; |
210
|
|
|
|
|
|
|
|
211
|
2
|
50
|
|
|
|
51366
|
if ($@) { |
212
|
|
|
|
|
|
|
# Brent's method failed |
213
|
0
|
|
|
|
|
0
|
$error = $poly->error; |
214
|
0
|
0
|
|
|
|
0
|
if ( grep( /^$error$/, |
215
|
|
|
|
|
|
|
Math::Polynom::ERROR_NAN, |
216
|
|
|
|
|
|
|
Math::Polynom::ERROR_MAX_DEPTH, |
217
|
|
|
|
|
|
|
Math::Polynom::ERROR_NOT_A_ROOT )) { |
218
|
|
|
|
|
|
|
# Brent's method was unable to approximate the root |
219
|
0
|
|
|
|
|
0
|
_debug("brent failed with error code: $error"); |
220
|
0
|
|
|
|
|
0
|
return undef; |
221
|
|
|
|
|
|
|
} else { |
222
|
|
|
|
|
|
|
# looks like a bug, either in Math::Polynom's implementation of Brent of in the arguments we sent to it |
223
|
0
|
|
|
|
|
0
|
_crash("brent", $poly, \%ARGS_BRENT, $@); |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
} |
226
|
|
|
|
|
|
|
} |
227
|
|
|
|
|
|
|
|
228
|
183
|
50
|
|
|
|
712
|
if ($root == 0) { |
229
|
|
|
|
|
|
|
# that would mean IRR = infinity, which is kind of not plausible |
230
|
0
|
|
|
|
|
0
|
_debug("got 0 as the root, meaning infinite IRR. impossible."); |
231
|
0
|
|
|
|
|
0
|
return undef; |
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# TODO: verify IRR against cashflow |
235
|
|
|
|
|
|
|
# TODO: is the IRR impossibly large? |
236
|
|
|
|
|
|
|
# TODO: try secant with other intervals |
237
|
|
|
|
|
|
|
# TODO: calculate the number of real roots of the polynomial, find them all and choose the most relevant? or die if more than 1? |
238
|
|
|
|
|
|
|
|
239
|
183
|
|
|
|
|
10426
|
return -1 + 1/$root; |
240
|
|
|
|
|
|
|
} |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
1; |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
__END__ |