Table of Contents

Equity options

This page follows QuantLib's EquityOption example: one put, valued with European, Bermudan and American exercise by each engine family. Its values match that example's output, but for the one marked †. The sample, EquityOptions.cs, carries QuantLib's notice.

The option and its market

A put struck at 40 on a stock at 36, with a 6% rate, no dividends and 20% volatility, expiring a year after its settlement; the Bermudan one can be exercised every three months. The curves and the volatility are values, and the process holds them:

var settlement = new Date(17, Month.May, 1998);
var maturity = new Date(17, Month.May, 1999);
double underlying = 36, strike = 40, dividendYield = 0.0, riskFreeRate = 0.06, volatility = 0.20;
DayCounter dayCounter = new Actual365Fixed();

var riskFree = new FlatForward(settlement, riskFreeRate, dayCounter);
var dividend = new FlatForward(settlement, dividendYield, dayCounter);
var process = new BlackScholesMertonProcess(underlying, dividend, riskFree,
    new BlackConstantVol(settlement, new TARGET(), volatility, dayCounter));

var payoff = new PlainVanillaPayoff(OptionType.Put, strike);
var european = new VanillaOption(payoff, new EuropeanExercise(maturity));
var american = new VanillaOption(payoff, new AmericanExercise(settlement, maturity));
Date[] exerciseDates = [.. Enumerable.Range(1, 4).Select(i => settlement + new Period(3 * i, TimeUnit.Months))];
var bermudan = new VanillaOption(payoff, new BermudanExercise(exerciseDates));

The engines

Closed forms and approximations: Black-Scholes, Black-Scholes with Vasicek rates, the Heston and Bates models with their parameters set to the same volatility, the American approximations of Barone-Adesi and Whaley, Bjerksund and Stensland and the QD+ fixed point, and the integral of the payoff:

rows.Add(("Black-Scholes", AnalyticEuropeanEngine.Calculate(european, context, process).NPV, null, null));
rows.Add(("Black-Vasicek", AnalyticBlackVasicekEngine.Calculate(
    european, context, process, new Vasicek(riskFreeRate, 0.3, 0.3, 0.15, 0.0), 0.5).NPV, null, null));
var heston = new HestonModel(new HestonProcess(
    riskFree, dividend, underlying, volatility * volatility, 1.0, volatility * volatility, 0.001, 0.0));
rows.Add(("Heston, semi-analytic", AnalyticHestonEngine.Calculate(european, context, heston).NPV, null, null));
var bates = new BatesModel(new BatesProcess(
    riskFree, dividend, underlying, volatility * volatility, 1.0, volatility * volatility, 0.001, 0.0,
    1e-14, 1e-14, 1e-14));
rows.Add(("Bates, semi-analytic", BatesEngine.Calculate(european, context, bates).NPV, null, null));
rows.Add(("Barone-Adesi-Whaley", null, null,
    BaroneAdesiWhaleyApproximationEngine.Calculate(american, context, process).NPV));
rows.Add(("Bjerksund-Stensland", null, null,
    BjerksundStenslandApproximationEngine.Calculate(american, context, process).NPV));
rows.Add(("QD+ fixed point, fast", null, null,
    QdFpAmericanEngine.Calculate(american, context, process, QdFpAmericanEngine.FastScheme()).NPV));
rows.Add(("QD+ fixed point, accurate", null, null,
    QdFpAmericanEngine.Calculate(american, context, process, QdFpAmericanEngine.AccurateScheme()).NPV));
rows.Add(("QD+ fixed point, high precision", null, null,
    QdFpAmericanEngine.Calculate(american, context, process, QdFpAmericanEngine.HighPrecisionScheme()).NPV));
rows.Add(("Integral", IntegralEngine.Calculate(european, context, process).NPV, null, null));

Finite differences and seven binomial trees, each on 801 time steps, for all three exercises:

int timeSteps = 801;
rows.Add(("Finite differences",
    FdBlackScholesVanillaEngine.Calculate(european, context, process, tGrid: timeSteps, xGrid: timeSteps - 1).NPV,
    FdBlackScholesVanillaEngine.Calculate(bermudan, context, process, tGrid: timeSteps, xGrid: timeSteps - 1).NPV,
    FdBlackScholesVanillaEngine.Calculate(american, context, process, tGrid: timeSteps, xGrid: timeSteps - 1).NPV));
(string, double?, double?, double?) Tree<TTree>(string name)
    where TTree : Tree, IBinomialTree<TTree> => (name,
    BinomialVanillaEngine.Calculate<TTree>(european, context, process, timeSteps).NPV,
    BinomialVanillaEngine.Calculate<TTree>(bermudan, context, process, timeSteps).NPV,
    BinomialVanillaEngine.Calculate<TTree>(american, context, process, timeSteps).NPV);
rows.Add(Tree<JarrowRudd>("Binomial, Jarrow-Rudd"));
rows.Add(Tree<CoxRossRubinstein>("Binomial, Cox-Ross-Rubinstein"));
rows.Add(Tree<AdditiveEQPBinomialTree>("Binomial, additive equiprobabilities"));
rows.Add(Tree<Trigeorgis>("Binomial, Trigeorgis"));
rows.Add(Tree<Tian>("Binomial, Tian"));
rows.Add(Tree<LeisenReimer>("Binomial, Leisen-Reimer"));
rows.Add(Tree<Joshi4>("Binomial, Joshi"));

Monte Carlo: crude paths to an absolute tolerance, a Sobol sequence, and Longstaff and Schwartz's regression for early exercise:

int seed = 42;
rows.Add(("Monte Carlo, crude", new MakeMCEuropeanEngine<PseudoRandom, GeneralStatistics>(process, seed)
    .WithSteps(1).WithAbsoluteTolerance(0.02).ToEngine()(european, context).NPV, null, null));
// QuantLib's example gives the Sobol engine no seed; a one-dimensional Sobol sequence reads none, and nanoql's
// seeds are explicit.
rows.Add(("Quasi-Monte Carlo, Sobol", new MakeMCEuropeanEngine<LowDiscrepancy, GeneralStatistics>(process, seed)
    .WithSteps(1).WithSamples(32768).ToEngine()(european, context).NPV, null, null));
rows.Add((LongstaffSchwartz, null, null,
    new MakeMCAmericanEngine<PseudoRandom, GeneralStatistics, PseudoRandom>(process, seed)
        .WithSteps(100).WithAntitheticVariate().WithCalibrationSamples(4096).WithAbsoluteTolerance(0.02)
        .ToEngine()(american, context).NPV));

The values

Method European Bermudan American
Black-Scholes 3.844308
Black-Vasicek 3.818577
Heston, semi-analytic 3.844306
Bates, semi-analytic 3.844306
Barone-Adesi-Whaley 4.459628
Bjerksund-Stensland 4.453064
QD+ fixed point, fast 4.486617
QD+ fixed point, accurate 4.486669
QD+ fixed point, high precision 4.486674
Integral 3.844309
Finite differences 3.844330 4.360765 4.486113
Binomial, Jarrow-Rudd 3.844132 4.361174 4.486552
Binomial, Cox-Ross-Rubinstein 3.843504 4.360861 4.486415
Binomial, additive equiprobabilities 3.836911 4.354455 4.480097
Binomial, Trigeorgis 3.843557 4.360909 4.486461
Binomial, Tian 3.844171 4.361176 4.486413
Binomial, Leisen-Reimer 3.844308 4.360713 4.486076
Binomial, Joshi 3.844308 4.360713 4.486076
Monte Carlo, crude 3.834522
Quasi-Monte Carlo, Sobol 3.844613
Monte Carlo, Longstaff-Schwartz † 4.454440

† Longstaff and Schwartz's engine draws its calibration paths from the seed QuantLib documents, the pricing seed plus an offset, where QuantLib's code takes its null. QuantLib's example given that seed prints this value (the divergence).

Each engine is a static function of the option, the context and the process, and the Monte Carlo engines take their seeds as arguments, so the table is the same on every run.