Simulation of the 3/2 model using the Quadratic exponential scheme#

In order to achieve maximum performance of Pytorch in a Monte-Carlo simulation it is critical to vectorize over the number of simulations. Thus we need to remove all ‘if’ statements from the Quadratic Exponential scheme. Here we are also comparing the numerical performance with a standard Milstein-Euler scheme

[10]:
#import torch
import numpy as np
from quant_analytics_torch.analytics import blackanalytics
from icecream import ic
from quant_analytics_torch.calculators.multivariatebrownianbridge import MultivariateBrownianBridge
from quant_analytics_torch.analytics.norminv import norminv
import matplotlib
from matplotlib import pyplot as plt
from quant_analytics_torch.analytics import maxsoft
from quant_analytics_torch.analytics.characteristicfunction import heston_option_price, threeovertwo_option_price

[11]:
strikes = [-0.5, -0.4, -0.3, -0.2, -0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5]
ivs_heston = []
ivs_3_2 = []

for it in strikes:
    strike = np.exp(it)
    price = heston_option_price(strike,1,0.04,1,0.04,1,-0.7,1)
    iv = blackanalytics.impliedvolatility(price,1,strike,1)
    ivs_heston.append(iv)

for it in strikes:
    strike = np.exp(it)
    price = threeovertwo_option_price(strike,1,0.04,22,0.09,8,-0.8,1)
    iv = blackanalytics.impliedvolatility(price.real,1,strike,1)
    ivs_3_2.append(iv)

print(ivs_heston)
print(ivs_3_2)
[0.329324589130455, 0.28804817090987134, 0.2559429643335339, 0.22570594900838947, 0.1765775635550539, 0.13437616926929888, 0.11274163089679885, 0.11420776457099098, 0.15594420454274932, 0.1533360730988036, inf]
[0.27490070281895745, 0.2585383473876623, 0.24188987704203105, 0.22506545950438872, 0.208247085020842, 0.19171477686187532, 0.17586904342594117, 0.16123827628813256, 0.14846157304813323, 0.13824363532984824, 0.13125438320876792]
[9]:
plt.plot(strikes,ivs_heston)
plt.plot(strikes,ivs_3_2)
[9]:
[<matplotlib.lines.Line2D at 0x7fe263369e90>]
../_images/examples_32QuadraticExponentialSimulation_4_1.png
[2]:
def milstein_euler_simulation(rho, theta, kappa, eta, T, paths, n_dt):
    dt = T/n_dt
    dim = n_dt
    sobol_engine =  torch.quasirandom.SobolEngine(2*dim)
    x = sobol_engine.draw(1)
    u = sobol_engine.draw(paths,dtype=torch.float64)

    states = 2
    dim = n_dt

    fm = torch.zeros(size=(states,states))

    fm[0][0] = 1.
    fm[0][1] = 0.
    fm[1][0] = 0.
    fm[1][1] = 1.
    fwd_cov = torch.zeros(size=(dim, states, states))

    for i in range(dim):
        fwd_cov[i] = fm

    multivariate_brownian = MultivariateBrownianBridge(fwd_cov)

    y = norminv(u)
    y = torch.reshape(y, shape=(dim,states,paths))

    dz = multivariate_brownian.path(y, True)

    X = torch.zeros([paths])
    V = torch.zeros([n_dt+1,paths])
    Vt = torch.zeros([paths])
    Vt[:] = theta
    V[0] = theta

    for i in range(n_dt):
        z2 = dz[i][1]
        z1 = dz[i][0]
        Vt = torch.maximum((Vt + kappa*theta*dt + eta*torch.sqrt(Vt*dt)*z2+0.25*eta*eta*dt*(z2*z2-1.))/(1+kappa*dt),torch.tensor(0.))
        X = X - V[i]*dt/2 + torch.sqrt(V[i]*dt)*(rho*z2 + torch.sqrt(1-rho*rho)*z1)
        V[i+1] = Vt
    return X
[3]:
def quadratic_exponential(rho, theta, kappa, eta, T, paths, n_dt):
    dt = T/n_dt
    dim = n_dt

    gamma1 = 0.5
    gamma2 = 0.5

    K0 = -(rho*kappa*theta)*dt/eta
    K1 = gamma1*dt*(-0.5+(kappa*rho/eta))-(rho/eta)
    K2 = gamma2*dt*(-0.5+(kappa*rho/eta))+(rho/eta)
    K3 = gamma1*dt*(1-rho**2)
    K4 = gamma2*dt*(1-rho**2)

    sobol_engine =  torch.quasirandom.SobolEngine(2*dim)
    x = sobol_engine.draw(1)
    u = sobol_engine.draw(paths,dtype=torch.float64)

    u = torch.transpose(u,0,1)
    # Random variable for the underlying
    u1 = u[:dim]
    # Random variables for the volatility
    u2 = u[dim:]

    states = 1
    dim = n_dt

    fm = torch.zeros(size=(states,states))

    fm[0][0] = 1.
    fwd_cov = torch.zeros(size=(dim, states, states))

    for i in range(dim):
        fwd_cov[i] = fm

    multivariate_brownian = MultivariateBrownianBridge(fwd_cov)

    y = norminv(u1)
    y = torch.reshape(y, shape=(dim,states,paths))

    dz = multivariate_brownian.path(y, True)

    X = torch.zeros([paths])
    V = torch.zeros([n_dt+1,paths])
    Vt = torch.zeros([paths])
    Vt[:] = theta
    V[0] = theta

    for i in range(n_dt):
        minusexpkappadt = torch.exp(-kappa*dt)
        m = theta+( Vt - theta) * minusexpkappadt
        s2 = ((Vt*eta**2) * minusexpkappadt/kappa)*(1-minusexpkappadt) + ( theta*eta**2)*((1-minusexpkappadt)**2)/(2*kappa)
        phi = s2/m**2
        #
        # Calculate the lower branch
        #
        psi_2 = 2/phi
        b2 = torch.maximum(psi_2-1+(torch.sqrt(psi_2))*(torch.sqrt(torch.maximum(psi_2-1, torch.tensor(0.)))),torch.tensor(0.))
        a = m/(1+b2)
        z2 = norminv(u2[i])
        Vnew_1 = a*(z2 + torch.sqrt(b2))**2
        #
        # Calulcate the upper branch
        #
        p = (phi-1)/(phi+1)
        beta = (1-p)/m
        Vnew_2 = torch.log((1-p)/(1-u2[i])) / beta
        Vnew_2 = torch.maximum(Vnew_2, torch.tensor(0.))
        phiC = 1.5
        #
        # Switch the branches
        #
        p_1 = maxsoft.soft_heavy_side_hyperbolic(phiC-phi, 0.00001)
        q_1 = 1. - p_1
        Vt = p_1 * Vnew_1 + q_1 * Vnew_2
        V[i+1] = Vt
        #
        # Update the underlying
        #
        #z1 = norminv(u1[i])
        z1 = dz[i][0]
        X = X + K0 + K1*V[i] + K2*Vt + (torch.sqrt(K3*V[i]+K4*Vt))*z1

    return X

[4]:
rho = torch.tensor(-0.27829, requires_grad=True)
theta = torch.tensor((0.3095)**2, requires_grad=True)
kappa = torch.tensor(1., requires_grad=True)
eta= torch.tensor(0.9288, requires_grad=True)

T = 1
paths =2**14-1
n_dt = 64
[5]:
X_milstein = milstein_euler_simulation(rho, theta, kappa, eta, T, paths, n_dt)
[6]:
X_qe = quadratic_exponential(rho, theta, kappa, eta, T, paths, n_dt)
[8]:
Sm = torch.exp(X_milstein)
Sq = torch.exp(X_qe)
Savg = torch.mean(Sm)
Pavg = torch.mean(torch.maximum(Sm-1.,torch.tensor(0.)))
print(Savg)
print(Pavg)
tensor(0.9971, grad_fn=<MeanBackward0>)
tensor(0.1043, grad_fn=<MeanBackward0>)
[11]:
nk = 21
k = np.linspace(0.7,1.3,nk)
ivm = torch.zeros(nk)
ivq = torch.zeros(nk)
ivh = torch.zeros(nk)

for it,ik in enumerate(k):
    pm = torch.mean(torch.maximum(Sm-ik,torch.tensor(0.))).detach().numpy()
    ivm[it] = blackanalytics.impliedvolatility(pm, 1., ik, T)
    pq = torch.mean(torch.maximum(Sq-ik,torch.tensor(0.))).detach().numpy()
    ivq[it] = blackanalytics.impliedvolatility(pq, 1., ik, T)
    ph = heston_option_price(ik, 1., theta.detach().numpy(), kappa.detach().numpy(), theta.detach().numpy(), eta.detach().numpy(), rho.detach().numpy(), T)
    ivh[it] = blackanalytics.impliedvolatility(ph, 1., ik, T)
/home/vscode/.local/lib/python3.11/site-packages/scipy/integrate/_quadpack_py.py:575: ComplexWarning: Casting complex values to real discards the imaginary part
  return _quadpack._qagse(func,a,b,args,full_output,epsabs,epsrel,limit)
[12]:
plt.plot(k,ivm)
plt.plot(k,ivq)
plt.plot(k,ivh)
[12]:
[<matplotlib.lines.Line2D at 0x7f7483548d50>]
../_images/examples_32QuadraticExponentialSimulation_12_1.png