CEV
rl.SwitchingCEVProcess(chain, S0, r, q, sigma, beta) is QuantLib’s CEV process with the volatility scale
sigma allowed to switch. The library prices it on a grid with FirstOrderFDEngine and SwitchingFDReferee.
The model
Reduction
The model is not affine and there is no exponential-affine solution. In the price variable the switched operator \(\tfrac12S^{2\beta}\partial_{SS}\) does not commute with the drift, but the commutator is a multiple of the operator itself,
and the forward removes it. With \(F_t = S_te^{(r - q)(T - t)}\),
The regime now only scales one fixed operator, so given the regime path \(F_T\) is the unit-scale CEV diffusion run for the total variance \(V\), and the price is a mixture:
with \(\chi^2(\cdot\,; d, \nu)\) the noncentral chi-square distribution function. This is exact for any chain. The law of \(V\) comes from a reduced system of the usual kind: in time to maturity, \(\mathbb{E}[e^{-zV}] = a_i(T)\) with \(g_i(\tau) = -z\,\sigma_i^2\,h(\tau)\).
Closed form
Expanding the transform with the common formula and replacing each power of \(z\) by a derivative of \(C\) in its variance argument gives, with \(\bar s, \tilde s = \tfrac12(\sigma_1^2 \pm \sigma_2^2)\),
evaluated at \(F_0 = S_0e^{(r - q)T}\) and \(\bar v = \bar s\,H_1\), where
The first-order terms read as on every page: half the variance of \(V\) times the convexity of the price in variance, and the shift of the mean of \(V\) towards the starting regime times the sensitivity to variance.
Python
from scipy.stats import ncx2
S0, r, q, beta, sigma, K, lam, T = 100.0, 0.02, 0.0, 0.6, [2.5, 1.2], 100.0, 10.0, 1.0
F = S0 * np.exp((r - q) * T)
def C(v):
scale = (1 - beta) ** 2 * v
x, y, d = K ** (2 * (1 - beta)) / scale, F ** (2 * (1 - beta)) / scale, 1 / (1 - beta)
return F * (1 - ncx2.cdf(x, d + 2, y)) - K * ncx2.cdf(y, d, x)
kh = 2 * (1 - beta) * (r - q)
H1, H2, hT = (np.exp(kh * T) - 1) / kh, (np.exp(2 * kh * T) - 1) / (2 * kh), np.exp(kh * T)
sbar, st = half([s * s for s in sigma])
vbar, dv = sbar * H1, 1e-3 * sbar * H1
Cv = (C(vbar + dv) - C(vbar - dv)) / (2 * dv)
Cvv = (C(vbar + dv) - 2 * C(vbar) + C(vbar - dv)) / dv ** 2
eps, disc = 1 / lam, np.exp(-r * T)
averaged = disc * C(vbar)
green_kubo = disc * eps / 2 * st ** 2 * H2 * Cvv
memory = disc * eps / 2 * st * hT * Cv
model = rl.SwitchingCEVProcess(rl.RegimeChain.twoState(lam, lam), S0, r, q, sigma, beta)
option = rl.VanillaOption(("call", K), maturity=T)
engine = rl.FirstOrderFDEngine(model, regime=0, n=801)
option.setPricingEngine(engine); first = option.NPV()
option.setPricingEngine(rl.SwitchingFDReferee(model, regime=0, n=801)); referee = option.NPV()
print(f" closed form library grid")
print(f"averaged {averaged:11.6f} {engine.averaged:11.6f}")
print(f"Green-Kubo term {green_kubo:11.6f} {engine.correction:11.6f}")
print(f"memory term {memory:11.6f} {engine.memory:11.6f}")
print(f"first order {averaged + green_kubo + memory:11.6f} {first:11.6f}")
print(f"switching price {referee:11.6f} (coupled equations on the grid)")
closed form library grid
averaged 13.249063 13.248985
Green-Kubo term -0.060274 -0.060275
memory term 0.191121 0.191122
first order 13.379911 13.379832
switching price 13.383492 (coupled equations on the grid)
The grid engine applies the first-order rule in the price variable without the change of variable, and returns the same two terms.