Skip to content

Keep the RS implied-standard-deviation approximation finite - #2835

Merged
lballabio merged 3 commits into
lballabio:masterfrom
Abhayindia:fix/rs-implied-stddev-nan
Oct 1, 2026
Merged

lballabio merged 3 commits into
lballabio:masterfrom
Abhayindia:fix/rs-implied-stddev-nan

Conversation

@Abhayindia

Copy link
Copy Markdown
Contributor

blackFormulaImpliedStdDevApproximationRS returns NaN over a sizeable part of ordinary parameter space, and blackFormulaImpliedStdDevLiRS inherits it because it seeds its iteration from this function.

Found by round-tripping: price an option with blackFormula, then feed the price back to each implied-standard-deviation inverse and check the library accepts its own output. Over 1488 priced cases the exact solver, the plain approximation and Chambers are all clean; RS returned a non-finite value 304 times.

On a plain grid of forward 1.0, call and put, moneyness 1.01 to 10 and standard deviation 0.01 to 1.5, 11 of the 49 cells were NaN.

Two separate causes

beta underflows to exactly zero for an in-the-money option at low volatility, so gamma = -pi/2 * log(0) is infinite and sqrt(gamma+y) - sqrt(gamma-y) evaluates as inf - inf. Writing that difference in its conjugate form, 2y / (sqrt(g+y) + sqrt(g-y)), is algebraically the same and tends to zero as gamma grows.

C = (R2 - (ey-1)^2) * ((ey+1)^2 - R2) / ey2 is exactly zero when the price sits on intrinsic, since R equals ey - 1 there. Rounding can leave it slightly negative instead, for example -8.2e-17 at forward 1.0, strike 1/1.1, and then beta is negative and log(beta) is NaN. Clamped at zero.

Why NaN is the wrong answer rather than an inaccurate one

In the failing cells the price has collapsed onto intrinsic, and blackFormulaImpliedStdDev returns exactly 0 there. After this change RS returns 0 too:

forward/strike true std dev exact RS before RS after
5 0.1 0 NaN 0
10 0.1 0 NaN 0
5 0.4 0.4 0.393236 0.393236
10 0.4 0.4 0.40932 0.40932

Values that were already finite are unchanged: the conjugate branch is only taken when a square root is not finite, and the clamp only engages where C was negative.

Verification

testImpliedStdDevApproximationRSStaysFinite covers moneyness 1.01 to 10 against standard deviations 0.01 to 1.5, calls and puts. Checked that it fails on master without the library change: 27 of its 28 assertions fail.

After the change the 1488-case sweep is clean for RS, and the full test suite passes: 1548 test cases, 614293 assertions.

Related, not addressed here

blackFormulaImpliedStdDevLiRS still throws on 336 of the 1488 cases. Before this change the failure was boost::math::quantile ... Probability argument is -nan, propagated from the RS guess. With the guess now finite it fails differently, boost::math::erfc_inv Overflow Error, when the normalized price reaches the 0 or 1 boundary and MaddockInverseCumulativeNormal overflows. That is a change inside the iteration rather than to a closed-form expression, so I have left it alone; happy to look at it separately if you would like.

@coveralls

coveralls commented Sep 28, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 78.252% (+0.001%) from 78.251% — Abhayindia:fix/rs-implied-stddev-nan into lballabio:master

@Abhayindia
Abhayindia force-pushed the fix/rs-implied-stddev-nan branch from 9312ed0 to 3c7b1a3 Compare September 29, 2026 17:24
Comment thread ql/pricingengines/blackformula.cpp Outdated
const auto sqrtDiff = [](Real g, Real yy) {
const Real hi = std::sqrt(g + yy), lo = std::sqrt(g - yy);
return (std::isfinite(hi) && std::isfinite(lo)) ? Real(hi - lo)
: Real(2.0 * yy / (hi + lo));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either hi or lo are not finite here. Shouldn't we replace this with the limit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced it with the limit - gamma is only non-finite when beta underflows, and sqrt(g+y)-sqrt(g-y) tends to 0 there, so the difference branch now returns 0 directly (the sum branch already yields +inf on its own). Over 1920 gridded cases gamma goes infinite 1182 times, and in every one the price is either exactly the intrinsic value or denormal, so 0 is the limit rather than a fallback.
The finiteness test still reports 27 failures without the guard and none with it.

@lballabio
lballabio merged commit fd71559 into lballabio:master Oct 1, 2026
45 checks passed
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