Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
cantaro86
GitHub Repository: cantaro86/Financial-Models-Numerical-Methods
Path: blob/master/src/FMNM/VG_pricer.py
1700 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Mon Aug 12 18:47:05 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
from scipy import signal
15
from scipy.integrate import quad
16
import scipy.stats as ss
17
import scipy.special as scps
18
19
import matplotlib.pyplot as plt
20
from matplotlib import cm
21
from FMNM.CF import cf_VG
22
from FMNM.probabilities import Q1, Q2
23
from functools import partial
24
from FMNM.FFT import fft_Lewis, IV_from_Lewis
25
26
27
class VG_pricer:
28
"""
29
Closed Formula.
30
Monte Carlo.
31
Finite-difference PIDE: Explicit-implicit scheme, with Brownian approximation
32
33
0 = dV/dt + (r -(1/2)sig^2 -w) dV/dx + (1/2)sig^2 d^V/dx^2
34
+ \int[ V(x+y) nu(dy) ] -(r+lam)V
35
"""
36
37
def __init__(self, Option_info, Process_info):
38
"""
39
Process_info: of type VG_process.
40
It contains the interest rate r and the VG parameters (sigma, theta, kappa)
41
42
Option_info: of type Option_param.
43
It contains (S0,K,T) i.e. current price, strike, maturity in years
44
"""
45
self.r = Process_info.r # interest rate
46
self.sigma = Process_info.sigma # VG parameter
47
self.theta = Process_info.theta # VG parameter
48
self.kappa = Process_info.kappa # VG parameter
49
self.exp_RV = Process_info.exp_RV # function to generate exponential VG Random Variables
50
self.w = -np.log(1 - self.theta * self.kappa - self.kappa / 2 * self.sigma**2) / self.kappa # coefficient w
51
52
self.S0 = Option_info.S0 # current price
53
self.K = Option_info.K # strike
54
self.T = Option_info.T # maturity in years
55
56
self.price = 0
57
self.S_vec = None
58
self.price_vec = None
59
self.mesh = None
60
self.exercise = Option_info.exercise
61
self.payoff = Option_info.payoff
62
63
def payoff_f(self, S):
64
if self.payoff == "call":
65
Payoff = np.maximum(S - self.K, 0)
66
elif self.payoff == "put":
67
Payoff = np.maximum(self.K - S, 0)
68
return Payoff
69
70
def closed_formula(self):
71
"""
72
VG closed formula. Put is obtained by put/call parity.
73
"""
74
75
def Psy(a, b, g):
76
f = lambda u: ss.norm.cdf(a / np.sqrt(u) + b * np.sqrt(u)) * u ** (g - 1) * np.exp(-u) / scps.gamma(g)
77
result = quad(f, 0, np.inf)
78
return result[0]
79
80
# Ugly parameters
81
xi = -self.theta / self.sigma**2
82
s = self.sigma / np.sqrt(1 + ((self.theta / self.sigma) ** 2) * (self.kappa / 2))
83
alpha = xi * s
84
85
c1 = self.kappa / 2 * (alpha + s) ** 2
86
c2 = self.kappa / 2 * alpha**2
87
d = 1 / s * (np.log(self.S0 / self.K) + self.r * self.T + self.T / self.kappa * np.log((1 - c1) / (1 - c2)))
88
89
# Closed formula
90
call = self.S0 * Psy(
91
d * np.sqrt((1 - c1) / self.kappa),
92
(alpha + s) * np.sqrt(self.kappa / (1 - c1)),
93
self.T / self.kappa,
94
) - self.K * np.exp(-self.r * self.T) * Psy(
95
d * np.sqrt((1 - c2) / self.kappa),
96
(alpha) * np.sqrt(self.kappa / (1 - c2)),
97
self.T / self.kappa,
98
)
99
100
if self.payoff == "call":
101
return call
102
elif self.payoff == "put":
103
return call - self.S0 + self.K * np.exp(-self.r * self.T)
104
else:
105
raise ValueError("invalid type. Set 'call' or 'put'")
106
107
def Fourier_inversion(self):
108
"""
109
Price obtained by inversion of the characteristic function
110
"""
111
k = np.log(self.K / self.S0) # log moneyness
112
cf_VG_b = partial(
113
cf_VG,
114
t=self.T,
115
mu=(self.r - self.w),
116
theta=self.theta,
117
sigma=self.sigma,
118
kappa=self.kappa,
119
)
120
121
right_lim = 5000 # using np.inf may create warnings
122
if self.payoff == "call":
123
call = self.S0 * Q1(k, cf_VG_b, right_lim) - self.K * np.exp(-self.r * self.T) * Q2(
124
k, cf_VG_b, right_lim
125
) # pricing function
126
return call
127
elif self.payoff == "put":
128
put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_VG_b, right_lim)) - self.S0 * (
129
1 - Q1(k, cf_VG_b, right_lim)
130
) # pricing function
131
return put
132
else:
133
raise ValueError("invalid type. Set 'call' or 'put'")
134
135
def MC(self, N, Err=False, Time=False):
136
"""
137
Variance Gamma Monte Carlo
138
Err = return Standard Error if True
139
Time = return execution time if True
140
"""
141
t_init = time()
142
143
S_T = self.exp_RV(self.S0, self.T, N)
144
V = scp.mean(np.exp(-self.r * self.T) * self.payoff_f(S_T), axis=0)
145
146
if Err is True:
147
if Time is True:
148
elapsed = time() - t_init
149
return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed
150
else:
151
return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T))
152
else:
153
if Time is True:
154
elapsed = time() - t_init
155
return V, elapsed
156
else:
157
return V
158
159
def FFT(self, K):
160
"""
161
FFT method. It returns a vector of prices.
162
K is an array of strikes
163
"""
164
K = np.array(K)
165
cf_VG_b = partial(
166
cf_VG,
167
t=self.T,
168
mu=(self.r - self.w),
169
theta=self.theta,
170
sigma=self.sigma,
171
kappa=self.kappa,
172
)
173
174
if self.payoff == "call":
175
return fft_Lewis(K, self.S0, self.r, self.T, cf_VG_b, interp="cubic")
176
elif self.payoff == "put": # put-call parity
177
return (
178
fft_Lewis(K, self.S0, self.r, self.T, cf_VG_b, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T)
179
)
180
else:
181
raise ValueError("invalid type. Set 'call' or 'put'")
182
183
def IV_Lewis(self):
184
"""Implied Volatility from the Lewis formula"""
185
186
cf_VG_b = partial(
187
cf_VG,
188
t=self.T,
189
mu=(self.r - self.w),
190
theta=self.theta,
191
sigma=self.sigma,
192
kappa=self.kappa,
193
)
194
195
if self.payoff == "call":
196
return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_VG_b)
197
elif self.payoff == "put":
198
raise NotImplementedError
199
else:
200
raise ValueError("invalid type. Set 'call' or 'put'")
201
202
def PIDE_price(self, steps, Time=False):
203
"""
204
steps = tuple with number of space steps and time steps
205
payoff = "call" or "put"
206
exercise = "European" or "American"
207
Time = Boolean. Execution time.
208
"""
209
t_init = time()
210
211
Nspace = steps[0]
212
Ntime = steps[1]
213
214
S_max = 6 * float(self.K)
215
S_min = float(self.K) / 6
216
x_max = np.log(S_max)
217
x_min = np.log(S_min)
218
219
dev_X = np.sqrt(self.sigma**2 + self.theta**2 * self.kappa) # std dev VG process
220
221
dx = (x_max - x_min) / (Nspace - 1)
222
extraP = int(np.floor(5 * dev_X / dx)) # extra points beyond the B.C.
223
x = np.linspace(x_min - extraP * dx, x_max + extraP * dx, Nspace + 2 * extraP) # space discretization
224
t, dt = np.linspace(0, self.T, Ntime, retstep=True) # time discretization
225
226
Payoff = self.payoff_f(np.exp(x))
227
offset = np.zeros(Nspace - 2)
228
V = np.zeros((Nspace + 2 * extraP, Ntime)) # grid initialization
229
230
if self.payoff == "call":
231
V[:, -1] = Payoff # terminal conditions
232
V[-extraP - 1 :, :] = np.exp(x[-extraP - 1 :]).reshape(extraP + 1, 1) * np.ones(
233
(extraP + 1, Ntime)
234
) - self.K * np.exp(-self.r * t[::-1]) * np.ones(
235
(extraP + 1, Ntime)
236
) # boundary condition
237
V[: extraP + 1, :] = 0
238
else:
239
V[:, -1] = Payoff
240
V[-extraP - 1 :, :] = 0
241
V[: extraP + 1, :] = self.K * np.exp(-self.r * t[::-1]) * np.ones((extraP + 1, Ntime))
242
243
A = self.theta / (self.sigma**2)
244
B = np.sqrt(self.theta**2 + 2 * self.sigma**2 / self.kappa) / self.sigma**2
245
246
def levy_m(y):
247
"""Levy measure VG"""
248
return np.exp(A * y - B * np.abs(y)) / (self.kappa * np.abs(y))
249
250
eps = 1.5 * dx # the cutoff near 0
251
lam = (
252
quad(levy_m, -(extraP + 1.5) * dx, -eps)[0] + quad(levy_m, eps, (extraP + 1.5) * dx)[0]
253
) # approximated intensity
254
255
def int_w(y):
256
"""integrator"""
257
return (np.exp(y) - 1) * levy_m(y)
258
259
int_s = lambda y: np.abs(y) * np.exp(A * y - B * np.abs(y)) / self.kappa # avoid division by zero
260
261
w = (
262
quad(int_w, -(extraP + 1.5) * dx, -eps)[0] + quad(int_w, eps, (extraP + 1.5) * dx)[0]
263
) # is the approx of omega
264
265
sig2 = quad(int_s, -eps, eps)[0] # the small jumps variance
266
267
dxx = dx * dx
268
a = (dt / 2) * ((self.r - w - 0.5 * sig2) / dx - sig2 / dxx)
269
b = 1 + dt * (sig2 / dxx + self.r + lam)
270
c = -(dt / 2) * ((self.r - w - 0.5 * sig2) / dx + sig2 / dxx)
271
D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc()
272
DD = splu(D)
273
274
nu = np.zeros(2 * extraP + 3) # Lévy measure vector
275
x_med = extraP + 1 # middle point in nu vector
276
x_nu = np.linspace(-(extraP + 1 + 0.5) * dx, (extraP + 1 + 0.5) * dx, 2 * (extraP + 2)) # integration domain
277
for i in range(len(nu)):
278
if (i == x_med) or (i == x_med - 1) or (i == x_med + 1):
279
continue
280
nu[i] = quad(levy_m, x_nu[i], x_nu[i + 1])[0]
281
282
if self.exercise == "European":
283
# Backward iteration
284
for i in range(Ntime - 2, -1, -1):
285
offset[0] = a * V[extraP, i]
286
offset[-1] = c * V[-1 - extraP, i]
287
V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(
288
V[:, i + 1], nu[::-1], mode="valid", method="auto"
289
)
290
V[extraP + 1 : -extraP - 1, i] = DD.solve(V_jump - offset)
291
elif self.exercise == "American":
292
for i in range(Ntime - 2, -1, -1):
293
offset[0] = a * V[extraP, i]
294
offset[-1] = c * V[-1 - extraP, i]
295
V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve(
296
V[:, i + 1], nu[::-1], mode="valid", method="auto"
297
)
298
V[extraP + 1 : -extraP - 1, i] = np.maximum(DD.solve(V_jump - offset), Payoff[extraP + 1 : -extraP - 1])
299
300
X0 = np.log(self.S0) # current log-price
301
self.S_vec = np.exp(x[extraP + 1 : -extraP - 1]) # vector of S
302
self.price = np.interp(X0, x, V[:, 0])
303
self.price_vec = V[extraP + 1 : -extraP - 1, 0]
304
self.mesh = V[extraP + 1 : -extraP - 1, :]
305
306
if Time is True:
307
elapsed = time() - t_init
308
return self.price, elapsed
309
else:
310
return self.price
311
312
def plot(self, axis=None):
313
if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray:
314
self.PIDE_price((5000, 4000))
315
316
plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff")
317
plt.plot(self.S_vec, self.price_vec, color="red", label="VG curve")
318
if type(axis) == list:
319
plt.axis(axis)
320
plt.xlabel("S")
321
plt.ylabel("price")
322
plt.title("VG price")
323
plt.legend(loc="upper left")
324
plt.show()
325
326
def mesh_plt(self):
327
if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray:
328
self.PDE_price((7000, 5000))
329
330
fig = plt.figure()
331
ax = fig.add_subplot(111, projection="3d")
332
333
X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec)
334
ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean)
335
ax.set_title("VG price surface")
336
ax.set_xlabel("S")
337
ax.set_ylabel("t")
338
ax.set_zlabel("V")
339
ax.view_init(30, -100) # this function rotates the 3d plot
340
plt.show()
341
342
def closed_formula_wrong(self):
343
"""
344
VG closed formula. This implementation seems correct, BUT IT DOES NOT WORK!!
345
Here I use the closed formula of Carr,Madan,Chang 1998.
346
With scps.kv, a modified Bessel function of second kind.
347
You can try to run it, but the output is slightly different from expected.
348
"""
349
350
def Phi(alpha, beta, gamm, x, y):
351
f = lambda u: u ** (alpha - 1) * (1 - u) ** (gamm - alpha - 1) * (1 - u * x) ** (-beta) * np.exp(u * y)
352
result = quad(f, 0.00000001, 0.99999999)
353
return (scps.gamma(gamm) / (scps.gamma(alpha) * scps.gamma(gamm - alpha))) * result[0]
354
355
def Psy(a, b, g):
356
c = np.abs(a) * np.sqrt(2 + b**2)
357
u = b / np.sqrt(2 + b**2)
358
359
value = (
360
(c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** g)
361
/ (np.sqrt(2 * np.pi) * g * scps.gamma(g))
362
* scps.kv(g + 0.5, c)
363
* Phi(g, 1 - g, 1 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u))
364
- np.sign(a)
365
* (c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** (1 + g))
366
/ (np.sqrt(2 * np.pi) * (g + 1) * scps.gamma(g))
367
* scps.kv(g - 0.5, c)
368
* Phi(g + 1, 1 - g, 2 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u))
369
+ np.sign(a)
370
* (c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** (1 + g))
371
/ (np.sqrt(2 * np.pi) * (g + 1) * scps.gamma(g))
372
* scps.kv(g - 0.5, c)
373
* Phi(g, 1 - g, 1 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u))
374
)
375
return value
376
377
# Ugly parameters
378
xi = -self.theta / self.sigma**2
379
s = self.sigma / np.sqrt(1 + ((self.theta / self.sigma) ** 2) * (self.kappa / 2))
380
alpha = xi * s
381
382
c1 = self.kappa / 2 * (alpha + s) ** 2
383
c2 = self.kappa / 2 * alpha**2
384
d = 1 / s * (np.log(self.S0 / self.K) + self.r * self.T + self.T / self.kappa * np.log((1 - c1) / (1 - c2)))
385
386
# Closed formula
387
call = self.S0 * Psy(
388
d * np.sqrt((1 - c1) / self.kappa),
389
(alpha + s) * np.sqrt(self.kappa / (1 - c1)),
390
self.T / self.kappa,
391
) - self.K * np.exp(-self.r * self.T) * Psy(
392
d * np.sqrt((1 - c2) / self.kappa),
393
(alpha) * np.sqrt(self.kappa / (1 - c2)),
394
self.T / self.kappa,
395
)
396
397
return call
398
399