Class: Xirr::RtSafe
Overview
Safeguarded Newton-Raphson root finder — the classic rtsafe.
It brackets a sign change of the net present value first, then each iteration takes a Newton step when that step lands inside the bracket and is shrinking the interval fast enough, and a bisection step otherwise. This keeps Newton's speed on well-behaved flows while retaining bisection's guaranteed convergence, in a single pass rather than running Newton to exhaustion and then bisecting separately.
The maintained bracket always encloses a sign change, so the result is a genuine root rather than a stalled non-root, and a long-dated flow whose raw Newton step would overflow takes a bisection step instead.
Constant Summary collapse
- BRACKET_CEILING =
Stop expanding the upper bound once it passes this — the flow has no root in a sane rate range.
1.0e7
Instance Attribute Summary
Attributes included from Base
Class Method Summary collapse
-
.bracket(flows, low, f_low, high) ⇒ Object
Expand the upper bound until the NPV changes sign, giving a bracket.
-
.find(flows, guess: 0.1, tolerance: Xirr.config.eps, iteration_limit: Xirr.config.iteration_limit, precision: Xirr.config.precision) ⇒ Float?
Pure solver over normalized
[time, amount]flows (time in years/periods). - .inside?(point, xlo, xhi) ⇒ Boolean
-
.move(x, xlo, xhi, f, df, dxold) ⇒ Array(Float, Float)
A Newton step when it's usable, a bisection step otherwise.
-
.newton_usable?(x, xlo, xhi, f, df, dxold) ⇒ Boolean
Prefer Newton when the derivative isn't flat, the step lands inside the bracket, and it shrinks the interval by at least half.
-
.present_value(flows, rate) ⇒ Object
Net present value of
flowsatrate: Σ amount / (1 + rate)^t. -
.present_value_derivative(flows, rate) ⇒ Object
Derivative of the NPV with respect to rate: Σ -t · amount / (1 + rate)^(t+1).
-
.rtsafe(flows, guess, tol, iteration_limit) ⇒ Object
Bracket a sign change, then run the safeguarded iteration from
guess(when it falls inside the bracket) or the midpoint. -
.safe_low(flows) ⇒ Object
The bracket's floor.
-
.search(flows, x, xlo, xhi, f, df, dxold, tol, iters) ⇒ Float?
Iterate to the root, taking a Newton or bisection step each time.
-
.straddles_zero?(a, b) ⇒ Boolean
Whether
aandbsit on opposite sides of zero.
Instance Method Summary collapse
-
#xirr(guess, options) ⇒ Float?
Solves the compacted Cashflow the instance was built with.
Methods included from Base
#initialize, #periods_from_start, #xnpv, #xnpv_derivative
Class Method Details
.bracket(flows, low, f_low, high) ⇒ Object
Expand the upper bound until the NPV changes sign, giving a bracket.
129 130 131 132 133 134 135 136 137 |
# File 'lib/xirr/rtsafe.rb', line 129 def self.bracket(flows, low, f_low, high) return nil if high > BRACKET_CEILING if straddles_zero?(f_low, present_value(flows, high)) [low, high] else bracket(flows, low, f_low, high * 2 + 1) end end |
.find(flows, guess: 0.1, tolerance: Xirr.config.eps, iteration_limit: Xirr.config.iteration_limit, precision: Xirr.config.precision) ⇒ Float?
37 38 39 40 41 42 43 44 45 46 |
# File 'lib/xirr/rtsafe.rb', line 37 def self.find(flows, guess: 0.1, tolerance: Xirr.config.eps, iteration_limit: Xirr.config.iteration_limit, precision: Xirr.config.precision) rate = rtsafe(flows, guess.to_f, tolerance.to_f, iteration_limit) return nil if rate.nil? || rate.nan? || rate.infinite? # Round before the floor check: a rate just above -1 can round down to it. rounded = rate.round(precision) rounded <= -1.0 ? nil : rounded rescue FloatDomainError, Math::DomainError nil end |
.inside?(point, xlo, xhi) ⇒ Boolean
114 115 116 |
# File 'lib/xirr/rtsafe.rb', line 114 def self.inside?(point, xlo, xhi) point >= [xlo, xhi].min && point <= [xlo, xhi].max end |
.move(x, xlo, xhi, f, df, dxold) ⇒ Array(Float, Float)
A Newton step when it's usable, a bisection step otherwise.
95 96 97 98 99 100 101 102 103 |
# File 'lib/xirr/rtsafe.rb', line 95 def self.move(x, xlo, xhi, f, df, dxold) if newton_usable?(x, xlo, xhi, f, df, dxold) dx = f / df [x - dx, dx] else dx = (xhi - xlo) / 2.0 [xlo + dx, dx] end end |
.newton_usable?(x, xlo, xhi, f, df, dxold) ⇒ Boolean
Prefer Newton when the derivative isn't flat, the step lands inside the
bracket, and it shrinks the interval by at least half. Comparing the Newton
point against the bracket — rather than the classic product form — avoids an
overflow in the steep zone near the bracket's floor. df != 0 short-circuits
before x - f / df.
110 111 112 |
# File 'lib/xirr/rtsafe.rb', line 110 def self.newton_usable?(x, xlo, xhi, f, df, dxold) df != 0.0 && inside?(x - f / df, xlo, xhi) && (2.0 * f).abs <= (dxold * df).abs end |
.present_value(flows, rate) ⇒ Object
Net present value of flows at rate: Σ amount / (1 + rate)^t
49 50 51 |
# File 'lib/xirr/rtsafe.rb', line 49 def self.present_value(flows, rate) flows.inject(0.0) { |sum, (t, amount)| sum + amount / (1.0 + rate) ** t } end |
.present_value_derivative(flows, rate) ⇒ Object
Derivative of the NPV with respect to rate: Σ -t · amount / (1 + rate)^(t+1)
54 55 56 |
# File 'lib/xirr/rtsafe.rb', line 54 def self.present_value_derivative(flows, rate) flows.inject(0.0) { |sum, (t, amount)| sum + (-t * amount / (1.0 + rate) ** (t + 1)) } end |
.rtsafe(flows, guess, tol, iteration_limit) ⇒ Object
Bracket a sign change, then run the safeguarded iteration from guess
(when it falls inside the bracket) or the midpoint.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/xirr/rtsafe.rb', line 60 def self.rtsafe(flows, guess, tol, iteration_limit) low = safe_low(flows) f_low = present_value(flows, low) bounds = bracket(flows, low, f_low, 1.0) return nil if bounds.nil? a, b = bounds # a == low, so the NPV at a is f_low. Orient so it is negative at xlo and # positive at xhi — the invariant the step selection relies on. xlo, xhi = f_low < 0.0 ? [a, b] : [b, a] x = (guess > a && guess < b) ? guess : (a + b) / 2.0 f = present_value(flows, x) df = present_value_derivative(flows, x) search(flows, x, xlo, xhi, f, df, (b - a).abs, tol, iteration_limit) end |
.safe_low(flows) ⇒ Object
The bracket's floor. As rate nears -1, +(1 + rate)^t+ underflows to zero
(then divides by zero) for large t, so raise the floor just enough that the
longest-dated flow's discount factor stays finite. For short-dated flows this
is the familiar -0.999999; for a 30-year monthly schedule it sits higher.
122 123 124 125 126 |
# File 'lib/xirr/rtsafe.rb', line 122 def self.safe_low(flows) max_t = flows.map { |t, _amount| t }.max max_t = 1.0 if max_t.nil? || max_t < 1.0 [1.0e-290 ** (1.0 / max_t), 1.0e-6].max - 1.0 end |
.search(flows, x, xlo, xhi, f, df, dxold, tol, iters) ⇒ Float?
Iterate to the root, taking a Newton or bisection step each time. Looped
(not recursed) so a large iteration_limit can't overflow the stack.
79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/xirr/rtsafe.rb', line 79 def self.search(flows, x, xlo, xhi, f, df, dxold, tol, iters) iters.times do nxt, dx = move(x, xlo, xhi, f, df, dxold) return nxt if dx.abs < tol f = present_value(flows, nxt) df = present_value_derivative(flows, nxt) f < 0.0 ? xlo = nxt : xhi = nxt x = nxt dxold = dx end nil end |
.straddles_zero?(a, b) ⇒ Boolean
Whether a and b sit on opposite sides of zero. Comparing signs rather
than the product a * b avoids overflow when the NPV is astronomically large
near the bracket's floor for long-dated flows.
142 143 144 |
# File 'lib/xirr/rtsafe.rb', line 142 def self.straddles_zero?(a, b) (a <= 0 && b >= 0) || (a >= 0 && b <= 0) end |
Instance Method Details
#xirr(guess, options) ⇒ Float?
Solves the compacted Cashflow the instance was built with.
27 28 29 30 31 |
# File 'lib/xirr/rtsafe.rb', line 27 def xirr(guess, ) limit = ( && [:iteration_limit]) || Xirr.config.iteration_limit start = guess || cf.irr_guess RtSafe.find(flows, guess: start.to_f, iteration_limit: limit) end |