Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
cantaro86
GitHub Repository: cantaro86/Financial-Models-Numerical-Methods
Path: blob/master/src/FMNM/Merton_pricer.py
1700 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Sun Aug 11 09:47:49 2019
5
6
@author: cantaro86
7
"""
8
9
from scipy import sparse
10
from scipy.sparse.linalg import splu
11
from time import time
12
import numpy as np
13
import scipy as scp
14
import scipy.stats as ss
15
from scipy import signal
16
import matplotlib.pyplot as plt
17
from matplotlib import cm
18
from FMNM.BS_pricer import BS_pricer
19
from math import factorial
20
from FMNM.CF import cf_mert
21
from FMNM.probabilities import Q1, Q2
22
from functools import partial
23
from FMNM.FFT import fft_Lewis, IV_from_Lewis
24
25
26
class Merton_pricer:
27
"""
28
Closed Formula.
29
Monte Carlo.
30
Finite-difference PIDE: Explicit-implicit scheme
31
32
0 = dV/dt + (r -(1/2)sig^2 -m) dV/dx + (1/2)sig^2 d^V/dx^2
33
+ \int[ V(x+y) nu(dy) ] -(r+lam)V
34
"""
35
36
def __init__(self, Option_info, Process_info):
37
"""
38
Process_info: of type Merton_process. It contains (r, sig, lam, muJ, sigJ) i.e.
39
interest rate, diffusion coefficient, jump activity and jump distribution params
40
41
Option_info: of type Option_param. It contains (S0,K,T) i.e. current price,
42
strike, maturity in years
43
"""
44
self.r = Process_info.r # interest rate
45
self.sig = Process_info.sig # diffusion coefficient
46
self.lam = Process_info.lam # jump activity
47
self.muJ = Process_info.muJ # jump mean
48
self.sigJ = Process_info.sigJ # jump std
49
self.exp_RV = Process_info.exp_RV # function to generate exponential Merton Random Variables
50
51
self.S0 = Option_info.S0 # current price
52
self.K = Option_info.K # strike
53
self.T = Option_info.T # maturity in years
54
55
self.price = 0
56
self.S_vec = None
57
self.price_vec = None
58
self.mesh = None
59
self.exercise = Option_info.exercise
60
self.payoff = Option_info.payoff
61
62
def payoff_f(self, S):
63
if self.payoff == "call":
64
Payoff = np.maximum(S - self.K, 0)
65
elif self.payoff == "put":
66
Payoff = np.maximum(self.K - S, 0)
67
return Payoff
68
69
def closed_formula(self):
70
"""
71
Merton closed formula.
72
"""
73
74
m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m
75
lam2 = self.lam * np.exp(self.muJ + (self.sigJ**2) / 2)
76
77
tot = 0
78
for i in range(18):
79
tot += (np.exp(-lam2 * self.T) * (lam2 * self.T) ** i / factorial(i)) * BS_pricer.BlackScholes(
80
self.payoff,
81
self.S0,
82
self.K,
83
self.T,
84
self.r - m + i * (self.muJ + 0.5 * self.sigJ**2) / self.T,
85
np.sqrt(self.sig**2 + (i * self.sigJ**2) / self.T),
86
)
87
return tot
88
89
def Fourier_inversion(self):
90
"""
91
Price obtained by inversion of the characteristic function
92
"""
93
k = np.log(self.K / self.S0) # log moneyness
94
m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m
95
cf_Mert = partial(
96
cf_mert,
97
t=self.T,
98
mu=(self.r - 0.5 * self.sig**2 - m),
99
sig=self.sig,
100
lam=self.lam,
101
muJ=self.muJ,
102
sigJ=self.sigJ,
103
)
104
105
if self.payoff == "call":
106
call = self.S0 * Q1(k, cf_Mert, np.inf) - self.K * np.exp(-self.r * self.T) * Q2(
107
k, cf_Mert, np.inf
108
) # pricing function
109
return call
110
elif self.payoff == "put":
111
put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_Mert, np.inf)) - self.S0 * (
112
1 - Q1(k, cf_Mert, np.inf)
113
) # pricing function
114
return put
115
else:
116
raise ValueError("invalid type. Set 'call' or 'put'")
117
118
def FFT(self, K):
119
"""
120
FFT method. It returns a vector of prices.
121
K is an array of strikes
122
"""
123
K = np.array(K)
124
m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m
125
cf_Mert = partial(
126
cf_mert,
127
t=self.T,
128
mu=(self.r - 0.5 * self.sig**2 - m),
129
sig=self.sig,
130
lam=self.lam,
131
muJ=self.muJ,
132
sigJ=self.sigJ,
133
)
134
135
if self.payoff == "call":
136
return fft_Lewis(K, self.S0, self.r, self.T, cf_Mert, interp="cubic")
137
elif self.payoff == "put": # put-call parity
138
return (
139
fft_Lewis(K, self.S0, self.r, self.T, cf_Mert, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T)
140
)
141
else:
142
raise ValueError("invalid type. Set 'call' or 'put'")
143
144
def IV_Lewis(self):
145
"""Implied Volatility from the Lewis formula"""
146
147
m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m
148
cf_Mert = partial(
149
cf_mert,
150
t=self.T,
151
mu=(self.r - 0.5 * self.sig**2 - m),
152
sig=self.sig,
153
lam=self.lam,
154
muJ=self.muJ,
155
sigJ=self.sigJ,
156
)
157
158
if self.payoff == "call":
159
return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_Mert)
160
elif self.payoff == "put":
161
raise NotImplementedError
162
else:
163
raise ValueError("invalid type. Set 'call' or 'put'")
164
165
def MC(self, N, Err=False, Time=False):
166
"""
167
Merton Monte Carlo
168
Err = return Standard Error if True
169
Time = return execution time if True
170
"""
171
t_init = time()
172
173
S_T = self.exp_RV(self.S0, self.T, N)
174
V = scp.mean(np.exp(-self.r * self.T) * self.payoff_f(S_T), axis=0)
175
176
if Err is True:
177
if Time is True:
178
elapsed = time() - t_init
179
return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed
180
else:
181
return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T))
182
else:
183
if Time is True:
184
elapsed = time() - t_init
185
return V, elapsed
186
else:
187
return V
188
189
def PIDE_price(self, steps, Time=False):
190
"""
191
steps = tuple with number of space steps and time steps
192
payoff = "call" or "put"
193
exercise = "European" or "American"
194
Time = Boolean. Execution time.
195
"""
196
t_init = time()
197
198
Nspace = steps[0]
199
Ntime = steps[1]
200
201
S_max = 6 * float(self.K)
202
S_min = float(self.K) / 6
203
x_max = np.log(S_max)
204
x_min = np.log(S_min)
205
206
dev_X = np.sqrt(self.lam * self.sigJ**2 + self.lam * self.muJ**2)
207
208
dx = (x_max - x_min) / (Nspace - 1)
209
extraP = int(np.floor(5 * dev_X / dx)) # extra points beyond the B.C.
210
x = np.linspace(x_min - extraP * dx, x_max + extraP * dx, Nspace + 2 * extraP) # space discretization
211
t, dt = np.linspace(0, self.T, Ntime, retstep=True) # time discretization
212
213
Payoff = self.payoff_f(np.exp(x))
214
offset = np.zeros(Nspace - 2)
215
V = np.zeros((Nspace + 2 * extraP, Ntime)) # grid initialization
216
217
if self.payoff == "call":
218
V[:, -1] = Payoff # terminal conditions
219
V[-extraP - 1 :, :] = np.exp(x[-extraP - 1 :]).reshape(extraP + 1, 1) * np.ones(
220
(extraP + 1, Ntime)
221
) - self.K * np.exp(-self.r * t[::-1]) * np.ones(
222
(extraP + 1, Ntime)
223
) # boundary condition
224
V[: extraP + 1, :] = 0
225
else:
226
V[:, -1] = Payoff
227
V[-extraP - 1 :, :] = 0
228
V[: extraP + 1, :] = self.K * np.exp(-self.r * t[::-1]) * np.ones((extraP + 1, Ntime))
229
230
cdf = ss.norm.cdf(
231
[np.linspace(-(extraP + 1 + 0.5) * dx, (extraP + 1 + 0.5) * dx, 2 * (extraP + 2))],
232
loc=self.muJ,
233
scale=self.sigJ,
234
)[0]
235
nu = self.lam * (cdf[1:] - cdf[:-1])
236
237
lam_appr = sum(nu)
238
m_appr = np.array([np.exp(i * dx) - 1 for i in range(-(extraP + 1), extraP + 2)]) @ nu
239
240
sig2 = self.sig**2
241
dxx = dx**2
242
a = (dt / 2) * ((self.r - m_appr - 0.5 * sig2) / dx - sig2 / dxx)
243
b = 1 + dt * (sig2 / dxx + self.r + lam_appr)
244
c = -(dt / 2) * ((self.r - m_appr - 0.5 * sig2) / dx + sig2 / dxx)
245
246
D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc()
247
DD = splu(D)
248
if self.exercise == "European":
249
for i in range(Ntime - 2, -1, -1):
250
offset[0] = a * V[extraP, i]
251
offset[-1] = c * V[-1 - extraP, i]
252
V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(
253
V[:, i + 1], nu[::-1], mode="valid", method="fft"
254
)
255
V[extraP + 1 : -extraP - 1, i] = DD.solve(V_jump - offset)
256
elif self.exercise == "American":
257
for i in range(Ntime - 2, -1, -1):
258
offset[0] = a * V[extraP, i]
259
offset[-1] = c * V[-1 - extraP, i]
260
V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(
261
V[:, i + 1], nu[::-1], mode="valid", method="fft"
262
)
263
V[extraP + 1 : -extraP - 1, i] = np.maximum(DD.solve(V_jump - offset), Payoff[extraP + 1 : -extraP - 1])
264
265
X0 = np.log(self.S0) # current log-price
266
self.S_vec = np.exp(x[extraP + 1 : -extraP - 1]) # vector of S
267
self.price = np.interp(X0, x, V[:, 0])
268
self.price_vec = V[extraP + 1 : -extraP - 1, 0]
269
self.mesh = V[extraP + 1 : -extraP - 1, :]
270
271
if Time is True:
272
elapsed = time() - t_init
273
return self.price, elapsed
274
else:
275
return self.price
276
277
def plot(self, axis=None):
278
if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray:
279
self.PIDE_price((5000, 4000))
280
281
plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff")
282
plt.plot(self.S_vec, self.price_vec, color="red", label="Merton curve")
283
if type(axis) == list:
284
plt.axis(axis)
285
plt.xlabel("S")
286
plt.ylabel("price")
287
plt.title("Merton price")
288
plt.legend(loc="upper left")
289
plt.show()
290
291
def mesh_plt(self):
292
if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray:
293
self.PDE_price((7000, 5000))
294
295
fig = plt.figure()
296
ax = fig.add_subplot(111, projection="3d")
297
298
X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec)
299
ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean)
300
ax.set_title("Merton price surface")
301
ax.set_xlabel("S")
302
ax.set_ylabel("t")
303
ax.set_zlabel("V")
304
ax.view_init(30, -100) # this function rotates the 3d plot
305
plt.show()
306
307