From: ThoML Date: 2008-02-11T02:14:57+09:00 Subject: Re: [QUIZ] Internal Rate of Return (#156) > x=1;9.times{x=x-(n=d=t=0;i.map{|e|n+=_=e*x**t-=1;d+=t*_/x};n/d)};x-1 Well, my solution is slightly more verbose. Which doesn't imply it yields better results. It uses the Secant method (which no other solution has used yet, ha!) and the code in the core method was shamelessly copied from wikipedia. Yeah! By default, my solution tries to find values that converge with with Float::EPSILON. If the values diverge, the interval is scanned for zero-values, which could also be done in the first place (since this allows to find multiple roots). Regards, Thomas. #!/usr/bin/env ruby19 require 'date' module IRR module_function # Use Secant method to find the roots. In case, the upper and lower # limit diverge, reset the start values. # # This method may miss certain IRRs outside the [0.0..1.0] # intervall. In such a case use #irrer or set the optional :a # (lower limit) and :b (upper limit) arguments. # # Based on: # http://en.wikipedia.org/w/index.php?title=Secant_method def irr(values, args={}) a = a0 = args[:a] || 0.0 b = b0 = args[:b] || 1.0 n = args[:n] || 100 e = args[:e] || Float::EPSILON c0 = (a - b).abs ab = nil n.times do fa = npv(values, a) fb = npv(values, b) c = (a - b) / (fa - fb) * fa; if c.nan? does_not_compute(values) elsif c.infinite? # break return c elsif c.abs < e return a end # Protect against bad start values. if c.abs > c0 ab ||= guess_start_values(values, args.merge(:min => a0, :max => b0)) if !ab.empty? a, b, _ = ab.shift c0 = (a - b).abs next end end b = a a = a - c c0 = c.abs end does_not_compute(values) end # Guess appropriate start values, return all solutions as Array. def irrer(values, args={}) guess_start_values(values, args).map do |a, b| irr(values, args.merge(:a => a, :b => b)) end end # Calculate the NPV for a hypothetical IRR. # Values are either an array of cash flows or of pairs [cash, # date or days]. def npv(values, irr) sum = 0 d0 = nil values.each_with_index do |(v, d), t| # I have no idea if this is the right way to deal with # irregular time series. if d if d0 t = (d - d0).to_f / 365.25 else d0 = d end end sum += v / (1 + irr) ** t end sum end def does_not_compute(values) raise RuntimeError, %{Does not compute: %s} % values.inspect end # Check whether computation will converge easily. def check_values(values) csgn = 0 val, dat = values[-1] values.reverse.each do |v, d| csgn += 1 if val * v < 0 val = v end return csgn == 1 end # Try to find appropriate start values. def guess_start_values(values, args={}) min = args[:min] || -1.0 max = args[:max] || 2.0 delta = args[:delta] || (max - min).to_f / (args[:steps] || 100) vals = [] b, fb = nil # The NPV is undefined for IRR < -100% or so they say. min.step(max, delta) do |a| fa = npv(values, a) if fb and !fa.infinite? and !fb.infinite? and fa * fb < 0 vals << [b, a] end b = a fb = fa end return vals end end if __FILE__ == $0 values = ARGV.map do |e| v, d = e.split(/,/) v = v.to_f d ? [v, Date.parse(d)] : v end puts "Default solution: #{IRR.irr(values) rescue puts $!.message}" begin IRR.irrer(values).zip(IRR.guess_start_values(values)) do |irr, (a, b)| puts '[%5.2f..%5.2f] %13.10f -> %13.10f' % [a, b, irr, IRR.npv(values, irr)] end rescue RuntimeError => e puts e.message end puts "Possibly incorrect IRR value(s)" unless IRR.check_values(values) end