From adcac43c9d2ed6f846c9d051187200088f583aa1 Mon Sep 17 00:00:00 2001 From: Tyler Riccardi <247895347+tylrcc@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:31:10 +0000 Subject: [PATCH] Add American option adjoint example Port of the AdjointAmericanEquityOption C++ example. Prices an American put with the finite-difference Black-Scholes engine (801 time steps) and computes rho, strike sensitivity, vega, delta, and dividend rho in a single adjoint pass. The example cross-checks the Greeks against bump-and-reprice finite differences and documents the one mismatch: the FD engine builds its operator from the dividend yield as a plain value, so the adjoint dividend rho is numerically zero while bumping gives about 9.05. The other four Greeks agree to within 6e-5 relative. --- Python/examples/american-option-adjoint.py | 198 +++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 Python/examples/american-option-adjoint.py diff --git a/Python/examples/american-option-adjoint.py b/Python/examples/american-option-adjoint.py new file mode 100644 index 00000000..d69e71b4 --- /dev/null +++ b/Python/examples/american-option-adjoint.py @@ -0,0 +1,198 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# # American options with sensitivities +# +# Copyright (©) 2004, 2005, 2006, 2007 StatPro Italia srl +# Copyright (©) 2024 Xcelerit Computing Limited. +# +# This file is part of QuantLibAAD, a Python wrapper for QuantLib enabled +# for risk computation using automatic differentiation. It uses XAD, +# a fast and comprehensive C++ library for automatic differentiation. +# +# QuantLibAAD and XAD are free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# QuantLibAAD is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% [markdown] +# This is a port of the `AdjointAmericanEquityOption` C++ example. It prices +# an American put option with the finite-difference Black-Scholes engine and +# computes the sensitivities of the price to the risk-free rate, the strike, +# the volatility, the underlying, and the dividend yield in a single adjoint +# pass. The results are cross-checked against bump-and-reprice finite +# differences. +# +# One caveat, also present in the C++ example: the FD engine builds its +# operator from the dividend yield as a plain value, so the adjoint dividend +# rho comes out as (numerically) zero even though bumping the yield does move +# the price. The other four sensitivities match the bumped values closely. + +# %% +import QuantLibAAD as ql +from xad.adj_1st import Tape +from xad import value +import pandas as pd + +# %% [markdown] +# ### Global parameters + +# %% +calendar = ql.TARGET() +todaysDate = ql.Date(15, ql.May, 1998) +settlementDate = ql.Date(17, ql.May, 1998) +ql.Settings.instance().evaluationDate = todaysDate +dayCounter = ql.Actual365Fixed() +maturity = ql.Date(17, ql.May, 1999) + +# setup and activate the tape +tape = Tape() +tape.activate() + +# %% +interactive = "get_ipython" in globals() + +# %% [markdown] +# ### Market data and option inputs + +# %% +riskFreeRate_in = ql.Real(0.06) +strike_in = ql.Real(40.0) +volatility_in = ql.Real(0.20) +underlying_in = ql.Real(36.0) +dividendYield_in = ql.Real(0.00) + +tape.registerInput(riskFreeRate_in) +tape.registerInput(strike_in) +tape.registerInput(volatility_in) +tape.registerInput(underlying_in) +tape.registerInput(dividendYield_in) + + +# %% [markdown] +# ### Pricing +# +# The option and its engine are built inside the recording, so that they are +# constructed from the registered inputs. + + +# %% +def priceAmerican(riskFreeRate, strike, volatility, underlying, dividendYield): + termStructure = ql.FlatForward(settlementDate, riskFreeRate, dayCounter) + dividendTS = ql.FlatForward(settlementDate, dividendYield, dayCounter) + volTS = ql.BlackConstantVol(settlementDate, calendar, volatility, dayCounter) + process = ql.BlackScholesMertonProcess( + ql.QuoteHandle(ql.SimpleQuote(underlying)), + ql.YieldTermStructureHandle(dividendTS), + ql.YieldTermStructureHandle(termStructure), + ql.BlackVolTermStructureHandle(volTS), + ) + option = ql.VanillaOption( + ql.PlainVanillaPayoff(ql.Option.Put, strike), + ql.AmericanExercise(settlementDate, maturity), + ) + timeSteps = 801 + option.setPricingEngine( + ql.FdBlackScholesVanillaEngine(process, timeSteps, timeSteps - 1) + ) + return option.NPV() + + +# %% +tape.newRecording() +optionValue = priceAmerican( + riskFreeRate_in, strike_in, volatility_in, underlying_in, dividendYield_in +) + +tape.registerOutput(optionValue) +optionValue.derivative = 1.0 +tape.computeAdjoints() + +print(f"American option value: {optionValue:.10f}") + +# %% [markdown] +# ### Greeks + +# %% +greeks = [ + ("Rho", riskFreeRate_in.derivative), + ("Strike sensitivity", strike_in.derivative), + ("Vega", volatility_in.derivative), + ("Delta", underlying_in.derivative), + ("Dividend rho", dividendYield_in.derivative), +] + +# %% +df = pd.DataFrame(greeks, columns=["Greek", "Value"]) +df.style.hide(axis="index") + +# %% +if not interactive: + print(df.to_string(index=False)) + +# %% [markdown] +# ### Cross-check by bumping +# +# Each input is bumped in turn and the option repriced without the tape. +# Four of the five finite differences match the adjoints; the dividend rho +# does not, for the reason given at the top of this example. + +# %% +eps = 1e-5 +base = [ + value(riskFreeRate_in), + value(strike_in), + value(volatility_in), + value(underlying_in), + value(dividendYield_in), +] +checks = [] +for i, (name, adjoint) in enumerate(greeks): + bumped = list(base) + bumped[i] += eps + fd = ( + value(priceAmerican(*[ql.Real(v) for v in bumped])) - value(optionValue) + ) / eps + checks.append((name, adjoint, fd)) + +# %% +checkDf = pd.DataFrame(checks, columns=["Greek", "Adjoint", "Bump and reprice"]) +checkDf.style.hide(axis="index") + +# %% +if not interactive: + print(checkDf.to_string(index=False)) + +# %% +tape.deactivate()