Path: blob/master/src/FMNM/BS_pricer.py
1700 views
#!/usr/bin/env python31# -*- coding: utf-8 -*-2"""3Created on Thu Jun 13 10:18:39 201945@author: cantaro866"""78import numpy as np9import scipy as scp10from scipy.sparse.linalg import spsolve11from scipy import sparse12from scipy.sparse.linalg import splu13import matplotlib.pyplot as plt14from matplotlib import cm15from time import time16import scipy.stats as ss17from FMNM.Solvers import Thomas18from FMNM.cython.solvers import SOR19from FMNM.CF import cf_normal20from FMNM.probabilities import Q1, Q221from functools import partial22from FMNM.FFT import fft_Lewis, IV_from_Lewis232425class BS_pricer:26"""27Closed Formula.28Monte Carlo.29Finite-difference Black-Scholes PDE:30df/dt + r df/dx + 1/2 sigma^2 d^f/dx^2 -rf = 031"""3233def __init__(self, Option_info, Process_info):34"""35Option_info: of type Option_param. It contains (S0,K,T)36i.e. current price, strike, maturity in years37Process_info: of type Diffusion_process. It contains (r, mu, sig) i.e.38interest rate, drift coefficient, diffusion coefficient39"""40self.r = Process_info.r # interest rate41self.sig = Process_info.sig # diffusion coefficient42self.S0 = Option_info.S0 # current price43self.K = Option_info.K # strike44self.T = Option_info.T # maturity in years45self.exp_RV = Process_info.exp_RV # function to generate solution of GBM4647self.price = 048self.S_vec = None49self.price_vec = None50self.mesh = None51self.exercise = Option_info.exercise52self.payoff = Option_info.payoff5354def payoff_f(self, S):55if self.payoff == "call":56Payoff = np.maximum(S - self.K, 0)57elif self.payoff == "put":58Payoff = np.maximum(self.K - S, 0)59return Payoff6061@staticmethod62def BlackScholes(payoff="call", S0=100.0, K=100.0, T=1.0, r=0.1, sigma=0.2):63"""Black Scholes closed formula:64payoff: call or put.65S0: float. initial stock/index level.66K: float strike price.67T: float maturity (in year fractions).68r: float constant risk-free short rate.69sigma: volatility factor in diffusion term."""7071d1 = (np.log(S0 / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))72d2 = (np.log(S0 / K) + (r - sigma**2 / 2) * T) / (sigma * np.sqrt(T))7374if payoff == "call":75return S0 * ss.norm.cdf(d1) - K * np.exp(-r * T) * ss.norm.cdf(d2)76elif payoff == "put":77return K * np.exp(-r * T) * ss.norm.cdf(-d2) - S0 * ss.norm.cdf(-d1)78else:79raise ValueError("invalid type. Set 'call' or 'put'")8081@staticmethod82def vega(sigma, S0, K, T, r):83"""BS vega: derivative of the price with respect to the volatility"""84d1 = (np.log(S0 / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))85return S0 * np.sqrt(T) * ss.norm.pdf(d1)8687def closed_formula(self):88"""89Black Scholes closed formula:90"""91d1 = (np.log(self.S0 / self.K) + (self.r + self.sig**2 / 2) * self.T) / (self.sig * np.sqrt(self.T))92d2 = (np.log(self.S0 / self.K) + (self.r - self.sig**2 / 2) * self.T) / (self.sig * np.sqrt(self.T))9394if self.payoff == "call":95return self.S0 * ss.norm.cdf(d1) - self.K * np.exp(-self.r * self.T) * ss.norm.cdf(d2)96elif self.payoff == "put":97return self.K * np.exp(-self.r * self.T) * ss.norm.cdf(-d2) - self.S0 * ss.norm.cdf(-d1)98else:99raise ValueError("invalid type. Set 'call' or 'put'")100101def Fourier_inversion(self):102"""103Price obtained by inversion of the characteristic function104"""105k = np.log(self.K / self.S0)106cf_GBM = partial(107cf_normal,108mu=(self.r - 0.5 * self.sig**2) * self.T,109sig=self.sig * np.sqrt(self.T),110) # function binding111112if self.payoff == "call":113call = self.S0 * Q1(k, cf_GBM, np.inf) - self.K * np.exp(-self.r * self.T) * Q2(114k, cf_GBM, np.inf115) # pricing function116return call117elif self.payoff == "put":118put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_GBM, np.inf)) - self.S0 * (1191 - Q1(k, cf_GBM, np.inf)120) # pricing function121return put122else:123raise ValueError("invalid type. Set 'call' or 'put'")124125def FFT(self, K):126"""127FFT method. It returns a vector of prices.128K is an array of strikes129"""130K = np.array(K)131cf_GBM = partial(132cf_normal,133mu=(self.r - 0.5 * self.sig**2) * self.T,134sig=self.sig * np.sqrt(self.T),135) # function binding136if self.payoff == "call":137return fft_Lewis(K, self.S0, self.r, self.T, cf_GBM, interp="cubic")138elif self.payoff == "put": # put-call parity139return (140fft_Lewis(K, self.S0, self.r, self.T, cf_GBM, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T)141)142else:143raise ValueError("invalid type. Set 'call' or 'put'")144145def IV_Lewis(self):146"""Implied Volatility from the Lewis formula"""147148cf_GBM = partial(149cf_normal,150mu=(self.r - 0.5 * self.sig**2) * self.T,151sig=self.sig * np.sqrt(self.T),152) # function binding153if self.payoff == "call":154return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_GBM)155elif self.payoff == "put":156raise NotImplementedError157else:158raise ValueError("invalid type. Set 'call' or 'put'")159160def MC(self, N, Err=False, Time=False):161"""162BS Monte Carlo163Err = return Standard Error if True164Time = return execution time if True165"""166t_init = time()167168S_T = self.exp_RV(self.S0, self.T, N)169PayOff = self.payoff_f(S_T)170V = scp.mean(np.exp(-self.r * self.T) * PayOff, axis=0)171172if Err is True:173if Time is True:174elapsed = time() - t_init175return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed176else:177return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T))178else:179if Time is True:180elapsed = time() - t_init181return V, elapsed182else:183return V184185def PDE_price(self, steps, Time=False, solver="splu"):186"""187steps = tuple with number of space steps and time steps188payoff = "call" or "put"189exercise = "European" or "American"190Time = Boolean. Execution time.191Solver = spsolve or splu or Thomas or SOR192"""193t_init = time()194195Nspace = steps[0]196Ntime = steps[1]197198S_max = 6 * float(self.K)199S_min = float(self.K) / 6200x_max = np.log(S_max)201x_min = np.log(S_min)202x0 = np.log(self.S0) # current log-price203204x, dx = np.linspace(x_min, x_max, Nspace, retstep=True)205t, dt = np.linspace(0, self.T, Ntime, retstep=True)206207self.S_vec = np.exp(x) # vector of S208Payoff = self.payoff_f(self.S_vec)209210V = np.zeros((Nspace, Ntime))211if self.payoff == "call":212V[:, -1] = Payoff213V[-1, :] = np.exp(x_max) - self.K * np.exp(-self.r * t[::-1])214V[0, :] = 0215else:216V[:, -1] = Payoff217V[-1, :] = 0218V[0, :] = Payoff[0] * np.exp(-self.r * t[::-1]) # Instead of Payoff[0] I could use K219# For s to 0, the limiting value is e^(-rT)(K-s)220221sig2 = self.sig**2222dxx = dx**2223a = (dt / 2) * ((self.r - 0.5 * sig2) / dx - sig2 / dxx)224b = 1 + dt * (sig2 / dxx + self.r)225c = -(dt / 2) * ((self.r - 0.5 * sig2) / dx + sig2 / dxx)226227D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc()228229offset = np.zeros(Nspace - 2)230231if solver == "spsolve":232if self.exercise == "European":233for i in range(Ntime - 2, -1, -1):234offset[0] = a * V[0, i]235offset[-1] = c * V[-1, i]236V[1:-1, i] = spsolve(D, (V[1:-1, i + 1] - offset))237elif self.exercise == "American":238for i in range(Ntime - 2, -1, -1):239offset[0] = a * V[0, i]240offset[-1] = c * V[-1, i]241V[1:-1, i] = np.maximum(spsolve(D, (V[1:-1, i + 1] - offset)), Payoff[1:-1])242elif solver == "Thomas":243if self.exercise == "European":244for i in range(Ntime - 2, -1, -1):245offset[0] = a * V[0, i]246offset[-1] = c * V[-1, i]247V[1:-1, i] = Thomas(D, (V[1:-1, i + 1] - offset))248elif self.exercise == "American":249for i in range(Ntime - 2, -1, -1):250offset[0] = a * V[0, i]251offset[-1] = c * V[-1, i]252V[1:-1, i] = np.maximum(Thomas(D, (V[1:-1, i + 1] - offset)), Payoff[1:-1])253elif solver == "SOR":254if self.exercise == "European":255for i in range(Ntime - 2, -1, -1):256offset[0] = a * V[0, i]257offset[-1] = c * V[-1, i]258V[1:-1, i] = SOR(a, b, c, (V[1:-1, i + 1] - offset), w=1.68, eps=1e-10, N_max=600)259elif self.exercise == "American":260for i in range(Ntime - 2, -1, -1):261offset[0] = a * V[0, i]262offset[-1] = c * V[-1, i]263V[1:-1, i] = np.maximum(264SOR(265a,266b,267c,268(V[1:-1, i + 1] - offset),269w=1.68,270eps=1e-10,271N_max=600,272),273Payoff[1:-1],274)275elif solver == "splu":276DD = splu(D)277if self.exercise == "European":278for i in range(Ntime - 2, -1, -1):279offset[0] = a * V[0, i]280offset[-1] = c * V[-1, i]281V[1:-1, i] = DD.solve(V[1:-1, i + 1] - offset)282elif self.exercise == "American":283for i in range(Ntime - 2, -1, -1):284offset[0] = a * V[0, i]285offset[-1] = c * V[-1, i]286V[1:-1, i] = np.maximum(DD.solve(V[1:-1, i + 1] - offset), Payoff[1:-1])287else:288raise ValueError("Solver is splu, spsolve, SOR or Thomas")289290self.price = np.interp(x0, x, V[:, 0])291self.price_vec = V[:, 0]292self.mesh = V293294if Time is True:295elapsed = time() - t_init296return self.price, elapsed297else:298return self.price299300def plot(self, axis=None):301if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray:302self.PDE_price((7000, 5000))303# print("run the PDE_price method")304# return305306plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff")307plt.plot(self.S_vec, self.price_vec, color="red", label="BS curve")308if type(axis) == list:309plt.axis(axis)310plt.xlabel("S")311plt.ylabel("price")312plt.title(f"{self.exercise} - Black Scholes price")313plt.legend()314plt.show()315316def mesh_plt(self):317if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray:318self.PDE_price((7000, 5000))319320fig = plt.figure()321ax = fig.add_subplot(111, projection="3d")322323X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec)324ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean)325ax.set_title(f"{self.exercise} - BS price surface")326ax.set_xlabel("S")327ax.set_ylabel("t")328ax.set_zlabel("V")329ax.view_init(30, -100) # this function rotates the 3d plot330plt.show()331332def LSM(self, N=10000, paths=10000, order=2):333"""334Longstaff-Schwartz Method for pricing American options335336N = number of time steps337paths = number of generated paths338order = order of the polynomial for the regression339"""340341if self.payoff != "put":342raise ValueError("invalid type. Set 'call' or 'put'")343344dt = self.T / (N - 1) # time interval345df = np.exp(-self.r * dt) # discount factor per time time interval346347X0 = np.zeros((paths, 1))348increments = ss.norm.rvs(349loc=(self.r - self.sig**2 / 2) * dt,350scale=np.sqrt(dt) * self.sig,351size=(paths, N - 1),352)353X = np.concatenate((X0, increments), axis=1).cumsum(1)354S = self.S0 * np.exp(X)355356H = np.maximum(self.K - S, 0) # intrinsic values for put option357V = np.zeros_like(H) # value matrix358V[:, -1] = H[:, -1]359360# Valuation by LS Method361for t in range(N - 2, 0, -1):362good_paths = H[:, t] > 0363rg = np.polyfit(S[good_paths, t], V[good_paths, t + 1] * df, 2) # polynomial regression364C = np.polyval(rg, S[good_paths, t]) # evaluation of regression365366exercise = np.zeros(len(good_paths), dtype=bool)367exercise[good_paths] = H[good_paths, t] > C368369V[exercise, t] = H[exercise, t]370V[exercise, t + 1 :] = 0371discount_path = V[:, t] == 0372V[discount_path, t] = V[discount_path, t + 1] * df373374V0 = np.mean(V[:, 1]) * df #375return V0376377378