Path: blob/master/src/FMNM/Merton_pricer.py
1700 views
#!/usr/bin/env python31# -*- coding: utf-8 -*-2"""3Created on Sun Aug 11 09:47:49 201945@author: cantaro866"""78from scipy import sparse9from scipy.sparse.linalg import splu10from time import time11import numpy as np12import scipy as scp13import scipy.stats as ss14from scipy import signal15import matplotlib.pyplot as plt16from matplotlib import cm17from FMNM.BS_pricer import BS_pricer18from math import factorial19from FMNM.CF import cf_mert20from FMNM.probabilities import Q1, Q221from functools import partial22from FMNM.FFT import fft_Lewis, IV_from_Lewis232425class Merton_pricer:26"""27Closed Formula.28Monte Carlo.29Finite-difference PIDE: Explicit-implicit scheme30310 = dV/dt + (r -(1/2)sig^2 -m) dV/dx + (1/2)sig^2 d^V/dx^232+ \int[ V(x+y) nu(dy) ] -(r+lam)V33"""3435def __init__(self, Option_info, Process_info):36"""37Process_info: of type Merton_process. It contains (r, sig, lam, muJ, sigJ) i.e.38interest rate, diffusion coefficient, jump activity and jump distribution params3940Option_info: of type Option_param. It contains (S0,K,T) i.e. current price,41strike, maturity in years42"""43self.r = Process_info.r # interest rate44self.sig = Process_info.sig # diffusion coefficient45self.lam = Process_info.lam # jump activity46self.muJ = Process_info.muJ # jump mean47self.sigJ = Process_info.sigJ # jump std48self.exp_RV = Process_info.exp_RV # function to generate exponential Merton Random Variables4950self.S0 = Option_info.S0 # current price51self.K = Option_info.K # strike52self.T = Option_info.T # maturity in years5354self.price = 055self.S_vec = None56self.price_vec = None57self.mesh = None58self.exercise = Option_info.exercise59self.payoff = Option_info.payoff6061def payoff_f(self, S):62if self.payoff == "call":63Payoff = np.maximum(S - self.K, 0)64elif self.payoff == "put":65Payoff = np.maximum(self.K - S, 0)66return Payoff6768def closed_formula(self):69"""70Merton closed formula.71"""7273m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m74lam2 = self.lam * np.exp(self.muJ + (self.sigJ**2) / 2)7576tot = 077for i in range(18):78tot += (np.exp(-lam2 * self.T) * (lam2 * self.T) ** i / factorial(i)) * BS_pricer.BlackScholes(79self.payoff,80self.S0,81self.K,82self.T,83self.r - m + i * (self.muJ + 0.5 * self.sigJ**2) / self.T,84np.sqrt(self.sig**2 + (i * self.sigJ**2) / self.T),85)86return tot8788def Fourier_inversion(self):89"""90Price obtained by inversion of the characteristic function91"""92k = np.log(self.K / self.S0) # log moneyness93m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m94cf_Mert = partial(95cf_mert,96t=self.T,97mu=(self.r - 0.5 * self.sig**2 - m),98sig=self.sig,99lam=self.lam,100muJ=self.muJ,101sigJ=self.sigJ,102)103104if self.payoff == "call":105call = self.S0 * Q1(k, cf_Mert, np.inf) - self.K * np.exp(-self.r * self.T) * Q2(106k, cf_Mert, np.inf107) # pricing function108return call109elif self.payoff == "put":110put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_Mert, np.inf)) - self.S0 * (1111 - Q1(k, cf_Mert, np.inf)112) # pricing function113return put114else:115raise ValueError("invalid type. Set 'call' or 'put'")116117def FFT(self, K):118"""119FFT method. It returns a vector of prices.120K is an array of strikes121"""122K = np.array(K)123m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m124cf_Mert = partial(125cf_mert,126t=self.T,127mu=(self.r - 0.5 * self.sig**2 - m),128sig=self.sig,129lam=self.lam,130muJ=self.muJ,131sigJ=self.sigJ,132)133134if self.payoff == "call":135return fft_Lewis(K, self.S0, self.r, self.T, cf_Mert, interp="cubic")136elif self.payoff == "put": # put-call parity137return (138fft_Lewis(K, self.S0, self.r, self.T, cf_Mert, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T)139)140else:141raise ValueError("invalid type. Set 'call' or 'put'")142143def IV_Lewis(self):144"""Implied Volatility from the Lewis formula"""145146m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m147cf_Mert = partial(148cf_mert,149t=self.T,150mu=(self.r - 0.5 * self.sig**2 - m),151sig=self.sig,152lam=self.lam,153muJ=self.muJ,154sigJ=self.sigJ,155)156157if self.payoff == "call":158return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_Mert)159elif self.payoff == "put":160raise NotImplementedError161else:162raise ValueError("invalid type. Set 'call' or 'put'")163164def MC(self, N, Err=False, Time=False):165"""166Merton Monte Carlo167Err = return Standard Error if True168Time = return execution time if True169"""170t_init = time()171172S_T = self.exp_RV(self.S0, self.T, N)173V = scp.mean(np.exp(-self.r * self.T) * self.payoff_f(S_T), axis=0)174175if Err is True:176if Time is True:177elapsed = time() - t_init178return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed179else:180return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T))181else:182if Time is True:183elapsed = time() - t_init184return V, elapsed185else:186return V187188def PIDE_price(self, steps, Time=False):189"""190steps = tuple with number of space steps and time steps191payoff = "call" or "put"192exercise = "European" or "American"193Time = Boolean. Execution time.194"""195t_init = time()196197Nspace = steps[0]198Ntime = steps[1]199200S_max = 6 * float(self.K)201S_min = float(self.K) / 6202x_max = np.log(S_max)203x_min = np.log(S_min)204205dev_X = np.sqrt(self.lam * self.sigJ**2 + self.lam * self.muJ**2)206207dx = (x_max - x_min) / (Nspace - 1)208extraP = int(np.floor(5 * dev_X / dx)) # extra points beyond the B.C.209x = np.linspace(x_min - extraP * dx, x_max + extraP * dx, Nspace + 2 * extraP) # space discretization210t, dt = np.linspace(0, self.T, Ntime, retstep=True) # time discretization211212Payoff = self.payoff_f(np.exp(x))213offset = np.zeros(Nspace - 2)214V = np.zeros((Nspace + 2 * extraP, Ntime)) # grid initialization215216if self.payoff == "call":217V[:, -1] = Payoff # terminal conditions218V[-extraP - 1 :, :] = np.exp(x[-extraP - 1 :]).reshape(extraP + 1, 1) * np.ones(219(extraP + 1, Ntime)220) - self.K * np.exp(-self.r * t[::-1]) * np.ones(221(extraP + 1, Ntime)222) # boundary condition223V[: extraP + 1, :] = 0224else:225V[:, -1] = Payoff226V[-extraP - 1 :, :] = 0227V[: extraP + 1, :] = self.K * np.exp(-self.r * t[::-1]) * np.ones((extraP + 1, Ntime))228229cdf = ss.norm.cdf(230[np.linspace(-(extraP + 1 + 0.5) * dx, (extraP + 1 + 0.5) * dx, 2 * (extraP + 2))],231loc=self.muJ,232scale=self.sigJ,233)[0]234nu = self.lam * (cdf[1:] - cdf[:-1])235236lam_appr = sum(nu)237m_appr = np.array([np.exp(i * dx) - 1 for i in range(-(extraP + 1), extraP + 2)]) @ nu238239sig2 = self.sig**2240dxx = dx**2241a = (dt / 2) * ((self.r - m_appr - 0.5 * sig2) / dx - sig2 / dxx)242b = 1 + dt * (sig2 / dxx + self.r + lam_appr)243c = -(dt / 2) * ((self.r - m_appr - 0.5 * sig2) / dx + sig2 / dxx)244245D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc()246DD = splu(D)247if self.exercise == "European":248for i in range(Ntime - 2, -1, -1):249offset[0] = a * V[extraP, i]250offset[-1] = c * V[-1 - extraP, i]251V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(252V[:, i + 1], nu[::-1], mode="valid", method="fft"253)254V[extraP + 1 : -extraP - 1, i] = DD.solve(V_jump - offset)255elif self.exercise == "American":256for i in range(Ntime - 2, -1, -1):257offset[0] = a * V[extraP, i]258offset[-1] = c * V[-1 - extraP, i]259V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(260V[:, i + 1], nu[::-1], mode="valid", method="fft"261)262V[extraP + 1 : -extraP - 1, i] = np.maximum(DD.solve(V_jump - offset), Payoff[extraP + 1 : -extraP - 1])263264X0 = np.log(self.S0) # current log-price265self.S_vec = np.exp(x[extraP + 1 : -extraP - 1]) # vector of S266self.price = np.interp(X0, x, V[:, 0])267self.price_vec = V[extraP + 1 : -extraP - 1, 0]268self.mesh = V[extraP + 1 : -extraP - 1, :]269270if Time is True:271elapsed = time() - t_init272return self.price, elapsed273else:274return self.price275276def plot(self, axis=None):277if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray:278self.PIDE_price((5000, 4000))279280plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff")281plt.plot(self.S_vec, self.price_vec, color="red", label="Merton curve")282if type(axis) == list:283plt.axis(axis)284plt.xlabel("S")285plt.ylabel("price")286plt.title("Merton price")287plt.legend(loc="upper left")288plt.show()289290def mesh_plt(self):291if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray:292self.PDE_price((7000, 5000))293294fig = plt.figure()295ax = fig.add_subplot(111, projection="3d")296297X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec)298ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean)299ax.set_title("Merton price surface")300ax.set_xlabel("S")301ax.set_ylabel("t")302ax.set_zlabel("V")303ax.view_init(30, -100) # this function rotates the 3d plot304plt.show()305306307