stochvolmodels 1.0.12__tar.gz → 1.0.15__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.12 → stochvolmodels-1.0.15}/PKG-INFO +32 -27
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/README.md +31 -27
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/pyproject.toml +1 -1
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/__init__.py +18 -4
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/test_option_chain.py +3 -2
- stochvolmodels-1.0.15/stochvolmodels/pricers/analytic/tdist.py +270 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/gmm_pricer.py +7 -7
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/heston_pricer.py +50 -11
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv_pricer.py +23 -4
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/model_pricer.py +4 -3
- stochvolmodels-1.0.15/stochvolmodels/pricers/tdist_pricer.py +203 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/tests/qv_pricer.py +1 -1
- stochvolmodels-1.0.12/stochvolmodels/pricers/analytic/tdist.py +0 -126
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/LICENSE.txt +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/__init__.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/fetch_option_chain.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/option_chain.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/__init__.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/__init__.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/bsm.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/__init__.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/tests/__init__.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/__init__.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/config.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/funcs.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/mc_payoffs.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/mgf_pricer.py +0 -0
- {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/plots.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stochvolmodels
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.15
|
|
4
4
|
Summary: Implementation of stochastic volatility models for option pricing
|
|
5
5
|
Home-page: https://github.com/ArturSepp/StochVolModels
|
|
6
6
|
License: LICENSE.txt
|
|
@@ -50,32 +50,6 @@ For the analytic implementation of stochastic volatility models, the package pro
|
|
|
50
50
|
1) Interface for analytical pricing of vanilla options using Fourier transform with closed-form solution for moment generating function
|
|
51
51
|
2) Interface for Monte-Carlo simulations of model dynamics
|
|
52
52
|
|
|
53
|
-
## Supporting Illustrations for Public Papers
|
|
54
|
-
|
|
55
|
-
As illustrations of different analytics, this packadge includes module ```my_papers```
|
|
56
|
-
with codes for computations and visualisations featured in several papers
|
|
57
|
-
for
|
|
58
|
-
|
|
59
|
-
1) "Log-normal Stochastic Volatility Model with Quadratic Drift" by Sepp A and Rakhmonov P, SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2522425
|
|
60
|
-
```python
|
|
61
|
-
stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
2) "What is a robust stochastic volatility model" by Sepp A and Rakhmonov P,
|
|
66
|
-
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4647027
|
|
67
|
-
```python
|
|
68
|
-
stochvolmodels/my_papers/volatility_models
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
3) "Valuation and Hedging of Cryptocurrency Inverse Options" by Sepp A and Lucic V,
|
|
73
|
-
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4606748
|
|
74
|
-
```python
|
|
75
|
-
stochvolmodels/my_papers/inverse_options
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
|
|
79
53
|
|
|
80
54
|
## Installation
|
|
81
55
|
```python
|
|
@@ -92,6 +66,8 @@ pip install stochvolmodels
|
|
|
92
66
|
3. [Comparison of model prices vs MC](#subparagraph3)
|
|
93
67
|
4. [Analysis and figures for the paper](#subparagraph4)
|
|
94
68
|
3. [Running Heston SV pricer](#heston)
|
|
69
|
+
4. [Supporting Illustrations for Public Papers](#papers)
|
|
70
|
+
|
|
95
71
|
|
|
96
72
|
Running model calibration to sample Bitcoin options data
|
|
97
73
|
|
|
@@ -256,3 +232,32 @@ pricer.plot_model_slices_in_params(option_slice=option_slice, params_dict=params
|
|
|
256
232
|
|
|
257
233
|
plt.show()
|
|
258
234
|
```
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
## Supporting Illustrations for Public Papers <a name="papers"></a>
|
|
238
|
+
|
|
239
|
+
As illustrations of different analytics, this packadge includes module ```my_papers```
|
|
240
|
+
with codes for computations and visualisations featured in several papers
|
|
241
|
+
for
|
|
242
|
+
|
|
243
|
+
1) "Log-normal Stochastic Volatility Model with Quadratic Drift" by Sepp A and Rakhmonov P, SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2522425
|
|
244
|
+
```python
|
|
245
|
+
stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
2) "What is a robust stochastic volatility model" by Sepp A and Rakhmonov P,
|
|
250
|
+
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4647027
|
|
251
|
+
```python
|
|
252
|
+
stochvolmodels/my_papers/volatility_models
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
3) "Valuation and Hedging of Cryptocurrency Inverse Options" by Sepp A and Lucic V,
|
|
257
|
+
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4606748
|
|
258
|
+
```python
|
|
259
|
+
stochvolmodels/my_papers/inverse_options
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
@@ -11,32 +11,6 @@ For the analytic implementation of stochastic volatility models, the package pro
|
|
|
11
11
|
1) Interface for analytical pricing of vanilla options using Fourier transform with closed-form solution for moment generating function
|
|
12
12
|
2) Interface for Monte-Carlo simulations of model dynamics
|
|
13
13
|
|
|
14
|
-
## Supporting Illustrations for Public Papers
|
|
15
|
-
|
|
16
|
-
As illustrations of different analytics, this packadge includes module ```my_papers```
|
|
17
|
-
with codes for computations and visualisations featured in several papers
|
|
18
|
-
for
|
|
19
|
-
|
|
20
|
-
1) "Log-normal Stochastic Volatility Model with Quadratic Drift" by Sepp A and Rakhmonov P, SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2522425
|
|
21
|
-
```python
|
|
22
|
-
stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
2) "What is a robust stochastic volatility model" by Sepp A and Rakhmonov P,
|
|
27
|
-
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4647027
|
|
28
|
-
```python
|
|
29
|
-
stochvolmodels/my_papers/volatility_models
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
3) "Valuation and Hedging of Cryptocurrency Inverse Options" by Sepp A and Lucic V,
|
|
34
|
-
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4606748
|
|
35
|
-
```python
|
|
36
|
-
stochvolmodels/my_papers/inverse_options
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
|
|
40
14
|
|
|
41
15
|
## Installation
|
|
42
16
|
```python
|
|
@@ -53,6 +27,8 @@ pip install stochvolmodels
|
|
|
53
27
|
3. [Comparison of model prices vs MC](#subparagraph3)
|
|
54
28
|
4. [Analysis and figures for the paper](#subparagraph4)
|
|
55
29
|
3. [Running Heston SV pricer](#heston)
|
|
30
|
+
4. [Supporting Illustrations for Public Papers](#papers)
|
|
31
|
+
|
|
56
32
|
|
|
57
33
|
Running model calibration to sample Bitcoin options data
|
|
58
34
|
|
|
@@ -216,4 +192,32 @@ pricer = HestonPricer()
|
|
|
216
192
|
pricer.plot_model_slices_in_params(option_slice=option_slice, params_dict=params_dict)
|
|
217
193
|
|
|
218
194
|
plt.show()
|
|
219
|
-
```
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
## Supporting Illustrations for Public Papers <a name="papers"></a>
|
|
199
|
+
|
|
200
|
+
As illustrations of different analytics, this packadge includes module ```my_papers```
|
|
201
|
+
with codes for computations and visualisations featured in several papers
|
|
202
|
+
for
|
|
203
|
+
|
|
204
|
+
1) "Log-normal Stochastic Volatility Model with Quadratic Drift" by Sepp A and Rakhmonov P, SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2522425
|
|
205
|
+
```python
|
|
206
|
+
stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
2) "What is a robust stochastic volatility model" by Sepp A and Rakhmonov P,
|
|
211
|
+
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4647027
|
|
212
|
+
```python
|
|
213
|
+
stochvolmodels/my_papers/volatility_models
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
3) "Valuation and Hedging of Cryptocurrency Inverse Options" by Sepp A and Lucic V,
|
|
218
|
+
SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4606748
|
|
219
|
+
```python
|
|
220
|
+
stochvolmodels/my_papers/inverse_options
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
|
|
@@ -61,10 +61,14 @@ from stochvolmodels.pricers.analytic.bachelier import (
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
from stochvolmodels.pricers.analytic.tdist import (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
pdf_tdist,
|
|
65
|
+
cdf_tdist,
|
|
66
|
+
cum_mean_tdist,
|
|
67
|
+
imply_drift_tdist,
|
|
68
|
+
compute_default_prob_tdist,
|
|
69
|
+
compute_forward_tdist,
|
|
70
|
+
compute_vanilla_price_tdist,
|
|
71
|
+
infer_implied_vol_tdist,
|
|
68
72
|
infer_tdist_implied_vols_from_model_slice_prices
|
|
69
73
|
)
|
|
70
74
|
|
|
@@ -111,13 +115,20 @@ from stochvolmodels.pricers.gmm_pricer import (
|
|
|
111
115
|
GmmPricer
|
|
112
116
|
)
|
|
113
117
|
|
|
118
|
+
from stochvolmodels.pricers.tdist_pricer import (
|
|
119
|
+
TdistParams,
|
|
120
|
+
TdistPricer
|
|
121
|
+
)
|
|
122
|
+
|
|
114
123
|
|
|
115
124
|
from stochvolmodels.data.option_chain import OptionChain, OptionSlice
|
|
116
125
|
|
|
126
|
+
"""
|
|
117
127
|
from stochvolmodels.data.fetch_option_chain import (generate_vol_chain_np,
|
|
118
128
|
load_option_chain,
|
|
119
129
|
sample_option_chain_at_times,
|
|
120
130
|
load_price_data)
|
|
131
|
+
"""
|
|
121
132
|
|
|
122
133
|
from stochvolmodels.data.test_option_chain import (
|
|
123
134
|
get_btc_test_chain_data,
|
|
@@ -148,3 +159,6 @@ from stochvolmodels.utils.plots import (
|
|
|
148
159
|
set_y_limits,
|
|
149
160
|
vol_slice_fit
|
|
150
161
|
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
test data with implied volatilities for VIX, SQQQ, Bitcoin, Gold, SPY
|
|
3
|
+
data are taken around Nov2021
|
|
3
4
|
data is used for logsv_model_wtih_quadratic_drift figures
|
|
4
5
|
"""
|
|
5
6
|
|
|
@@ -852,10 +853,10 @@ def get_qv_options_test_chain_data(num_strikes: int = 21) -> OptionChain:
|
|
|
852
853
|
forwards = array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
|
|
853
854
|
discfactors = array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
|
|
854
855
|
strikes_ttm = np.linspace(0.75, 1.5, num_strikes)
|
|
855
|
-
strikes_ttms = (strikes_ttm, strikes_ttm, strikes_ttm, strikes_ttm)
|
|
856
|
+
strikes_ttms = (strikes_ttm, strikes_ttm, strikes_ttm, strikes_ttm, strikes_ttm, strikes_ttm)
|
|
856
857
|
|
|
857
858
|
optiontypes_ttm = np.full(strikes_ttm.shape, 'C')
|
|
858
|
-
optiontypes_ttms = (optiontypes_ttm, optiontypes_ttm, optiontypes_ttm, optiontypes_ttm)
|
|
859
|
+
optiontypes_ttms = (optiontypes_ttm, optiontypes_ttm, optiontypes_ttm, optiontypes_ttm, optiontypes_ttm, optiontypes_ttm)
|
|
859
860
|
|
|
860
861
|
data = OptionChain(ids=ids,
|
|
861
862
|
ttms=ttms,
|
|
@@ -0,0 +1,270 @@
|
|
|
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) -> Union[float, np.ndarray]:
|
|
37
|
+
"""
|
|
38
|
+
cumulative expected value
|
|
39
|
+
h = int^{x}_{-\infty} u f(u)du
|
|
40
|
+
"""
|
|
41
|
+
upsilon = compute_upsilon(vol=vol, ttm=ttm, nu=nu)
|
|
42
|
+
z = (x-mu*ttm) / upsilon
|
|
43
|
+
norm = (gamma(0.5*(1.0+nu)) / gamma(0.5*nu))*np.sqrt(nu/np.pi) / (1.0-nu)
|
|
44
|
+
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))
|
|
45
|
+
return h
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def imply_drift_tdist(rf_rate: float = 0.0, vol: float = 0.2, nu: float = 3.0, ttm: float = 0.25) -> float:
|
|
49
|
+
"""
|
|
50
|
+
imply drift of t-distribution under risk-neutral measure
|
|
51
|
+
"""
|
|
52
|
+
rf_return = (np.exp(rf_rate*ttm) - 1.0)
|
|
53
|
+
|
|
54
|
+
def func(mu: float) -> float:
|
|
55
|
+
x_star = -(1.0+ttm*mu)
|
|
56
|
+
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
|
|
57
|
+
|
|
58
|
+
mu = fsolve(func, x0=rf_rate, xtol=1e-10)
|
|
59
|
+
return mu[0]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def compute_default_prob_tdist(ttm: float,
|
|
63
|
+
vol: float,
|
|
64
|
+
nu: float = 4.5,
|
|
65
|
+
rf_rate: float = 0.0
|
|
66
|
+
) -> Union[float, np.ndarray]:
|
|
67
|
+
"""
|
|
68
|
+
imply drift of t-distribution under risk-neutral measure
|
|
69
|
+
"""
|
|
70
|
+
risk_neutral_mu = imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm)
|
|
71
|
+
x_star = -(1.0+risk_neutral_mu*ttm)
|
|
72
|
+
default_prob = cdf_tdist(x=x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
73
|
+
return default_prob
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def compute_forward_tdist(spot: Union[float, np.ndarray],
|
|
77
|
+
ttm: float,
|
|
78
|
+
vol: float,
|
|
79
|
+
nu: float = 4.5,
|
|
80
|
+
rf_rate: float = 0.0
|
|
81
|
+
) -> Union[float, np.ndarray]:
|
|
82
|
+
"""
|
|
83
|
+
imply drift of t-distribution under risk-neutral measure
|
|
84
|
+
"""
|
|
85
|
+
risk_neutral_mu = imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm)
|
|
86
|
+
x_star = -(1.0+risk_neutral_mu*ttm)
|
|
87
|
+
c_1 = cdf_tdist(x=x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
88
|
+
h_1 = cum_mean_tdist(x=x_star, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
89
|
+
forward = spot * ((1.0 + risk_neutral_mu*ttm)*(1.0-c_1)-h_1)
|
|
90
|
+
return forward
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def compute_vanilla_price_tdist(spot: Union[float, np.ndarray],
|
|
94
|
+
strikes: Union[float, np.ndarray],
|
|
95
|
+
ttm: float,
|
|
96
|
+
vol: float,
|
|
97
|
+
nu: float = 4.5,
|
|
98
|
+
optiontypes: Union[str, np.ndarray] = 'C',
|
|
99
|
+
rf_rate: float = 0.0,
|
|
100
|
+
is_compute_risk_neutral_mu: bool = True
|
|
101
|
+
) -> Union[float, np.ndarray]:
|
|
102
|
+
"""
|
|
103
|
+
option pricer for t-distribution
|
|
104
|
+
"""
|
|
105
|
+
discfactor = np.exp(-rf_rate*ttm)
|
|
106
|
+
if is_compute_risk_neutral_mu:
|
|
107
|
+
risk_neutral_mu = imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm)
|
|
108
|
+
else:
|
|
109
|
+
risk_neutral_mu = rf_rate
|
|
110
|
+
spot_star = spot*(1.0 + risk_neutral_mu*ttm)
|
|
111
|
+
x_lower_bound = -1.0-risk_neutral_mu*ttm
|
|
112
|
+
|
|
113
|
+
def compute(strike_: Union[float, np.ndarray], optiontype_: str) -> float:
|
|
114
|
+
y = strike_ / spot - (1.0 + risk_neutral_mu*ttm)
|
|
115
|
+
c_y = cdf_tdist(x=y, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
116
|
+
h_y = cum_mean_tdist(x=y, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
117
|
+
if optiontype_ == 'C' or optiontype_ == 'IC':
|
|
118
|
+
price_ = (-spot * h_y + (spot_star-strike_)*(1.0-c_y))
|
|
119
|
+
elif optiontype_ == 'P' or optiontype_ == 'IP':
|
|
120
|
+
c_1 = cdf_tdist(x=x_lower_bound, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
121
|
+
h_1 = cum_mean_tdist(x=x_lower_bound, mu=0.0, vol=vol, nu=nu, ttm=ttm)
|
|
122
|
+
price_ = discfactor * ((strike_ - spot_star) * (c_y - c_1) - spot * (h_y - h_1)+strike_*c_1)
|
|
123
|
+
else:
|
|
124
|
+
raise NotImplementedError(f"optiontype")
|
|
125
|
+
return price_
|
|
126
|
+
|
|
127
|
+
if isinstance(optiontypes, str):
|
|
128
|
+
price = compute(strikes, optiontypes)
|
|
129
|
+
else:
|
|
130
|
+
price = np.zeros_like(strikes)
|
|
131
|
+
for idx, (strike_, optiontype_) in enumerate(zip(strikes, optiontypes)):
|
|
132
|
+
price[idx] = compute(strike_, optiontype_)
|
|
133
|
+
return price
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def infer_implied_vol_tdist(spot: float,
|
|
137
|
+
ttm: float,
|
|
138
|
+
strike: float,
|
|
139
|
+
given_price: float,
|
|
140
|
+
rf_rate: float = 0.0,
|
|
141
|
+
optiontype: str = 'C',
|
|
142
|
+
nu: float = 4.5,
|
|
143
|
+
tol: float = 1e-12,
|
|
144
|
+
is_bounds_to_nan: bool = False
|
|
145
|
+
) -> float:
|
|
146
|
+
"""
|
|
147
|
+
compute normal implied vol
|
|
148
|
+
"""
|
|
149
|
+
x1, x2 = 0.05, 10.0 # starting values
|
|
150
|
+
f = compute_vanilla_price_tdist(spot=spot, strikes=strike, ttm=ttm, vol=x1, nu=nu, rf_rate=rf_rate, optiontypes=optiontype) - given_price
|
|
151
|
+
fmid = compute_vanilla_price_tdist(spot=spot, strikes=strike, ttm=ttm, vol=x2, nu=nu, rf_rate=rf_rate, optiontypes=optiontype) - given_price
|
|
152
|
+
if f*fmid < 0.0:
|
|
153
|
+
if f < 0.0:
|
|
154
|
+
rtb = x1
|
|
155
|
+
dx = x2-x1
|
|
156
|
+
else:
|
|
157
|
+
rtb = x2
|
|
158
|
+
dx = x1-x2
|
|
159
|
+
xmid = rtb
|
|
160
|
+
for j in range(0, 100):
|
|
161
|
+
dx = dx*0.5
|
|
162
|
+
xmid = rtb+dx
|
|
163
|
+
fmid = compute_vanilla_price_tdist(spot=spot, strikes=strike, ttm=ttm, vol=xmid, nu=nu, rf_rate=rf_rate, optiontypes=optiontype) - given_price
|
|
164
|
+
if fmid <= 0.0:
|
|
165
|
+
rtb = xmid
|
|
166
|
+
if np.abs(fmid) < tol:
|
|
167
|
+
break
|
|
168
|
+
v1 = xmid
|
|
169
|
+
else:
|
|
170
|
+
if f < 0:
|
|
171
|
+
v1 = x1
|
|
172
|
+
else:
|
|
173
|
+
v1 = x2
|
|
174
|
+
if is_bounds_to_nan: # in case vol was inferred it will return nan
|
|
175
|
+
if np.abs(v1-x1) < tol or np.abs(v1-x2) < tol:
|
|
176
|
+
v1 = np.nan
|
|
177
|
+
return v1
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def infer_tdist_implied_vols_from_model_slice_prices(ttm: float,
|
|
181
|
+
spot: float,
|
|
182
|
+
strikes: np.ndarray,
|
|
183
|
+
optiontypes: np.ndarray,
|
|
184
|
+
model_prices: np.ndarray,
|
|
185
|
+
rf_rate: float,
|
|
186
|
+
nu: float
|
|
187
|
+
) -> np.ndarray:
|
|
188
|
+
model_vol_ttm = np.zeros_like(strikes)
|
|
189
|
+
for idx, (strike, model_price, optiontype) in enumerate(zip(strikes, model_prices, optiontypes)):
|
|
190
|
+
model_vol_ttm[idx] = infer_implied_vol_tdist(spot=spot, ttm=ttm, rf_rate=rf_rate,
|
|
191
|
+
given_price=model_price,
|
|
192
|
+
strike=strike,
|
|
193
|
+
optiontype=optiontype,
|
|
194
|
+
nu=nu)
|
|
195
|
+
return model_vol_ttm
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class UnitTests(Enum):
|
|
199
|
+
PLOT_PDF = 1
|
|
200
|
+
PLOT_CDF = 2
|
|
201
|
+
PLOT_CUM_X = 3
|
|
202
|
+
PLOT_H = 4
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def run_unit_test(unit_test: UnitTests):
|
|
206
|
+
|
|
207
|
+
import qis as qis
|
|
208
|
+
|
|
209
|
+
x = np.linspace(-5.0, 5.0, 20000)
|
|
210
|
+
dx = x[1] - x[0]
|
|
211
|
+
ttm = 1.0
|
|
212
|
+
mu_vols = {'mu=0.0, vol=0.2': (0.0, 0.2),
|
|
213
|
+
'mu=0.2, vol=0.2': (0.2, 0.2),
|
|
214
|
+
'mu=0.2, vol=0.4': (0.2, 0.4)}
|
|
215
|
+
|
|
216
|
+
if unit_test == UnitTests.PLOT_PDF:
|
|
217
|
+
pdfs = {}
|
|
218
|
+
for key, mu_vol in mu_vols.items():
|
|
219
|
+
pdf = dx * pdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
220
|
+
pdfs[key] = pd.Series(pdf, index=x)
|
|
221
|
+
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)))}")
|
|
222
|
+
pdfs = pd.DataFrame.from_dict(pdfs, orient='columns')
|
|
223
|
+
qis.plot_line(df=pdfs)
|
|
224
|
+
|
|
225
|
+
elif unit_test == UnitTests.PLOT_CDF:
|
|
226
|
+
pdfs = {}
|
|
227
|
+
cpdfs = {}
|
|
228
|
+
for key, mu_vol in mu_vols.items():
|
|
229
|
+
pdf = dx * pdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
230
|
+
cpdf = cdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
231
|
+
pdfs[f"{key}_pdf_sum"] = pd.Series(np.cumsum(pdf), index=x)
|
|
232
|
+
cpdfs[f"{key}_cdf"] = pd.Series(cpdf, index=x)
|
|
233
|
+
pdfs = pd.DataFrame.from_dict(pdfs, orient='columns')
|
|
234
|
+
cpdfs = pd.DataFrame.from_dict(cpdfs, orient='columns')
|
|
235
|
+
df = pd.concat([pdfs, cpdfs], axis=1)
|
|
236
|
+
colors = qis.get_n_colors(n=len(mu_vols.keys()))
|
|
237
|
+
qis.plot_line(df=df, colors=2*colors)
|
|
238
|
+
|
|
239
|
+
elif unit_test == UnitTests.PLOT_CUM_X:
|
|
240
|
+
pdfs = {}
|
|
241
|
+
cpdfs = {}
|
|
242
|
+
for key, mu_vol in mu_vols.items():
|
|
243
|
+
pdf = dx * pdf_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
244
|
+
cpdf = cum_mean_tdist(x=x, mu=mu_vol[0], vol=mu_vol[1], nu=3.0, ttm=ttm)
|
|
245
|
+
pdfs[f"{key}_h_pdf_sum"] = pd.Series(np.cumsum(x*pdf), index=x)
|
|
246
|
+
cpdfs[f"{key}_t_h"] = pd.Series(cpdf, index=x)
|
|
247
|
+
pdfs = pd.DataFrame.from_dict(pdfs, orient='columns')
|
|
248
|
+
cpdfs = pd.DataFrame.from_dict(cpdfs, orient='columns')
|
|
249
|
+
df = pd.concat([pdfs, cpdfs], axis=1)
|
|
250
|
+
colors = qis.get_n_colors(n=len(mu_vols.keys()))
|
|
251
|
+
qis.plot_line(df=df, colors=2*colors)
|
|
252
|
+
|
|
253
|
+
elif unit_test == UnitTests.PLOT_H:
|
|
254
|
+
x = np.linspace(-10.0, 10.0, 2000)
|
|
255
|
+
h = pd.Series(cum_mean_tdist(x=x, mu=0.5, vol=1.0, nu=3.0, ttm=1.0), index=x, name='h')
|
|
256
|
+
qis.plot_line(df=h, xlabel='x')
|
|
257
|
+
|
|
258
|
+
plt.show()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == '__main__':
|
|
262
|
+
|
|
263
|
+
unit_test = UnitTests.PLOT_CUM_X
|
|
264
|
+
|
|
265
|
+
is_run_all_tests = False
|
|
266
|
+
if is_run_all_tests:
|
|
267
|
+
for unit_test in UnitTests:
|
|
268
|
+
run_unit_test(unit_test=unit_test)
|
|
269
|
+
else:
|
|
270
|
+
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
|
|
@@ -14,10 +14,7 @@ from stochvolmodels.utils.funcs import to_flat_np_array, timer, npdf1
|
|
|
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
|
|
@@ -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,
|
|
@@ -191,6 +189,7 @@ def compute_gmm_vanilla_price(gmm_weights: np.ndarray,
|
|
|
191
189
|
price = 0.0
|
|
192
190
|
for gmm_weight, gmm_mu, gmm_vol in zip(gmm_weights, gmm_mus, gmm_vols):
|
|
193
191
|
forward_i = forward*np.exp((gmm_mu+0.5*gmm_vol*gmm_vol)*ttm)
|
|
192
|
+
# forward is vol-adjusted
|
|
194
193
|
price_i = bsm.compute_bsm_vanilla_price(forward=forward_i,
|
|
195
194
|
strike=strike,
|
|
196
195
|
ttm=ttm,
|
|
@@ -268,6 +267,7 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
268
267
|
|
|
269
268
|
import seaborn as sns
|
|
270
269
|
import qis as qis
|
|
270
|
+
from stochvolmodels.data.test_option_chain import get_btc_test_chain_data
|
|
271
271
|
|
|
272
272
|
if unit_test == UnitTests.CALIBRATOR:
|
|
273
273
|
option_chain = get_btc_test_chain_data()
|
|
@@ -13,10 +13,10 @@ from enum import Enum
|
|
|
13
13
|
|
|
14
14
|
# stochvolmodels
|
|
15
15
|
from stochvolmodels.utils.funcs import to_flat_np_array, set_time_grid, timer
|
|
16
|
-
from stochvolmodels.utils.mgf_pricer import get_transform_var_grid, vanilla_slice_pricer_with_mgf_grid
|
|
17
16
|
from stochvolmodels.utils.config import VariableType
|
|
18
17
|
from stochvolmodels.utils.mc_payoffs import compute_mc_vars_payoff
|
|
19
18
|
from stochvolmodels.pricers.model_pricer import ModelParams, ModelPricer
|
|
19
|
+
import stochvolmodels.utils.mgf_pricer as mgfp
|
|
20
20
|
|
|
21
21
|
# data
|
|
22
22
|
from stochvolmodels.data.option_chain import OptionChain
|
|
@@ -192,11 +192,14 @@ def heston_chain_pricer(v0: float,
|
|
|
192
192
|
strikes_ttms: Tuple[np.ndarray, ...],
|
|
193
193
|
optiontypes_ttms: Tuple[np.ndarray, ...],
|
|
194
194
|
discfactors: np.ndarray,
|
|
195
|
+
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
195
196
|
vol_scaler: float = None # run calibration on same vol_scaler
|
|
196
197
|
) -> List[np.ndarray]:
|
|
197
198
|
|
|
198
199
|
# starting values
|
|
199
|
-
|
|
200
|
+
if vol_scaler is None:
|
|
201
|
+
vol_scaler = np.minimum(0.3, np.sqrt(v0*ttms[0]))
|
|
202
|
+
phi_grid, psi_grid, theta_grid = mgfp.get_transform_var_grid(vol_scaler=vol_scaler)
|
|
200
203
|
a_t0, b_t0 = np.zeros(phi_grid.shape[0], dtype=np.complex128), np.zeros(phi_grid.shape[0], dtype=np.complex128)
|
|
201
204
|
ttm0 = 0.0
|
|
202
205
|
|
|
@@ -213,13 +216,26 @@ def heston_chain_pricer(v0: float,
|
|
|
213
216
|
psi_grid=psi_grid,
|
|
214
217
|
a_t0=a_t0,
|
|
215
218
|
b_t0=b_t0)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
|
|
220
|
+
if variable_type == VariableType.LOG_RETURN:
|
|
221
|
+
option_prices = mgfp.vanilla_slice_pricer_with_mgf_grid(log_mgf_grid=log_mgf_grid,
|
|
222
|
+
phi_grid=phi_grid,
|
|
223
|
+
forward=forward,
|
|
224
|
+
strikes=strikes_ttm,
|
|
225
|
+
optiontypes=optiontypes_ttm,
|
|
226
|
+
discfactor=discfactor)
|
|
227
|
+
|
|
228
|
+
elif variable_type == VariableType.Q_VAR:
|
|
229
|
+
option_prices = mgfp.slice_qvar_pricer_with_a_grid(log_mgf_grid=log_mgf_grid,
|
|
230
|
+
psi_grid=psi_grid,
|
|
231
|
+
ttm=ttm,
|
|
232
|
+
forward=forward,
|
|
233
|
+
strikes=strikes_ttm,
|
|
234
|
+
optiontypes=optiontypes_ttm,
|
|
235
|
+
discfactor=discfactor)
|
|
236
|
+
else:
|
|
237
|
+
raise NotImplementedError(f"variable_type={variable_type}")
|
|
238
|
+
|
|
223
239
|
model_prices_ttms.append(option_prices)
|
|
224
240
|
ttm0 = ttm
|
|
225
241
|
|
|
@@ -330,10 +346,12 @@ class UnitTests(Enum):
|
|
|
330
346
|
SLICE_PRICER = 2
|
|
331
347
|
CALIBRATOR = 3
|
|
332
348
|
MC_COMPARISION = 4
|
|
333
|
-
|
|
349
|
+
MC_COMPARISION_QVAR = 5
|
|
334
350
|
|
|
335
351
|
def run_unit_test(unit_test: UnitTests):
|
|
336
352
|
|
|
353
|
+
import stochvolmodels.data.test_option_chain as chains
|
|
354
|
+
|
|
337
355
|
if unit_test == UnitTests.CHAIN_PRICER:
|
|
338
356
|
params = HestonParams(v0=0.85**2,
|
|
339
357
|
theta=1.4**2,
|
|
@@ -392,12 +410,33 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
392
410
|
heston_pricer.plot_model_ivols_vs_mc(option_chain=option_chain,
|
|
393
411
|
params=BTC_HESTON_PARAMS)
|
|
394
412
|
|
|
413
|
+
elif unit_test == UnitTests.MC_COMPARISION_QVAR:
|
|
414
|
+
from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar
|
|
415
|
+
from stochvolmodels.pricers.logsv_pricer import LogSvParams
|
|
416
|
+
heston_pricer = HestonPricer()
|
|
417
|
+
ttms = {'1m': 1.0/12.0, '6m': 0.5}
|
|
418
|
+
option_chain = chains.get_qv_options_test_chain_data()
|
|
419
|
+
option_chain = OptionChain.get_slices_as_chain(option_chain, ids=list(ttms.keys()))
|
|
420
|
+
LOGSV_BTC_PARAMS = LogSvParams(sigma0=0.8376, theta=1.0413, kappa1=3.1844, kappa2=3.058, beta=0.1514,
|
|
421
|
+
volvol=1.8458)
|
|
422
|
+
|
|
423
|
+
forwards = np.array([compute_analytic_qvar(params=LOGSV_BTC_PARAMS, ttm=ttm, n_terms=4) for ttm in ttms.values()])
|
|
424
|
+
print(f"QV forwards = {forwards}")
|
|
425
|
+
|
|
426
|
+
option_chain.forwards = forwards # replace forwards to imply BSM vols
|
|
427
|
+
option_chain.strikes_ttms = List(forward * strikes_ttm for forward, strikes_ttm in zip(option_chain.forwards, option_chain.strikes_ttms))
|
|
428
|
+
|
|
429
|
+
fig = heston_pricer.plot_model_ivols_vs_mc(option_chain=option_chain,
|
|
430
|
+
params=BTC_HESTON_PARAMS,
|
|
431
|
+
variable_type=VariableType.Q_VAR,
|
|
432
|
+
nb_path=200000)
|
|
433
|
+
|
|
395
434
|
plt.show()
|
|
396
435
|
|
|
397
436
|
|
|
398
437
|
if __name__ == '__main__':
|
|
399
438
|
|
|
400
|
-
unit_test = UnitTests.
|
|
439
|
+
unit_test = UnitTests.CALIBRATOR
|
|
401
440
|
|
|
402
441
|
is_run_all_tests = False
|
|
403
442
|
if is_run_all_tests:
|
|
@@ -770,13 +770,16 @@ class UnitTests(Enum):
|
|
|
770
770
|
SLICE_PRICER = 2
|
|
771
771
|
CALIBRATOR = 3
|
|
772
772
|
MC_COMPARISION = 4
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
773
|
+
MC_COMPARISION_QVAR = 5
|
|
774
|
+
VOL_PATHS = 6
|
|
775
|
+
TERMINAL_VALUES = 7
|
|
776
|
+
MMA_INVERSE_MEASURE_VS_MC = 8
|
|
776
777
|
|
|
777
778
|
|
|
778
779
|
def run_unit_test(unit_test: UnitTests):
|
|
779
780
|
|
|
781
|
+
import stochvolmodels.data.test_option_chain as chains
|
|
782
|
+
|
|
780
783
|
if unit_test == UnitTests.CHAIN_PRICER:
|
|
781
784
|
option_chain = get_btc_test_chain_data()
|
|
782
785
|
logsv_pricer = LogSVPricer()
|
|
@@ -822,6 +825,22 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
822
825
|
logsv_pricer.plot_model_ivols_vs_mc(option_chain=option_chain,
|
|
823
826
|
params=LOGSV_BTC_PARAMS)
|
|
824
827
|
|
|
828
|
+
elif unit_test == UnitTests.MC_COMPARISION_QVAR:
|
|
829
|
+
from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar
|
|
830
|
+
logsv_pricer = LogSVPricer()
|
|
831
|
+
ttms = {'1m': 1.0/12.0, '6m': 0.5}
|
|
832
|
+
option_chain = chains.get_qv_options_test_chain_data()
|
|
833
|
+
option_chain = OptionChain.get_slices_as_chain(option_chain, ids=list(ttms.keys()))
|
|
834
|
+
forwards = np.array([compute_analytic_qvar(params=LOGSV_BTC_PARAMS, ttm=ttm, n_terms=4) for ttm in ttms.values()])
|
|
835
|
+
print(f"QV forwards = {forwards}")
|
|
836
|
+
|
|
837
|
+
option_chain.forwards = forwards # replace forwards to imply BSM vols
|
|
838
|
+
option_chain.strikes_ttms = List(forward * strikes_ttm for forward, strikes_ttm in zip(option_chain.forwards, option_chain.strikes_ttms))
|
|
839
|
+
|
|
840
|
+
fig = logsv_pricer.plot_model_ivols_vs_mc(option_chain=option_chain,
|
|
841
|
+
params=LOGSV_BTC_PARAMS,
|
|
842
|
+
variable_type=VariableType.Q_VAR)
|
|
843
|
+
|
|
825
844
|
elif unit_test == UnitTests.VOL_PATHS:
|
|
826
845
|
logsv_pricer = LogSVPricer()
|
|
827
846
|
nb_path = 10
|
|
@@ -859,7 +878,7 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
859
878
|
|
|
860
879
|
if __name__ == '__main__':
|
|
861
880
|
|
|
862
|
-
unit_test = UnitTests.
|
|
881
|
+
unit_test = UnitTests.MC_COMPARISION_QVAR
|
|
863
882
|
|
|
864
883
|
is_run_all_tests = False
|
|
865
884
|
if is_run_all_tests:
|
|
@@ -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
|
|
@@ -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
|
{stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/bachelier.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/affine_expansion.py
RENAMED
|
File without changes
|
{stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/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
|