[QUIZ] 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 Quiz <james@grayproductions.net>
To: ruby-talk ML <ruby-talk@ruby-lang.org>
Sent: Friday, February 8, 2008 8:01:13 AM
Subject: [QUIZ] Internal Rate of Return (#156)

The
three
rules
of
Ruby
Quiz:

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

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

http://www.rubyquiz.com/

3.
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

http://en.wikipedia.org/wiki/Internal_rate_of_return)
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

http://en.wikipedia.org/wiki/Net_present_value),
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
(http://en.wikipedia.org/wiki/Internal_rate_of_return),
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