Numerical Methods and Algorithms in Finance › Root-Finding Algorithms

Michael BrenndoerferNovember 1, 202525 min read
Quantitative FinanceAll chapters
Part 1 of 15Mathematical Foundations
Chapter 8Numerical Methods and Algorithms in Finance › Root-Finding Algorithms
Reading section 2 of 10 · Root-Finding Algorithms · 25 minSection 2 of 10 · 25 min

Book contents

Section 2 of 10 · 25 min read

Numerical Methods and Algorithms in Finance › Root-Finding AlgorithmsOne section · Sections in this chapter2 / 10 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Root-finding algorithms solve equations of the form f(x)=0f(x) = 0, where you seek the value x∗x^* such that the function equals zero. In finance, these often appear when you need to invert a pricing formula and no convenient analytic inverse is available. Given an observed market price, what input parameter produces that price? This question is central to financial calculations, from basic bond analytics to derivatives pricing.

The examples below express price as a function of yield or volatility, then supply a target price and recover the parameter. That is an inverse problem. Market quotation conventions can instead use yields or volatilities; here, the supplied price is the input to the numerical inversion.

Consider the bond pricing equation:

P=∑t=1TC(1+y)t+F(1+y)TP = \sum_{t=1}^{T} \frac{C}{(1+y)^t} + \frac{F}{(1+y)^T}

where:

  • PP: observed market price of the bond
  • CC: periodic coupon payment
  • FF: face value (par value) of the bond
  • TT: total number of payment periods until maturity
  • yy: yield per payment period (the discount rate we seek), with y>−1y>-1

The payments are equally spaced, TT is a positive integer, and this example assumes nonnegative coupons and positive principal. The restriction y>−1y>-1 keeps every periodic discount base positive; an algebraic root outside that domain is not a financial yield under this convention.

The first term sums the present values of all coupon payments, while the second discounts the principal repaid at maturity. The yield is the single periodic rate that reproduces the supplied price for these fixed cash flows; it is not a complete term-structure or credit model. Setting z=(1+y)−1z=(1+y)^{-1} turns the price equation into a polynomial of degree TT. Low-degree and specially structured cases can have analytic solutions. A 10-year bond with semiannual payments gives a degree-20 equation, which can be inverted with the numerical methods developed here.

For a European call on a non-dividend-paying stock under the Black-Scholes assumptions, price is a function of volatility σ\sigma:

C=S0N(d1)−Ke−rTN(d2)C = S_0 N(d_1) - Ke^{-rT}N(d_2)

where:

  • CC: call option price
  • S0S_0: current stock price
  • KK: strike price
  • TT: time to expiration
  • rr: risk-free interest rate
  • N(⋅)N(\cdot): cumulative standard normal distribution function
  • d1,d2d_1, d_2: parameters depending on volatility σ\sigma (defined later)

Given an observed option price CmarketC_{market}, finding the implied volatility σimp\sigma_{imp} requires solving C(σ)−Cmarket=0C(\sigma) - C_{market} = 0. The implementation below solves this residual numerically because σ\sigma enters both d1d_1 and d2d_2 and therefore both normal-CDF terms.

Out[2]:
Visualization
Line chart showing the nonlinear inverse relationship between bond price and yield.
Bond price as a function of yield-to-maturity. The marked yield is found by numerically inverting this coupon bond's price equation.

The Bisection Method

The bisection method is a simple root-finding algorithm with guaranteed convergence when its initial interval brackets a root. It exploits the intermediate value theorem: if a continuous function ff has opposite signs at two points aa and bb, then it must cross zero somewhere between them. This elegant mathematical principle guarantees that a root exists within the interval, and gives the foundation for a systematic search strategy.

The intuition behind bisection is straightforward. If a continuous function has a sign-changing bracket, checking the midpoint either finds an exact root or identifies a smaller sign-changing bracket. Each iteration halves the interval width. In exact arithmetic this converges to a bracketed root; implementations must also handle endpoint roots, finite-precision stagnation, and stopping criteria.

The algorithm proceeds by repeatedly halving the interval:

  1. Start with an interval [a,b][a, b] where f(a)f(a) and f(b)f(b) have opposite signs
  2. Compute the midpoint c=a+b2c = \frac{a + b}{2}
  3. Evaluate f(c)f(c)
  4. If f(c)=0f(c) = 0, return cc. Otherwise, replace aa with cc when f(c)f(c) has the same sign as f(a)f(a); if not, replace bb with cc.
  5. Repeat until the interval is sufficiently small

Step 4 requires careful attention. Since f(a)f(a) and f(b)f(b) have opposite signs and the function is continuous, the root must lie between them. After computing f(c)f(c), an exact zero ends the search. Otherwise, if f(c)f(c) shares the same sign as f(a)f(a), then the root lies between cc and bb (where signs still differ). If f(c)f(c) shares the same sign as f(b)f(b), the root lies between aa and cc. In either nonzero case, we've reduced the search interval by exactly half while maintaining the sign change that brackets the root.

Each iteration halves the interval width, so after nn iterations, the error is at most:

Errorn≤b−a2n\text{Error}_n \leq \frac{b-a}{2^n}

where:

  • b−ab-a: initial interval width
  • nn: number of iterations performed
  • 2n2^n: factor by which the interval shrinks after nn halvings

For positive initial width and ϵ>0\epsilon>0, the required number of halvings is max⁡{0,⌈log⁡2((b−a)/ϵ)⌉}\max\{0,\lceil\log_2((b-a)/\epsilon)\rceil\}. If the interval is already narrow enough, no halving is needed.

An interval of width one needs 34 halvings to become no wider than 10−1010^{-10}. The width bound does not depend on the function's derivatives, although the relationship between residual and root error depends on scaling and conditioning.

Out[3]:
Visualization
Function plot with shaded intervals illustrating successive bisection steps toward the root.
The bisection method progressively narrows the interval containing the root by evaluating the function at the midpoint.
Semi-log plot showing interval width shrinking by half each bisection iteration.
Convergence rate of bisection showing exponential reduction in interval width with each iteration.

Let's implement bisection to find the yield-to-maturity of a bond:

Here tol is an absolute yield half-width, not a price tolerance. An exact computed price match also stops the search. Compare signs directly: the mathematical notation f(a)f(b)<0f(a)f(b)<0 does not require multiplying residuals in floating-point code, where that product can underflow. Check the repriced residual separately if monetary accuracy is the target.

In[4]:
Code
import numpy as np


def bond_price(y, coupon, face_value, periods):
    """Calculate bond price given yield-to-maturity."""
    validate_bond_inputs(y, coupon, face_value, periods)
    cash_flows = np.full(periods, coupon, dtype=float)
    cash_flows[-1] += face_value  # Add face value to final payment
    times = np.arange(1, periods + 1)
    discount_factors = (1.0 + y) ** (-times)
    price = np.sum(cash_flows * discount_factors)
    if not np.isfinite(price):
        raise RuntimeError("Bond price is not finite in this arithmetic")
    return price


def bisection_ytm(
    target_price,
    coupon,
    face_value,
    periods,
    a=0.001,
    b=0.5,
    tol=1e-8,
    max_iter=100,
):
    """Find YTM; tol bounds the yield half-width, not dollar error."""
    validate_solver_controls(tol, max_iter)
    if not np.isfinite(target_price) or target_price <= 0:
        raise ValueError("target_price must be finite and positive")
    if not np.all(np.isfinite([a, b])) or not -1 < a < b:
        raise ValueError("Require finite ordered endpoints -1 < a < b")

    # Define the function to find root of
    def f(y):
        return bond_price(y, coupon, face_value, periods) - target_price

    # Handle endpoint roots, then verify a strict sign-changing bracket
    fa, fb = f(a), f(b)
    if fa == 0:
        return a, 0
    if fb == 0:
        return b, 0
    if np.signbit(fa) == np.signbit(fb):
        raise ValueError("Function must have opposite signs at interval endpoints")

    for i in range(max_iter):
        c = a + (b - a) / 2
        fc = f(c)

        if fc == 0 or (b - a) / 2 < tol:
            return c, i + 1
        if c == a or c == b:
            raise RuntimeError("Bisection stagnated before the yield tolerance was met")

        if np.signbit(fa) != np.signbit(fc):
            b = c
            fb = fc
        else:
            a = c
            fa = fc

    raise RuntimeError(f"Bisection did not converge in {max_iter} iterations")

Now let's apply this to a concrete example. Consider a 10-year bond with a 5% annual coupon, 1,000facevalue,tradingat1,000 face value, trading at 925:

Out[6]:
Console
Yield-to-Maturity: 0.06019975 (6.019975%)
Iterations required: 26
Verification - calculated price: $925.00
Target price: $925.00
Pricing error: $0.0000491782

The bond trades below par (at a discount), so the yield exceeds the coupon rate. With the displayed stopping rules, bisection takes 26 iterations and returns 6.019975%; the independently solved reference root is 6.019974222%. Over the bond's life, the investor receives the coupons and, absent default, the capital gain from the price converging toward par at maturity.

Newton-Raphson Method

While bisection is reliable, it converges slowly. Near a simple root, when the initial guess lies in a suitable convergence basin and the derivative remains nonzero, Newton-Raphson can converge much faster by using derivative information. The function value gives the current residual, while the derivative supplies the local direction and scale for a Newton step. Together, they support an informed step toward the root rather than another interval halving.

The geometric intuition is illuminating. At any point xnx_n, we can approximate the function by its tangent line, a linear approximation that matches both the function's value and its slope at that point. Where does this tangent line cross zero? That crossing point becomes our next estimate xn+1x_{n+1}. If the function is reasonably well-behaved near the root, the tangent line provides an excellent local approximation, and following it leads us closer to the root.

Starting from an initial guess x0x_0, each iteration updates:

xn+1=xn−f(xn)f′(xn)x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}

where:

  • xnx_n: current estimate of the root
  • f(xn)f(x_n): function value at current estimate
  • f′(xn)f'(x_n): derivative of function at current estimate
  • xn+1x_{n+1}: improved estimate after one iteration

Geometrically, this finds where the tangent line to ff at xnx_n crosses the x-axis. The ratio f(xn)/f′(xn)f(x_n)/f'(x_n) is the correction implied by that local linear model; it is not, in general, the actual distance to a root. Starting from y=f(xn)+f′(xn)(x−xn)y = f(x_n) + f'(x_n)(x - x_n), setting y=0y = 0 and solving for xx yields the Newton-Raphson update.

Out[7]:
Visualization
Diagram showing how Newton-Raphson method uses tangent lines to iterate toward a root.
For the displayed polynomial and starting value, each Newton-Raphson step follows the tangent from the current point to its zero crossing, moving the iterate rapidly toward the root.

Near a simple root, Newton-Raphson has at least quadratic convergence when the derivative is locally Lipschitz (for example, when the function is twice continuously differentiable) and the starting point is in the local convergence basin. In that asymptotic regime, the number of correct digits roughly doubles with each iteration, until finite precision limits the calculation.

Quadratic Convergence

An algorithm has at least quadratic local convergence if ∣en+1∣≤C∣en∣2|e_{n+1}| \leq C|e_n|^2 for one fixed constant CC and all sufficiently small errors, where en=xn−x∗e_n=x_n-x^* is the error relative to the root. This supports the rough doubling-of-digits description; it is not an exact promise of 2, 4, 8, then 16 decimal places.

Near a simple root and inside its convergence basin, quadratic convergence can make Newton-Raphson require fewer iterations than bisection. That comparison remains problem-dependent because derivative evaluation, starting values, multiple roots, and safeguards affect cost and convergence.

The main requirement is computing the derivative f′(x)f'(x). For bond pricing, the derivative of price with respect to yield is:

dPdy=−∑t=1Tt⋅C(1+y)t+1−T⋅F(1+y)T+1\frac{dP}{dy} = -\sum_{t=1}^{T} \frac{t \cdot C}{(1+y)^{t+1}} - \frac{T \cdot F}{(1+y)^{T+1}}

where:

  • dPdy\frac{dP}{dy}: rate of change of bond price with respect to yield (negative of dollar duration)
  • tt: time period index
  • CC: periodic coupon payment
  • TT: total number of periods
  • FF: face value
  • (1+y)t+1(1+y)^{t+1}: discount factor raised to power t+1t+1 (one more than in the price formula due to differentiation)

For these positive fixed cash flows and y>−1y>-1, the derivative is negative: prices fall as yields rise. Its negative is dollar duration under this yield convention. For a one-basis-point yield rise, −P′(y)×10−4-P'(y)\times10^{-4} approximates the dollar loss magnitude; the signed price change is ΔP≈P′(y)×10−4\Delta P\approx P'(y)\times10^{-4}. A cash flow's relative sensitivity is t/(1+y)t/(1+y), so longer-dated payments have greater sensitivity for equal present values. Absolute dollar sensitivity also depends on the payment amount and discounting; it need not grow with maturity at fixed undiscounted payment.

The derivative formula emerges directly from applying the power rule to each term in the bond pricing equation. For a single cash flow CFCF at time tt, we have ddy[CF⋅(1+y)−t]=−t⋅CF⋅(1+y)−(t+1)\frac{d}{dy}[CF \cdot (1+y)^{-t}] = -t \cdot CF \cdot (1+y)^{-(t+1)}. Summing over all cash flows gives the complete expression.

In[8]:
Code
def bond_price_derivative(y, coupon, face_value, periods):
    """Calculate derivative of bond price with respect to yield."""
    validate_bond_inputs(y, coupon, face_value, periods)
    cash_flows = np.full(periods, coupon, dtype=float)
    cash_flows[-1] += face_value
    times = np.arange(1, periods + 1)
    derivatives = -times * cash_flows * (1.0 + y) ** (-(times + 1))
    derivative = np.sum(derivatives)
    if not np.isfinite(derivative):
        raise RuntimeError("Bond derivative is not finite in this arithmetic")
    return derivative


def newton_raphson_ytm(
    target_price,
    coupon,
    face_value,
    periods,
    x0=0.05,
    tol=1e-8,
    max_iter=100,
    price_tol=1e-8,
):
    """Require a small yield step and a separately checked dollar residual."""
    validate_solver_controls(tol, max_iter)
    validate_solver_controls(price_tol, max_iter)
    validate_bond_inputs(x0, coupon, face_value, periods)
    if not np.isfinite(target_price) or target_price <= 0:
        raise ValueError("target_price must be finite and positive")
    x = float(x0)

    for i in range(max_iter):
        f_x = bond_price(x, coupon, face_value, periods) - target_price
        fp_x = bond_price_derivative(x, coupon, face_value, periods)

        if fp_x == 0:
            raise RuntimeError("Derivative is zero in this arithmetic")

        x_new = x - f_x / fp_x
        if not np.isfinite(x_new) or x_new <= -1:
            raise RuntimeError(
                "Newton step left the financial yield domain; use a bracketed method"
            )
        residual_new = bond_price(x_new, coupon, face_value, periods) - target_price

        if abs(x_new - x) < tol and abs(residual_new) < price_tol:
            return x_new, i + 1
        if x_new == x:
            raise RuntimeError("Newton stagnated before the price tolerance was met")

        x = x_new

    raise RuntimeError(f"Newton-Raphson did not converge in {max_iter} iterations")
Out[10]:
Console
Convergence Comparison:
Newton-Raphson: YTM = 0.06019974, iterations = 4
Bisection:      YTM = 0.06019975, iterations = 26

Bisection used 6.5x as many iterations as Newton-Raphson

In this bond example, Newton-Raphson converges in 4 iterations and bisection in 26. For Newton, tol controls the yield step and price_tol controls the repriced residual in monetary units; both must pass. It rejects steps leaving y>−1y>-1 rather than safeguarding every proposal. Bisection controls its yield bracket instead. Fewer iterations can matter in repeated inversions, but wall-clock performance also depends on derivative cost, safeguards, implementation, and hardware.

Out[11]:
Visualization
Semi-log plot comparing the convergence rates of bisection and Newton-Raphson methods.
Convergence comparison for the worked bond example. Errors are measured against the independently solved root; Newton-Raphson begins with the initial guess at iteration 0 and reaches the stopping floor in fewer updates, while bisection begins with its first midpoint at iteration 1 and reduces its bracket linearly.

Computing Implied Volatility

Implied volatility is a direct application of root-finding: choose the model volatility that reproduces a supplied option price. The example uses numerical inversion for that translation.

The Black-Scholes formula for a European call option is:

C=S0N(d1)−Ke−rTN(d2)C = S_0 N(d_1) - Ke^{-rT}N(d_2)

where:

  • CC: call option price
  • S0S_0: current stock price
  • KK: strike price of the option
  • TT: time to expiration in years
  • rr: risk-free interest rate (continuously compounded)
  • σ\sigma: volatility of the underlying asset (the parameter we seek)
  • d1=ln⁡(S0/K)+(r+σ2/2)TσTd_1 = \frac{\ln(S_0/K) + (r + \sigma^2/2)T}{\sigma\sqrt{T}}: the standardized threshold that appears in the stock-weighted payoff term and in delta
  • d2=d1−σTd_2 = d_1 - \sigma\sqrt{T}: the standardized threshold whose normal CDF is the risk-neutral exercise probability in this no-dividend model
  • N(⋅)N(\cdot): cumulative standard normal distribution function

Under the no-dividend Black-Scholes assumptions, S0N(d1)S_0N(d_1) and Ke−rTN(d2)Ke^{-rT}N(d_2) are the two discounted risk-neutral payoff components. In particular, N(d2)N(d_2) is the exercise probability under the money-market risk-neutral measure; N(d1)N(d_1) has a stock-numeraire interpretation and is also the call's delta. Both parameters depend nonlinearly on σ\sigma; the implementation inverts the pricing equation numerically.

To use Newton-Raphson, we need the derivative of the option price with respect to volatility, known as vega:

Vega=∂C∂σ=S0T n(d1)\text{Vega} = \frac{\partial C}{\partial \sigma} = S_0 \sqrt{T} \, n(d_1)

where:

  • n(⋅)n(\cdot): standard normal probability density function n(x)=12πe−x2/2n(x) = \frac{1}{\sqrt{2\pi}}e^{-x^2/2}
  • S0TS_0 \sqrt{T}: scales the sensitivity by stock price and time horizon
  • n(d1)n(d_1): is maximized at d1=0d_1=0; on common parameter slices this occurs near forward at-the-money, and the value falls as ∣d1∣|d_1| grows

For positive time and positive volatility, Black-Scholes call vega is positive: greater model volatility increases the value of the call's convex, one-sided payoff. At the zero-volatility boundary, the right-hand vega limit is zero except exactly forward at-the-money, where it equals S0T/2πS_0\sqrt{T}/\sqrt{2\pi}. Vega is often largest near forward at-the-money and can be small for extreme moneyness or short expiry. When vega is tiny, an unsafeguarded Newton step can overshoot the admissible volatility range.

The simplicity of the vega formula, despite the complexity of the Black-Scholes price itself, reflects the lognormal structure of the model and the fact that differentiating the normal CDF yields the normal PDF. Here the analytic vega is computed directly from the Black-Scholes inputs and a normal-PDF evaluation, avoiding the extra repricings required by a finite-difference derivative. Its runtime cost remains implementation-dependent.

In[12]:
Code
from scipy.stats import norm


def black_scholes_call(S, K, T, r, sigma):
    """Calculate Black-Scholes call option price."""
    if not np.all(np.isfinite([S, K, T, r, sigma])):
        raise ValueError("Black-Scholes inputs must be finite")
    if S <= 0 or K <= 0:
        raise ValueError("S and K must be positive")
    if T < 0 or sigma < 0:
        raise ValueError("T and sigma must be nonnegative")
    if T == 0:
        return max(S - K, 0)
    if sigma == 0:
        return max(S - K * np.exp(-r * T), 0)

    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)

    return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)


def vega(S, K, T, r, sigma):
    """Calculate option vega (sensitivity to volatility)."""
    if not np.all(np.isfinite([S, K, T, r, sigma])):
        raise ValueError("Vega inputs must be finite")
    if S <= 0 or K <= 0:
        raise ValueError("S and K must be positive")
    if T < 0 or sigma < 0:
        raise ValueError("T and sigma must be nonnegative")
    if T == 0:
        return 0
    if sigma == 0:
        forward_log_moneyness = np.log(S / K) + r * T
        # Eight epsilons covers exp/log construction roundoff in log-moneyness.
        forward_atm_atol = 8 * np.finfo(float).eps
        if np.isclose(forward_log_moneyness, 0.0, rtol=0.0, atol=forward_atm_atol):
            return S * np.sqrt(T) / np.sqrt(2 * np.pi)
        return 0

    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    return S * np.sqrt(T) * norm.pdf(d1)


def implied_volatility(market_price, S, K, T, r, sigma0=0.2, tol=1e-8, max_iter=100):
    """Find implied volatility; tol is a monetary pricing-residual tolerance."""
    validate_solver_controls(tol, max_iter)
    if not np.all(np.isfinite([market_price, S, K, T, r, sigma0])):
        raise ValueError("Inversion inputs must be finite")
    if S <= 0 or K <= 0 or T <= 0:
        raise ValueError("S, K, and T must be positive")

    lower_price = max(S - K * np.exp(-r * T), 0.0)
    upper_price = S
    if not lower_price <= market_price < upper_price:
        raise ValueError("Price lies outside the no-arbitrage Black-Scholes bounds")

    if market_price == lower_price:
        return 0.0, 0

    lo, hi = 0.0, 1.0
    while black_scholes_call(S, K, T, r, hi) < market_price and hi < 10.0:
        hi = min(2 * hi, 10.0)
    if black_scholes_call(S, K, T, r, hi) < market_price:
        raise RuntimeError("Could not bracket an implied volatility at or below 10.0")

    sigma = min(max(sigma0, lo), hi)

    for i in range(max_iter):
        price = black_scholes_call(S, K, T, r, sigma)
        v = vega(S, K, T, r, sigma)

        if abs(price - market_price) < tol:
            return sigma, i + 1
        if price < market_price:
            lo = sigma
        else:
            hi = sigma

        newton = sigma - (price - market_price) / v if abs(v) >= 1e-12 else np.nan
        # Force a midpoint step at least every fourth update for bracket progress.
        use_newton = np.isfinite(newton) and lo < newton < hi and i % 4 != 3
        sigma_new = newton if use_newton else lo + (hi - lo) / 2
        if sigma_new == sigma:
            raise RuntimeError(
                "Volatility stagnated before the price tolerance was met"
            )

        sigma = sigma_new

    raise RuntimeError(
        f"Implied-volatility solver did not converge in {max_iter} iterations"
    )

Let's calculate implied volatility for a sample option:

The lower attainable price returns the zero-volatility endpoint explicitly. Interior prices use a bracket starting at zero; its upper search is capped at volatility 10, a computational limit rather than a model bound. tol is in price units, and a small volatility step cannot independently report success. If floating-point repricing cannot meet that residual tolerance, the solver reports failure. This is still a teaching implementation, not a cancellation-resistant option-pricing library.

Out[14]:
Console
Implied Volatility: 0.253091 (25.31%)
Iterations: 4
Verification:
  Market price:     $3.50
  Calculated price: $3.500000
  Difference:       $0.0000000000

The safeguarded algorithm converges in a few iterations for this synthetic option. Real workloads and refresh policies are system-specific; nearby solved quotes or previous solutions can provide useful starting values.

Out[15]:
Visualization
Line chart showing call option price increasing with implied volatility, with market price and implied volatility marked.
For positive time and nondegenerate Black-Scholes inputs, call price increases with volatility. A unique implied volatility exists when the observed price lies within the model's attainable no-arbitrage bounds.
Line chart showing option vega versus volatility, illustrating where Newton-Raphson steps are most stable.
Vega along the displayed fixed-option volatility slice. Larger vega makes a given pricing residual produce a smaller Newton volatility step.

Practical Considerations

When implementing root-finding in production systems, several considerations arise.

Initial guess selection affects Newton-Raphson convergence. A nearby quote, the previous solution, or another data-driven estimate is preferable to a universal percentage guess. A valid price bracket and safeguards prevent negative-volatility iterates and provide a fallback when the Newton proposal is unsuitable.

Hybrid methods can combine fast Newton or interpolation steps with bracketed convergence. To obtain the interval-halving guarantee, preserve a valid sign-changing bracket and force sufficient progress, for example through periodic midpoint steps. Merely checking that a proposal stays in a reasonable range does not guarantee convergence.

Brent's method combines bisection, secant, and inverse quadratic interpolation. For a continuous function with a valid sign-changing bracket and suitable finite-precision checks, it retains that bracket while using faster steps when its safeguards permit them.