Calculating IRR (Internal Rate of Return) in Ruby using recursion
Sanity checking
You should probably add some sanity checks. It's entirely possible to provide a range that'll never produce a result.
Separate output from calculation
Your test code ends up printing an extra blank line because #irr doesn't actually return anything. It'd be nicer to let #irr actually return the rate it found; you can do whatever you want with it afterward. Right now it just prints.
Too many splats
Variadic arguments are neat, but aren't really that necessary here. It'd be simpler and more straightforward to pass an actual array for the amounts.
Repetition
No need to do call present_value_of_series two-three times in a row, like you do here:

if present_value_of_series(rate, *amounts) &lt; 0
  if present_value_of_series(rate, *amounts) &gt;= -precision_level
    #debug puts "present value = #{present_value_of_series(rate, amounts)}"


Just stick it in a variable.
#present_value_of_series
This can be simplified by not treating the initial value as a separate argument, and using #reduce to do the sum:

def present_value_of_series(rate, amounts)
  amounts.each_with_index.reduce(0) do |sum, (amount, index)|
    sum + amount / (rate + 1)**index
  end
end


Note that this also matches your other code where you do treat the amounts as an array. No need for * splats.

As a general note, you rarely need while or for loops when iterating arrays in Ruby. There's almost always a neater way.
#irr
Since you're dealing with a range, it'd be nice to use, well, a Range. A range also gives you a #step iterator, which is useful here.


Here's what I came up with. It works the same as yours (recursion), but I can't say I'm super happy with it - returning from inside the #detect block feel wrong, but it avoids recalculating the present value afterward to check it against -1.

def irr(range, step, amounts)
  rate = range.step(step).detect do |rate|
    present_value = present_value_of_series(rate, amounts)
    if present_value &lt; 0
      return rate if present_value &gt;= -1
      true
    end
  end

  if rate.nil?
    raise "No solution"
  else
    irr((rate-step..rate), step / 10, amounts)
  end
end


Still needs extra sanity checks, though.

A possible optimization (trading some memory for computation) would be to use Array#bsearch instead of #detect. It does however mean that the range has to be made into an array first (e.g. range.step(step).to_a.bsearch ...).

Speaking of binary searching, you could also do:

def irr(min_rate, max_rate, amounts)
  range = max_rate - min_rate
  raise "No solution" if range &lt;= Float::EPSILON * 2

  rate = range.fdiv(2) + min_rate
  present_value = present_value_of_series(rate, amounts)

  if present_value &gt; 0
    irr(rate, max_rate, amounts)
  elsif present_value &lt; -1
    irr(min_rate, rate, amounts)
  else
    rate
  end
end


Basic idea is that it keeps guessing in the direction of the answer by testing a rate midway between min_rate and max_rate. It gives up if the difference between min_rate and max_rate would introduce floating point precision loss.

It's the fastest solution of the bunch, though it could perhaps be smarter about checking the initial min/max before starting the iteration (it could do that up front by calculating present values for max and min, and only then, if the range makes sense, handle the iteration in separate method). It should probably have a precision argument as well, but that's trivial.