sdevpy 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdevpy/__init__.py +0 -0
- sdevpy/analytics/bachelier.py +66 -0
- sdevpy/analytics/black.py +81 -0
- sdevpy/analytics/fbsabr.py +183 -0
- sdevpy/analytics/mcheston.py +203 -0
- sdevpy/analytics/mcsabr.py +221 -0
- sdevpy/analytics/mczabr.py +220 -0
- sdevpy/analytics/sabr.py +72 -0
- sdevpy/example.py +2 -0
- sdevpy/machinelearning/callbacks.py +112 -0
- sdevpy/machinelearning/datasets.py +32 -0
- sdevpy/machinelearning/learningmodel.py +151 -0
- sdevpy/machinelearning/learningschedules.py +23 -0
- sdevpy/machinelearning/topology.py +65 -0
- sdevpy/maths/interpolations.py +28 -0
- sdevpy/maths/metrics.py +14 -0
- sdevpy/maths/optimization.py +1 -0
- sdevpy/maths/rand.py +99 -0
- sdevpy/projects/datafiles.py +28 -0
- sdevpy/projects/pinns/ernst_pinns.py +324 -0
- sdevpy/projects/pinns/pinns.py +345 -0
- sdevpy/projects/pinns/pinns_worst_of.py +635 -0
- sdevpy/projects/stovol/stovolgen.py +65 -0
- sdevpy/projects/stovol/stovolplot.py +110 -0
- sdevpy/projects/stovol/stovoltrain.py +247 -0
- sdevpy/projects/stovol/xsabrfit.py +255 -0
- sdevpy/settings.py +14 -0
- sdevpy/test.py +199 -0
- sdevpy/tools/clipboard.py +40 -0
- sdevpy/tools/constants.py +3 -0
- sdevpy/tools/filemanager.py +59 -0
- sdevpy/tools/jsonmanager.py +48 -0
- sdevpy/tools/timegrids.py +89 -0
- sdevpy/tools/timer.py +32 -0
- sdevpy/volsurfacegen/fbsabrgenerator.py +64 -0
- sdevpy/volsurfacegen/mchestongenerator.py +216 -0
- sdevpy/volsurfacegen/mcsabrgenerator.py +228 -0
- sdevpy/volsurfacegen/mczabrgenerator.py +227 -0
- sdevpy/volsurfacegen/sabrgenerator.py +282 -0
- sdevpy/volsurfacegen/smilegenerator.py +124 -0
- sdevpy/volsurfacegen/stovolfactory.py +44 -0
- sdevpy-0.0.1.dist-info/LICENSE +21 -0
- sdevpy-0.0.1.dist-info/METADATA +21 -0
- sdevpy-0.0.1.dist-info/RECORD +46 -0
- sdevpy-0.0.1.dist-info/WHEEL +5 -0
- sdevpy-0.0.1.dist-info/top_level.txt +1 -0
sdevpy/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
""" Utilities for Bachelier model """
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.stats import norm
|
|
4
|
+
from scipy.optimize import minimize_scalar
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def price(expiry, strike, is_call, fwd, vol):
|
|
8
|
+
""" Option price under the Bachelier model """
|
|
9
|
+
stdev = vol * expiry**0.5
|
|
10
|
+
d = (fwd - strike) / stdev
|
|
11
|
+
wd = d if is_call else -d
|
|
12
|
+
return stdev * (wd * norm.cdf(wd) + norm.pdf(d))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def implied_vol(expiry, strike, is_call, fwd, fwd_price):
|
|
16
|
+
""" P. Jaeckel's method in "Implied Normal Volatility", 6th Jun. 2017 """
|
|
17
|
+
m = fwd - strike
|
|
18
|
+
abs_m = np.abs(m)
|
|
19
|
+
# Special case at ATM
|
|
20
|
+
if abs_m < 1e-8:
|
|
21
|
+
return fwd_price * np.sqrt(2.0 * np.pi) / np.sqrt(expiry)
|
|
22
|
+
|
|
23
|
+
# General case
|
|
24
|
+
tilde_phi_star_c = -0.001882039271
|
|
25
|
+
theta = 1.0 if is_call else -1.0
|
|
26
|
+
|
|
27
|
+
tilde_phi_star = -np.abs(fwd_price - np.maximum(theta * m, 0.0)) / abs_m
|
|
28
|
+
em5 = 1e-5
|
|
29
|
+
|
|
30
|
+
if tilde_phi_star < tilde_phi_star_c:
|
|
31
|
+
g = 1.0 / (tilde_phi_star - 0.5)
|
|
32
|
+
g2 = g**2
|
|
33
|
+
em3 = 1e-3
|
|
34
|
+
num = 0.032114372355 - g2 * (0.016969777977 - g2 * (2.6207332461 * em3
|
|
35
|
+
- 9.6066952861 * em5 * g2))
|
|
36
|
+
den = 1.0 - g2 * (0.6635646938 - g2 * (0.14528712196 - 0.010472855461 * g2))
|
|
37
|
+
eta_bar = num / den
|
|
38
|
+
xb = g * (eta_bar * g2 + 1.0 / np.sqrt(2.0 * np.pi))
|
|
39
|
+
else:
|
|
40
|
+
h = np.sqrt(-np.log(-tilde_phi_star))
|
|
41
|
+
num = 9.4883409779 - h * (9.6320903635 - h * (0.58556997323 + 2.1464093351 * h))
|
|
42
|
+
den = 1.0 - h * (0.65174820867 + h * (1.5120247828 + 6.6437847132 * em5 * h))
|
|
43
|
+
xb = num / den
|
|
44
|
+
|
|
45
|
+
q = (norm.cdf(xb) + norm.pdf(xb) / xb - tilde_phi_star) / norm.pdf(xb)
|
|
46
|
+
xb2 = xb**2
|
|
47
|
+
num = 3.0 * q * xb2 * (2.0 - q * xb * (2.0 + xb2))
|
|
48
|
+
den = 6.0 + q * xb * (-12.0 + xb * (6.0 * q + xb * (-6.0 + q * xb * (3.0 + xb2))))
|
|
49
|
+
xs = xb + num / den
|
|
50
|
+
sigma = abs_m / (np.abs(xs) * np.sqrt(expiry))
|
|
51
|
+
return sigma
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def implied_vol_solve(expiry, strike, is_call, fwd, fwd_price):
|
|
55
|
+
""" Direct method by numerical inversion using Brent """
|
|
56
|
+
options = {'xtol': 1e-4, 'maxiter': 100, 'disp': False}
|
|
57
|
+
xmin = 1e-6
|
|
58
|
+
xmax = 1.0
|
|
59
|
+
|
|
60
|
+
def error(vol):
|
|
61
|
+
premium = price(expiry, strike, is_call, fwd, vol)
|
|
62
|
+
return (premium - fwd_price) ** 2
|
|
63
|
+
|
|
64
|
+
res = minimize_scalar(fun=error, bracket=(xmin, xmax), options=options, method='brent')
|
|
65
|
+
|
|
66
|
+
return res.x
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
""" Utilities for Black-Scholes model """
|
|
2
|
+
import numpy as np
|
|
3
|
+
import scipy.stats
|
|
4
|
+
from scipy.optimize import minimize_scalar
|
|
5
|
+
import py_vollib.black.implied_volatility as jaeckel
|
|
6
|
+
|
|
7
|
+
N = scipy.stats.norm.cdf
|
|
8
|
+
# Ninv = scipy.stats.norm.ppf
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def price(expiry, strike, is_call, fwd, vol):
|
|
12
|
+
""" Option price under the Black-Scholes model """
|
|
13
|
+
w = 1.0 if is_call else -1.0
|
|
14
|
+
s = vol * np.sqrt(expiry)
|
|
15
|
+
d1 = np.log(fwd / strike) / s + 0.5 * s
|
|
16
|
+
d2 = d1 - s
|
|
17
|
+
return w * (fwd * N(w * d1) - strike * N(w * d2))
|
|
18
|
+
|
|
19
|
+
def implied_vol_jaeckel(expiry, strike, is_call, fwd, fwd_price):
|
|
20
|
+
""" Black-Scholes implied volatility using P. Jaeckel's 'Let's be rational' method,
|
|
21
|
+
from package py_vollib. Install with pip install py_vollib or at
|
|
22
|
+
https://pypi.org/project/py_vollib/. Unfortunately we found it has instabilities
|
|
23
|
+
near ATM. """
|
|
24
|
+
flag = 'c' if is_call else 'p'
|
|
25
|
+
p = fwd_price
|
|
26
|
+
iv = jaeckel.implied_volatility_of_undiscounted_option_price(p, fwd, strike, expiry, flag)
|
|
27
|
+
return iv
|
|
28
|
+
|
|
29
|
+
def implied_vol(expiry, strike, is_call, fwd, fwd_price):
|
|
30
|
+
""" Direct method by numerical inversion using Brent """
|
|
31
|
+
options = {'xtol': 1e-4, 'maxiter': 100, 'disp': False}
|
|
32
|
+
xmin = 1e-6
|
|
33
|
+
xmax = 1.0
|
|
34
|
+
|
|
35
|
+
def error(vol):
|
|
36
|
+
premium = price(expiry, strike, is_call, fwd, vol)
|
|
37
|
+
return (premium - fwd_price) ** 2
|
|
38
|
+
|
|
39
|
+
res = minimize_scalar(fun=error, bracket=(xmin, xmax), options=options, method='brent')
|
|
40
|
+
|
|
41
|
+
return res.x
|
|
42
|
+
|
|
43
|
+
# def performance(spot_vol, repo_rate, div_rate, expiry, strike, fixings):
|
|
44
|
+
# shape = spot_vol.shape
|
|
45
|
+
# num_underlyings = int(shape[0] / 2)
|
|
46
|
+
# # print(num_underlyings)
|
|
47
|
+
# forward_perf = 1.0
|
|
48
|
+
# vol2 = 0.0
|
|
49
|
+
# for i in range(num_underlyings):
|
|
50
|
+
# forward = spot_vol[2 * i] * np.exp((repo_rate - div_rate) * expiry)
|
|
51
|
+
# perf = forward # / fixings[i]
|
|
52
|
+
# # perf = forward / fixings[i]
|
|
53
|
+
# forward_perf = forward_perf * perf
|
|
54
|
+
# vol = spot_vol[2 * i + 1]
|
|
55
|
+
# vol2 = vol2 + np.power(vol, 2)
|
|
56
|
+
#
|
|
57
|
+
# vol = np.sqrt(vol2)
|
|
58
|
+
# # print(vol)
|
|
59
|
+
#
|
|
60
|
+
# # return forward_perf
|
|
61
|
+
# return black_formula(forward_perf, strike, vol, expiry, True)
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
EXPIRY = 1.0
|
|
65
|
+
VOL = 0.25
|
|
66
|
+
IS_CALL = True
|
|
67
|
+
NUM_POINTS = 100
|
|
68
|
+
# FWD = 100
|
|
69
|
+
# K = 100
|
|
70
|
+
# p = price(EXPIRY, K, IS_CALL, FWD, VOL)
|
|
71
|
+
# iv = implied_vol(EXPIRY, K, IS_CALL, FWD, p)
|
|
72
|
+
# print(iv)
|
|
73
|
+
f_space = np.linspace(100, 120, NUM_POINTS)
|
|
74
|
+
k_space = np.linspace(20, 2180, NUM_POINTS)
|
|
75
|
+
prices = price(EXPIRY, k_space, IS_CALL, f_space, VOL)
|
|
76
|
+
# print(prices)
|
|
77
|
+
implied_vols = []
|
|
78
|
+
for i, k in enumerate(k_space):
|
|
79
|
+
implied_vols.append(implied_vol(EXPIRY, k, IS_CALL, f_space[i], prices[i]))
|
|
80
|
+
|
|
81
|
+
# print(implied_vols)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
""" Monte-Carlo simulation for Free-Boundary SABR model (vanillas) """
|
|
2
|
+
import numpy as np
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import scipy.stats as sp
|
|
5
|
+
# from analytics.sabr import calculate_alpha
|
|
6
|
+
from tools.timegrids import SimpleTimeGridBuilder
|
|
7
|
+
from tools import timer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def price(expiries, strikes, are_calls, fwd, parameters, num_mc=10000, points_per_year=10,
|
|
11
|
+
scheme='Andersen'):
|
|
12
|
+
""" Calculate vanilla prices under Free-Boundary SABR model by Monte-Carlo simulation"""
|
|
13
|
+
floor = 0.00001
|
|
14
|
+
|
|
15
|
+
# Temporarily turn off the warnings for division by 0. This is because on certain paths,
|
|
16
|
+
# the spot becomes so close to 0 that Python effectively handles it as 0. This results in
|
|
17
|
+
# a warning when taking a negative power of it. However, this is not an issue as Python
|
|
18
|
+
# correctly finds +infinity and since we use a floor, this case is correctly handled.
|
|
19
|
+
np.seterr(divide='ignore')
|
|
20
|
+
|
|
21
|
+
# Build time grid
|
|
22
|
+
time_grid_builder = SimpleTimeGridBuilder(points_per_year=points_per_year)
|
|
23
|
+
time_grid_builder.add_grid(expiries)
|
|
24
|
+
time_grid = time_grid_builder.complete_grid()
|
|
25
|
+
num_factors = 2
|
|
26
|
+
|
|
27
|
+
# Find payoff times
|
|
28
|
+
is_payoff = np.in1d(time_grid, expiries)
|
|
29
|
+
|
|
30
|
+
# Retrieve parameters
|
|
31
|
+
lnvol = parameters['LnVol']
|
|
32
|
+
beta = parameters['Beta']
|
|
33
|
+
nu = parameters['Nu']
|
|
34
|
+
rho = parameters['Rho']
|
|
35
|
+
alpha = calculate_fbsabr_alpha(lnvol, fwd, beta)
|
|
36
|
+
nu2 = nu**2
|
|
37
|
+
sqrtmrho2 = np.sqrt(1.0 - rho**2)
|
|
38
|
+
|
|
39
|
+
# Draw all gaussians
|
|
40
|
+
# gaussians = rand.gaussians(num_steps, num_mc, num_factors, rand_method)
|
|
41
|
+
|
|
42
|
+
# Define dimensions
|
|
43
|
+
mean = np.zeros(num_factors)
|
|
44
|
+
corr = np.zeros((num_factors, num_factors))
|
|
45
|
+
for c in range(num_factors):
|
|
46
|
+
corr[c, c] = 1.0
|
|
47
|
+
|
|
48
|
+
# Draw for each step
|
|
49
|
+
seed = 42
|
|
50
|
+
rng = np.random.RandomState(seed)
|
|
51
|
+
|
|
52
|
+
# Initialize paths
|
|
53
|
+
spot = np.ones((2 * num_mc, 1)) * fwd
|
|
54
|
+
vol = np.ones((2 * num_mc, 1)) * 1.0
|
|
55
|
+
|
|
56
|
+
# Loop over time grid
|
|
57
|
+
ts = te = 0
|
|
58
|
+
payoff_count = 0
|
|
59
|
+
mc_prices = []
|
|
60
|
+
for i, t in enumerate(time_grid):
|
|
61
|
+
# print("time iteration " + str(i))
|
|
62
|
+
ts = te
|
|
63
|
+
te = t
|
|
64
|
+
dt = te - ts
|
|
65
|
+
sqrt_dt = np.sqrt(dt)
|
|
66
|
+
|
|
67
|
+
# Evolve
|
|
68
|
+
dz = rng.multivariate_normal(mean, corr, size=num_mc) * sqrt_dt
|
|
69
|
+
dz = np.concatenate((dz, -dz), axis=0) # Antithetic paths
|
|
70
|
+
dz0 = dz[:, 0].reshape(-1, 1)
|
|
71
|
+
dz1 = dz[:, 1].reshape(-1, 1)
|
|
72
|
+
|
|
73
|
+
vols = vol
|
|
74
|
+
abs_f = np.maximum(np.abs(spot), floor)
|
|
75
|
+
|
|
76
|
+
# Evolve vol
|
|
77
|
+
vol *= np.exp(-0.5 * nu2 * dt + nu * dz1)
|
|
78
|
+
|
|
79
|
+
# Evolve spot
|
|
80
|
+
if scheme == 'Euler':
|
|
81
|
+
dw = rho * dz1 + sqrtmrho2 * dz0
|
|
82
|
+
spot = spot + alpha * abs_f**beta * dw * vols
|
|
83
|
+
elif scheme == 'Andersen':
|
|
84
|
+
vole = vol
|
|
85
|
+
spot = spot + alpha * abs_f**beta * (sqrtmrho2 * vols * dz0 + rho / nu * (vole - vols))
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError("Unknown scheme in FBSABR: " + scheme)
|
|
88
|
+
|
|
89
|
+
# Calculate payoff
|
|
90
|
+
if is_payoff[i]:
|
|
91
|
+
w = [1.0 if is_call else -1.0 for is_call in are_calls[payoff_count]]
|
|
92
|
+
w = np.asarray(w).reshape(1, -1)
|
|
93
|
+
k = np.asarray(strikes[payoff_count]).reshape(1, -1)
|
|
94
|
+
payoff = np.maximum(w * (spot - k), 0.0)
|
|
95
|
+
rpayoff = np.mean(payoff, axis=0)
|
|
96
|
+
mc_prices.append(rpayoff)
|
|
97
|
+
payoff_count += 1
|
|
98
|
+
|
|
99
|
+
np.seterr(divide='warn')
|
|
100
|
+
|
|
101
|
+
return np.asarray(mc_prices)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def calculate_fbsabr_alpha(ln_vol, fwd, beta):
|
|
105
|
+
""" Calculate parameter alpha with our definition in terms of ln_vol, i.e.
|
|
106
|
+
alpha = ln_vol * fwd ^ (1.0 - beta) """
|
|
107
|
+
floor = 0.00001
|
|
108
|
+
abs_f = np.maximum(np.abs(fwd), floor)
|
|
109
|
+
return ln_vol * abs_f ** (1.0 - beta)
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
EXPIRIES = [0.05, 0.10, 0.25, 0.5]
|
|
113
|
+
NSTRIKES = 50
|
|
114
|
+
FWD = -0.005
|
|
115
|
+
SHIFT = 0.03
|
|
116
|
+
SFWD = FWD + SHIFT
|
|
117
|
+
IS_CALL = False
|
|
118
|
+
ARE_CALLS = [IS_CALL] * NSTRIKES
|
|
119
|
+
ARE_CALLS = [ARE_CALLS] * len(EXPIRIES)
|
|
120
|
+
LNVOL = 0.25
|
|
121
|
+
# Spread method
|
|
122
|
+
# SPREADS = np.linspace(-200, 200, NSTRIKES)
|
|
123
|
+
# SPREADS = np.asarray([SPREADS] * len(EXPIRIES))
|
|
124
|
+
# STRIKES = FWD + SPREADS / 10000.0
|
|
125
|
+
# SSTRIKES = STRIKES + SHIFT
|
|
126
|
+
# XAXIS = SPREADS
|
|
127
|
+
# Distribution method
|
|
128
|
+
np_expiries = np.asarray(EXPIRIES).reshape(-1, 1)
|
|
129
|
+
PERCENT = np.linspace(0.01, 0.99, NSTRIKES)
|
|
130
|
+
PERCENT = np.asarray([PERCENT] * len(EXPIRIES))
|
|
131
|
+
ITO = -0.5 * LNVOL**2 * np_expiries
|
|
132
|
+
DIFF = LNVOL * np.sqrt(np_expiries) * sp.norm.ppf(PERCENT)
|
|
133
|
+
SSTRIKES = SFWD * np.exp(ITO + DIFF)
|
|
134
|
+
STRIKES = SSTRIKES - SHIFT
|
|
135
|
+
XAXIS = STRIKES
|
|
136
|
+
|
|
137
|
+
PARAMETERS = {'LnVol': LNVOL, 'Beta': 0.1, 'Nu': 0.50, 'Rho': -0.25}
|
|
138
|
+
NUM_MC = 100 * 1000
|
|
139
|
+
POINTS_PER_YEAR = 25
|
|
140
|
+
# SCHEME = 'Andersen'
|
|
141
|
+
SCHEME = 'Euler'
|
|
142
|
+
|
|
143
|
+
# Calculate MC prices
|
|
144
|
+
mc_timer = timer.Stopwatch("MC")
|
|
145
|
+
mc_timer.trigger()
|
|
146
|
+
MC_PRICES = price(EXPIRIES, STRIKES, ARE_CALLS, FWD, PARAMETERS, NUM_MC, POINTS_PER_YEAR,
|
|
147
|
+
scheme=SCHEME)
|
|
148
|
+
mc_timer.stop()
|
|
149
|
+
mc_timer.print()
|
|
150
|
+
|
|
151
|
+
# print(MC_PRICES)
|
|
152
|
+
|
|
153
|
+
# Convert to IV and compare against approximate closed-form
|
|
154
|
+
import black
|
|
155
|
+
import bachelier
|
|
156
|
+
mc_ivs = []
|
|
157
|
+
for a, expiry in enumerate(EXPIRIES):
|
|
158
|
+
mc_iv = []
|
|
159
|
+
for j, sstrike in enumerate(SSTRIKES[a]):
|
|
160
|
+
# mc_iv.append(black.implied_vol(expiry, sstrike, IS_CALL, SFWD, MC_PRICES[a, j]))
|
|
161
|
+
mc_iv.append(bachelier.implied_vol(expiry, STRIKES[a, j], IS_CALL, FWD, MC_PRICES[a, j]))
|
|
162
|
+
mc_ivs.append(mc_iv)
|
|
163
|
+
|
|
164
|
+
plt.figure(figsize=(10, 8))
|
|
165
|
+
plt.subplots_adjust(hspace=0.40)
|
|
166
|
+
plt.subplot(2, 2, 1)
|
|
167
|
+
plt.plot(XAXIS[0], mc_ivs[0], label='MC')
|
|
168
|
+
plt.legend(loc='best')
|
|
169
|
+
plt.title(f"Expiry: {EXPIRIES[0]}")
|
|
170
|
+
plt.subplot(2, 2, 2)
|
|
171
|
+
plt.plot(XAXIS[1], mc_ivs[1], label='MC')
|
|
172
|
+
plt.legend(loc='best')
|
|
173
|
+
plt.title(f"Expiry: {EXPIRIES[1]}")
|
|
174
|
+
plt.subplot(2, 2, 3)
|
|
175
|
+
plt.plot(XAXIS[2], mc_ivs[2], label='MC')
|
|
176
|
+
plt.legend(loc='best')
|
|
177
|
+
plt.title(f"Expiry: {EXPIRIES[2]}")
|
|
178
|
+
plt.subplot(2, 2, 4)
|
|
179
|
+
plt.plot(XAXIS[3], mc_ivs[3], label='MC')
|
|
180
|
+
plt.legend(loc='best')
|
|
181
|
+
plt.title(f"Expiry: {EXPIRIES[3]}")
|
|
182
|
+
|
|
183
|
+
plt.show()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
""" Monte-Carlo simulation for Heston model (vanillas). The model is defined as
|
|
2
|
+
dS = sqrt(v) * S * dW
|
|
3
|
+
dv = kappa * (theta - v) * dt + xi * sqrt(v) * dZ with <dW, dZ> = rho
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import scipy.stats as sp
|
|
8
|
+
# from analytics.sabr import calculate_alpha
|
|
9
|
+
from tools.timegrids import SimpleTimeGridBuilder
|
|
10
|
+
from tools import timer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def price(expiries, strikes, are_calls, fwd, parameters, num_mc=10000, points_per_year=10):
|
|
14
|
+
""" Calculate vanilla prices under Heston model by Monte-Carlo simulation"""
|
|
15
|
+
scale = fwd
|
|
16
|
+
if scale < 0.0:
|
|
17
|
+
raise ValueError("Negative forward")
|
|
18
|
+
|
|
19
|
+
# Temporarily turn off the warnings for division by 0. This is because on certain paths,
|
|
20
|
+
# the spot becomes so close to 0 that Python effectively handles it as 0. This results in
|
|
21
|
+
# a warning when taking a negative power of it. However, this is not an issue as Python
|
|
22
|
+
# correctly finds +infinity and since we use a floor, this case is correctly handled.
|
|
23
|
+
np.seterr(divide='ignore')
|
|
24
|
+
|
|
25
|
+
# Build time grid
|
|
26
|
+
time_grid_builder = SimpleTimeGridBuilder(points_per_year=points_per_year)
|
|
27
|
+
time_grid_builder.add_grid(expiries)
|
|
28
|
+
time_grid = time_grid_builder.complete_grid()
|
|
29
|
+
num_factors = 2
|
|
30
|
+
|
|
31
|
+
# Find payoff times
|
|
32
|
+
is_payoff = np.in1d(time_grid, expiries)
|
|
33
|
+
|
|
34
|
+
# Retrieve parameters
|
|
35
|
+
lnvol = parameters['LnVol']
|
|
36
|
+
kappa = parameters['Kappa']
|
|
37
|
+
theta = parameters['Theta']
|
|
38
|
+
xi = parameters['Xi']
|
|
39
|
+
rho = parameters['Rho']
|
|
40
|
+
sqrtmrho2 = np.sqrt(1.0 - rho**2)
|
|
41
|
+
v0 = calculate_v0(lnvol)
|
|
42
|
+
|
|
43
|
+
# Draw all gaussians
|
|
44
|
+
# gaussians = rand.gaussians(num_steps, num_mc, num_factors, rand_method)
|
|
45
|
+
|
|
46
|
+
# Define dimensions
|
|
47
|
+
mean = np.zeros(num_factors)
|
|
48
|
+
corr = np.zeros((num_factors, num_factors))
|
|
49
|
+
for c in range(num_factors):
|
|
50
|
+
corr[c, c] = 1.0
|
|
51
|
+
|
|
52
|
+
# Draw for each step
|
|
53
|
+
seed = 42
|
|
54
|
+
rng = np.random.RandomState(seed)
|
|
55
|
+
|
|
56
|
+
# Initialize paths
|
|
57
|
+
spot = np.ones((2 * num_mc, 1)) * fwd
|
|
58
|
+
vol2 = np.ones((2 * num_mc, 1)) * v0
|
|
59
|
+
|
|
60
|
+
# Loop over time grid
|
|
61
|
+
ts = te = 0
|
|
62
|
+
payoff_count = 0
|
|
63
|
+
mc_prices = []
|
|
64
|
+
for i, t in enumerate(time_grid):
|
|
65
|
+
ts = te
|
|
66
|
+
te = t
|
|
67
|
+
dt = te - ts
|
|
68
|
+
sqrt_dt = np.sqrt(dt)
|
|
69
|
+
|
|
70
|
+
# Evolve
|
|
71
|
+
dz = rng.multivariate_normal(mean, corr, size=num_mc) * sqrt_dt
|
|
72
|
+
dz = np.concatenate((dz, -dz), axis=0) # Antithetic paths
|
|
73
|
+
dz0 = dz[:, 0].reshape(-1, 1)
|
|
74
|
+
dz1 = dz[:, 1].reshape(-1, 1)
|
|
75
|
+
|
|
76
|
+
# Evolve vol
|
|
77
|
+
vol2s = np.abs(vol2)
|
|
78
|
+
sqrt_vol2s = np.sqrt(vol2s)
|
|
79
|
+
vol2e = vol2s + kappa * (theta - vol2s) * dt + xi * sqrt_vol2s * dz1
|
|
80
|
+
vol2e = np.abs(vol2e)
|
|
81
|
+
vol2 = vol2e
|
|
82
|
+
|
|
83
|
+
# Evolve spot
|
|
84
|
+
intvol2 = 0.5 * (vol2s + vol2e) * dt
|
|
85
|
+
ito = 0.5 * intvol2
|
|
86
|
+
dw = rho * dz1 + sqrtmrho2 * dz0
|
|
87
|
+
spot *= np.exp(-ito + sqrt_vol2s * dw)
|
|
88
|
+
|
|
89
|
+
# Calculate payoff
|
|
90
|
+
if is_payoff[i]:
|
|
91
|
+
w = [1.0 if is_call else -1.0 for is_call in are_calls[payoff_count]]
|
|
92
|
+
w = np.asarray(w).reshape(1, -1)
|
|
93
|
+
k = np.asarray(strikes[payoff_count]).reshape(1, -1)
|
|
94
|
+
payoff = np.maximum(w * (spot - k), 0.0)
|
|
95
|
+
rpayoff = np.mean(payoff, axis=0)
|
|
96
|
+
mc_prices.append(rpayoff)
|
|
97
|
+
payoff_count += 1
|
|
98
|
+
|
|
99
|
+
np.seterr(divide='warn')
|
|
100
|
+
|
|
101
|
+
return np.asarray(mc_prices)
|
|
102
|
+
|
|
103
|
+
def calculate_v0(lnvol):
|
|
104
|
+
return lnvol**2
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
EXPIRIES = [0.5, 1.0, 5.0, 10.0]
|
|
109
|
+
NSTRIKES = 50
|
|
110
|
+
FWD = -0.005
|
|
111
|
+
SHIFT = 0.03
|
|
112
|
+
SFWD = FWD + SHIFT
|
|
113
|
+
IS_CALL = False
|
|
114
|
+
ARE_CALLS = [IS_CALL] * NSTRIKES
|
|
115
|
+
ARE_CALLS = [ARE_CALLS] * len(EXPIRIES)
|
|
116
|
+
LNVOL = 0.25
|
|
117
|
+
# Spread method
|
|
118
|
+
# SPREADS = np.linspace(-200, 200, NSTRIKES)
|
|
119
|
+
# SPREADS = np.asarray([SPREADS] * len(EXPIRIES))
|
|
120
|
+
# STRIKES = FWD + SPREADS / 10000.0
|
|
121
|
+
# SSTRIKES = STRIKES + SHIFT
|
|
122
|
+
# XAXIS = SPREADS
|
|
123
|
+
# Distribution method
|
|
124
|
+
np_expiries = np.asarray(EXPIRIES).reshape(-1, 1)
|
|
125
|
+
PERCENT = np.linspace(0.01, 0.99, NSTRIKES)
|
|
126
|
+
PERCENT = np.asarray([PERCENT] * len(EXPIRIES))
|
|
127
|
+
ITO = -0.5 * LNVOL**2 * np_expiries
|
|
128
|
+
DIFF = LNVOL * np.sqrt(np_expiries) * sp.norm.ppf(PERCENT)
|
|
129
|
+
SSTRIKES = SFWD * np.exp(ITO + DIFF)
|
|
130
|
+
STRIKES = SSTRIKES - SHIFT
|
|
131
|
+
XAXIS = STRIKES
|
|
132
|
+
|
|
133
|
+
THETA = LNVOL**2
|
|
134
|
+
PARAMETERS = {'LnVol': LNVOL, 'Kappa': 1.0, 'Theta': THETA, 'Xi': 0.50, 'Rho': -0.25}
|
|
135
|
+
NUM_MC = 100 * 1000
|
|
136
|
+
POINTS_PER_YEAR = 25
|
|
137
|
+
# SCHEME = 'LogAndersen'
|
|
138
|
+
# SCHEME = 'LogEuler'
|
|
139
|
+
|
|
140
|
+
# Calculate MC prices
|
|
141
|
+
mc_timer = timer.Stopwatch("MC")
|
|
142
|
+
mc_timer.trigger()
|
|
143
|
+
MC_PRICES = price(EXPIRIES, SSTRIKES, ARE_CALLS, SFWD, PARAMETERS, NUM_MC, POINTS_PER_YEAR)
|
|
144
|
+
mc_timer.stop()
|
|
145
|
+
mc_timer.print()
|
|
146
|
+
|
|
147
|
+
# Convert to IV and compare against approximate closed-form
|
|
148
|
+
import black
|
|
149
|
+
import bachelier
|
|
150
|
+
mc_ivs = []
|
|
151
|
+
n_ivs = []
|
|
152
|
+
for a, expiry in enumerate(EXPIRIES):
|
|
153
|
+
mc_iv = []
|
|
154
|
+
cf_iv = []
|
|
155
|
+
n_iv = []
|
|
156
|
+
for j, sstrike in enumerate(SSTRIKES[a]):
|
|
157
|
+
mc_iv.append(black.implied_vol(expiry, sstrike, IS_CALL, SFWD, MC_PRICES[a, j]))
|
|
158
|
+
n_iv.append(bachelier.implied_vol_solve(expiry, STRIKES[a, j], IS_CALL, FWD,
|
|
159
|
+
MC_PRICES[a, j]))
|
|
160
|
+
mc_ivs.append(mc_iv)
|
|
161
|
+
n_ivs.append(n_iv)
|
|
162
|
+
|
|
163
|
+
plt.figure(figsize=(10, 8))
|
|
164
|
+
plt.subplots_adjust(hspace=0.40)
|
|
165
|
+
plt.subplot(2, 2, 1)
|
|
166
|
+
plt.plot(XAXIS[0], mc_ivs[0], label='MC')
|
|
167
|
+
plt.legend(loc='best')
|
|
168
|
+
plt.title(f"Expiry: {EXPIRIES[0]}")
|
|
169
|
+
plt.subplot(2, 2, 2)
|
|
170
|
+
plt.plot(XAXIS[1], mc_ivs[1], label='MC')
|
|
171
|
+
plt.legend(loc='best')
|
|
172
|
+
plt.title(f"Expiry: {EXPIRIES[1]}")
|
|
173
|
+
plt.subplot(2, 2, 3)
|
|
174
|
+
plt.plot(XAXIS[2], mc_ivs[2], label='MC')
|
|
175
|
+
plt.legend(loc='best')
|
|
176
|
+
plt.title(f"Expiry: {EXPIRIES[2]}")
|
|
177
|
+
plt.subplot(2, 2, 4)
|
|
178
|
+
plt.plot(XAXIS[3], mc_ivs[3], label='MC')
|
|
179
|
+
plt.legend(loc='best')
|
|
180
|
+
plt.title(f"Expiry: {EXPIRIES[3]}")
|
|
181
|
+
|
|
182
|
+
plt.show()
|
|
183
|
+
|
|
184
|
+
# plt.figure(figsize=(10, 8))
|
|
185
|
+
# plt.subplots_adjust(hspace=0.40)
|
|
186
|
+
# plt.subplot(2, 2, 1)
|
|
187
|
+
# plt.plot(XAXIS[0], n_ivs[0], label='MC')
|
|
188
|
+
# plt.legend(loc='best')
|
|
189
|
+
# plt.title(f"NVOL Expiry: {EXPIRIES[0]}")
|
|
190
|
+
# plt.subplot(2, 2, 2)
|
|
191
|
+
# plt.plot(XAXIS[1], n_ivs[1], label='MC')
|
|
192
|
+
# plt.legend(loc='best')
|
|
193
|
+
# plt.title(f"NVOL Expiry: {EXPIRIES[1]}")
|
|
194
|
+
# plt.subplot(2, 2, 3)
|
|
195
|
+
# plt.plot(XAXIS[2], n_ivs[2], label='MC')
|
|
196
|
+
# plt.legend(loc='best')
|
|
197
|
+
# plt.title(f"NVOL Expiry: {EXPIRIES[2]}")
|
|
198
|
+
# plt.subplot(2, 2, 4)
|
|
199
|
+
# plt.plot(XAXIS[3], n_ivs[3], label='MC')
|
|
200
|
+
# plt.legend(loc='best')
|
|
201
|
+
# plt.title(f"NVOL Expiry: {EXPIRIES[3]}")
|
|
202
|
+
|
|
203
|
+
# plt.show()
|