sdevpy 0.0.1__tar.gz
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-0.0.1/LICENSE +21 -0
- sdevpy-0.0.1/PKG-INFO +20 -0
- sdevpy-0.0.1/README.md +6 -0
- sdevpy-0.0.1/pyproject.toml +25 -0
- sdevpy-0.0.1/setup.cfg +4 -0
- sdevpy-0.0.1/src/sdevpy/__init__.py +0 -0
- sdevpy-0.0.1/src/sdevpy/analytics/bachelier.py +66 -0
- sdevpy-0.0.1/src/sdevpy/analytics/black.py +81 -0
- sdevpy-0.0.1/src/sdevpy/analytics/fbsabr.py +183 -0
- sdevpy-0.0.1/src/sdevpy/analytics/mcheston.py +203 -0
- sdevpy-0.0.1/src/sdevpy/analytics/mcsabr.py +221 -0
- sdevpy-0.0.1/src/sdevpy/analytics/mczabr.py +220 -0
- sdevpy-0.0.1/src/sdevpy/analytics/sabr.py +72 -0
- sdevpy-0.0.1/src/sdevpy/example.py +2 -0
- sdevpy-0.0.1/src/sdevpy/machinelearning/callbacks.py +112 -0
- sdevpy-0.0.1/src/sdevpy/machinelearning/datasets.py +32 -0
- sdevpy-0.0.1/src/sdevpy/machinelearning/learningmodel.py +151 -0
- sdevpy-0.0.1/src/sdevpy/machinelearning/learningschedules.py +23 -0
- sdevpy-0.0.1/src/sdevpy/machinelearning/topology.py +65 -0
- sdevpy-0.0.1/src/sdevpy/maths/interpolations.py +28 -0
- sdevpy-0.0.1/src/sdevpy/maths/metrics.py +14 -0
- sdevpy-0.0.1/src/sdevpy/maths/optimization.py +1 -0
- sdevpy-0.0.1/src/sdevpy/maths/rand.py +99 -0
- sdevpy-0.0.1/src/sdevpy/projects/datafiles.py +28 -0
- sdevpy-0.0.1/src/sdevpy/projects/pinns/ernst_pinns.py +324 -0
- sdevpy-0.0.1/src/sdevpy/projects/pinns/pinns.py +345 -0
- sdevpy-0.0.1/src/sdevpy/projects/pinns/pinns_worst_of.py +635 -0
- sdevpy-0.0.1/src/sdevpy/projects/stovol/stovolgen.py +65 -0
- sdevpy-0.0.1/src/sdevpy/projects/stovol/stovolplot.py +110 -0
- sdevpy-0.0.1/src/sdevpy/projects/stovol/stovoltrain.py +247 -0
- sdevpy-0.0.1/src/sdevpy/projects/stovol/xsabrfit.py +255 -0
- sdevpy-0.0.1/src/sdevpy/settings.py +14 -0
- sdevpy-0.0.1/src/sdevpy/test.py +199 -0
- sdevpy-0.0.1/src/sdevpy/tools/clipboard.py +40 -0
- sdevpy-0.0.1/src/sdevpy/tools/constants.py +3 -0
- sdevpy-0.0.1/src/sdevpy/tools/filemanager.py +59 -0
- sdevpy-0.0.1/src/sdevpy/tools/jsonmanager.py +48 -0
- sdevpy-0.0.1/src/sdevpy/tools/timegrids.py +89 -0
- sdevpy-0.0.1/src/sdevpy/tools/timer.py +32 -0
- sdevpy-0.0.1/src/sdevpy/volsurfacegen/fbsabrgenerator.py +64 -0
- sdevpy-0.0.1/src/sdevpy/volsurfacegen/mchestongenerator.py +216 -0
- sdevpy-0.0.1/src/sdevpy/volsurfacegen/mcsabrgenerator.py +228 -0
- sdevpy-0.0.1/src/sdevpy/volsurfacegen/mczabrgenerator.py +227 -0
- sdevpy-0.0.1/src/sdevpy/volsurfacegen/sabrgenerator.py +282 -0
- sdevpy-0.0.1/src/sdevpy/volsurfacegen/smilegenerator.py +124 -0
- sdevpy-0.0.1/src/sdevpy/volsurfacegen/stovolfactory.py +44 -0
- sdevpy-0.0.1/src/sdevpy.egg-info/PKG-INFO +20 -0
- sdevpy-0.0.1/src/sdevpy.egg-info/SOURCES.txt +49 -0
- sdevpy-0.0.1/src/sdevpy.egg-info/dependency_links.txt +1 -0
- sdevpy-0.0.1/src/sdevpy.egg-info/requires.txt +1 -0
- sdevpy-0.0.1/src/sdevpy.egg-info/top_level.txt +1 -0
sdevpy-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2023] [Sebastien Gurrieri]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sdevpy-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sdevpy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python package for Machine Learning in Finance
|
|
5
|
+
Author-email: Sebastien Gurrieri <sebgur@gmail.com>
|
|
6
|
+
Project-URL: Git page, https://github.com/sebgur/SDev.Python
|
|
7
|
+
Project-URL: SDev Finance, http://sdev-finance.com/
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# SDev.Python
|
|
16
|
+
|
|
17
|
+
Python repository for various projects in Machine Learning in relation to Finance.This is a simple example package. In the current release,
|
|
18
|
+
we work on stochastic volatility surfaces and their calibration through Machine Learning methods.
|
|
19
|
+
|
|
20
|
+
See other work on our main website [SDev-Finance](http://sdev-finance.com/).
|
sdevpy-0.0.1/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# SDev.Python
|
|
2
|
+
|
|
3
|
+
Python repository for various projects in Machine Learning in relation to Finance.This is a simple example package. In the current release,
|
|
4
|
+
we work on stochastic volatility surfaces and their calibration through Machine Learning methods.
|
|
5
|
+
|
|
6
|
+
See other work on our main website [SDev-Finance](http://sdev-finance.com/).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sdevpy"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Sebastien Gurrieri", email="sebgur@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Python package for Machine Learning in Finance"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"pandas",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
"Git page" = "https://github.com/sebgur/SDev.Python"
|
|
25
|
+
"SDev Finance" = "http://sdev-finance.com/"
|
sdevpy-0.0.1/setup.cfg
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()
|