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.
Files changed (33) hide show
  1. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/PKG-INFO +32 -27
  2. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/README.md +31 -27
  3. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/pyproject.toml +1 -1
  4. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/__init__.py +18 -4
  5. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/test_option_chain.py +3 -2
  6. stochvolmodels-1.0.15/stochvolmodels/pricers/analytic/tdist.py +270 -0
  7. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/gmm_pricer.py +7 -7
  8. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/heston_pricer.py +50 -11
  9. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv_pricer.py +23 -4
  10. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/model_pricer.py +4 -3
  11. stochvolmodels-1.0.15/stochvolmodels/pricers/tdist_pricer.py +203 -0
  12. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/tests/qv_pricer.py +1 -1
  13. stochvolmodels-1.0.12/stochvolmodels/pricers/analytic/tdist.py +0 -126
  14. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/LICENSE.txt +0 -0
  15. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/__init__.py +0 -0
  16. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/fetch_option_chain.py +0 -0
  17. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/data/option_chain.py +0 -0
  18. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/__init__.py +0 -0
  19. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/__init__.py +0 -0
  20. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
  21. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/bsm.py +0 -0
  22. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
  23. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/__init__.py +0 -0
  24. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
  25. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
  26. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/tests/__init__.py +0 -0
  27. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
  28. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/__init__.py +0 -0
  29. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/config.py +0 -0
  30. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/funcs.py +0 -0
  31. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/mc_payoffs.py +0 -0
  32. {stochvolmodels-1.0.12 → stochvolmodels-1.0.15}/stochvolmodels/utils/mgf_pricer.py +0 -0
  33. {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.12
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
+
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "stochvolmodels"
3
- version = "1.0.12"
3
+ version = "1.0.15"
4
4
  description = "Implementation of stochastic volatility models for option pricing"
5
5
  license = "LICENSE.txt"
6
6
  authors = ["Artur Sepp <artursepp@gmail.com>"]
@@ -61,10 +61,14 @@ from stochvolmodels.pricers.analytic.bachelier import (
61
61
  )
62
62
 
63
63
  from stochvolmodels.pricers.analytic.tdist import (
64
- t_cum,
65
- compute_tdist_price,
66
- compute_compute_negative_prob,
67
- infer_tdist_implied_vol,
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)) - discfactor
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': 300}
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
- phi_grid, psi_grid, theta_grid = get_transform_var_grid(vol_scaler=vol_scaler or np.sqrt(v0*ttms[-1]))
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
- option_prices = vanilla_slice_pricer_with_mgf_grid(log_mgf_grid=log_mgf_grid,
218
- phi_grid=phi_grid,
219
- forward=forward,
220
- discfactor=discfactor,
221
- strikes=strikes_ttm,
222
- optiontypes=optiontypes_ttm)
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.MC_COMPARISION
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
- VOL_PATHS = 5
774
- TERMINAL_VALUES = 6
775
- MMA_INVERSE_MEASURE_VS_MC = 7
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.VOL_PATHS
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
- from stochvolmodels.pricers.analytic import mgf_pricer as mgfp
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
-