stochvolmodels 1.0.14__tar.gz → 1.0.16__tar.gz

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