Skip to content

Optimize Jamshidian root finding - #2664

Open
jewonj0620 wants to merge 1 commit into
lballabio:masterfrom
jewonj0620:feature/optimize-jamshidian-rstar
Open

Optimize Jamshidian root finding#2664
jewonj0620 wants to merge 1 commit into
lballabio:masterfrom
jewonj0620:feature/optimize-jamshidian-rstar

Conversation

@jewonj0620

Copy link
Copy Markdown
Contributor

Optimize JamshidianSwaptionEngine by caching affine discount-bond ratio coefficients before running the Brent root solver.

I already patched my engine and works pretty well.

For a one-factor affine model, each discount-bond ratio has the form:

P(t,T,x) / P(t,S,x) = c * exp(d * x)

The coefficients are now computed once from rates 0 and 1. Root evaluations therefore require only an exponential instead of repeatedly calling the model for every cash flow.

The cached ratios are also reused when calculating the final bond-option strikes.

Local benchmark using 20,000 recalculations of a 5Y×10Y Hull–White European swaption:

  • Before: 0.489 seconds
  • After: 0.143 seconds

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 74.956% (+0.002%) from 74.954% — jewonj0620:feature/optimize-jamshidian-rstar into lballabio:master

@lballabio

Copy link
Copy Markdown
Owner

Thanks! May you work out the math a bit more in the comments in the code?

@jewonj0620

jewonj0620 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! May you work out the math a bit more in the comments in the code?

For exercise time (t_e), value time (T_0), and fixed-payment time (T_i),

$$ P(t_e,T;r)=A(t_e,T)e^{-B(t_e,T)r}. $$

The bond-price ratio used in Jamshidian’s root finder is therefore

$$ R_i(r) = \frac{P(t_e,T_i;r)}{P(t_e,T_0;r)} = \frac{A(t_e,T_i)}{A(t_e,T_0)}e^{-[B(t_e,T_i)-B(t_e,T_0)]r}. $$

Thus, the ratio has the exact exponential-affine form

$$ R_i(r)=c_i e^{d_i r}. $$

Evaluating it at (r=0) and (r=1) gives

$$ c_i=R_i(0), \qquad d_i=\log\left(\frac{R_i(1)}{R_i(0)}\right). $$

Therefore, for any rate (r),

$$ R_i(r) =R_i(0)\exp\left(\log\left(\frac{R_i(1)}{R_i(0)}\right)r\right). $$

This corresponds to the precomputation in the PR:

discountRatios_[i] = R_i(0);
rateCoefficients_[i] = std::log(R_i(1) / R_i(0));

Each root-function evaluation then uses

discountBondRatio(i, r) = discountRatios_[i] * std::exp(rateCoefficients_[i] * r);

Jamshidian’s root equation itself is unchanged:
$$
f(r)=N-\sum_i C_iR_i(r)=0.
$$

If Brent performs (E) evaluations for (m) fixed-leg cash flows, the previous implementation makes approximately
$$
(E+1)(m+1)
$$

calls to discountBond(): (m+1) per root evaluation and another (m+1) when constructing the bond-option strikes.

The new implementation makes only
$$
2(m+1)
$$
calls during initialization—one set at (r=0) and one at (r=1). Root evaluations and final strike construction then reuse the cached exponential-affine representation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants