src/lib/pricing/brent.ts

97 lines
export class NoBracketError extends Error {}

/**
 * Widens [a, b] outward geometrically until f changes sign.
 * The forward is monotonic in every solvable input, so one sign change ⇒ one root.
 */
export function expandBracket(
  f: (x: number) => number,
  a: number,
  b: number,
  maxIter = 50,
  factor = 1.6,
): [number, number] {
  let fa = f(a);
  let fb = f(b);
  for (let i = 0; i < maxIter; i++) {
    if (Number.isFinite(fa) && Number.isFinite(fb) && fa * fb <= 0) return [a, b];
    if (Math.abs(fa) < Math.abs(fb)) {
      a += factor * (a - b);
      fa = f(a);
    } else {
      b += factor * (b - a);
      fb = f(b);
    }
  }
  throw new NoBracketError(`No sign change found in [${a}, ${b}]`);
}

/** Brent's method (inverse quadratic interpolation + secant + bisection fallback). */
export function brent(
  f: (x: number) => number,
  lo: number,
  hi: number,
  tol = 1e-12,
  maxIter = 200,
): number {
  let a = lo;
  let b = hi;
  let fa = f(a);
  let fb = f(b);
  if (fa === 0) return a;
  if (fb === 0) return b;
  if (fa * fb > 0) throw new NoBracketError(`Root not bracketed in [${lo}, ${hi}]`);

  let c = a;
  let fc = fa;
  let d = b - a;
  let e = d;

  for (let i = 0; i < maxIter; i++) {
    if (fb * fc > 0) {
      c = a;
      fc = fa;
      d = e = b - a;
    }
    if (Math.abs(fc) < Math.abs(fb)) {
      a = b; b = c; c = a;
      fa = fb; fb = fc; fc = fa;
    }
    const tol1 = 2 * Number.EPSILON * Math.abs(b) + 0.5 * tol;
    const xm = 0.5 * (c - b);
    if (Math.abs(xm) <= tol1 || fb === 0) return b;

    if (Math.abs(e) >= tol1 && Math.abs(fa) > Math.abs(fb)) {
      const s = fb / fa;
      let p: number;
      let q: number;
      if (a === c) {
        p = 2 * xm * s;
        q = 1 - s;
      } else {
        const qq = fa / fc;
        const r = fb / fc;
        p = s * (2 * xm * qq * (qq - r) - (b - a) * (r - 1));
        q = (qq - 1) * (r - 1) * (s - 1);
      }
      if (p > 0) q = -q;
      p = Math.abs(p);
      if (2 * p < Math.min(3 * xm * q - Math.abs(tol1 * q), Math.abs(e * q))) {
        e = d;
        d = p / q;
      } else {
        d = xm;
        e = d;
      }
    } else {
      d = xm;
      e = d;
    }
    a = b;
    fa = fb;
    b += Math.abs(d) > tol1 ? d : Math.sign(xm) * tol1;
    fb = f(b);
  }
  return b;
}