Path: blob/master/src/FMNM/VG_pricer.py
1700 views
#!/usr/bin/env python31# -*- coding: utf-8 -*-2"""3Created on Mon Aug 12 18:47:05 201945@author: cantaro866"""78from scipy import sparse9from scipy.sparse.linalg import splu10from time import time11import numpy as np12import scipy as scp13from scipy import signal14from scipy.integrate import quad15import scipy.stats as ss16import scipy.special as scps1718import matplotlib.pyplot as plt19from matplotlib import cm20from FMNM.CF import cf_VG21from FMNM.probabilities import Q1, Q222from functools import partial23from FMNM.FFT import fft_Lewis, IV_from_Lewis242526class VG_pricer:27"""28Closed Formula.29Monte Carlo.30Finite-difference PIDE: Explicit-implicit scheme, with Brownian approximation31320 = dV/dt + (r -(1/2)sig^2 -w) dV/dx + (1/2)sig^2 d^V/dx^233+ \int[ V(x+y) nu(dy) ] -(r+lam)V34"""3536def __init__(self, Option_info, Process_info):37"""38Process_info: of type VG_process.39It contains the interest rate r and the VG parameters (sigma, theta, kappa)4041Option_info: of type Option_param.42It contains (S0,K,T) i.e. current price, strike, maturity in years43"""44self.r = Process_info.r # interest rate45self.sigma = Process_info.sigma # VG parameter46self.theta = Process_info.theta # VG parameter47self.kappa = Process_info.kappa # VG parameter48self.exp_RV = Process_info.exp_RV # function to generate exponential VG Random Variables49self.w = -np.log(1 - self.theta * self.kappa - self.kappa / 2 * self.sigma**2) / self.kappa # coefficient w5051self.S0 = Option_info.S0 # current price52self.K = Option_info.K # strike53self.T = Option_info.T # maturity in years5455self.price = 056self.S_vec = None57self.price_vec = None58self.mesh = None59self.exercise = Option_info.exercise60self.payoff = Option_info.payoff6162def payoff_f(self, S):63if self.payoff == "call":64Payoff = np.maximum(S - self.K, 0)65elif self.payoff == "put":66Payoff = np.maximum(self.K - S, 0)67return Payoff6869def closed_formula(self):70"""71VG closed formula. Put is obtained by put/call parity.72"""7374def Psy(a, b, g):75f = lambda u: ss.norm.cdf(a / np.sqrt(u) + b * np.sqrt(u)) * u ** (g - 1) * np.exp(-u) / scps.gamma(g)76result = quad(f, 0, np.inf)77return result[0]7879# Ugly parameters80xi = -self.theta / self.sigma**281s = self.sigma / np.sqrt(1 + ((self.theta / self.sigma) ** 2) * (self.kappa / 2))82alpha = xi * s8384c1 = self.kappa / 2 * (alpha + s) ** 285c2 = self.kappa / 2 * alpha**286d = 1 / s * (np.log(self.S0 / self.K) + self.r * self.T + self.T / self.kappa * np.log((1 - c1) / (1 - c2)))8788# Closed formula89call = self.S0 * Psy(90d * np.sqrt((1 - c1) / self.kappa),91(alpha + s) * np.sqrt(self.kappa / (1 - c1)),92self.T / self.kappa,93) - self.K * np.exp(-self.r * self.T) * Psy(94d * np.sqrt((1 - c2) / self.kappa),95(alpha) * np.sqrt(self.kappa / (1 - c2)),96self.T / self.kappa,97)9899if self.payoff == "call":100return call101elif self.payoff == "put":102return call - self.S0 + self.K * np.exp(-self.r * self.T)103else:104raise ValueError("invalid type. Set 'call' or 'put'")105106def Fourier_inversion(self):107"""108Price obtained by inversion of the characteristic function109"""110k = np.log(self.K / self.S0) # log moneyness111cf_VG_b = partial(112cf_VG,113t=self.T,114mu=(self.r - self.w),115theta=self.theta,116sigma=self.sigma,117kappa=self.kappa,118)119120right_lim = 5000 # using np.inf may create warnings121if self.payoff == "call":122call = self.S0 * Q1(k, cf_VG_b, right_lim) - self.K * np.exp(-self.r * self.T) * Q2(123k, cf_VG_b, right_lim124) # pricing function125return call126elif self.payoff == "put":127put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_VG_b, right_lim)) - self.S0 * (1281 - Q1(k, cf_VG_b, right_lim)129) # pricing function130return put131else:132raise ValueError("invalid type. Set 'call' or 'put'")133134def MC(self, N, Err=False, Time=False):135"""136Variance Gamma Monte Carlo137Err = return Standard Error if True138Time = return execution time if True139"""140t_init = time()141142S_T = self.exp_RV(self.S0, self.T, N)143V = scp.mean(np.exp(-self.r * self.T) * self.payoff_f(S_T), axis=0)144145if Err is True:146if Time is True:147elapsed = time() - t_init148return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed149else:150return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T))151else:152if Time is True:153elapsed = time() - t_init154return V, elapsed155else:156return V157158def FFT(self, K):159"""160FFT method. It returns a vector of prices.161K is an array of strikes162"""163K = np.array(K)164cf_VG_b = partial(165cf_VG,166t=self.T,167mu=(self.r - self.w),168theta=self.theta,169sigma=self.sigma,170kappa=self.kappa,171)172173if self.payoff == "call":174return fft_Lewis(K, self.S0, self.r, self.T, cf_VG_b, interp="cubic")175elif self.payoff == "put": # put-call parity176return (177fft_Lewis(K, self.S0, self.r, self.T, cf_VG_b, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T)178)179else:180raise ValueError("invalid type. Set 'call' or 'put'")181182def IV_Lewis(self):183"""Implied Volatility from the Lewis formula"""184185cf_VG_b = partial(186cf_VG,187t=self.T,188mu=(self.r - self.w),189theta=self.theta,190sigma=self.sigma,191kappa=self.kappa,192)193194if self.payoff == "call":195return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_VG_b)196elif self.payoff == "put":197raise NotImplementedError198else:199raise ValueError("invalid type. Set 'call' or 'put'")200201def PIDE_price(self, steps, Time=False):202"""203steps = tuple with number of space steps and time steps204payoff = "call" or "put"205exercise = "European" or "American"206Time = Boolean. Execution time.207"""208t_init = time()209210Nspace = steps[0]211Ntime = steps[1]212213S_max = 6 * float(self.K)214S_min = float(self.K) / 6215x_max = np.log(S_max)216x_min = np.log(S_min)217218dev_X = np.sqrt(self.sigma**2 + self.theta**2 * self.kappa) # std dev VG process219220dx = (x_max - x_min) / (Nspace - 1)221extraP = int(np.floor(5 * dev_X / dx)) # extra points beyond the B.C.222x = np.linspace(x_min - extraP * dx, x_max + extraP * dx, Nspace + 2 * extraP) # space discretization223t, dt = np.linspace(0, self.T, Ntime, retstep=True) # time discretization224225Payoff = self.payoff_f(np.exp(x))226offset = np.zeros(Nspace - 2)227V = np.zeros((Nspace + 2 * extraP, Ntime)) # grid initialization228229if self.payoff == "call":230V[:, -1] = Payoff # terminal conditions231V[-extraP - 1 :, :] = np.exp(x[-extraP - 1 :]).reshape(extraP + 1, 1) * np.ones(232(extraP + 1, Ntime)233) - self.K * np.exp(-self.r * t[::-1]) * np.ones(234(extraP + 1, Ntime)235) # boundary condition236V[: extraP + 1, :] = 0237else:238V[:, -1] = Payoff239V[-extraP - 1 :, :] = 0240V[: extraP + 1, :] = self.K * np.exp(-self.r * t[::-1]) * np.ones((extraP + 1, Ntime))241242A = self.theta / (self.sigma**2)243B = np.sqrt(self.theta**2 + 2 * self.sigma**2 / self.kappa) / self.sigma**2244245def levy_m(y):246"""Levy measure VG"""247return np.exp(A * y - B * np.abs(y)) / (self.kappa * np.abs(y))248249eps = 1.5 * dx # the cutoff near 0250lam = (251quad(levy_m, -(extraP + 1.5) * dx, -eps)[0] + quad(levy_m, eps, (extraP + 1.5) * dx)[0]252) # approximated intensity253254def int_w(y):255"""integrator"""256return (np.exp(y) - 1) * levy_m(y)257258int_s = lambda y: np.abs(y) * np.exp(A * y - B * np.abs(y)) / self.kappa # avoid division by zero259260w = (261quad(int_w, -(extraP + 1.5) * dx, -eps)[0] + quad(int_w, eps, (extraP + 1.5) * dx)[0]262) # is the approx of omega263264sig2 = quad(int_s, -eps, eps)[0] # the small jumps variance265266dxx = dx * dx267a = (dt / 2) * ((self.r - w - 0.5 * sig2) / dx - sig2 / dxx)268b = 1 + dt * (sig2 / dxx + self.r + lam)269c = -(dt / 2) * ((self.r - w - 0.5 * sig2) / dx + sig2 / dxx)270D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc()271DD = splu(D)272273nu = np.zeros(2 * extraP + 3) # Lévy measure vector274x_med = extraP + 1 # middle point in nu vector275x_nu = np.linspace(-(extraP + 1 + 0.5) * dx, (extraP + 1 + 0.5) * dx, 2 * (extraP + 2)) # integration domain276for i in range(len(nu)):277if (i == x_med) or (i == x_med - 1) or (i == x_med + 1):278continue279nu[i] = quad(levy_m, x_nu[i], x_nu[i + 1])[0]280281if self.exercise == "European":282# Backward iteration283for i in range(Ntime - 2, -1, -1):284offset[0] = a * V[extraP, i]285offset[-1] = c * V[-1 - extraP, i]286V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(287V[:, i + 1], nu[::-1], mode="valid", method="auto"288)289V[extraP + 1 : -extraP - 1, i] = DD.solve(V_jump - offset)290elif self.exercise == "American":291for i in range(Ntime - 2, -1, -1):292offset[0] = a * V[extraP, i]293offset[-1] = c * V[-1 - extraP, i]294V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(295V[:, i + 1], nu[::-1], mode="valid", method="auto"296)297V[extraP + 1 : -extraP - 1, i] = np.maximum(DD.solve(V_jump - offset), Payoff[extraP + 1 : -extraP - 1])298299X0 = np.log(self.S0) # current log-price300self.S_vec = np.exp(x[extraP + 1 : -extraP - 1]) # vector of S301self.price = np.interp(X0, x, V[:, 0])302self.price_vec = V[extraP + 1 : -extraP - 1, 0]303self.mesh = V[extraP + 1 : -extraP - 1, :]304305if Time is True:306elapsed = time() - t_init307return self.price, elapsed308else:309return self.price310311def plot(self, axis=None):312if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray:313self.PIDE_price((5000, 4000))314315plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff")316plt.plot(self.S_vec, self.price_vec, color="red", label="VG curve")317if type(axis) == list:318plt.axis(axis)319plt.xlabel("S")320plt.ylabel("price")321plt.title("VG price")322plt.legend(loc="upper left")323plt.show()324325def mesh_plt(self):326if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray:327self.PDE_price((7000, 5000))328329fig = plt.figure()330ax = fig.add_subplot(111, projection="3d")331332X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec)333ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean)334ax.set_title("VG price surface")335ax.set_xlabel("S")336ax.set_ylabel("t")337ax.set_zlabel("V")338ax.view_init(30, -100) # this function rotates the 3d plot339plt.show()340341def closed_formula_wrong(self):342"""343VG closed formula. This implementation seems correct, BUT IT DOES NOT WORK!!344Here I use the closed formula of Carr,Madan,Chang 1998.345With scps.kv, a modified Bessel function of second kind.346You can try to run it, but the output is slightly different from expected.347"""348349def Phi(alpha, beta, gamm, x, y):350f = lambda u: u ** (alpha - 1) * (1 - u) ** (gamm - alpha - 1) * (1 - u * x) ** (-beta) * np.exp(u * y)351result = quad(f, 0.00000001, 0.99999999)352return (scps.gamma(gamm) / (scps.gamma(alpha) * scps.gamma(gamm - alpha))) * result[0]353354def Psy(a, b, g):355c = np.abs(a) * np.sqrt(2 + b**2)356u = b / np.sqrt(2 + b**2)357358value = (359(c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** g)360/ (np.sqrt(2 * np.pi) * g * scps.gamma(g))361* scps.kv(g + 0.5, c)362* Phi(g, 1 - g, 1 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u))363- np.sign(a)364* (c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** (1 + g))365/ (np.sqrt(2 * np.pi) * (g + 1) * scps.gamma(g))366* scps.kv(g - 0.5, c)367* Phi(g + 1, 1 - g, 2 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u))368+ np.sign(a)369* (c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** (1 + g))370/ (np.sqrt(2 * np.pi) * (g + 1) * scps.gamma(g))371* scps.kv(g - 0.5, c)372* Phi(g, 1 - g, 1 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u))373)374return value375376# Ugly parameters377xi = -self.theta / self.sigma**2378s = self.sigma / np.sqrt(1 + ((self.theta / self.sigma) ** 2) * (self.kappa / 2))379alpha = xi * s380381c1 = self.kappa / 2 * (alpha + s) ** 2382c2 = self.kappa / 2 * alpha**2383d = 1 / s * (np.log(self.S0 / self.K) + self.r * self.T + self.T / self.kappa * np.log((1 - c1) / (1 - c2)))384385# Closed formula386call = self.S0 * Psy(387d * np.sqrt((1 - c1) / self.kappa),388(alpha + s) * np.sqrt(self.kappa / (1 - c1)),389self.T / self.kappa,390) - self.K * np.exp(-self.r * self.T) * Psy(391d * np.sqrt((1 - c2) / self.kappa),392(alpha) * np.sqrt(self.kappa / (1 - c2)),393self.T / self.kappa,394)395396return call397398399