sdevpy 0.9.0__tar.gz → 1.0.0__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.9.0/src/sdevpy.egg-info → sdevpy-1.0.0}/PKG-INFO +1 -1
- {sdevpy-0.9.0 → sdevpy-1.0.0}/pyproject.toml +2 -2
- sdevpy-1.0.0/src/sdevpy/__init__.py +1 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/analytics/black.py +46 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/machinelearning/datasets.py +5 -0
- sdevpy-1.0.0/src/sdevpy/montecarlo/smoothers.py +48 -0
- sdevpy-1.0.0/src/sdevpy/projects/aad/aad_mc.py +281 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/settings.py +6 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/test.py +5 -3
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/volsurfacegen/mchestongenerator.py +2 -4
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/volsurfacegen/mczabrgenerator.py +2 -4
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/volsurfacegen/sabrgenerator.py +3 -4
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/volsurfacegen/smilegenerator.py +14 -27
- {sdevpy-0.9.0 → sdevpy-1.0.0/src/sdevpy.egg-info}/PKG-INFO +1 -1
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy.egg-info/SOURCES.txt +2 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy.egg-info/requires.txt +1 -0
- sdevpy-0.9.0/src/sdevpy/__init__.py +0 -1
- {sdevpy-0.9.0 → sdevpy-1.0.0}/LICENSE +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/README.md +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/setup.cfg +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/analytics/bachelier.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/analytics/fbsabr.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/analytics/mcheston.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/analytics/mcsabr.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/analytics/mczabr.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/analytics/sabr.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/machinelearning/callbacks.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/machinelearning/learningmodel.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/machinelearning/learningschedules.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/machinelearning/topology.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/maths/interpolations.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/maths/metrics.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/maths/optimization.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/maths/rand.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/projects/datafiles.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/projects/stovol/stovolgen.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/projects/stovol/stovolplot.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/projects/stovol/stovoltrain.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/tools/clipboard.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/tools/constants.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/tools/filemanager.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/tools/jsonmanager.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/tools/timegrids.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/tools/timer.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/volsurfacegen/fbsabrgenerator.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/volsurfacegen/mcsabrgenerator.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy/volsurfacegen/stovolfactory.py +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy.egg-info/dependency_links.txt +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/src/sdevpy.egg-info/top_level.txt +0 -0
- {sdevpy-0.9.0 → sdevpy-1.0.0}/tests/test.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdevpy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Sebastien Gurrieri", email="sebgur@gmail.com" },
|
|
10
10
|
]
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
20
|
"pandas","pyperclip","py_vollib","numpy","tensorflow",
|
|
21
|
-
"scikit-learn"
|
|
21
|
+
"scikit-learn", "tensorflow_probability"
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
[project.urls]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.0'
|
|
@@ -3,9 +3,13 @@ import numpy as np
|
|
|
3
3
|
import scipy.stats
|
|
4
4
|
from scipy.optimize import minimize_scalar
|
|
5
5
|
import py_vollib.black.implied_volatility as jaeckel
|
|
6
|
+
import tensorflow as tf
|
|
7
|
+
import tensorflow_probability as tfp
|
|
8
|
+
from sdevpy import settings
|
|
6
9
|
|
|
7
10
|
N = scipy.stats.norm.cdf
|
|
8
11
|
# Ninv = scipy.stats.norm.ppf
|
|
12
|
+
tf_N = tfp.distributions.Normal(0.0, 1.0)
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
def price(expiry, strike, is_call, fwd, vol):
|
|
@@ -40,6 +44,48 @@ def implied_vol(expiry, strike, is_call, fwd, fwd_price):
|
|
|
40
44
|
|
|
41
45
|
return res.x
|
|
42
46
|
|
|
47
|
+
|
|
48
|
+
def price_and_greeks(expiry, strike, spot, vol, rate, div):
|
|
49
|
+
""" Calculate call PV and sensitivities by AAD on Black-Scholes CF """
|
|
50
|
+
tf_spot = tf.convert_to_tensor(spot, dtype='float32')
|
|
51
|
+
tf_vol = tf.convert_to_tensor(vol)
|
|
52
|
+
tf_time = tf.convert_to_tensor(expiry, dtype='float32')
|
|
53
|
+
tf_rate = tf.convert_to_tensor(rate, dtype='float32')
|
|
54
|
+
tf_div = tf.constant(div)
|
|
55
|
+
|
|
56
|
+
with tf.GradientTape(persistent=True) as tape:
|
|
57
|
+
tape.watch([tf_spot, tf_vol, tf_time, tf_rate])
|
|
58
|
+
with tf.GradientTape(persistent=True) as tape2nd:
|
|
59
|
+
tape2nd.watch([tf_spot, tf_vol])
|
|
60
|
+
|
|
61
|
+
fwd = tf_spot * tf.math.exp((tf_rate - tf_div) * tf_time)
|
|
62
|
+
stdev = tf_vol * tf.math.sqrt(tf_time)
|
|
63
|
+
d1 = tf.math.log(fwd / strike) / stdev + 0.5 * stdev
|
|
64
|
+
d2 = d1 - stdev
|
|
65
|
+
df = tf.math.exp(-tf_rate * tf_time)
|
|
66
|
+
pv = df * (fwd * tf_N.cdf(d1) - strike * tf_N.cdf(d2))
|
|
67
|
+
|
|
68
|
+
# Calculate delta and vega
|
|
69
|
+
g_delta = tape2nd.gradient(pv, tf_spot)
|
|
70
|
+
g_vega = tape2nd.gradient(pv, tf_vol)
|
|
71
|
+
|
|
72
|
+
delta = tape.gradient(pv, tf_spot)
|
|
73
|
+
gamma = tape.gradient(g_delta, tf_spot)
|
|
74
|
+
vega = tape.gradient(pv, tf_vol)
|
|
75
|
+
theta = tape.gradient(pv, tf_time)
|
|
76
|
+
dv01 = tape.gradient(pv, tf_rate)
|
|
77
|
+
volga = tape.gradient(g_vega, tf_vol)
|
|
78
|
+
vanna = tape.gradient(g_delta, tf_vol)
|
|
79
|
+
|
|
80
|
+
# Scale
|
|
81
|
+
vega = vega.numpy() * settings.VEGA_SCALING
|
|
82
|
+
theta = -theta.numpy() * settings.THETA_SCALING
|
|
83
|
+
dv01 = dv01.numpy() * settings.DV01_SCALING
|
|
84
|
+
volga = volga.numpy() * settings.VOLGA_SCALING
|
|
85
|
+
vanna = vanna.numpy() * settings.VANNA_SCALING
|
|
86
|
+
|
|
87
|
+
return [pv.numpy(), delta.numpy(), gamma.numpy(), vega, theta, dv01, volga, vanna]
|
|
88
|
+
|
|
43
89
|
# def performance(spot_vol, repo_rate, div_rate, expiry, strike, fixings):
|
|
44
90
|
# shape = spot_vol.shape
|
|
45
91
|
# num_underlyings = int(shape[0] / 2)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
""" Payoff smoothers for numerical methods """
|
|
2
|
+
import numpy as np
|
|
3
|
+
import tensorflow as tf
|
|
4
|
+
# import scipy.stats
|
|
5
|
+
# import tensorflow_probability as tfp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Smoothing parameters (for the max function)
|
|
9
|
+
SMOOTH_VOL = 0.40
|
|
10
|
+
SMOOTH_TIME = 10.0 / 365.0
|
|
11
|
+
SMOOTH_STDEV = SMOOTH_VOL * np.sqrt(SMOOTH_TIME)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Numpy versions (non-AAD)
|
|
15
|
+
# N = scipy.stats.norm
|
|
16
|
+
|
|
17
|
+
def approx_cdf(x):
|
|
18
|
+
""" Simple approximation of CDF """
|
|
19
|
+
return 1.0 / (1.0 + np.exp(-x / 0.5879))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def smooth_call(spot, strike):
|
|
23
|
+
""" Smoothing of call payoff """
|
|
24
|
+
d1 = np.log(spot / strike) / SMOOTH_STDEV + 0.5 * SMOOTH_STDEV
|
|
25
|
+
d2 = d1 - SMOOTH_STDEV
|
|
26
|
+
n1 = approx_cdf(d1)
|
|
27
|
+
n2 = approx_cdf(d2)
|
|
28
|
+
return spot * n1 - 0.5 * strike * (n1 + n2) # Average
|
|
29
|
+
# return spot * N.cdf(d1) - strike * N.cdf(d2) # BS, overestimates
|
|
30
|
+
# return (spot - strike) * N.cdf(d1) # Underestimates
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Tensorflow versions (AAD)
|
|
34
|
+
# tf_N = tfp.distributions.Normal(0.0, 1.0)
|
|
35
|
+
|
|
36
|
+
def tf_approx_cdf(x):
|
|
37
|
+
""" Simple approximation of CDF """
|
|
38
|
+
return 1.0 / (1.0 + tf.math.exp(-x / 0.5879))
|
|
39
|
+
|
|
40
|
+
def tf_smooth_call(spot, strike):
|
|
41
|
+
""" Smoothing of call payoff """
|
|
42
|
+
d1 = tf.math.log(spot / strike) / SMOOTH_STDEV + 0.5 * SMOOTH_STDEV
|
|
43
|
+
d2 = d1 - SMOOTH_STDEV
|
|
44
|
+
n1 = tf_approx_cdf(d1)
|
|
45
|
+
n2 = tf_approx_cdf(d2)
|
|
46
|
+
return spot * n1 - 0.5 * strike * (n1 + n2) # Average
|
|
47
|
+
# return spot * tf_N.cdf(d1) - strike * tf_N.cdf(d2) # BS, overestimates
|
|
48
|
+
# return (spot - strike) * tf_N.cdf(d1) # Underestimates
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
""" Calculate PV and Greeks of a call option on a single Black-Scholes underlying. We compare
|
|
2
|
+
standard MC with Greeks obtained by bump and reprice vs AAD MC vs Closed-Form. """
|
|
3
|
+
import numpy as np
|
|
4
|
+
import tensorflow as tf
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from sdevpy.montecarlo import smoothers
|
|
7
|
+
from sdevpy import settings
|
|
8
|
+
from sdevpy.analytics import black
|
|
9
|
+
from sdevpy.tools.timer import Stopwatch
|
|
10
|
+
|
|
11
|
+
# ################ Runtime configuration ##########################################################
|
|
12
|
+
# Parameters
|
|
13
|
+
EXPIRY = 2
|
|
14
|
+
SPOT = 100.0
|
|
15
|
+
VOL = 0.20
|
|
16
|
+
RATE = 0.04
|
|
17
|
+
DIV = 0.01
|
|
18
|
+
STRIKE = 100.0
|
|
19
|
+
|
|
20
|
+
# Bumps for differentials
|
|
21
|
+
SPOT_BUMP = 0.01 # Percentage of spot
|
|
22
|
+
VOL_BUMP = 0.01 # Percentage of vol
|
|
23
|
+
TIME_BUMP = 1.0 / 365.0
|
|
24
|
+
RATE_BUMP = 1.0 / 10000.0 # In bps
|
|
25
|
+
|
|
26
|
+
# Scalings
|
|
27
|
+
VEGA_SCL = settings.VEGA_SCALING
|
|
28
|
+
THETA_SCL = settings.THETA_SCALING
|
|
29
|
+
DV01_SCL = settings.DV01_SCALING
|
|
30
|
+
VOLGA_SCL = settings.VOLGA_SCALING
|
|
31
|
+
VANNA_SCL = settings.VANNA_SCALING
|
|
32
|
+
|
|
33
|
+
# Random generator seed
|
|
34
|
+
SEED = 42
|
|
35
|
+
|
|
36
|
+
# Use smoother in standard MC (always used in AAD)
|
|
37
|
+
USE_SMOOTHER_STD = False
|
|
38
|
+
|
|
39
|
+
# ################ Helper functions ###############################################################
|
|
40
|
+
def print_val(values_, is_pv_vector=False):
|
|
41
|
+
""" Simple screen printing of valuation results """
|
|
42
|
+
if is_pv_vector:
|
|
43
|
+
print(f"PV: {values_[0][0]:,.4f}")
|
|
44
|
+
else:
|
|
45
|
+
print(f"PV: {values_[0]:,.4f}")
|
|
46
|
+
print(f"Delta: {values_[1]:,.4f}")
|
|
47
|
+
print(f"Gamma: {values_[2]:,.4f}")
|
|
48
|
+
print(f"Vega: {values_[3]:,.4f}")
|
|
49
|
+
print(f"Theta: {values_[4]:,.4f}")
|
|
50
|
+
print(f"DV01: {values_[5]:,.4f}")
|
|
51
|
+
print(f"Volga: {values_[6]:,.4f}")
|
|
52
|
+
print(f"Vanna: {values_[7]:,.4f}")
|
|
53
|
+
|
|
54
|
+
# ################ Standard Monte-Carlo #########################################################
|
|
55
|
+
# Standard simulator
|
|
56
|
+
def simulate_std(spot_, vol_, time_, rate_, gaussians):
|
|
57
|
+
""" Standard MC simulation for PV """
|
|
58
|
+
# Calculate deterministic forward
|
|
59
|
+
fwd = spot_ * np.exp((rate_ - DIV) * time_)
|
|
60
|
+
|
|
61
|
+
# Calculate final spot paths
|
|
62
|
+
stdev = vol_ * np.sqrt(time_)
|
|
63
|
+
future_spot = fwd * np.exp(-0.5 * stdev * stdev + stdev * gaussians)
|
|
64
|
+
|
|
65
|
+
# Calculate discounted payoff
|
|
66
|
+
df = np.exp(-rate_ * time_)
|
|
67
|
+
if USE_SMOOTHER_STD:
|
|
68
|
+
payoff = df * smoothers.smooth_call(future_spot, STRIKE) # Use smoothing
|
|
69
|
+
else:
|
|
70
|
+
payoff = df * np.maximum(future_spot - STRIKE, 0)
|
|
71
|
+
|
|
72
|
+
# Reduce
|
|
73
|
+
pv = np.mean(payoff, axis=0)
|
|
74
|
+
|
|
75
|
+
return pv
|
|
76
|
+
|
|
77
|
+
# Sensitivities by bumps
|
|
78
|
+
def calculate_std(spot_, vol_, time_, rate_, num_mc):
|
|
79
|
+
""" Calculate PV and sensitivities by bumps on MC """
|
|
80
|
+
rng = np.random.RandomState(SEED)
|
|
81
|
+
# Calculate gaussians only once
|
|
82
|
+
gaussians = rng.normal(0.0, 1.0, (num_mc, 1))
|
|
83
|
+
|
|
84
|
+
pv = simulate_std(spot_, vol_, EXPIRY, rate_, gaussians)
|
|
85
|
+
|
|
86
|
+
# Delta-Gamma
|
|
87
|
+
spot_u = spot_ * (1.0 + SPOT_BUMP)
|
|
88
|
+
pv_spot_up = simulate_std(spot_u, vol_, time_, rate_, gaussians)
|
|
89
|
+
|
|
90
|
+
spot_d = spot_ * (1.0 - SPOT_BUMP)
|
|
91
|
+
pv_spot_down = simulate_std(spot_d, vol_, time_, rate_, gaussians)
|
|
92
|
+
|
|
93
|
+
# Vega-Volga
|
|
94
|
+
vol_u = vol_ * (1.0 + VOL_BUMP)
|
|
95
|
+
pv_vol_up = simulate_std(spot_, vol_u, time_, rate_, gaussians)
|
|
96
|
+
|
|
97
|
+
vol_d = vol_ * (1.0 - VOL_BUMP)
|
|
98
|
+
pv_vol_down = simulate_std(spot_, vol_d, time_, rate_, gaussians)
|
|
99
|
+
|
|
100
|
+
# Theta
|
|
101
|
+
pv_expiry_down = simulate_std(spot_, vol_, time_ - TIME_BUMP, rate_, gaussians)
|
|
102
|
+
|
|
103
|
+
# DV01
|
|
104
|
+
pv_rate_up = simulate_std(spot_, vol_, time_, rate_ + RATE_BUMP, gaussians)
|
|
105
|
+
pv_rate_down = simulate_std(spot_, vol_, time_, rate_ - RATE_BUMP, gaussians)
|
|
106
|
+
|
|
107
|
+
# Vanna
|
|
108
|
+
pv_uu = simulate_std(spot_u, vol_u, time_, rate_, gaussians)
|
|
109
|
+
pv_ud = simulate_std(spot_u, vol_d, time_, rate_, gaussians)
|
|
110
|
+
pv_du = simulate_std(spot_d, vol_u, time_, rate_, gaussians)
|
|
111
|
+
pv_dd = simulate_std(spot_d, vol_d, time_, rate_, gaussians)
|
|
112
|
+
|
|
113
|
+
# FDM
|
|
114
|
+
delta = (pv_spot_up - pv_spot_down) / (2.0 * SPOT_BUMP * spot_)
|
|
115
|
+
gamma = (pv_spot_up + pv_spot_down - 2.0 * pv) / np.power(SPOT_BUMP * spot_, 2)
|
|
116
|
+
vega = (pv_vol_up - pv_vol_down) / (2.0 * VOL_BUMP * vol_) * VEGA_SCL
|
|
117
|
+
theta = (pv_expiry_down - pv) / TIME_BUMP * THETA_SCL
|
|
118
|
+
dv01 = (pv_rate_up - pv_rate_down) / (2.0 * RATE_BUMP) * DV01_SCL
|
|
119
|
+
volga_size = np.power(VOL_BUMP * vol_, 2)
|
|
120
|
+
volga = (pv_vol_up + pv_vol_down - 2.0 * pv) / volga_size * VOLGA_SCL
|
|
121
|
+
vanna_size = 4.0 * SPOT_BUMP * spot_ * VOL_BUMP * vol_
|
|
122
|
+
vanna = (pv_uu - pv_ud - pv_du + pv_dd) / vanna_size * VANNA_SCL
|
|
123
|
+
|
|
124
|
+
return [pv, delta, gamma, vega, theta, dv01, volga, vanna]
|
|
125
|
+
|
|
126
|
+
# Test bumps
|
|
127
|
+
NUM_MC = 100 * 1000
|
|
128
|
+
print(f">> Running {NUM_MC:,} Standard MC Simulations")
|
|
129
|
+
values = calculate_std(SPOT, VOL, EXPIRY, RATE, NUM_MC)
|
|
130
|
+
print_val([v[0] for v in values])
|
|
131
|
+
|
|
132
|
+
# ################ AAD Monte-Carlo ###############################################################
|
|
133
|
+
# AAD 2nd order simulator with smoothing
|
|
134
|
+
def calculate_aad(spot_, vol_, time_, rate_, num_mc):
|
|
135
|
+
""" Calculate PV and sensitivities by AAD MC """
|
|
136
|
+
rng = np.random.RandomState(SEED)
|
|
137
|
+
gaussians = rng.normal(0.0, 1.0, (num_mc, 1))
|
|
138
|
+
|
|
139
|
+
tf_spot = tf.convert_to_tensor(spot_, dtype='float32')
|
|
140
|
+
tf_vol = tf.convert_to_tensor(vol_)
|
|
141
|
+
tf_time = tf.convert_to_tensor(time_, dtype='float32')
|
|
142
|
+
tf_rate = tf.convert_to_tensor(rate_, dtype='float32')
|
|
143
|
+
tf_div = tf.constant(DIV)
|
|
144
|
+
|
|
145
|
+
with tf.GradientTape(persistent=True) as tape:
|
|
146
|
+
tape.watch([tf_spot, tf_vol, tf_time, tf_rate])
|
|
147
|
+
with tf.GradientTape(persistent=True) as tape2nd:
|
|
148
|
+
tape2nd.watch([tf_spot, tf_vol])
|
|
149
|
+
|
|
150
|
+
# Calculate deterministic forward
|
|
151
|
+
fwd = tf_spot * tf.math.exp((tf_rate - tf_div) * tf_time)
|
|
152
|
+
|
|
153
|
+
# Calculate final spot paths
|
|
154
|
+
stdev = tf_vol * tf.math.sqrt(tf_time)
|
|
155
|
+
future_spot = fwd * tf.math.exp(-0.5 * stdev * stdev + stdev * gaussians)
|
|
156
|
+
|
|
157
|
+
# Calculate discounted payoff
|
|
158
|
+
df = tf.math.exp(-tf_rate * tf_time)
|
|
159
|
+
payoff = df * smoothers.tf_smooth_call(future_spot, STRIKE)
|
|
160
|
+
|
|
161
|
+
# Reduce
|
|
162
|
+
pv = tf.reduce_mean(payoff, axis=0)
|
|
163
|
+
|
|
164
|
+
# Calculate delta and vega
|
|
165
|
+
g_delta = tape2nd.gradient(pv, tf_spot)
|
|
166
|
+
g_vega = tape2nd.gradient(pv, tf_vol)
|
|
167
|
+
|
|
168
|
+
delta = tape.gradient(pv, tf_spot)
|
|
169
|
+
gamma = tape.gradient(g_delta, tf_spot)
|
|
170
|
+
vega = tape.gradient(pv, tf_vol)
|
|
171
|
+
theta = tape.gradient(pv, tf_time)
|
|
172
|
+
dv01 = tape.gradient(pv, tf_rate)
|
|
173
|
+
volga = tape.gradient(g_vega, tf_vol)
|
|
174
|
+
vanna = tape.gradient(g_delta, tf_vol)
|
|
175
|
+
|
|
176
|
+
# Scale
|
|
177
|
+
vega = vega.numpy() * VEGA_SCL
|
|
178
|
+
theta = -theta.numpy() * THETA_SCL
|
|
179
|
+
dv01 = dv01.numpy() * DV01_SCL
|
|
180
|
+
volga = volga.numpy() * VOLGA_SCL
|
|
181
|
+
vanna = vanna.numpy() * VANNA_SCL
|
|
182
|
+
|
|
183
|
+
return [pv.numpy(), delta.numpy(), gamma.numpy(), vega, theta, dv01, volga, vanna]
|
|
184
|
+
|
|
185
|
+
# Test AAD with smoothing
|
|
186
|
+
NUM_MC = 100 * 1000
|
|
187
|
+
print(f">> Running {NUM_MC:,} AAD MC Simulations")
|
|
188
|
+
values = calculate_aad(SPOT, VOL, EXPIRY, RATE, NUM_MC)
|
|
189
|
+
|
|
190
|
+
print_val(values, is_pv_vector=True)
|
|
191
|
+
|
|
192
|
+
############# Test against CF #####################################################################
|
|
193
|
+
# Calculate with closed-form
|
|
194
|
+
print(">> Calculating closed-form (CF)")
|
|
195
|
+
values = black.price_and_greeks(EXPIRY, STRIKE, SPOT, VOL, RATE, DIV)
|
|
196
|
+
print_val(values)
|
|
197
|
+
|
|
198
|
+
######### Numerical Tests #########################################################################
|
|
199
|
+
# Ladder tests
|
|
200
|
+
NUM_POINTS = 100 # Number of loops/points in the charts
|
|
201
|
+
MIN_S = 20.0
|
|
202
|
+
MAX_S = 200.0
|
|
203
|
+
NUM_MC = 10 * 1000 # Number of simulations
|
|
204
|
+
spots = np.linspace(MIN_S, MAX_S, NUM_POINTS, dtype='double') # spot ladder
|
|
205
|
+
print(f">> Calculating {NUM_POINTS:,} points with {NUM_MC:,} simulations over spot range " +
|
|
206
|
+
f"[{MIN_S:,}, {MAX_S:,}]")
|
|
207
|
+
|
|
208
|
+
# Vectorize for broadcasting
|
|
209
|
+
vols = np.ones(NUM_POINTS, dtype='float32') * VOL
|
|
210
|
+
times = np.ones(NUM_POINTS, dtype='float32') * EXPIRY
|
|
211
|
+
rates = np.ones(NUM_POINTS, dtype='float32') * RATE
|
|
212
|
+
|
|
213
|
+
# Bump method
|
|
214
|
+
print("> Calculating with bumps...")
|
|
215
|
+
print(f"Number of simulations: {NUM_MC:,}")
|
|
216
|
+
timer_bmp = Stopwatch("Bump-MC")
|
|
217
|
+
timer_bmp.trigger()
|
|
218
|
+
results_bmp = calculate_std(spots, vols, times, rates, NUM_MC)
|
|
219
|
+
timer_bmp.stop()
|
|
220
|
+
|
|
221
|
+
# AAD
|
|
222
|
+
print("Calculating with AAD...")
|
|
223
|
+
print(f"Number of simulations: {NUM_MC:,}")
|
|
224
|
+
timer_aad = Stopwatch("AAD-MC")
|
|
225
|
+
timer_aad.trigger()
|
|
226
|
+
results_aad = calculate_aad(spots, vols, times, rates, NUM_MC)
|
|
227
|
+
timer_aad.stop()
|
|
228
|
+
|
|
229
|
+
# AD closed-form
|
|
230
|
+
print("Calculating with closed-form...")
|
|
231
|
+
timer_cf = Stopwatch("CF")
|
|
232
|
+
timer_cf.trigger()
|
|
233
|
+
results_adcf = black.price_and_greeks(times, STRIKE, spots, vols, rates, DIV)
|
|
234
|
+
timer_cf.stop()
|
|
235
|
+
|
|
236
|
+
print("Calculation complete!")
|
|
237
|
+
timer_bmp.print()
|
|
238
|
+
timer_aad.print()
|
|
239
|
+
timer_cf.print()
|
|
240
|
+
|
|
241
|
+
#### Charts
|
|
242
|
+
results_bmp = np.array(results_bmp)
|
|
243
|
+
results_aad = np.array(results_aad)
|
|
244
|
+
results_adcf = np.array(results_adcf)
|
|
245
|
+
|
|
246
|
+
# Select viewing range
|
|
247
|
+
VIEW_MIN = MIN_S # Choose larger to zoom in
|
|
248
|
+
VIEW_MAX = MAX_S # Choose smaller to zoom in
|
|
249
|
+
|
|
250
|
+
view_range = [i for i in range(NUM_POINTS) if spots[i] >= VIEW_MIN and spots[i] <= VIEW_MAX]
|
|
251
|
+
start = view_range[0]
|
|
252
|
+
end = view_range[-1]
|
|
253
|
+
|
|
254
|
+
def plot_value(plt_idx, name, result_idx, legend_location):
|
|
255
|
+
""" Plot 3-way comparison between standard MC, AAD MC and closed-form """
|
|
256
|
+
plt.subplot(4, 2, plt_idx)
|
|
257
|
+
plt.title(name)
|
|
258
|
+
plt.xlabel('Spot')
|
|
259
|
+
plt.plot(spots[start:end], results_bmp[result_idx][start:end], 'green', alpha=0.7,
|
|
260
|
+
label='Bump-MC')
|
|
261
|
+
plt.plot(spots[start:end], results_adcf[result_idx][start:end], color='blue', alpha=0.8,
|
|
262
|
+
label='CF')
|
|
263
|
+
plt.plot(spots[start:end], results_aad[result_idx][start:end], color='red',
|
|
264
|
+
label='AAD-MC')
|
|
265
|
+
plt.legend(loc=legend_location)
|
|
266
|
+
|
|
267
|
+
# Plot results
|
|
268
|
+
plt.ioff()
|
|
269
|
+
plt.figure(figsize=(15, 16))
|
|
270
|
+
plt.subplots_adjust(hspace=0.40, wspace=0.20)
|
|
271
|
+
|
|
272
|
+
plot_value(1, "PV", 0, 'upper left')
|
|
273
|
+
plot_value(2, "Delta", 1, 'upper left')
|
|
274
|
+
plot_value(3, "Vega", 3, 'upper right')
|
|
275
|
+
plot_value(4, "Gamma", 2, 'upper right')
|
|
276
|
+
plot_value(5, "Volga", 6, 'upper left')
|
|
277
|
+
plot_value(6, "Vanna", 7, 'upper right')
|
|
278
|
+
plot_value(7, "Theta", 4, 'upper right')
|
|
279
|
+
plot_value(8, "DV01", 5, 'lower right')
|
|
280
|
+
|
|
281
|
+
plt.show()
|
|
@@ -4,6 +4,12 @@ import os
|
|
|
4
4
|
# Global variables
|
|
5
5
|
WORKFOLDER = r"C:\temp\sdevpy"
|
|
6
6
|
|
|
7
|
+
VEGA_SCALING = 0.01 # Vega shown for 1% absolute moves
|
|
8
|
+
THETA_SCALING = 1.0 / 365.0 # Theta shown for 1d moves
|
|
9
|
+
DV01_SCALING = 1.0 / 10000.0 # DV01 shown for 1bp moves
|
|
10
|
+
VOLGA_SCALING = VEGA_SCALING**2
|
|
11
|
+
VANNA_SCALING = VEGA_SCALING
|
|
12
|
+
|
|
7
13
|
# Disable debug warnings in tensorflow
|
|
8
14
|
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
|
9
15
|
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
# import scipy.stats as sp
|
|
5
5
|
# import json
|
|
6
6
|
# import matplotlib.pyplot as plt
|
|
7
|
-
import requests, zipfile
|
|
8
|
-
import io
|
|
7
|
+
# import requests, zipfile
|
|
8
|
+
# import io
|
|
9
9
|
import pandas as pd
|
|
10
|
-
from io import BytesIO
|
|
10
|
+
# from io import BytesIO
|
|
11
|
+
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
# URL = 'https://drive.google.com/file/d/10dKi82fW2arlKnOahNv9i5igfiydwMnc/view?usp=sharing'
|
|
13
15
|
|
|
@@ -108,9 +108,7 @@ class McHestonGenerator(SmileGenerator):
|
|
|
108
108
|
|
|
109
109
|
return prices
|
|
110
110
|
|
|
111
|
-
def
|
|
112
|
-
data_df = SmileGenerator.from_file(data_file, shuffle)
|
|
113
|
-
|
|
111
|
+
def retrieve_datasets_no_shuffle(self, data_df):
|
|
114
112
|
# Retrieve suitable data
|
|
115
113
|
t = data_df.Ttm
|
|
116
114
|
strike = data_df.K
|
|
@@ -128,7 +126,7 @@ class McHestonGenerator(SmileGenerator):
|
|
|
128
126
|
y_set = np.asarray(nvol)
|
|
129
127
|
y_set = np.reshape(y_set, (num_samples, 1))
|
|
130
128
|
|
|
131
|
-
return x_set, y_set
|
|
129
|
+
return x_set, y_set
|
|
132
130
|
|
|
133
131
|
def price_surface_mod(self, model, expiries, strikes, are_calls, fwd, parameters):
|
|
134
132
|
# Retrieve parameters
|
|
@@ -108,9 +108,7 @@ class McZabrGenerator(SmileGenerator):
|
|
|
108
108
|
|
|
109
109
|
return prices
|
|
110
110
|
|
|
111
|
-
def
|
|
112
|
-
data_df = SmileGenerator.from_file(data_file, shuffle)
|
|
113
|
-
|
|
111
|
+
def retrieve_datasets_no_shuffle(self, data_df):
|
|
114
112
|
# Retrieve suitable data
|
|
115
113
|
t = data_df.Ttm
|
|
116
114
|
strike = data_df.K
|
|
@@ -128,7 +126,7 @@ class McZabrGenerator(SmileGenerator):
|
|
|
128
126
|
y_set = np.asarray(nvol)
|
|
129
127
|
y_set = np.reshape(y_set, (num_samples, 1))
|
|
130
128
|
|
|
131
|
-
return x_set, y_set
|
|
129
|
+
return x_set, y_set
|
|
132
130
|
|
|
133
131
|
def price_surface_mod(self, model, expiries, strikes, are_calls, fwd, parameters):
|
|
134
132
|
# Retrieve parameters
|
|
@@ -109,9 +109,7 @@ class SabrGenerator(SmileGenerator):
|
|
|
109
109
|
|
|
110
110
|
return np.asarray(prices)
|
|
111
111
|
|
|
112
|
-
def
|
|
113
|
-
data_df = SmileGenerator.from_file(data_file, shuffle)
|
|
114
|
-
|
|
112
|
+
def retrieve_datasets_no_shuffle(self, data_df):
|
|
115
113
|
# Retrieve suitable data
|
|
116
114
|
t = data_df.Ttm
|
|
117
115
|
strike = data_df.K
|
|
@@ -128,7 +126,8 @@ class SabrGenerator(SmileGenerator):
|
|
|
128
126
|
y_set = np.asarray(nvol)
|
|
129
127
|
y_set = np.reshape(y_set, (num_samples, 1))
|
|
130
128
|
|
|
131
|
-
return x_set, y_set
|
|
129
|
+
return x_set, y_set
|
|
130
|
+
|
|
132
131
|
|
|
133
132
|
def price_surface_mod(self, model, expiries, strikes, are_calls, fwd, parameters):
|
|
134
133
|
# Retrieve parameters
|
|
@@ -4,14 +4,12 @@ import numpy as np
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import scipy.stats as sp
|
|
6
6
|
from sdevpy.analytics import bachelier
|
|
7
|
-
|
|
7
|
+
from sdevpy.machinelearning import datasets
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SmileGenerator(ABC):
|
|
11
11
|
""" Base class for smile generation """
|
|
12
12
|
def __init__(self, shift=0.0, num_expiries=15, num_strikes=10, seed=42):
|
|
13
|
-
# self.num_curve_parameters = 0
|
|
14
|
-
# self.num_vol_parameters = 0
|
|
15
13
|
self.is_call = False # Use put options by default
|
|
16
14
|
self.shift = shift
|
|
17
15
|
self.num_strikes = num_strikes
|
|
@@ -28,13 +26,22 @@ class SmileGenerator(ABC):
|
|
|
28
26
|
def price(self, expiries, strikes, are_calls, fwd, parameters):
|
|
29
27
|
""" Calculate option price under the specified model and its parameters """
|
|
30
28
|
|
|
31
|
-
@abstractmethod
|
|
32
29
|
def retrieve_datasets(self, data_file, shuffle=False):
|
|
33
30
|
""" Retrieve dataset stored in tsv file """
|
|
31
|
+
data_df = SmileGenerator.from_file(data_file, shuffle)
|
|
32
|
+
x_set, y_set = self.retrieve_datasets_from_df(data_df, False)
|
|
33
|
+
return x_set, y_set, data_df
|
|
34
|
+
|
|
35
|
+
def retrieve_datasets_from_df(self, data_df, shuffle=False):
|
|
36
|
+
""" Retrieve dataset from dataframe """
|
|
37
|
+
if shuffle:
|
|
38
|
+
data_df = datasets.shuffle_dataframe(data_df)
|
|
39
|
+
|
|
40
|
+
return self.retrieve_datasets_no_shuffle(data_df)
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def retrieve_datasets_no_shuffle(self, data_df):
|
|
44
|
+
""" Retrieve dataset from dataframe without shuffling """
|
|
38
45
|
|
|
39
46
|
def price_surface_ref(self, expiries, strikes, are_calls, fwd, parameters):
|
|
40
47
|
""" Calculate a surface of prices for given parameters using the generating model """
|
|
@@ -65,10 +72,6 @@ class SmileGenerator(ABC):
|
|
|
65
72
|
|
|
66
73
|
return strikes
|
|
67
74
|
|
|
68
|
-
# def num_parameters(self):
|
|
69
|
-
# """ Total number of parameters (curve + vol) """
|
|
70
|
-
# return self.num_curve_parameters + self.num_vol_parameters
|
|
71
|
-
|
|
72
75
|
def to_nvol(self, data_df, cleanse=True, min_vol=0.0001, max_vol=0.1):
|
|
73
76
|
""" Calculate normal implied vol and remove errors. Further remove points that are not
|
|
74
77
|
in the given min/max range """
|
|
@@ -92,20 +95,6 @@ class SmileGenerator(ABC):
|
|
|
92
95
|
except (Exception,):
|
|
93
96
|
nvol.append(-9999)
|
|
94
97
|
|
|
95
|
-
# Add test on Shifted BS
|
|
96
|
-
# shift = 0.03
|
|
97
|
-
# bsvol = []
|
|
98
|
-
# for i in range(num_samples):
|
|
99
|
-
# if i % num_print == 0:
|
|
100
|
-
# print(f"Converting to lognormal vol, batch {int(i/num_print)+1:,} out of
|
|
101
|
-
# {num_batches:,}")
|
|
102
|
-
# try:
|
|
103
|
-
# bsvol.append(black.implied_vol(t[i], strike[i] + shift, self.is_call,
|
|
104
|
-
# fwd[i] + shift, price[i]))
|
|
105
|
-
# except (Exception,):
|
|
106
|
-
# bsvol.append(-9999)
|
|
107
|
-
|
|
108
|
-
|
|
109
98
|
np.seterr(divide='warn') # Set back to warning
|
|
110
99
|
|
|
111
100
|
data_df['NVol'] = nvol
|
|
@@ -115,8 +104,6 @@ class SmileGenerator(ABC):
|
|
|
115
104
|
if cleanse:
|
|
116
105
|
data_df = data_df.drop(data_df[data_df.NVol > max_vol].index)
|
|
117
106
|
data_df = data_df.drop(data_df[data_df.NVol < min_vol].index)
|
|
118
|
-
# data_df = data_df.drop(data_df[data_df.BSVol < 0.01].index)
|
|
119
|
-
# data_df = data_df.drop(data_df[data_df.BSVol > 0.99].index)
|
|
120
107
|
|
|
121
108
|
return data_df
|
|
122
109
|
|
|
@@ -25,7 +25,9 @@ src/sdevpy/maths/interpolations.py
|
|
|
25
25
|
src/sdevpy/maths/metrics.py
|
|
26
26
|
src/sdevpy/maths/optimization.py
|
|
27
27
|
src/sdevpy/maths/rand.py
|
|
28
|
+
src/sdevpy/montecarlo/smoothers.py
|
|
28
29
|
src/sdevpy/projects/datafiles.py
|
|
30
|
+
src/sdevpy/projects/aad/aad_mc.py
|
|
29
31
|
src/sdevpy/projects/stovol/stovolgen.py
|
|
30
32
|
src/sdevpy/projects/stovol/stovolplot.py
|
|
31
33
|
src/sdevpy/projects/stovol/stovoltrain.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.9.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|