Math Tools
Solvers
QuantLib provides several types of one-dimensional solvers to solve the roots of single-parameter functions,
Where is a function over a real number field.
The types of solvers provided by QuantLib are:
Brent
Bisection
Secant
Ridder
Newton (requires member function derivative , calculates derivative)
FalsePosition
The constructors for these solvers are default constructors and do not accept parameters. For example, the construction statement for the Brent solver instance is:
mySolv = Brent()
There are two ways to call the solver’s member function:
mySolv.solve(f, accuracy, guess, step)
mySolv.solve(f, accuracy, guess, xMin, xMax)
f |
single parameter function or function object, the return value is a floating point number |
accuracy |
Floating-point number representing the solution precision used to stop the calculation |
guess |
a floating-point number, the initial guess for the root |
step |
Floating point number. In the first calling method, there is no limit to the range of the root. The algorithm needs to search by itself to determine a range. step specifies the step size of the search algorithm. |
xMin, xMax |
floating point numbers, left and right interval range |
ql.Settings.instance().evaluationDate = ql.Date(15,6,2020)
crv = ql.FlatForward(2, ql.TARGET(), 0.05, ql.Actual360())
yts = ql.YieldTermStructureHandle(crv)
engine = ql.DiscountingSwapEngine(yts)
schedule = ql.MakeSchedule(ql.Date(15,9,2020), ql.Date(15,9,2021), ql.Period('6M'))
index = ql.Euribor3M(yts)
floatingLeg = ql.IborLeg([100], schedule, index)
def swapFairRate(rate):
fixedLeg = ql.FixedRateLeg(schedule, ql.Actual360(), [100.], [rate])
swap = ql.Swap(fixedLeg, floatingLeg)
swap.setPricingEngine(engine)
return swap.NPV()
solver = ql.Brent()
accuracy = 1e-5
guess = 0.0
step = 0.001
solver.solve(swapFairRate, accuracy, guess, step)
Integration
Gaussian Quadrature
Gaussian Quadrature evaluates an integral on a set interval. For example, Gauss-Legendre evaluates the definite integral on the inverval [-1,1]
import numpy as np
import QuantLib as ql
f = lambda x: x**2
g = lambda x: np.exp(x)
quad_ql_legendre = ql.GaussLegendreIntegration(128)
quad_ql_legendre(f), quad_ql_legendre(g)
Scipy also has an implementation that we can compare:
from scipy.integrate import quad
quad(f, -1, 1)[0], quad(g, -1, 1)[0]
Scipy requests an interval [a,b] to operate over. We can achieve the same thing with ql if we scale the input parameters using a wrapper function:
def quad_ql_ab(f, a, b, quad):
multiplier, ratio = (b+a) / 2, (b-a) / 2
y = lambda x: f(ratio*x + multiplier)
return quad(y) * ratio
quad_ql = ql.GaussLegendreIntegration(128)
quad_ql_ab(f, -2, 2, quad_ql), quad_ql_ab(g, -2, 2, quad_ql), quad(f, -2, 2)[0], quad(g, -2, 2)[0]
Other supported quadratures are:
GaussLegendreIntegration,
GaussChebyshevIntegration,
GaussChebyshev2ndIntegration,
GaussLaguerreIntegration,
GaussHermiteIntegration,
GaussJacobiIntegration,
GaussHyperbolicIntegration,
GaussGegenbauerIntegration
The intervals and additional parameters for each quadrature varies (see eg. https://mathworld.wolfram.com/GaussianQuadrature.html)
Interpolation
Interpolation is one of the most commonly used tools in quantitative finance. The standard application scenario is interpolation of yield curves, volatility smile curves, and volatility surfaces. quantlib-python provides the following one- and two-dimensional interpolation methods:
- XXXInterpolation(x, y)
x : sequence of floating-point numbers, several discrete arguments
y : sequence of floating-point numbers, the value of the function corresponding to the argument, the same length as x
The interpolation class defines the __call__ method. The usage of an interpolation class object is as follows, as a function
i(x, allowExtrapolation)
x : floating point number, the point to be interpolated
allowExtrapolation : boolean. Setting allowExtrapolation to True means extrapolation is allowed. The default value is False.
1D interpolation method
LinearInterpolation (1-D)
LogLinearInterpolation (1-D)
CubicInterpolation (1-D)
LogCubicInterpolation (1-D)
ForwardFlatInterpolation (1-D)
BackwardFlatInterpolation (1-D)
LogParabolicInterpolation (1-D)
import QuantLib as ql
import numpy as np
import matplotlib.pyplot as plt
X = [1., 2., 3., 4., 5.]
Y = [0.5, 0.6, 0.7, 0.8, 0.9]
methods = {
'Linear Interpolation': ql.LinearInterpolation(X, Y),
'LogLinearInterpolation': ql.LogLinearInterpolation(X, Y),
'CubicNaturalSpline': ql.CubicNaturalSpline(X, Y),
'LogCubicNaturalSpline': ql.LogCubicNaturalSpline(X, Y),
'ForwardFlatInterpolation': ql.ForwardFlatInterpolation(X, Y),
'BackwardFlatInterpolation': ql.BackwardFlatInterpolation(X, Y),
'LogParabolic': ql.LogParabolic(X, Y)
}
xx = np.linspace(1, 10)
fig = plt.figure(figsize=(15,4))
plt.scatter(X, Y, label='Original Data')
for name, i in methods.items():
yy = [i(x, allowExtrapolation=True) for x in xx]
plt.plot(xx, yy, label=name);
plt.legend();
2D Interpolation Methods
BilinearInterpolation (2-D)
BicubicSpline (2-D)
import pandas as pd
X = [1., 2., 3., 4., 5.]
Y = [0.6, 0.7, 0.8, 0.9]
Z = [[(x-3)**2 + y for x in X] for y in Y]
df = pd.DataFrame(Z, columns=X, index=Y)
i = ql.BilinearInterpolation(X, Y, Z)
XX = np.linspace(0, 5, 9)
YY = np.linspace(0.55, 1.0, 10)
extrapolated = pd.DataFrame(
[[i(x,y, True) for x in XX] for y in YY],
columns=XX,
index=YY)
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_title("Surface Interpolation")
Xs, Ys = np.meshgrid(XX, YY)
surf = ax.plot_surface(
Xs, Ys, extrapolated, rstride=1, cstride=1, cmap=cm.coolwarm
)
fig.colorbar(surf, shrink=0.5, aspect=5);
Optimization
Random Number Generators
Quantlib-Python provides the following three uniformly distributed (pseudo) random number generators:
ql.KnuthUniformRng, Knuth algorithm
ql.LecuyerUniformRng, L’Ecuyer algorithm
ql.MersenneTwisterUniformRng, the famous Mersenne-Twister algorithm
The constructor of the random number generator,
- RandomNumberGenerator(seed)
where seed is an integer, with a default value of 0, used as a seed to initialize the corresponding deterministic sequence
Member functions of the random number generator:
next() : Returns a SampleNumber object as the result of the simulation.
r = rng.next()
v = r.value(r)
The user obtains a series of random numbers by repeatedly calling the member function next(). It should be noted that the type of r is SampleNumber , and the corresponding floating-point number needs to be called by calling value() .
The most common distribution in random simulations is the normal distribution. There are four types of normally distributed random number generators provided by quantlib-python:
CentralLimit[X]GaussianRng
BoxMuller[X]GaussianRng
MoroInvCumulative[X]GaussianRng
InvCumulative[X]GaussianRng
Where [X] refers to a uniform random number generator.
Specifically, there are 12 types of 4 types of generators:
CentralLimitLecuyerGaussianRng
CentralLimitKnuthGaussianRng
CentralLimitMersenneTwisterGaussianRng
BoxMullerLecuyerGaussianRng
BoxMullerKnuthGaussianRng
BoxMullerMersenneTwisterGaussianRng
MoroInvCumulativeLecuyerGaussianRng
MoroInvCumulativeKnuthGaussianRng
MoroInvCumulativeMersenneTwisterGaussianRng
InvCumulativeLecuyerGaussianRng
InvCumulativeKnuthGaussianRng
InvCumulativeMersenneTwisterGaussianRng
Constructor of random number generator:
- GaussianRandomNumberGenerator(RandomNumberGenerator)
BoxMullerMersenneTwisterGaussianRng distributed random number generators accept a corresponding uniformly distributed random number generator as the source.
Taking BoxMullerMersenneTwisterGaussianRng as an example, you need to configure a MersenneTwisterUniformRng object as the source of random numbers, and use the classic Box-Muller algorithm to obtain normal distributed random numbers.
seed = 12324
unifMt = ql.MersenneTwisterUniformRng(seed)
bmGauss = ql.BoxMullerMersenneTwisterGaussianRng(unifMt)
for i in range(5):
print(bmGauss.next().value())
Compared with the “pseudo” random numbers described earlier, another important type of random numbers in random simulations becomes “quasi” random numbers, also known as low-bias sequences. Because of better convergence, quasi-random numbers are often used in the simulation of high-dimensional random variables. There are two types of quasi-random numbers provided by quantlib-python,
HaltonRsg : Halton sequence
SobolRsg : Sobol sequence
HaltonRsg
- HaltonRsg(dimensionality, seed, randomStart, randomShift)
where,
dimensionality : integer, set the dimension;
seed : an integer, with a default value of 0, used as a seed to initialize the corresponding deterministic sequence;
randomStart : boolean, the default is True , whether to start randomly;
randomShift : Boolean, default is False , whether to shift randomly.
Member function of HaltonRsg,
nextSequence() : returns a SampleRealVector object as the result of the simulation;
lastSequence() : returns a SampleRealVector object as the result of the previous simulation;
dimension() : Returns the dimension.
SobolRsg
- SobolRsg(dimensionality, seed, directionIntegers=Jaeckel)
where,
dimensionality : integer, set the dimension;
seed : an integer, with a default value of 0, used as a seed to initialize the corresponding deterministic sequence;
directionIntegers : a built-in variable of quantlib-python. The default value is SobolRsg.Jaeckel , which is used to initialize Sobol sequences.
Member functions of SobolRsg,
nextSequence() : returns a SampleRealVector object as the result of the simulation;
lastSequence() : returns a SampleRealVector object as the result of the previous simulation;
dimension() : Returns the dimension.
skipTo(n) : n is an integer, skip to the nth dimension of the sampling result;
nextInt32Sequence() : Returns an IntVector object.
Path Generators
QuantLib provides several wrapper functions to produce sample paths from a given stochastic process
GaussianMultiPathGenerator
Generate paths from an arbitrary stochastic process using pseudorandom numbers
- ql.GaussianMultiPathGenerator(stochasticProcess, times, sequenceGenerator, brownianBridge=False)
timestep, length, numPaths = 24, 2, 2**15
today = ql.Date().todaysDate()
riskFreeTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.05, ql.Actual365Fixed()))
dividendTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.01, ql.Actual365Fixed()))
initialValue = ql.QuoteHandle(ql.SimpleQuote(100))
v0, kappa, theta, rho, sigma = 0.005, 0.8, 0.008, 0.2, 0.1
hestonProcess = ql.HestonProcess(riskFreeTS, dividendTS, initialValue, v0, kappa, theta, sigma, rho)
times = ql.TimeGrid(length, timestep)
dimension = hestonProcess.factors()
rng = ql.UniformRandomSequenceGenerator(dimension * timestep, ql.UniformRandomGenerator())
sequenceGenerator = ql.GaussianRandomSequenceGenerator(rng)
pathGenerator = ql.GaussianMultiPathGenerator(hestonProcess, list(times), sequenceGenerator, False)
# paths[0] will contain spot paths, paths[1] will contain vol paths
paths = [[] for i in range(dimension)]
for i in range(numPaths):
samplePath = pathGenerator.next()
values = samplePath.value()
spot = values[0]
for j in range(dimension):
paths[j].append([x for x in values[j]])
GaussianSobolMultiPathGenerator
Generate paths from an arbitrary stochastic process using low discrepency numbers
- ql.GaussianSobolMultiPathGenerator(stochasticProcess, times, sequenceGenerator, brownianBridge=False)
timestep, length, numPaths = 24, 2, 2**15
today = ql.Date().todaysDate()
riskFreeTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.05, ql.Actual365Fixed()))
dividendTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.01, ql.Actual365Fixed()))
initialValue = ql.QuoteHandle(ql.SimpleQuote(100))
v0, kappa, theta, rho, sigma = 0.005, 0.8, 0.008, 0.2, 0.1
hestonProcess = ql.HestonProcess(riskFreeTS, dividendTS, initialValue, v0, kappa, theta, sigma, rho)
times = ql.TimeGrid(length, timestep)
dimension = hestonProcess.factors()
rng = ql.UniformLowDiscrepancySequenceGenerator(dimension * timestep)
sequenceGenerator = ql.GaussianLowDiscrepancySequenceGenerator(rng)
pathGenerator = ql.GaussianSobolMultiPathGenerator(hestonProcess, list(times), sequenceGenerator, False)
# paths[0] will contain spot paths, paths[1] will contain vol paths
paths = [[] for i in range(dimension)]
for i in range(numPaths):
samplePath = pathGenerator.next()
values = samplePath.value()
spot = values[0]
for j in range(dimension):
paths[j].append([x for x in values[j]])
Statistics
Convention Calculators
BlackDeltaCalculator
A calculator class to calculate the relevant strike for FX-style delta-maturity-vol quotes (see Reiswich D., Wystup U., 2010. A Guide to FX Options Quoting Conventions)
- ql.BlackDeltaCalculator(optionType, deltaType, spot, domesticDcf, foreignDcf, volRootT)
import numpy as np
today = ql.Date().todaysDate()
dayCounter = ql.Actual365Fixed()
spot = 100
rd, rf = 0.02, 0.05
ratesTs = ql.YieldTermStructureHandle(ql.FlatForward(today, rd, dayCounter))
dividendTs = ql.YieldTermStructureHandle(ql.FlatForward(today, rf, dayCounter))
# Details about the delta quote
optionType = ql.Option.Put
vol = 0.07
maturity = 1.0
deltaType = ql.DeltaVolQuote.Fwd # Also supports: Spot, PaSpot, PaFwd
# Set up the calculator
localDcf, foreignDcf = ratesTs.discount(maturity), dividendTs.discount(maturity)
stdDev = np.sqrt(maturity) * vol
calc = ql.BlackDeltaCalculator(optionType, deltaType, spot, localDcf, foreignDcf, stdDev)
To calculate the strike for a given call/put delta (negative for put delta)
delta = -0.3
calc.strikeFromDelta(delta)
Or if this is an ATM quote, specify the ATM convention
atmType = ql.DeltaVolQuote.AtmFwd # Also supports: AtmSpot, AtmDeltaNeutral, AtmVegaMax, AtmGammaMax, AtmPutCall50
calc.atmStrike(atmType)