Class: Xirr::Brent
Overview
Brent's method: a derivative-free root finder combining inverse quadratic interpolation, the secant method, and bisection. It reuses RtSafe's bracketing, so it is as robust as the default solver but never evaluates the NPV derivative — each iteration is cheaper, though it needs more of them.
In practice it roughly ties RtSafe; it is offered for very large cashflows,
where the cheaper per-iteration cost can win. Select it with
xirr(method: :brent). Unlike the Newton-based solvers it ignores the initial
guess — it works from the bracket.
Instance Attribute Summary
Attributes included from Base
Class Method Summary collapse
-
.find(flows, tolerance: Xirr.config.eps, iteration_limit: Xirr.config.iteration_limit, precision: Xirr.config.precision) ⇒ Float?
Pure solver over normalized
[time, amount]flows. -
.zbrent(flows, tol, iteration_limit) ⇒ Object
Bracket a sign change (reusing RtSafe), then iterate Brent's method within it.
Instance Method Summary collapse
Methods included from Base
#initialize, #periods_from_start, #xnpv, #xnpv_derivative
Class Method Details
.find(flows, tolerance: Xirr.config.eps, iteration_limit: Xirr.config.iteration_limit, precision: Xirr.config.precision) ⇒ Float?
Pure solver over normalized [time, amount] flows.
27 28 29 30 31 32 33 34 35 |
# File 'lib/xirr/brent.rb', line 27 def self.find(flows, tolerance: Xirr.config.eps, iteration_limit: Xirr.config.iteration_limit, precision: Xirr.config.precision) rate = zbrent(flows, tolerance.to_f, iteration_limit) return nil if rate.nil? || rate.nan? || rate.infinite? rounded = rate.round(precision) rounded <= -1.0 ? nil : rounded rescue FloatDomainError, Math::DomainError nil end |
.zbrent(flows, tol, iteration_limit) ⇒ Object
Bracket a sign change (reusing RtSafe), then iterate Brent's method within it. Returns the rate, or nil if it can't bracket or converge.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/xirr/brent.rb', line 39 def self.zbrent(flows, tol, iteration_limit) low = RtSafe.safe_low(flows) f_low = RtSafe.present_value(flows, low) bounds = RtSafe.bracket(flows, low, f_low, 1.0) return nil if bounds.nil? a, b = bounds fa = f_low # a == low fb = RtSafe.present_value(flows, b) c = a fc = fa d = e = b - a iteration_limit.times do # Keep c as the contrapoint — opposite sign to b, so [b, c] brackets. if (fb.positive? && fc.positive?) || (fb.negative? && fc.negative?) c = a fc = fa d = e = b - a end # Ensure b is the better estimate. if fc.abs < fb.abs a = b b = c c = a fa = fb fb = fc fc = fa end tol1 = 2.0 * Float::EPSILON * b.abs + 0.5 * tol xm = 0.5 * (c - b) return b if xm.abs <= tol1 || fb.zero? if e.abs >= tol1 && fa.abs > fb.abs s = fb / fa if a == c # Secant step. p = 2.0 * xm * s q = 1.0 - s else # Inverse quadratic interpolation. q = fa / fc r = fb / fc p = s * (2.0 * xm * q * (q - r) - (b - a) * (r - 1.0)) q = (q - 1.0) * (r - 1.0) * (s - 1.0) end q = -q if p.positive? p = p.abs min1 = 3.0 * xm * q - (tol1 * q).abs min2 = (e * q).abs if 2.0 * p < (min1 < min2 ? min1 : min2) e = d d = p / q # accept interpolation else d = e = xm # fall back to bisection end else d = e = xm # bounds decreasing too slowly; bisect end a = b fa = fb b += d.abs > tol1 ? d : (xm.positive? ? tol1 : -tol1) fb = RtSafe.present_value(flows, b) end nil end |