Why did QuantLib produce this number?¶

This notebook answers one question for one trade: which inputs, constants and code paths produced this QuantLib price, and do the sensitivities agree with a separate numerical check?

The trade is a one-year European call with spot 100, strike 105, a flat 5% risk-free rate and a flat 2% dividend yield (both continuously compounded) and 20% volatility, using Actual/365 Fixed. From 7 October 2026 to 7 October 2027 is exactly 365 days, so T = 1.0. QuantLib 1.41 prices it with AnalyticEuropeanEngine while AADC Tracer records calculation evidence.

Scope: this walkthrough covers one instrument, one engine and flat curves. Sensitivities are local first-order derivatives at these inputs. Strike, maturity, calendar and day count are fixed. The trace records selected QuantLib operations rather than every executed instruction, and it does not cover code outside those traced operations. Re-record after an input change that may take a different branch. The evidence shows what ran; conceptual soundness, calibration, data quality, materiality and approval remain validation judgements.

1. Install the traced QuantLib build¶

Run the next cell once in a clean Linux x86-64 environment. Version 1.41.0 contains QuantLib 1.41, matching the source links below. The Community Edition is for non-commercial and academic use; commercial use requires a MatLogica licence.

In [1]:
%pip install -q "aadc-quantlib-tracing==1.41.0"
Note: you may need to restart the kernel to use updated packages.

2. Load the tracer helper¶

Start Jupyter from the repository root or the notebooks folder. TracerProbe marks declared inputs, records an output and assembles the price, sensitivities, hard-coded values and input-dependent branches used below.

In [2]:
from pathlib import Path
from math import erf, exp, log, sqrt
import json, sys

notebook_dir = Path.cwd() if Path.cwd().name == 'notebooks' else Path.cwd() / 'notebooks'
sys.path.insert(0, str(notebook_dir))
from tracer_probe import TracerProbe
import aadc_quantlib_tracing as ql
print('QuantLib', ql.__version__)
QuantLib 1.41

3. Price the option while recording calculation evidence¶

Spot, rate, dividend yield and volatility are registered with probe.input. They are the only quantities differentiated. Strike 105, the one-year maturity from 7 October 2026, option type, TARGET calendar and Actual/365 Fixed day count are fixed settings and have no reported sensitivities.

In [3]:
today = ql.Date(7, 10, 2026)
ql.Settings.instance().evaluationDate = today

probe = TracerProbe()
spot = probe.input('spot', 100.0)
rate = probe.input('rate', 0.05)
dividend_yield = probe.input('dividend_yield', 0.02)
volatility = probe.input('volatility', 0.20)

dc = ql.Actual365Fixed()
risk_free = ql.YieldTermStructureHandle(ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(rate)), dc))
dividend = ql.YieldTermStructureHandle(ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(dividend_yield)), dc))
vol_curve = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(today, ql.TARGET(), ql.QuoteHandle(ql.SimpleQuote(volatility)), dc))
process = ql.BlackScholesMertonProcess(ql.QuoteHandle(ql.SimpleQuote(spot)), dividend, risk_free, vol_curve)
option = ql.VanillaOption(ql.PlainVanillaPayoff(ql.Option.Call, 105.0), ql.EuropeanExercise(today + ql.Period(1, ql.Years)))
option.setPricingEngine(ql.AnalyticEuropeanEngine(process))
probe.output('npv', option.NPV())
report = probe.finish()
report['outputs']
[aadc] Community Edition -- licensed for non-commercial and academic use only. For commercial use or advanced features contact info@matlogica.com
Out[3]:
{'npv': 6.9869195320551185}

Check the price against the Black–Scholes–Merton formula¶

For this one-year trade, the reference formula provides a calculation independent of the recorded AADC operations. A difference near machine precision shows that the traced price agrees with that formula for these inputs; it does not validate the model choice.

In [4]:
S, K, r, q, sigma, T = 100.0, 105.0, 0.05, 0.02, 0.20, 1.0
d1 = (log(S / K) + (r - q + 0.5 * sigma**2) * T) / (sigma * sqrt(T))
d2 = d1 - sigma * sqrt(T)
normal_cdf = lambda x: 0.5 * (1.0 + erf(x / sqrt(2.0)))
reference_price = S * exp(-q * T) * normal_cdf(d1) - K * exp(-r * T) * normal_cdf(d2)
{'traced_price': report['outputs']['npv'], 'reference_price': reference_price, 'difference': report['outputs']['npv'] - reference_price}
Out[4]:
{'traced_price': 6.9869195320551185,
 'reference_price': 6.986919532055111,
 'difference': 7.105427357601002e-15}

4. Check adjoint sensitivities with separate QuantLib re-pricing¶

Each adjoint sensitivity is compared with a central finite difference from fresh QuantLib calculations outside the AADC recording. The bump is 1e-6 × max(1, |input|). Sensitivities are per unit change: for example, the rate result per basis point is the reported rate sensitivity multiplied by 0.0001. Agreement is a numerical check, not a conclusion about the model's assumptions.

In [5]:
base = {'spot': 100.0, 'rate': 0.05, 'dividend_yield': 0.02, 'volatility': 0.20}

def passive_price(values):
    risk_free = ql.YieldTermStructureHandle(ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(values['rate'])), dc))
    dividend = ql.YieldTermStructureHandle(ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(values['dividend_yield'])), dc))
    vol_curve = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(today, ql.TARGET(), ql.QuoteHandle(ql.SimpleQuote(values['volatility'])), dc))
    process = ql.BlackScholesMertonProcess(ql.QuoteHandle(ql.SimpleQuote(values['spot'])), dividend, risk_free, vol_curve)
    option = ql.VanillaOption(ql.PlainVanillaPayoff(ql.Option.Call, 105.0), ql.EuropeanExercise(today + ql.Period(1, ql.Years)))
    option.setPricingEngine(ql.AnalyticEuropeanEngine(process))
    return float(option.NPV())

checks = []
for name, adjoint in report['sensitivities']['npv'].items():
    step = 1e-6 * max(1.0, abs(base[name]))
    up, down = dict(base), dict(base)
    up[name], down[name] = base[name] + step, base[name] - step
    finite_difference = (passive_price(up) - passive_price(down)) / (2 * step)
    checks.append({'input': name, 'adjoint': adjoint, 'separate_finite_difference': finite_difference, 'absolute_difference': abs(adjoint - finite_difference), 'bump': step})
checks
Out[5]:
[{'input': 'spot',
  'adjoint': 0.49246480953918137,
  'separate_finite_difference': 0.492464809562776,
  'absolute_difference': 2.359462625278752e-11,
  'bump': 9.999999999999999e-05},
 {'input': 'rate',
  'adjoint': 42.259561421863026,
  'separate_finite_difference': 42.259561427560044,
  'absolute_difference': 5.697017968486762e-09,
  'bump': 1e-06},
 {'input': 'dividend_yield',
  'adjoint': -49.24648095391813,
  'separate_finite_difference': -49.246480956277594,
  'absolute_difference': 2.3594637355017767e-09,
  'bump': 1e-06},
 {'input': 'volatility',
  'adjoint': 39.103553941263925,
  'separate_finite_difference': 39.10355394642906,
  'absolute_difference': 5.165134098206181e-09,
  'bump': 1e-06}]

5. Inspect implementation evidence¶

A hard-coded value is a passive number recorded with QuantLib source provenance rather than declared as an input. It may be an initial value, sentinel, intermediate tolerance or model setting, so its source context matters. An input-dependent branch is a code choice whose condition depended on an active input; kind IBOOL2BOOL marks a comparison on an active value that was turned into an ordinary true/false decision. count reports how often the recorded item was reached.

The cell shows the first 12 items from each list and prints the full totals. Hard-coded values are sorted by source file and line; input-dependent branches are listed in the order the probe reports them. The saved JSON in section 6 contains every item the probe reported. These lists do not prove that each value is material, and the trace does not claim to capture every executed instruction.

In [6]:
source_base = 'https://github.com/lballabio/QuantLib/blob/v1.41/'
limit = 12
constants = [dict(item, source=f"{source_base}{item['file']}#L{item['line']}") for item in report['hidden_constants'][:limit]]
decisions = [dict(item, source=f"{source_base}{item['file']}#L{item['line']}") for item in report['conditional_decisions'][:limit]]
{'hard_coded_values_shown': constants, 'input_dependent_branches_shown': decisions, 'hard_coded_values_total': len(report['hidden_constants']), 'input_dependent_branches_total': len(report['conditional_decisions'])}
Out[6]:
{'hard_coded_values_shown': [{'file': 'ql/instrument.cpp',
   'line': 27,
   'function': 'Instrument',
   'value': 3.4028234663852886e+38,
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/instrument.cpp#L27'},
  {'file': 'ql/instrument.hpp',
   'line': 117,
   'function': 'reset',
   'value': 3.4028234663852886e+38,
   'count': 1,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/instrument.hpp#L117'},
  {'file': 'ql/instrument.hpp',
   'line': 170,
   'function': 'NPV',
   'value': 3.4028234663852886e+38,
   'count': 1,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/instrument.hpp#L170'},
  {'file': 'ql/instruments/oneassetoption.cpp',
   'line': 28,
   'function': 'OneAssetOption',
   'value': 0.0,
   'count': 11,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/instruments/oneassetoption.cpp#L28'},
  {'file': 'ql/instruments/oneassetoption.hpp',
   'line': 69,
   'function': 'results',
   'value': 0.0,
   'count': 7,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/instruments/oneassetoption.hpp#L69'},
  {'file': 'ql/interestrate.cpp',
   'line': 30,
   'function': 'InterestRate',
   'value': 0.0,
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/interestrate.cpp#L30'},
  {'file': 'ql/interestrate.cpp',
   'line': 31,
   'function': 'InterestRate',
   'value': 3.4028234663852886e+38,
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/interestrate.cpp#L31'},
  {'file': 'ql/interestrate.cpp',
   'line': 33,
   'function': 'InterestRate',
   'value': 0.0,
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/interestrate.cpp#L33'},
  {'file': 'ql/interestrate.cpp',
   'line': 46,
   'function': 'compoundFactor',
   'value': 0.0,
   'count': 3,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/interestrate.cpp#L46'},
  {'file': 'ql/interestrate.cpp',
   'line': 47,
   'function': 'compoundFactor',
   'value': 3.4028234663852886e+38,
   'count': 3,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/interestrate.cpp#L47'},
  {'file': 'ql/math/comparison.hpp',
   'line': 71,
   'function': 'close',
   'value': 0.0,
   'count': 6,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/math/comparison.hpp#L71'},
  {'file': 'ql/math/comparison.hpp',
   'line': 72,
   'function': 'close',
   'value': 8.697191480061655e-29,
   'count': 3,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/math/comparison.hpp#L72'}],
 'input_dependent_branches_shown': [{'file': 'ql/quotes/simplequote.hpp',
   'line': 69,
   'kind': 'IBOOL2BOOL',
   'count': 4,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/quotes/simplequote.hpp#L69'},
  {'file': 'ql/interestrate.cpp',
   'line': 47,
   'kind': 'IBOOL2BOOL',
   'count': 3,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/interestrate.cpp#L47'},
  {'file': 'ql/pricingengines/vanilla/analyticeuropeanengine.cpp',
   'line': 69,
   'kind': 'IBOOL2BOOL',
   'count': 1,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/pricingengines/vanilla/analyticeuropeanengine.cpp#L69'},
  {'file': 'ql/pricingengines/blackcalculator.cpp',
   'line': 68,
   'kind': 'IBOOL2BOOL',
   'count': 1,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/pricingengines/blackcalculator.cpp#L68'},
  {'file': 'ql/pricingengines/blackcalculator.cpp',
   'line': 72,
   'kind': 'IBOOL2BOOL',
   'count': 1,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/pricingengines/blackcalculator.cpp#L72'},
  {'file': 'ql/pricingengines/blackcalculator.cpp',
   'line': 74,
   'kind': 'IBOOL2BOOL',
   'count': 1,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/pricingengines/blackcalculator.cpp#L74'},
  {'file': 'ql/pricingengines/blackcalculator.cpp',
   'line': 77,
   'kind': 'IBOOL2BOOL',
   'count': 1,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/pricingengines/blackcalculator.cpp#L77'},
  {'file': 'ql/math/errorfunction.cpp',
   'line': 200,
   'kind': 'IBOOL2BOOL',
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/math/errorfunction.cpp#L200'},
  {'file': 'ql/math/errorfunction.cpp',
   'line': 209,
   'kind': 'IBOOL2BOOL',
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/math/errorfunction.cpp#L209'},
  {'file': 'ql/math/errorfunction.cpp',
   'line': 210,
   'kind': 'IBOOL2BOOL',
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/math/errorfunction.cpp#L210'},
  {'file': 'ql/math/distributions/normaldistribution.cpp',
   'line': 36,
   'kind': 'IBOOL2BOOL',
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/math/distributions/normaldistribution.cpp#L36'},
  {'file': 'ql/math/distributions/normaldistribution.hpp',
   'line': 279,
   'kind': 'IBOOL2BOOL',
   'count': 2,
   'source': 'https://github.com/lballabio/QuantLib/blob/v1.41/ql/math/distributions/normaldistribution.hpp#L279'}],
 'hard_coded_values_total': 57,
 'input_dependent_branches_total': 23}

6. Save the full report¶

quantlib-tracer-report.json contains the declared inputs, price, sensitivities, replay checks that re-evaluate the recorded AADC calculation (stored but not used as the independent check in this notebook), and the full lists of recorded hard-coded values (key hidden_constants) and input-dependent branches (key conditional_decisions) with source locations. It records calculation evidence; it is not a validation conclusion.

In [7]:
output = Path('quantlib-tracer-report.json')
output.write_text(json.dumps(report, indent=2, default=str) + '\n', encoding='utf-8')
output
Out[7]:
PosixPath('quantlib-tracer-report.json')