Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
cantaro86
GitHub Repository: cantaro86/Financial-Models-Numerical-Methods
Path: blob/master/src/FMNM/BS_pricer.py
1700 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Thu Jun 13 10:18:39 2019
5
6
@author: cantaro86
7
"""
8
9
import numpy as np
10
import scipy as scp
11
from scipy.sparse.linalg import spsolve
12
from scipy import sparse
13
from scipy.sparse.linalg import splu
14
import matplotlib.pyplot as plt
15
from matplotlib import cm
16
from time import time
17
import scipy.stats as ss
18
from FMNM.Solvers import Thomas
19
from FMNM.cython.solvers import SOR
20
from FMNM.CF import cf_normal
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 BS_pricer:
27
"""
28
Closed Formula.
29
Monte Carlo.
30
Finite-difference Black-Scholes PDE:
31
df/dt + r df/dx + 1/2 sigma^2 d^f/dx^2 -rf = 0
32
"""
33
34
def __init__(self, Option_info, Process_info):
35
"""
36
Option_info: of type Option_param. It contains (S0,K,T)
37
i.e. current price, strike, maturity in years
38
Process_info: of type Diffusion_process. It contains (r, mu, sig) i.e.
39
interest rate, drift coefficient, diffusion coefficient
40
"""
41
self.r = Process_info.r # interest rate
42
self.sig = Process_info.sig # diffusion coefficient
43
self.S0 = Option_info.S0 # current price
44
self.K = Option_info.K # strike
45
self.T = Option_info.T # maturity in years
46
self.exp_RV = Process_info.exp_RV # function to generate solution of GBM
47
48
self.price = 0
49
self.S_vec = None
50
self.price_vec = None
51
self.mesh = None
52
self.exercise = Option_info.exercise
53
self.payoff = Option_info.payoff
54
55
def payoff_f(self, S):
56
if self.payoff == "call":
57
Payoff = np.maximum(S - self.K, 0)
58
elif self.payoff == "put":
59
Payoff = np.maximum(self.K - S, 0)
60
return Payoff
61
62
@staticmethod
63
def BlackScholes(payoff="call", S0=100.0, K=100.0, T=1.0, r=0.1, sigma=0.2):
64
"""Black Scholes closed formula:
65
payoff: call or put.
66
S0: float. initial stock/index level.
67
K: float strike price.
68
T: float maturity (in year fractions).
69
r: float constant risk-free short rate.
70
sigma: volatility factor in diffusion term."""
71
72
d1 = (np.log(S0 / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
73
d2 = (np.log(S0 / K) + (r - sigma**2 / 2) * T) / (sigma * np.sqrt(T))
74
75
if payoff == "call":
76
return S0 * ss.norm.cdf(d1) - K * np.exp(-r * T) * ss.norm.cdf(d2)
77
elif payoff == "put":
78
return K * np.exp(-r * T) * ss.norm.cdf(-d2) - S0 * ss.norm.cdf(-d1)
79
else:
80
raise ValueError("invalid type. Set 'call' or 'put'")
81
82
@staticmethod
83
def vega(sigma, S0, K, T, r):
84
"""BS vega: derivative of the price with respect to the volatility"""
85
d1 = (np.log(S0 / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
86
return S0 * np.sqrt(T) * ss.norm.pdf(d1)
87
88
def closed_formula(self):
89
"""
90
Black Scholes closed formula:
91
"""
92
d1 = (np.log(self.S0 / self.K) + (self.r + self.sig**2 / 2) * self.T) / (self.sig * np.sqrt(self.T))
93
d2 = (np.log(self.S0 / self.K) + (self.r - self.sig**2 / 2) * self.T) / (self.sig * np.sqrt(self.T))
94
95
if self.payoff == "call":
96
return self.S0 * ss.norm.cdf(d1) - self.K * np.exp(-self.r * self.T) * ss.norm.cdf(d2)
97
elif self.payoff == "put":
98
return self.K * np.exp(-self.r * self.T) * ss.norm.cdf(-d2) - self.S0 * ss.norm.cdf(-d1)
99
else:
100
raise ValueError("invalid type. Set 'call' or 'put'")
101
102
def Fourier_inversion(self):
103
"""
104
Price obtained by inversion of the characteristic function
105
"""
106
k = np.log(self.K / self.S0)
107
cf_GBM = partial(
108
cf_normal,
109
mu=(self.r - 0.5 * self.sig**2) * self.T,
110
sig=self.sig * np.sqrt(self.T),
111
) # function binding
112
113
if self.payoff == "call":
114
call = self.S0 * Q1(k, cf_GBM, np.inf) - self.K * np.exp(-self.r * self.T) * Q2(
115
k, cf_GBM, np.inf
116
) # pricing function
117
return call
118
elif self.payoff == "put":
119
put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_GBM, np.inf)) - self.S0 * (
120
1 - Q1(k, cf_GBM, np.inf)
121
) # pricing function
122
return put
123
else:
124
raise ValueError("invalid type. Set 'call' or 'put'")
125
126
def FFT(self, K):
127
"""
128
FFT method. It returns a vector of prices.
129
K is an array of strikes
130
"""
131
K = np.array(K)
132
cf_GBM = partial(
133
cf_normal,
134
mu=(self.r - 0.5 * self.sig**2) * self.T,
135
sig=self.sig * np.sqrt(self.T),
136
) # function binding
137
if self.payoff == "call":
138
return fft_Lewis(K, self.S0, self.r, self.T, cf_GBM, interp="cubic")
139
elif self.payoff == "put": # put-call parity
140
return (
141
fft_Lewis(K, self.S0, self.r, self.T, cf_GBM, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T)
142
)
143
else:
144
raise ValueError("invalid type. Set 'call' or 'put'")
145
146
def IV_Lewis(self):
147
"""Implied Volatility from the Lewis formula"""
148
149
cf_GBM = partial(
150
cf_normal,
151
mu=(self.r - 0.5 * self.sig**2) * self.T,
152
sig=self.sig * np.sqrt(self.T),
153
) # function binding
154
if self.payoff == "call":
155
return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_GBM)
156
elif self.payoff == "put":
157
raise NotImplementedError
158
else:
159
raise ValueError("invalid type. Set 'call' or 'put'")
160
161
def MC(self, N, Err=False, Time=False):
162
"""
163
BS Monte Carlo
164
Err = return Standard Error if True
165
Time = return execution time if True
166
"""
167
t_init = time()
168
169
S_T = self.exp_RV(self.S0, self.T, N)
170
PayOff = self.payoff_f(S_T)
171
V = scp.mean(np.exp(-self.r * self.T) * PayOff, axis=0)
172
173
if Err is True:
174
if Time is True:
175
elapsed = time() - t_init
176
return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed
177
else:
178
return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T))
179
else:
180
if Time is True:
181
elapsed = time() - t_init
182
return V, elapsed
183
else:
184
return V
185
186
def PDE_price(self, steps, Time=False, solver="splu"):
187
"""
188
steps = tuple with number of space steps and time steps
189
payoff = "call" or "put"
190
exercise = "European" or "American"
191
Time = Boolean. Execution time.
192
Solver = spsolve or splu or Thomas or SOR
193
"""
194
t_init = time()
195
196
Nspace = steps[0]
197
Ntime = steps[1]
198
199
S_max = 6 * float(self.K)
200
S_min = float(self.K) / 6
201
x_max = np.log(S_max)
202
x_min = np.log(S_min)
203
x0 = np.log(self.S0) # current log-price
204
205
x, dx = np.linspace(x_min, x_max, Nspace, retstep=True)
206
t, dt = np.linspace(0, self.T, Ntime, retstep=True)
207
208
self.S_vec = np.exp(x) # vector of S
209
Payoff = self.payoff_f(self.S_vec)
210
211
V = np.zeros((Nspace, Ntime))
212
if self.payoff == "call":
213
V[:, -1] = Payoff
214
V[-1, :] = np.exp(x_max) - self.K * np.exp(-self.r * t[::-1])
215
V[0, :] = 0
216
else:
217
V[:, -1] = Payoff
218
V[-1, :] = 0
219
V[0, :] = Payoff[0] * np.exp(-self.r * t[::-1]) # Instead of Payoff[0] I could use K
220
# For s to 0, the limiting value is e^(-rT)(K-s)
221
222
sig2 = self.sig**2
223
dxx = dx**2
224
a = (dt / 2) * ((self.r - 0.5 * sig2) / dx - sig2 / dxx)
225
b = 1 + dt * (sig2 / dxx + self.r)
226
c = -(dt / 2) * ((self.r - 0.5 * sig2) / dx + sig2 / dxx)
227
228
D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc()
229
230
offset = np.zeros(Nspace - 2)
231
232
if solver == "spsolve":
233
if self.exercise == "European":
234
for i in range(Ntime - 2, -1, -1):
235
offset[0] = a * V[0, i]
236
offset[-1] = c * V[-1, i]
237
V[1:-1, i] = spsolve(D, (V[1:-1, i + 1] - offset))
238
elif self.exercise == "American":
239
for i in range(Ntime - 2, -1, -1):
240
offset[0] = a * V[0, i]
241
offset[-1] = c * V[-1, i]
242
V[1:-1, i] = np.maximum(spsolve(D, (V[1:-1, i + 1] - offset)), Payoff[1:-1])
243
elif solver == "Thomas":
244
if self.exercise == "European":
245
for i in range(Ntime - 2, -1, -1):
246
offset[0] = a * V[0, i]
247
offset[-1] = c * V[-1, i]
248
V[1:-1, i] = Thomas(D, (V[1:-1, i + 1] - offset))
249
elif self.exercise == "American":
250
for i in range(Ntime - 2, -1, -1):
251
offset[0] = a * V[0, i]
252
offset[-1] = c * V[-1, i]
253
V[1:-1, i] = np.maximum(Thomas(D, (V[1:-1, i + 1] - offset)), Payoff[1:-1])
254
elif solver == "SOR":
255
if self.exercise == "European":
256
for i in range(Ntime - 2, -1, -1):
257
offset[0] = a * V[0, i]
258
offset[-1] = c * V[-1, i]
259
V[1:-1, i] = SOR(a, b, c, (V[1:-1, i + 1] - offset), w=1.68, eps=1e-10, N_max=600)
260
elif self.exercise == "American":
261
for i in range(Ntime - 2, -1, -1):
262
offset[0] = a * V[0, i]
263
offset[-1] = c * V[-1, i]
264
V[1:-1, i] = np.maximum(
265
SOR(
266
a,
267
b,
268
c,
269
(V[1:-1, i + 1] - offset),
270
w=1.68,
271
eps=1e-10,
272
N_max=600,
273
),
274
Payoff[1:-1],
275
)
276
elif solver == "splu":
277
DD = splu(D)
278
if self.exercise == "European":
279
for i in range(Ntime - 2, -1, -1):
280
offset[0] = a * V[0, i]
281
offset[-1] = c * V[-1, i]
282
V[1:-1, i] = DD.solve(V[1:-1, i + 1] - offset)
283
elif self.exercise == "American":
284
for i in range(Ntime - 2, -1, -1):
285
offset[0] = a * V[0, i]
286
offset[-1] = c * V[-1, i]
287
V[1:-1, i] = np.maximum(DD.solve(V[1:-1, i + 1] - offset), Payoff[1:-1])
288
else:
289
raise ValueError("Solver is splu, spsolve, SOR or Thomas")
290
291
self.price = np.interp(x0, x, V[:, 0])
292
self.price_vec = V[:, 0]
293
self.mesh = V
294
295
if Time is True:
296
elapsed = time() - t_init
297
return self.price, elapsed
298
else:
299
return self.price
300
301
def plot(self, axis=None):
302
if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray:
303
self.PDE_price((7000, 5000))
304
# print("run the PDE_price method")
305
# return
306
307
plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff")
308
plt.plot(self.S_vec, self.price_vec, color="red", label="BS curve")
309
if type(axis) == list:
310
plt.axis(axis)
311
plt.xlabel("S")
312
plt.ylabel("price")
313
plt.title(f"{self.exercise} - Black Scholes price")
314
plt.legend()
315
plt.show()
316
317
def mesh_plt(self):
318
if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray:
319
self.PDE_price((7000, 5000))
320
321
fig = plt.figure()
322
ax = fig.add_subplot(111, projection="3d")
323
324
X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec)
325
ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean)
326
ax.set_title(f"{self.exercise} - BS price surface")
327
ax.set_xlabel("S")
328
ax.set_ylabel("t")
329
ax.set_zlabel("V")
330
ax.view_init(30, -100) # this function rotates the 3d plot
331
plt.show()
332
333
def LSM(self, N=10000, paths=10000, order=2):
334
"""
335
Longstaff-Schwartz Method for pricing American options
336
337
N = number of time steps
338
paths = number of generated paths
339
order = order of the polynomial for the regression
340
"""
341
342
if self.payoff != "put":
343
raise ValueError("invalid type. Set 'call' or 'put'")
344
345
dt = self.T / (N - 1) # time interval
346
df = np.exp(-self.r * dt) # discount factor per time time interval
347
348
X0 = np.zeros((paths, 1))
349
increments = ss.norm.rvs(
350
loc=(self.r - self.sig**2 / 2) * dt,
351
scale=np.sqrt(dt) * self.sig,
352
size=(paths, N - 1),
353
)
354
X = np.concatenate((X0, increments), axis=1).cumsum(1)
355
S = self.S0 * np.exp(X)
356
357
H = np.maximum(self.K - S, 0) # intrinsic values for put option
358
V = np.zeros_like(H) # value matrix
359
V[:, -1] = H[:, -1]
360
361
# Valuation by LS Method
362
for t in range(N - 2, 0, -1):
363
good_paths = H[:, t] > 0
364
rg = np.polyfit(S[good_paths, t], V[good_paths, t + 1] * df, 2) # polynomial regression
365
C = np.polyval(rg, S[good_paths, t]) # evaluation of regression
366
367
exercise = np.zeros(len(good_paths), dtype=bool)
368
exercise[good_paths] = H[good_paths, t] > C
369
370
V[exercise, t] = H[exercise, t]
371
V[exercise, t + 1 :] = 0
372
discount_path = V[:, t] == 0
373
V[discount_path, t] = V[discount_path, t + 1] * df
374
375
V0 = np.mean(V[:, 1]) * df #
376
return V0
377
378