stochvolmodels 1.0.14__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.14 → stochvolmodels-1.0.15}/PKG-INFO +1 -1
  2. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/pyproject.toml +1 -1
  3. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/__init__.py +13 -4
  4. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/data/test_option_chain.py +1 -0
  5. stochvolmodels-1.0.15/stochvolmodels/pricers/analytic/tdist.py +270 -0
  6. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/gmm_pricer.py +6 -7
  7. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/model_pricer.py +4 -3
  8. stochvolmodels-1.0.15/stochvolmodels/pricers/tdist_pricer.py +203 -0
  9. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/tests/qv_pricer.py +1 -1
  10. stochvolmodels-1.0.14/stochvolmodels/pricers/analytic/tdist.py +0 -126
  11. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/LICENSE.txt +0 -0
  12. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/README.md +0 -0
  13. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/data/__init__.py +0 -0
  14. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/data/fetch_option_chain.py +0 -0
  15. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/data/option_chain.py +0 -0
  16. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/__init__.py +0 -0
  17. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/__init__.py +0 -0
  18. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
  19. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/analytic/bsm.py +0 -0
  20. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
  21. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/heston_pricer.py +0 -0
  22. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/__init__.py +0 -0
  23. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
  24. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
  25. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/pricers/logsv_pricer.py +0 -0
  26. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/tests/__init__.py +0 -0
  27. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
  28. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/utils/__init__.py +0 -0
  29. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/utils/config.py +0 -0
  30. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/utils/funcs.py +0 -0
  31. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/utils/mc_payoffs.py +0 -0
  32. {stochvolmodels-1.0.14 → stochvolmodels-1.0.15}/stochvolmodels/utils/mgf_pricer.py +0 -0
  33. {stochvolmodels-1.0.14 → 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.14
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "stochvolmodels"
3
- version = "1.0.14"
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,6 +115,11 @@ 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
 
@@ -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
 
@@ -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,
@@ -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
@@ -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
-