Re: Internal Rate of Return (#156)

Here’s my solution. I used Newton’s method to quickly find the zero of
the NPV function.

Unfortunately, my calculus book failed to mention that Newton’s method
will fail to converge when the function enters a horizontal asymptote if
it’s moving toward y=0 as it does so, which happens in this function at
deeply negative values of IRR. Thus, my solution can be a bit
unreliable, especially for negative values of IRR.

Anyway, here’s the code:

DX = 1e-4
EPSILON = 1e-7

def nderiv(f)
lambda {|x| (f[x+DX]-f[x-DX])/(2*DX)}
end

MAX_ITERATIONS = 500
def find_zero(f, start)
iterations = 0
f_prime = nderiv(f)
x = start
until f[x].abs < EPSILON or (iterations+=1) > MAX_ITERATIONS
x = x - f[x]/f_prime[x]
end
if iterations > MAX_ITERATIONS
nil
else
x
end
end

def irr(cash_flows)
net_value = lambda do |irr|
(0…cash_flows.length).to_a.inject(0) do |s,t|
s+cash_flows[t]/((1+irr)**t)
end
end

find_zero(net_value,0.1) or find_zero(net_value,-0.1)
end

----- Original Message ----
From: Ruby Q. [email protected]
To: ruby-talk ML [email protected]
Sent: Friday, February 8, 2008 8:01:13 AM
Subject: [QUIZ] Internal Rate of Return (#156)

The
three
rules
of
Ruby
Quiz:

Please
do
not
post
any
solutions
or
spoiler
discussion
for
this
quiz
until
48
hours
have
passed
from
the
time
on
this
message.

Support
Ruby
Quiz
by
submitting
ideas
as
often
as
you
can:

http://www.rubyquiz.com/

Enjoy!

Suggestion:
A
[QUIZ]
in
the
subject
of
emails
about
the
problem
helps
everyone
on
Ruby
Talk
follow
the
discussion.
Please
reply
to
the
original
quiz
message,
if
you
can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by
Harrison
Reiser

Internal
Rate
of
Return
(IRR
–
Internal rate of return - Wikipedia)
is
a
common
financial
metric,
used
by
investment
firms
to
predict
the
profitability
of
a
company
or
project.
Finding
the
IRR
of
a
company
amounts
to
solving
for
it
in
the
equation
for
Net
Present
Value
(NPV
–
Net present value - Wikipedia),
another
valuable
decision-making
metric:

N

C_t

NPV

Σ

t=0
(1
+
IRR)**t

This
week’s
quiz
is
to
calculate
the
IRR
for
any
given
variable-length
list
of
numbers,
which
represent
yearly
cash
flows,
the
C_t’s
in
the
formula
above:
C_0,
C_1,
etc.
(C_0
is
typically
a
negative
value,
corresponding
to
the
initial
investment
into
the
project.)
From
the
example
in
the
Wikipedia
article
(Internal rate of return - Wikipedia),
for
instance,
you
should
be
able
to
produce
a
rate
of
17.09%
(to
four
decimal
places,
let’s
say)
from
this
or
a
similar
command:

irr([-100,+30,+35,+40,+45])

=>
0.1709…

Keep
in
mind
that
an
IRR
greater
than
100%
is
possible.
Extra
credit
if
you
can
also
correctly
handle
input
that
produces
negative
rates,
disregarding
the
fact
that
they
make
no
sense.

  ____________________________________________________________________________________

Be a better friend, newshound, and
know-it-all with Yahoo! Mobile. Try it now.
http://mobile.yahoo.com/;_ylt=Ahu06i62sR8HDtDypao8Wcj9tAcJ

Here’s my solution. Like others, it uses Newton’s method. It may have
problems because it always uses zero as its initial estimate, though
all the test data I’ve thrown at it seems to work properly. The one
thing that is different than the other solutions I’ve seen posted is
that I wrapped everything up in a class. This is also my first quiz.

class IncomeStream
DEFAULT_TOLERANCE = 0.000_000_05

def self.irr(*flows)
self.new(flows).irr
end

def self.npv(discount_rate, *flows)
self.new(flows).npv(discount_rate)
end

def self.dpv(amount, time)
lambda {|rate| (-time) * amount * (1+rate)**(-time-1)}
end

def self.pv(amount, time)
lambda {|rate| amount * (1+rate)**(-time)}
end

def initialize(flows)
@flows = flows.to_a
if (not @flows.empty?) and (@flows.first.kind_of? Numeric)
@flows.each_index {|idx| @flows[idx]=[@flows[idx],idx]}
end
@pvs = @flows.map {|amount, time| IncomeStream.pv(amount, time)}
@dpvs = @flows.map {|amount, time| IncomeStream.dpv(amount, time)}
end

def npv(rate)
@pvs.inject(0) {|npv, item| npv+item.call(rate)}
end

def dnpv(rate)
@dpvs.inject(0) {|dnpv, item| dnpv+item.call(rate)}
end

def irr(tolerance=DEFAULT_TOLERANCE)
return nil if @flows.empty? or @flows.all? {|amount, time| amount =
0}
rate = 0.0
while (((n=npv(rate)).abs > tolerance) and rate.finite?)
d = dnpv(rate)
rate -= n/d
end
rate
end