stochvolmodels 1.0.14__tar.gz → 1.0.16__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.
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/PKG-INFO +1 -1
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/pyproject.toml +1 -1
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/__init__.py +14 -6
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/data/test_option_chain.py +1 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/analytic/bsm.py +3 -0
- stochvolmodels-1.0.16/stochvolmodels/pricers/analytic/tdist.py +271 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/gmm_pricer.py +9 -10
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/model_pricer.py +4 -3
- stochvolmodels-1.0.16/stochvolmodels/pricers/tdist_pricer.py +203 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/tests/qv_pricer.py +1 -1
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/utils/funcs.py +3 -9
- stochvolmodels-1.0.14/stochvolmodels/pricers/analytic/tdist.py +0 -126
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/LICENSE.txt +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/README.md +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/data/__init__.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/data/fetch_option_chain.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/data/option_chain.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/__init__.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/analytic/__init__.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/heston_pricer.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/logsv/__init__.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/logsv_pricer.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/tests/__init__.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/utils/__init__.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/utils/config.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/utils/mc_payoffs.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/utils/mgf_pricer.py +0 -0
- {stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/utils/plots.py +0 -0
|
@@ -22,8 +22,7 @@ from stochvolmodels.utils.funcs import (
|
|
|
22
22
|
to_flat_np_array,
|
|
23
23
|
update_kwargs,
|
|
24
24
|
ncdf,
|
|
25
|
-
npdf
|
|
26
|
-
npdf1
|
|
25
|
+
npdf
|
|
27
26
|
)
|
|
28
27
|
|
|
29
28
|
from stochvolmodels.pricers.analytic.bsm import (
|
|
@@ -61,10 +60,14 @@ from stochvolmodels.pricers.analytic.bachelier import (
|
|
|
61
60
|
)
|
|
62
61
|
|
|
63
62
|
from stochvolmodels.pricers.analytic.tdist import (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
pdf_tdist,
|
|
64
|
+
cdf_tdist,
|
|
65
|
+
cum_mean_tdist,
|
|
66
|
+
imply_drift_tdist,
|
|
67
|
+
compute_default_prob_tdist,
|
|
68
|
+
compute_forward_tdist,
|
|
69
|
+
compute_vanilla_price_tdist,
|
|
70
|
+
infer_implied_vol_tdist,
|
|
68
71
|
infer_tdist_implied_vols_from_model_slice_prices
|
|
69
72
|
)
|
|
70
73
|
|
|
@@ -111,6 +114,11 @@ from stochvolmodels.pricers.gmm_pricer import (
|
|
|
111
114
|
GmmPricer
|
|
112
115
|
)
|
|
113
116
|
|
|
117
|
+
from stochvolmodels.pricers.tdist_pricer import (
|
|
118
|
+
TdistParams,
|
|
119
|
+
TdistPricer
|
|
120
|
+
)
|
|
121
|
+
|
|
114
122
|
|
|
115
123
|
from stochvolmodels.data.option_chain import OptionChain, OptionSlice
|
|
116
124
|
|
|
@@ -249,6 +249,8 @@ def compute_bsm_strike_from_delta(ttm: float,
|
|
|
249
249
|
Vega
|
|
250
250
|
****************************
|
|
251
251
|
"""
|
|
252
|
+
|
|
253
|
+
|
|
252
254
|
@njit
|
|
253
255
|
def compute_bsm_vanilla_vega(ttm: float,
|
|
254
256
|
forward: float,
|
|
@@ -308,6 +310,7 @@ Gamma
|
|
|
308
310
|
****************************
|
|
309
311
|
"""
|
|
310
312
|
|
|
313
|
+
|
|
311
314
|
@njit
|
|
312
315
|
def compute_bsm_vanilla_gamma(ttm: float,
|
|
313
316
|
forward: float,
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from scipy.stats import t
|
|
5
|
+
from scipy.special import betainc, gamma
|
|
6
|
+
from scipy.optimize import fsolve
|
|
7
|
+
from typing import Union
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_upsilon(vol: float, ttm: float, nu: float) -> float:
|
|
12
|
+
if nu <= 2.0:
|
|
13
|
+
raise ValueError(f"{nu} must be > 2.0")
|
|
14
|
+
return vol*np.sqrt(ttm*(nu-2.0)/nu)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def pdf_tdist(x: Union[np.ndarray, float], mu: float, vol: float, nu: float, ttm: float) -> Union[float, np.ndarray]:
|
|
18
|
+
upsilon = compute_upsilon(vol=vol, ttm=ttm, nu=nu)
|
|
19
|
+
z = (x - mu * ttm) / upsilon
|
|
20
|
+
c = (1.0/np.sqrt(np.pi*nu))*(gamma(0.5*(nu+1.0)) / gamma(0.5*nu))/upsilon
|
|
21
|
+
f = np.power(1.0+np.square(z) / nu, -0.5*(nu+1.0))
|
|
22
|
+
return c*f
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cdf_tdist(x: Union[np.ndarray, float], mu: float, vol: float, nu: float, ttm: float) -> Union[float, np.ndarray]:
|
|
26
|
+
"""
|
|
27
|
+
cumulative distribution of cumullative location-scale t-distribution
|
|
28
|
+
cdf = int^{x}_{-\infty} f(u)du
|
|
29
|
+
"""
|
|
30
|
+
upsilon = compute_upsilon(vol=vol, ttm=ttm, nu=nu)
|
|
31
|
+
z = (x-mu*ttm) / upsilon
|
|
32
|
+
cdf = 0.5*(1.0 + np.sign(z)*(1.0-betainc(nu/2.0, 0.5, nu/(np.square(z)+nu))))
|
|
33
|
+
return cdf
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cum_mean_tdist(x: Union[np.ndarray, float], mu: float = 0, vol: float = 0.2, nu: float = 3.0, ttm: float = 0.25
|
|
37
|
+
) -> Union[float, np.ndarray]:
|
|
38
|
+
"""
|
|
39
|
+
cumulative expected value
|
|
40
|
+
h = int^{x}_{-\infty} u f(u)du
|
|
41
|
+
"""
|
|
42
|
+
upsilon = compute_upsilon(vol=vol, ttm=ttm, nu=nu)
|
|
43
|
+
z = (x-mu*ttm) / upsilon
|
|
44
|
+
norm = (gamma(0.5*(1.0+nu)) / gamma(0.5*nu))*np.sqrt(nu/np.pi) / (1.0-nu)
|
|
45
|
+
h = mu * cdf_tdist(x, mu=mu, vol=vol, nu=nu, ttm=ttm) + upsilon * norm * np.power(1.0 + np.square(z) / nu, -0.5 * (nu - 1.0))
|
|
46
|
+
return h
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def imply_drift_tdist(rf_rate: float = 0.0, vol: float = 0.2, nu: float = 3.0, ttm: float = 0.25) -> float:
|
|
50
|
+
"""
|
|
51
|
+
imply drift of t-distribution under risk-neutral measure
|
|
52
|
+
"""
|
|
53
|
+
rf_return = (np.exp(rf_rate*ttm) - 1.0)
|
|
54
|
+
|
|
55
|
+
def func(mu: float) -> float:
|
|
56
|
+
x_star = -(1.0+ttm*mu)
|
|
57
|
+
return mu * ttm - cdf_tdist(x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm) - cum_mean_tdist(x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm) - rf_return
|
|
58
|
+
|
|
59
|
+
mu = fsolve(func, x0=rf_rate, xtol=1e-10)
|
|
60
|
+
return mu[0]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def compute_default_prob_tdist(ttm: float,
|
|
64
|
+
vol: float,
|
|
65
|
+
nu: float = 4.5,
|
|
66
|
+
rf_rate: float = 0.0
|
|
67
|
+
) -> Union[float, np.ndarray]:
|
|
68
|
+
"""
|
|
69
|
+
imply drift of t-distribution under risk-neutral measure
|
|
70
|
+
"""
|
|
71
|
+
risk_neutral_mu = imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm)
|
|
72
|
+
x_star = -(1.0+risk_neutral_mu*ttm)
|
|
73
|
+
default_prob = cdf_tdist(x=x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
74
|
+
return default_prob
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_forward_tdist(spot: Union[float, np.ndarray],
|
|
78
|
+
ttm: float,
|
|
79
|
+
vol: float,
|
|
80
|
+
nu: float = 4.5,
|
|
81
|
+
rf_rate: float = 0.0
|
|
82
|
+
) -> Union[float, np.ndarray]:
|
|
83
|
+
"""
|
|
84
|
+
imply drift of t-distribution under risk-neutral measure
|
|
85
|
+
"""
|
|
86
|
+
risk_neutral_mu = imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm)
|
|
87
|
+
x_star = -(1.0+risk_neutral_mu*ttm)
|
|
88
|
+
c_1 = cdf_tdist(x=x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
89
|
+
h_1 = cum_mean_tdist(x=x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
90
|
+
forward = spot * ((1.0 + risk_neutral_mu*ttm)*(1.0-c_1)-h_1)
|
|
91
|
+
return forward
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def compute_vanilla_price_tdist(spot: Union[float, np.ndarray],
|
|
95
|
+
strikes: Union[float, np.ndarray],
|
|
96
|
+
ttm: float,
|
|
97
|
+
vol: float,
|
|
98
|
+
nu: float = 4.5,
|
|
99
|
+
optiontypes: Union[str, np.ndarray] = 'C',
|
|
100
|
+
rf_rate: float = 0.0,
|
|
101
|
+
is_compute_risk_neutral_mu: bool = True
|
|
102
|
+
) -> Union[float, np.ndarray]:
|
|
103
|
+
"""
|
|
104
|
+
option pricer for t-distribution
|
|
105
|
+
"""
|
|
106
|
+
discfactor = np.exp(-rf_rate*ttm)
|
|
107
|
+
if is_compute_risk_neutral_mu:
|
|
108
|
+
risk_neutral_mu = imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm)
|
|
109
|
+
else:
|
|
110
|
+
risk_neutral_mu = rf_rate
|
|
111
|
+
spot_star = spot*(1.0 + risk_neutral_mu*ttm)
|
|
112
|
+
x_lower_bound = -1.0-risk_neutral_mu*ttm
|
|
113
|
+
|
|
114
|
+
def compute(strike_: Union[float, np.ndarray], optiontype_: str) -> float:
|
|
115
|
+
y = strike_ / spot - (1.0 + risk_neutral_mu*ttm)
|
|
116
|
+
c_y = cdf_tdist(x=y, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
117
|
+
h_y = cum_mean_tdist(x=y, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
118
|
+
if optiontype_ == 'C' or optiontype_ == 'IC':
|
|
119
|
+
price_ = (-spot * h_y + (spot_star-strike_)*(1.0-c_y))
|
|
120
|
+
elif optiontype_ == 'P' or optiontype_ == 'IP':
|
|
121
|
+
c_1 = cdf_tdist(x=x_lower_bound, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
122
|
+
h_1 = cum_mean_tdist(x=x_lower_bound, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
123
|
+
price_ = discfactor * ((strike_ - spot_star) * (c_y - c_1) - spot * (h_y - h_1)+strike_*c_1)
|
|
124
|
+
else:
|
|
125
|
+
raise NotImplementedError(f"optiontype")
|
|
126
|
+
return price_
|
|
127
|
+
|
|
128
|
+
if isinstance(optiontypes, str):
|
|
129
|
+
price = compute(strikes, optiontypes)
|
|
130
|
+
else:
|
|
131
|
+
price = np.zeros_like(strikes)
|
|
132
|
+
for idx, (strike_, optiontype_) in enumerate(zip(strikes, optiontypes)):
|
|
133
|
+
price[idx] = compute(strike_, optiontype_)
|
|
134
|
+
return price
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def infer_implied_vol_tdist(spot: float,
|
|
138
|
+
ttm: float,
|
|
139
|
+
strike: float,
|
|
140
|
+
given_price: float,
|
|
141
|
+
rf_rate: float = 0.0,
|
|
142
|
+
optiontype: str = 'C',
|
|
143
|
+
nu: float = 4.5,
|
|
144
|
+
tol: float = 1e-12,
|
|
145
|
+
is_bounds_to_nan: bool = False
|
|
146
|
+
) -> float:
|
|
147
|
+
"""
|
|
148
|
+
compute normal implied vol
|
|
149
|
+
"""
|
|
150
|
+
x1, x2 = 0.05, 10.0 # starting values
|
|
151
|
+
f = compute_vanilla_price_tdist(spot=spot, strikes=strike, ttm=ttm, vol=x1, nu=nu, rf_rate=rf_rate, optiontypes=optiontype) - given_price
|
|
152
|
+
fmid = compute_vanilla_price_tdist(spot=spot, strikes=strike, ttm=ttm, vol=x2, nu=nu, rf_rate=rf_rate, optiontypes=optiontype) - given_price
|
|
153
|
+
if f*fmid < 0.0:
|
|
154
|
+
if f < 0.0:
|
|
155
|
+
rtb = x1
|
|
156
|
+
dx = x2-x1
|
|
157
|
+
else:
|
|
158
|
+
rtb = x2
|
|
159
|
+
dx = x1-x2
|
|
160
|
+
xmid = rtb
|
|
161
|
+
for j in range(0, 100):
|
|
162
|
+
dx = dx*0.5
|
|
163
|
+
xmid = rtb+dx
|
|
164
|
+
fmid = compute_vanilla_price_tdist(spot=spot, strikes=strike, ttm=ttm, vol=xmid, nu=nu, rf_rate=rf_rate, optiontypes=optiontype) - given_price
|
|
165
|
+
if fmid <= 0.0:
|
|
166
|
+
rtb = xmid
|
|
167
|
+
if np.abs(fmid) < tol:
|
|
168
|
+
break
|
|
169
|
+
v1 = xmid
|
|
170
|
+
else:
|
|
171
|
+
if f < 0:
|
|
172
|
+
v1 = x1
|
|
173
|
+
else:
|
|
174
|
+
v1 = x2
|
|
175
|
+
if is_bounds_to_nan: # in case vol was inferred it will return nan
|
|
176
|
+
if np.abs(v1-x1) < tol or np.abs(v1-x2) < tol:
|
|
177
|
+
v1 = np.nan
|
|
178
|
+
return v1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def infer_tdist_implied_vols_from_model_slice_prices(ttm: float,
|
|
182
|
+
spot: float,
|
|
183
|
+
strikes: np.ndarray,
|
|
184
|
+
optiontypes: np.ndarray,
|
|
185
|
+
model_prices: np.ndarray,
|
|
186
|
+
rf_rate: float,
|
|
187
|
+
nu: float
|
|
188
|
+
) -> np.ndarray:
|
|
189
|
+
model_vol_ttm = np.zeros_like(strikes)
|
|
190
|
+
for idx, (strike, model_price, optiontype) in enumerate(zip(strikes, model_prices, optiontypes)):
|
|
191
|
+
model_vol_ttm[idx] = infer_implied_vol_tdist(spot=spot, ttm=ttm, rf_rate=rf_rate,
|
|
192
|
+
given_price=model_price,
|
|
193
|
+
strike=strike,
|
|
194
|
+
optiontype=optiontype,
|
|
195
|
+
nu=nu)
|
|
196
|
+
return model_vol_ttm
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class UnitTests(Enum):
|
|
200
|
+
PLOT_PDF = 1
|
|
201
|
+
PLOT_CDF = 2
|
|
202
|
+
PLOT_CUM_X = 3
|
|
203
|
+
PLOT_H = 4
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def run_unit_test(unit_test: UnitTests):
|
|
207
|
+
|
|
208
|
+
import qis as qis
|
|
209
|
+
|
|
210
|
+
x = np.linspace(-5.0, 5.0, 20000)
|
|
211
|
+
dx = x[1] - x[0]
|
|
212
|
+
ttm = 1.0
|
|
213
|
+
mu_vols = {'mu=0.0, vol=0.2': (0.0, 0.2),
|
|
214
|
+
'mu=0.2, vol=0.2': (0.2, 0.2),
|
|
215
|
+
'mu=0.2, vol=0.4': (0.2, 0.4)}
|
|
216
|
+
|
|
217
|
+
if unit_test == UnitTests.PLOT_PDF:
|
|
218
|
+
pdfs = {}
|
|
219
|
+
for key, mu_vol in mu_vols.items():
|
|
220
|
+
pdf = dx * pdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
221
|
+
pdfs[key] = pd.Series(pdf, index=x)
|
|
222
|
+
print(f"{key}: sum={np.sum(pdf)}, mean={np.sum(x*pdf)}, std={np.sqrt(np.sum(np.square(x)*pdf)-np.square(np.sum(x*pdf)))}")
|
|
223
|
+
pdfs = pd.DataFrame.from_dict(pdfs, orient='columns')
|
|
224
|
+
qis.plot_line(df=pdfs)
|
|
225
|
+
|
|
226
|
+
elif unit_test == UnitTests.PLOT_CDF:
|
|
227
|
+
pdfs = {}
|
|
228
|
+
cpdfs = {}
|
|
229
|
+
for key, mu_vol in mu_vols.items():
|
|
230
|
+
pdf = dx * pdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
231
|
+
cpdf = cdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
232
|
+
pdfs[f"{key}_pdf_sum"] = pd.Series(np.cumsum(pdf), index=x)
|
|
233
|
+
cpdfs[f"{key}_cdf"] = pd.Series(cpdf, index=x)
|
|
234
|
+
pdfs = pd.DataFrame.from_dict(pdfs, orient='columns')
|
|
235
|
+
cpdfs = pd.DataFrame.from_dict(cpdfs, orient='columns')
|
|
236
|
+
df = pd.concat([pdfs, cpdfs], axis=1)
|
|
237
|
+
colors = qis.get_n_colors(n=len(mu_vols.keys()))
|
|
238
|
+
qis.plot_line(df=df, colors=2*colors)
|
|
239
|
+
|
|
240
|
+
elif unit_test == UnitTests.PLOT_CUM_X:
|
|
241
|
+
pdfs = {}
|
|
242
|
+
cpdfs = {}
|
|
243
|
+
for key, mu_vol in mu_vols.items():
|
|
244
|
+
pdf = dx * pdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
245
|
+
cpdf = cum_mean_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
246
|
+
pdfs[f"{key}_h_pdf_sum"] = pd.Series(np.cumsum(x*pdf), index=x)
|
|
247
|
+
cpdfs[f"{key}_t_h"] = pd.Series(cpdf, index=x)
|
|
248
|
+
pdfs = pd.DataFrame.from_dict(pdfs, orient='columns')
|
|
249
|
+
cpdfs = pd.DataFrame.from_dict(cpdfs, orient='columns')
|
|
250
|
+
df = pd.concat([pdfs, cpdfs], axis=1)
|
|
251
|
+
colors = qis.get_n_colors(n=len(mu_vols.keys()))
|
|
252
|
+
qis.plot_line(df=df, colors=2*colors)
|
|
253
|
+
|
|
254
|
+
elif unit_test == UnitTests.PLOT_H:
|
|
255
|
+
x = np.linspace(-10.0, 10.0, 2000)
|
|
256
|
+
h = pd.Series(cum_mean_tdist(x=x, mu=0.5, vol=1.0, nu=3.0, ttm=1.0), index=x, name='h')
|
|
257
|
+
qis.plot_line(df=h, xlabel='x')
|
|
258
|
+
|
|
259
|
+
plt.show()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == '__main__':
|
|
263
|
+
|
|
264
|
+
unit_test = UnitTests.PLOT_CUM_X
|
|
265
|
+
|
|
266
|
+
is_run_all_tests = False
|
|
267
|
+
if is_run_all_tests:
|
|
268
|
+
for unit_test in UnitTests:
|
|
269
|
+
run_unit_test(unit_test=unit_test)
|
|
270
|
+
else:
|
|
271
|
+
run_unit_test(unit_test=unit_test)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
implementation of gaussian mixture pricer
|
|
2
|
+
implementation of gaussian mixture pricer and calibration
|
|
3
3
|
"""
|
|
4
4
|
import numpy as np
|
|
5
5
|
import matplotlib.pyplot as plt
|
|
@@ -10,14 +10,11 @@ from numba.typed import List
|
|
|
10
10
|
from typing import Tuple
|
|
11
11
|
from enum import Enum
|
|
12
12
|
|
|
13
|
-
from stochvolmodels.utils.funcs import to_flat_np_array, timer
|
|
13
|
+
from stochvolmodels.utils.funcs import to_flat_np_array, timer
|
|
14
14
|
import stochvolmodels.pricers.analytic.bsm as bsm
|
|
15
15
|
from stochvolmodels.pricers.model_pricer import ModelParams, ModelPricer
|
|
16
16
|
from stochvolmodels.utils.config import VariableType
|
|
17
|
-
|
|
18
|
-
# data
|
|
19
17
|
from stochvolmodels.data.option_chain import OptionChain
|
|
20
|
-
from stochvolmodels.data.test_option_chain import get_btc_test_chain_data
|
|
21
18
|
|
|
22
19
|
|
|
23
20
|
@dataclass
|
|
@@ -40,7 +37,7 @@ class GmmParams(ModelParams):
|
|
|
40
37
|
state_pdfs = np.zeros((len(x), len(self.gmm_weights)))
|
|
41
38
|
agg_pdf = np.zeros_like(x)
|
|
42
39
|
for idx, (gmm_weight, mu, vol) in enumerate(zip(self.gmm_weights, self.gmm_mus, self.gmm_vols)):
|
|
43
|
-
state_pdf =
|
|
40
|
+
state_pdf = npdf(x, mu=mu*self.ttm, vol=vol*np.sqrt(self.ttm))
|
|
44
41
|
state_pdfs[:, idx] = state_pdf
|
|
45
42
|
agg_pdf += gmm_weight*state_pdf
|
|
46
43
|
return state_pdfs, agg_pdf
|
|
@@ -48,7 +45,7 @@ class GmmParams(ModelParams):
|
|
|
48
45
|
def compute_pdf(self, x: np.ndarray):
|
|
49
46
|
pdfs = np.zeros_like(x)
|
|
50
47
|
for gmm_weight, mu, vol in zip(self.gmm_weights, self.gmm_mus, self.gmm_vols):
|
|
51
|
-
pdfs = pdfs + gmm_weight*
|
|
48
|
+
pdfs = pdfs + gmm_weight*npdf(x, mu=mu*self.ttm, vol=vol*np.sqrt(self.ttm))
|
|
52
49
|
return pdfs
|
|
53
50
|
|
|
54
51
|
|
|
@@ -95,7 +92,6 @@ class GmmPricer(ModelPricer):
|
|
|
95
92
|
if len(ttms) > 1:
|
|
96
93
|
raise NotImplementedError(f"cannot calibrate to multiple slices")
|
|
97
94
|
ttm = ttms[0]
|
|
98
|
-
discfactor = option_chain.discfactors[0]
|
|
99
95
|
|
|
100
96
|
# p0 = (gmm_weights, gmm_mus, gmm_vols)
|
|
101
97
|
if params0 is not None:
|
|
@@ -138,11 +134,12 @@ class GmmPricer(ModelPricer):
|
|
|
138
134
|
return np.sum(params.gmm_weights) - 1.0
|
|
139
135
|
|
|
140
136
|
def martingale(pars: np.ndarray) -> float:
|
|
137
|
+
# we set to 1.0, mutplication with foward will be set by pricing
|
|
141
138
|
params = parse_model_params(pars=pars)
|
|
142
|
-
return np.sum(params.gmm_weights*np.exp((params.gmm_mus+0.5*params.gmm_vols*params.gmm_vols)*ttm)) -
|
|
139
|
+
return np.sum(params.gmm_weights*np.exp((params.gmm_mus+0.5*params.gmm_vols*params.gmm_vols)*ttm)) - 1.0
|
|
143
140
|
|
|
144
141
|
constraints = ({'type': 'eq', 'fun': weights_sum}, {'type': 'eq', 'fun': martingale})
|
|
145
|
-
options = {'disp': True, 'ftol': 1e-10, 'maxiter':
|
|
142
|
+
options = {'disp': True, 'ftol': 1e-10, 'maxiter': 500}
|
|
146
143
|
|
|
147
144
|
res = minimize(objective, p0, args=None, method='SLSQP', constraints=constraints, bounds=bounds, options=options)
|
|
148
145
|
fit_params = parse_model_params(pars=res.x)
|
|
@@ -175,6 +172,7 @@ class GmmPricer(ModelPricer):
|
|
|
175
172
|
fit_params[ids_] = params0
|
|
176
173
|
return fit_params
|
|
177
174
|
|
|
175
|
+
|
|
178
176
|
@njit
|
|
179
177
|
def compute_gmm_vanilla_price(gmm_weights: np.ndarray,
|
|
180
178
|
gmm_mus: np.ndarray,
|
|
@@ -269,6 +267,7 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
269
267
|
|
|
270
268
|
import seaborn as sns
|
|
271
269
|
import qis as qis
|
|
270
|
+
from stochvolmodels.data.test_option_chain import get_btc_test_chain_data
|
|
272
271
|
|
|
273
272
|
if unit_test == UnitTests.CALIBRATOR:
|
|
274
273
|
option_chain = get_btc_test_chain_data()
|
|
@@ -324,6 +324,7 @@ class ModelPricer(ABC):
|
|
|
324
324
|
headers: Optional[List[str]] = None,
|
|
325
325
|
xvar_format: str = None,
|
|
326
326
|
figsize: Tuple[float, float] = plot.FIGSIZE,
|
|
327
|
+
title: str = None,
|
|
327
328
|
axs: List[plt.Subplot] = None,
|
|
328
329
|
**kwargs
|
|
329
330
|
) -> plt.Figure:
|
|
@@ -369,11 +370,11 @@ class ModelPricer(ABC):
|
|
|
369
370
|
model_vols = pd.Series(model_ivols[idx], index=strikes, name=f"Model Fit: mse={mse2:0.2%}")
|
|
370
371
|
if option_chain.ids is not None:
|
|
371
372
|
if headers is not None:
|
|
372
|
-
title = f"{headers[idx]} slice - {option_chain.ids[idx]}"
|
|
373
|
+
title = title or f"{headers[idx]} slice - {option_chain.ids[idx]}"
|
|
373
374
|
else:
|
|
374
|
-
title = f"Slice - {option_chain.ids[idx]}"
|
|
375
|
+
title = title or f"Slice - {option_chain.ids[idx]}"
|
|
375
376
|
else:
|
|
376
|
-
title = f"{ttm=:0.2f}"
|
|
377
|
+
title = title or f"{ttm=:0.2f}"
|
|
377
378
|
|
|
378
379
|
if is_log_strike_xaxis:
|
|
379
380
|
atm_points = {'ATM': (0.0, atm_vols[idx])}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
implementation of gaussian mixture pricer and calibration
|
|
3
|
+
"""
|
|
4
|
+
import numpy as np
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from scipy.optimize import minimize
|
|
8
|
+
from numba.typed import List
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
# sv
|
|
13
|
+
import stochvolmodels.pricers.analytic.tdist as td
|
|
14
|
+
from stochvolmodels.utils.funcs import to_flat_np_array, timer
|
|
15
|
+
from stochvolmodels.pricers.model_pricer import ModelParams, ModelPricer
|
|
16
|
+
from stochvolmodels.utils.config import VariableType
|
|
17
|
+
|
|
18
|
+
# data
|
|
19
|
+
from stochvolmodels.data.option_chain import OptionChain
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TdistParams(ModelParams):
|
|
24
|
+
drift: float
|
|
25
|
+
vol: float
|
|
26
|
+
nu: float
|
|
27
|
+
ttm: float # ttm is important as all params are fixed to this ttm, it is not part of calibration
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TdistPricer(ModelPricer):
|
|
31
|
+
|
|
32
|
+
def price_chain(self, option_chain: OptionChain, params: TdistParams, **kwargs) -> np.ndarray:
|
|
33
|
+
"""
|
|
34
|
+
implementation of generic method price_chain using heston wrapper for tdist prices
|
|
35
|
+
"""
|
|
36
|
+
model_prices_ttms = tdist_vanilla_chain_pricer(drift=params.drift,
|
|
37
|
+
vol=params.vol,
|
|
38
|
+
nu=params.nu,
|
|
39
|
+
ttms=option_chain.ttms,
|
|
40
|
+
forwards=option_chain.forwards,
|
|
41
|
+
strikes_ttms=option_chain.strikes_ttms,
|
|
42
|
+
optiontypes_ttms=option_chain.optiontypes_ttms,
|
|
43
|
+
discfactors=option_chain.discfactors)
|
|
44
|
+
|
|
45
|
+
return model_prices_ttms
|
|
46
|
+
|
|
47
|
+
def model_mc_price_chain(self, option_chain: OptionChain, params: TdistParams,
|
|
48
|
+
nb_path: int = 100000,
|
|
49
|
+
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
50
|
+
**kwargs
|
|
51
|
+
) -> (List[np.ndarray], List[np.ndarray]):
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@timer
|
|
55
|
+
def calibrate_model_params_to_chain_slice(self,
|
|
56
|
+
option_chain: OptionChain,
|
|
57
|
+
params0: TdistParams = None,
|
|
58
|
+
is_vega_weighted: bool = True,
|
|
59
|
+
is_unit_ttm_vega: bool = False,
|
|
60
|
+
**kwargs
|
|
61
|
+
) -> TdistParams:
|
|
62
|
+
"""
|
|
63
|
+
implementation of model calibration interface
|
|
64
|
+
fit: TdistParams
|
|
65
|
+
nb: always use option_chain with one slice because we need martingale condition per slice
|
|
66
|
+
"""
|
|
67
|
+
ttms = option_chain.ttms
|
|
68
|
+
if len(ttms) > 1:
|
|
69
|
+
raise NotImplementedError(f"cannot calibrate to multiple slices")
|
|
70
|
+
ttm = ttms[0]
|
|
71
|
+
rf_rate = option_chain.discount_rates[0]
|
|
72
|
+
|
|
73
|
+
# p0 = (gmm_weights, gmm_mus, gmm_vols)
|
|
74
|
+
if params0 is not None:
|
|
75
|
+
p0 = np.array([params0.vol, params0.nu])
|
|
76
|
+
else:
|
|
77
|
+
p0 = np.array([0.2, 3.0])
|
|
78
|
+
|
|
79
|
+
vol_bounds = [(0.05, 10.0)]
|
|
80
|
+
nu_bounds = [(2.01, 20.0)]
|
|
81
|
+
bounds = np.concatenate((vol_bounds, nu_bounds))
|
|
82
|
+
|
|
83
|
+
x, y = option_chain.get_chain_data_as_xy()
|
|
84
|
+
market_vols = to_flat_np_array(y) # market mid quotes
|
|
85
|
+
if is_vega_weighted:
|
|
86
|
+
vegas_ttms = option_chain.get_chain_vegas(is_unit_ttm_vega=is_unit_ttm_vega)
|
|
87
|
+
vegas_ttms = [vegas_ttm/sum(vegas_ttm) for vegas_ttm in vegas_ttms]
|
|
88
|
+
weights = to_flat_np_array(vegas_ttms)
|
|
89
|
+
else:
|
|
90
|
+
weights = np.ones_like(market_vols)
|
|
91
|
+
|
|
92
|
+
def parse_model_params(pars: np.ndarray) -> TdistParams:
|
|
93
|
+
vol = pars[0]
|
|
94
|
+
nu = pars[1]
|
|
95
|
+
drift = td.imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm)
|
|
96
|
+
return TdistParams(vol=vol, nu=nu, drift=drift, ttm=ttm)
|
|
97
|
+
|
|
98
|
+
def objective(pars: np.ndarray, args: np.ndarray) -> float:
|
|
99
|
+
params = parse_model_params(pars=pars)
|
|
100
|
+
model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params)
|
|
101
|
+
resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
|
|
102
|
+
return resid
|
|
103
|
+
|
|
104
|
+
options = {'disp': True, 'ftol': 1e-10, 'maxiter': 500}
|
|
105
|
+
res = minimize(objective, p0, args=None, method='SLSQP', bounds=bounds, options=options)
|
|
106
|
+
fit_params = parse_model_params(pars=res.x)
|
|
107
|
+
|
|
108
|
+
return fit_params
|
|
109
|
+
|
|
110
|
+
@timer
|
|
111
|
+
def calibrate_model_params_to_chain(self,
|
|
112
|
+
option_chain: OptionChain,
|
|
113
|
+
is_vega_weighted: bool = True,
|
|
114
|
+
is_unit_ttm_vega: bool = False,
|
|
115
|
+
**kwargs
|
|
116
|
+
) -> List[str, TdistParams]:
|
|
117
|
+
"""
|
|
118
|
+
model params are fitted per slice
|
|
119
|
+
need to splic chain to slices
|
|
120
|
+
"""
|
|
121
|
+
fit_params = {}
|
|
122
|
+
params0 = None
|
|
123
|
+
for ids_ in option_chain.ids:
|
|
124
|
+
option_chain0 = OptionChain.get_slices_as_chain(option_chain, ids=[ids_])
|
|
125
|
+
params0 = self.calibrate_model_params_to_chain_slice(option_chain=option_chain0,
|
|
126
|
+
params0=params0,
|
|
127
|
+
is_vega_weighted=is_vega_weighted,
|
|
128
|
+
is_unit_ttm_vega=is_unit_ttm_vega,
|
|
129
|
+
**kwargs)
|
|
130
|
+
fit_params[ids_] = params0
|
|
131
|
+
return fit_params
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def tdist_vanilla_chain_pricer(vol: float,
|
|
135
|
+
nu: float,
|
|
136
|
+
drift: float,
|
|
137
|
+
ttms: np.ndarray,
|
|
138
|
+
forwards: np.ndarray,
|
|
139
|
+
strikes_ttms: Tuple[np.ndarray, ...],
|
|
140
|
+
optiontypes_ttms: Tuple[np.ndarray, ...],
|
|
141
|
+
discfactors: np.ndarray,
|
|
142
|
+
) -> np.ndarray:
|
|
143
|
+
"""
|
|
144
|
+
vectorised bsm deltas for array of aligned strikes, vols, and optiontypes
|
|
145
|
+
"""
|
|
146
|
+
# outputs as numpy lists
|
|
147
|
+
model_prices_ttms = List()
|
|
148
|
+
for ttm, forward, discfactor, strikes_ttm, optiontypes_ttm in zip(ttms, forwards, discfactors, strikes_ttms,
|
|
149
|
+
optiontypes_ttms):
|
|
150
|
+
option_prices_ttm = td.compute_vanilla_price_tdist(spot=forward*discfactor,
|
|
151
|
+
strikes=strikes_ttm,
|
|
152
|
+
ttm=ttm,
|
|
153
|
+
vol=vol,
|
|
154
|
+
nu=nu,
|
|
155
|
+
optiontypes=optiontypes_ttm,
|
|
156
|
+
rf_rate=drift,
|
|
157
|
+
is_compute_risk_neutral_mu=False # drift is already adjusted
|
|
158
|
+
)
|
|
159
|
+
model_prices_ttms.append(option_prices_ttm)
|
|
160
|
+
|
|
161
|
+
return model_prices_ttms
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class UnitTests(Enum):
|
|
165
|
+
CALIBRATOR = 1
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def run_unit_test(unit_test: UnitTests):
|
|
169
|
+
|
|
170
|
+
import seaborn as sns
|
|
171
|
+
import qis as qis
|
|
172
|
+
import stochvolmodels.data.test_option_chain as chains
|
|
173
|
+
|
|
174
|
+
if unit_test == UnitTests.CALIBRATOR:
|
|
175
|
+
# option_chain = chains.get_btc_test_chain_data()
|
|
176
|
+
option_chain = chains.get_spy_test_chain_data()
|
|
177
|
+
# option_chain = chains.get_gld_test_chain_data()
|
|
178
|
+
|
|
179
|
+
tdist_pricer = TdistPricer()
|
|
180
|
+
fit_params = tdist_pricer.calibrate_model_params_to_chain(option_chain=option_chain)
|
|
181
|
+
|
|
182
|
+
with sns.axes_style('darkgrid'):
|
|
183
|
+
fig, axs = plt.subplots(2, 2, figsize=(14, 12), tight_layout=True)
|
|
184
|
+
axs = qis.to_flat_list(axs)
|
|
185
|
+
|
|
186
|
+
for idx, (key, params) in enumerate(fit_params.items()):
|
|
187
|
+
print(f"{key}: {params}")
|
|
188
|
+
option_chain0 = OptionChain.get_slices_as_chain(option_chain, ids=[key])
|
|
189
|
+
tdist_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain0, params=params, axs=[axs[idx]])
|
|
190
|
+
|
|
191
|
+
plt.show()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == '__main__':
|
|
195
|
+
|
|
196
|
+
unit_test = UnitTests.CALIBRATOR
|
|
197
|
+
|
|
198
|
+
is_run_all_tests = False
|
|
199
|
+
if is_run_all_tests:
|
|
200
|
+
for unit_test in UnitTests:
|
|
201
|
+
run_unit_test(unit_test=unit_test)
|
|
202
|
+
else:
|
|
203
|
+
run_unit_test(unit_test=unit_test)
|
|
@@ -6,7 +6,7 @@ import numpy as np
|
|
|
6
6
|
import matplotlib.pyplot as plt
|
|
7
7
|
from enum import Enum
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import stochvolmodels.utils.mgf_pricer as mgfp
|
|
10
10
|
from stochvolmodels.pricers.logsv import affine_expansion as afe
|
|
11
11
|
from stochvolmodels.utils.config import VariableType
|
|
12
12
|
from stochvolmodels.pricers.logsv_pricer import LogSVPricer, LogSvParams
|
|
@@ -5,7 +5,7 @@ import functools
|
|
|
5
5
|
import time
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
|
-
from numba import njit
|
|
8
|
+
from numba import njit
|
|
9
9
|
from numba.typed import List
|
|
10
10
|
from typing import Tuple, Dict, Any, Optional, Union
|
|
11
11
|
|
|
@@ -92,11 +92,5 @@ def ncdf(x: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
@njit(cache=False, fastmath=True)
|
|
95
|
-
def npdf(x: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
96
|
-
return np.exp(-0.5*np.square(x))/np.sqrt(2.0*np.pi)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@njit(cache=False, fastmath=True)
|
|
100
|
-
def npdf1(x: Union[float, np.ndarray], mu: float, vol: float) -> Union[float, np.ndarray]:
|
|
101
|
-
vol2 = vol*vol
|
|
102
|
-
return np.exp(-0.5*np.square(x-mu)/vol2)/np.sqrt(2.0*np.pi*vol2)
|
|
95
|
+
def npdf(x: Union[float, np.ndarray], mu: float = 0.0, vol: float = 1.0) -> Union[float, np.ndarray]:
|
|
96
|
+
return np.exp(-0.5*np.square((x-mu)/vol))/(vol*np.sqrt(2.0*np.pi))
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from scipy.stats import t
|
|
3
|
-
from scipy.special import gamma
|
|
4
|
-
from typing import Union
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def t_cum(y: float, nu: float) -> float:
|
|
8
|
-
"""
|
|
9
|
-
cumulative pdf ot t-distribution
|
|
10
|
-
"""
|
|
11
|
-
c = (1.0/np.sqrt(np.pi*nu))* (nu/(nu-1.0)) * gamma(0.5*(nu+1.0)) / gamma(0.5*nu)
|
|
12
|
-
f = np.power(1.0+np.square(y) / nu, -0.5*(nu-1.0))
|
|
13
|
-
return c * f
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def compute_tdist_price(forward: Union[float, np.ndarray],
|
|
17
|
-
strikes: Union[float, np.ndarray],
|
|
18
|
-
ttm: float,
|
|
19
|
-
vol: float,
|
|
20
|
-
nu: float = 4.5,
|
|
21
|
-
optiontypes: Union[str, np.ndarray] = 'C',
|
|
22
|
-
discfactor: float = 1.0
|
|
23
|
-
) -> Union[float, np.ndarray]:
|
|
24
|
-
"""
|
|
25
|
-
bsm pricer for forward
|
|
26
|
-
"""
|
|
27
|
-
# scaler = vol*np.sqrt(ttm)*np.sqrt(0.5*nu)*((nu-1.0)/nu)*gamma(0.5*nu)/gamma(0.5*(nu+1.0))
|
|
28
|
-
ups = vol * np.sqrt(ttm) * np.sqrt((nu - 2.0) / nu)
|
|
29
|
-
|
|
30
|
-
def compute(strike_: Union[float, np.ndarray], optiontype_: str) -> float:
|
|
31
|
-
y = strike_ / forward - 1.0
|
|
32
|
-
if optiontype_ == 'C' or optiontype_ == 'IC':
|
|
33
|
-
price_ = discfactor * (forward * t_cum(y/ups, nu=nu) * ups + (forward - strike_) * (1.0 - t.cdf(y/ups, nu)))
|
|
34
|
-
elif optiontype_ == 'P' or optiontype_ == 'IP':
|
|
35
|
-
price_ = discfactor * (forward * t_cum(y/ups, nu=nu) * ups - (forward - strike_) * t.cdf(y/ups, nu))
|
|
36
|
-
else:
|
|
37
|
-
raise NotImplementedError(f"optiontype")
|
|
38
|
-
return price_
|
|
39
|
-
|
|
40
|
-
if isinstance(optiontypes, str):
|
|
41
|
-
price = compute(strikes, optiontypes)
|
|
42
|
-
else:
|
|
43
|
-
price = np.zeros_like(strikes)
|
|
44
|
-
for idx, (strike_, optiontype_) in enumerate(zip(strikes, optiontypes)):
|
|
45
|
-
price[idx] = compute(strike_, optiontype_)
|
|
46
|
-
return price
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def compute_compute_negative_prob(ttms: np.ndarray,
|
|
50
|
-
vol: float,
|
|
51
|
-
drift: float = 0.0,
|
|
52
|
-
nu: float = 3.5
|
|
53
|
-
) -> Union[float, np.ndarray]:
|
|
54
|
-
"""
|
|
55
|
-
bsm pricer for forward
|
|
56
|
-
"""
|
|
57
|
-
probs = np.zeros_like(ttms)
|
|
58
|
-
q = vol*np.sqrt(0.5*nu)*((nu-1.0)/nu)*gamma(0.5*nu)/gamma(0.5*(nu+1.0))
|
|
59
|
-
for idx, ttm in enumerate(ttms):
|
|
60
|
-
scaler = q*np.sqrt(ttm)
|
|
61
|
-
ups = 1.0 / scaler
|
|
62
|
-
probs[idx] = t.cdf(-(1.0+drift*ttm)*ups, nu)
|
|
63
|
-
return probs
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def infer_tdist_implied_vol(forward: float,
|
|
67
|
-
ttm: float,
|
|
68
|
-
strike: float,
|
|
69
|
-
given_price: float,
|
|
70
|
-
discfactor: float = 1.0,
|
|
71
|
-
optiontype: str = 'C',
|
|
72
|
-
nu: float = 4.5,
|
|
73
|
-
tol: float = 1e-12,
|
|
74
|
-
is_bounds_to_nan: bool = False
|
|
75
|
-
) -> float:
|
|
76
|
-
"""
|
|
77
|
-
compute normal implied vol
|
|
78
|
-
"""
|
|
79
|
-
x1, x2 = 0.05, 10.0 # starting values
|
|
80
|
-
f = compute_tdist_price(forward=forward, strikes=strike, ttm=ttm, vol=x1, nu=nu, discfactor=discfactor, optiontypes=optiontype) - given_price
|
|
81
|
-
fmid = compute_tdist_price(forward=forward, strikes=strike, ttm=ttm, vol=x2, nu=nu, discfactor=discfactor, optiontypes=optiontype) - given_price
|
|
82
|
-
if f*fmid < 0.0:
|
|
83
|
-
if f < 0.0:
|
|
84
|
-
rtb = x1
|
|
85
|
-
dx = x2-x1
|
|
86
|
-
else:
|
|
87
|
-
rtb = x2
|
|
88
|
-
dx = x1-x2
|
|
89
|
-
xmid = rtb
|
|
90
|
-
for j in range(0, 100):
|
|
91
|
-
dx = dx*0.5
|
|
92
|
-
xmid = rtb+dx
|
|
93
|
-
fmid = compute_tdist_price(forward=forward, strikes=strike, ttm=ttm, vol=xmid, nu=nu, discfactor=discfactor, optiontypes=optiontype) - given_price
|
|
94
|
-
if fmid <= 0.0:
|
|
95
|
-
rtb = xmid
|
|
96
|
-
if np.abs(fmid) < tol:
|
|
97
|
-
break
|
|
98
|
-
v1 = xmid
|
|
99
|
-
else:
|
|
100
|
-
if f < 0:
|
|
101
|
-
v1 = x1
|
|
102
|
-
else:
|
|
103
|
-
v1 = x2
|
|
104
|
-
if is_bounds_to_nan: # in case vol was inferred it will return nan
|
|
105
|
-
if np.abs(v1-x1) < tol or np.abs(v1-x2) < tol:
|
|
106
|
-
v1 = np.nan
|
|
107
|
-
return v1
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def infer_tdist_implied_vols_from_model_slice_prices(ttm: float,
|
|
111
|
-
forward: float,
|
|
112
|
-
strikes: np.ndarray,
|
|
113
|
-
optiontypes: np.ndarray,
|
|
114
|
-
model_prices: np.ndarray,
|
|
115
|
-
discfactor: float,
|
|
116
|
-
nu: float
|
|
117
|
-
) -> np.ndarray:
|
|
118
|
-
model_vol_ttm = np.zeros_like(strikes)
|
|
119
|
-
for idx, (strike, model_price, optiontype) in enumerate(zip(strikes, model_prices, optiontypes)):
|
|
120
|
-
model_vol_ttm[idx] = infer_tdist_implied_vol(forward=forward, ttm=ttm, discfactor=discfactor,
|
|
121
|
-
given_price=model_price,
|
|
122
|
-
strike=strike,
|
|
123
|
-
optiontype=optiontype,
|
|
124
|
-
nu=nu)
|
|
125
|
-
return model_vol_ttm
|
|
126
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/analytic/bachelier.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/logsv/affine_expansion.py
RENAMED
|
File without changes
|
{stochvolmodels-1.0.14 → stochvolmodels-1.0.16}/stochvolmodels/pricers/logsv/vol_moments_ode.py
RENAMED
|
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
|