Skip to content

Fix American call approximations with negative risk-free rates - #2728

Open
mkzung wants to merge 1 commit into
lballabio:masterfrom
mkzung:fix/american-approximations-below-intrinsic
Open

Fix American call approximations with negative risk-free rates#2728
mkzung wants to merge 1 commit into
lballabio:masterfrom
mkzung:fix/american-approximations-below-intrinsic

Conversation

@mkzung

@mkzung mkzung commented Aug 15, 2026

Copy link
Copy Markdown

The defect

BaroneAdesiWhaleyApproximationEngine and JuQuadraticApproximationEngine return the European
value for any call whose dividend yield is not positive:

if (dividendDiscount>=1.0 && payoff->optionType()==Option::Call) {
    // early exercise never optimal

That is half of the condition. Early exercise of a call is never optimal when the underlying pays no
dividend and the rate is non-negative. With r < q <= 0 the discounted strike grows with
maturity, so exercising early is optimal, and both engines return a price below intrinsic.

S=100, K=80, r=-0.05, q=0, vol=0.03, T=3y, evaluation date 15 August 2026, Actual/360, intrinsic 20:

  BaroneAdesiWhaley              7.050879903   below intrinsic
  JuQuadratic                    7.050879903   below intrinsic
  BjerksundStensland            20.000000000
  QdPlusAmerican                20.000079592
  QdFpAmerican                  20.000002715
  FdBlackScholes 800            20.000000000
  Binomial CRR 2001             20.000000000

  European analytic              7.050879903

Equal to the European price to every digit, which pins the code path.

It is not one corner. Over a 9800-point grid of 7 strikes, 7 rates including 4 negative, 4 dividend
yields, 5 vols, 5 maturities and both types, BAW came back below intrinsic 153 times, every one a
call at a non-positive rate. Bjerksund-Stensland: 0.

The change

BjerksundStenslandApproximationEngine already carries the right condition at
bjerksundstenslandengine.cpp:470, so this uses the same one:

if (dividendDiscount>=1.0 && dividendDiscount>=riskFreeDiscount &&
    payoff->optionType()==Option::Call) {

Behaviour changes only for r < q <= 0. Anything with r >= 0 keeps its old path, since q <= 0
and r >= 0 already give dividendDiscount >= 1 >= riskFreeDiscount.

Those inputs now reach BaroneAdesiWhaleyApproximationEngine::criticalPrice, which has rejected
negative rates since #1291. Puts and calls with positive dividends went there already; this closes
the path that did not.

Test

Extends testBaroneAdesiWhaleyNegativeRates, added for #1291, which already covers a put and a call
with dividends. Ju is checked alongside because juquadraticengine.cpp:105 calls the same
criticalPrice, so it raises the same error.

Reverting the two engines and keeping the test gives two failures, americanoption.cpp:2315 and
:2320, both exception Error expected but not raised.

Validation

Complete suite 1440 test cases, no errors, 2 m 58 s, Release and C++20. On the 9800-point grid BAW
goes from 153 below intrinsic to 0, worst miss 40.00 to 0. Ju's negative-rate cases close with it,
153 to 0; its remaining 9 are at positive rates and are the approximation's own accuracy. Ju's 1097
NaN results, all at exactly r = 0, are a separate defect this does not touch.

The Barone-Adesi-Whaley and Ju quadratic engines returned the European
value for any call with a non-positive dividend yield. With a negative
risk-free rate below that yield, early exercise of the call is optimal
and the returned value falls below intrinsic. Require the dividend
discount factor to be at least the risk-free one, as the
Bjerksund-Stensland engine already does, so that these inputs reach the
critical-price calculation and its negative-rate check.
@boring-cyborg

boring-cyborg Bot commented Aug 15, 2026

Copy link
Copy Markdown

Thanks for opening this pull request! It might take a while before we look at it, so don't worry if there seems to be no feedback. We'll get to it.

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.

1 participant