stochvolmodels 1.0.20__tar.gz → 1.0.22__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 (73) hide show
  1. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/PKG-INFO +2 -1
  2. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/pyproject.toml +1 -1
  3. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/__init__.py +1 -1
  4. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/option_chain.py +23 -0
  5. stochvolmodels-1.0.22/stochvolmodels/examples/quick_run_lognormal_sv_pricer.py +34 -0
  6. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_lognormal_sv_pricer.py +1 -3
  7. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/heston_pricer.py +1 -1
  8. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv/affine_expansion.py +24 -16
  9. stochvolmodels-1.0.22/stochvolmodels/pricers/logsv/logsv_params.py +176 -0
  10. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv/vol_moments_ode.py +49 -6
  11. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv_pricer.py +47 -159
  12. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/model_pricer.py +3 -3
  13. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/tests/qv_pricer.py +2 -1
  14. stochvolmodels-1.0.22/stochvolmodels/utils/var_swap_pricer.py +26 -0
  15. stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging/README.md +0 -8
  16. stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging/logsv_figures.py +0 -61
  17. stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging/run_logsv_for_il_payoff.py +0 -150
  18. stochvolmodels-1.0.20/stochvolmodels/my_papers/inverse_options/README.md +0 -10
  19. stochvolmodels-1.0.20/stochvolmodels/my_papers/inverse_options/compare_net_delta.py +0 -168
  20. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/README.md +0 -13
  21. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/article_figures.py +0 -325
  22. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/calibrations.py +0 -190
  23. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/compare_admis_reg.py +0 -128
  24. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/model_fit_to_options_timeseries.py +0 -252
  25. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/moments_vol_qvar.py +0 -239
  26. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/ode_sol_in_time.py +0 -308
  27. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/steady_state_pdf.py +0 -251
  28. stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/vol_drift.py +0 -82
  29. stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia/check_kernel.py +0 -20
  30. stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia/gmm_slides.py +0 -501
  31. stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia/q_kernel.py +0 -53
  32. stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/__init__.py +0 -0
  33. stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/illustrations.py +0 -197
  34. stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/market_data_fit.py +0 -66
  35. stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/mc_pricer_with_kernel.py +0 -97
  36. stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/README.md +0 -10
  37. stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/__init__.py +0 -0
  38. stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/article_figures.py +0 -216
  39. stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/autocorr_fit.py +0 -235
  40. stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/load_data.py +0 -64
  41. stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/ss_distribution_fit.py +0 -327
  42. stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/vol_beta.py +0 -60
  43. stochvolmodels-1.0.20/stochvolmodels/pricers/__init__.py +0 -0
  44. stochvolmodels-1.0.20/stochvolmodels/pricers/analytic/__init__.py +0 -0
  45. stochvolmodels-1.0.20/stochvolmodels/pricers/logsv/__init__.py +0 -0
  46. stochvolmodels-1.0.20/stochvolmodels/tests/__init__.py +0 -0
  47. stochvolmodels-1.0.20/stochvolmodels/utils/__init__.py +0 -0
  48. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/LICENSE.txt +0 -0
  49. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/README.md +0 -0
  50. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/__init__.py +0 -0
  51. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/fetch_option_chain.py +0 -0
  52. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/test_option_chain.py +0 -0
  53. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_gmm_fit.py +0 -0
  54. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_heston.py +0 -0
  55. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_heston_sv_pricer.py +0 -0
  56. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_pricing_options_on_qvar.py +0 -0
  57. {stochvolmodels-1.0.20/stochvolmodels/my_papers → stochvolmodels-1.0.22/stochvolmodels/pricers}/__init__.py +0 -0
  58. {stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging → stochvolmodels-1.0.22/stochvolmodels/pricers/analytic}/__init__.py +0 -0
  59. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
  60. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/analytic/bsm.py +0 -0
  61. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/analytic/tdist.py +0 -0
  62. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/gmm_pricer.py +0 -0
  63. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
  64. {stochvolmodels-1.0.20/stochvolmodels/my_papers/inverse_options → stochvolmodels-1.0.22/stochvolmodels/pricers/logsv}/__init__.py +0 -0
  65. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/tdist_pricer.py +0 -0
  66. {stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift → stochvolmodels-1.0.22/stochvolmodels/tests}/__init__.py +0 -0
  67. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
  68. {stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia → stochvolmodels-1.0.22/stochvolmodels/utils}/__init__.py +0 -0
  69. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/config.py +0 -0
  70. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/funcs.py +0 -0
  71. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/mc_payoffs.py +0 -0
  72. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/mgf_pricer.py +0 -0
  73. {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/plots.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stochvolmodels
3
- Version: 1.0.20
3
+ Version: 1.0.22
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
@@ -24,6 +24,7 @@ Classifier: Programming Language :: Python :: 3.9
24
24
  Classifier: Programming Language :: Python :: 3.10
25
25
  Classifier: Programming Language :: Python :: 3.11
26
26
  Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Programming Language :: Python :: 3.13
27
28
  Classifier: Programming Language :: Python :: 3 :: Only
28
29
  Classifier: Topic :: Office/Business :: Financial :: Investment
29
30
  Requires-Dist: matplotlib (>=3.5.2)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "stochvolmodels"
3
- version = "1.0.20"
3
+ version = "1.0.22"
4
4
  description = "Implementation of stochastic volatility models for option pricing"
5
5
  license = "LICENSE.txt"
6
6
  authors = ["Artur Sepp <artursepp@gmail.com>"]
@@ -104,10 +104,10 @@ from stochvolmodels.pricers.heston_pricer import (
104
104
  from stochvolmodels.pricers.logsv_pricer import (
105
105
  LOGSV_BTC_PARAMS,
106
106
  LogSVPricer,
107
- LogSvParams,
108
107
  LogsvModelCalibrationType,
109
108
  ConstraintsType
110
109
  )
110
+ from stochvolmodels.pricers.logsv.logsv_params import LogSvParams
111
111
 
112
112
  from stochvolmodels.pricers.gmm_pricer import (
113
113
  GmmParams,
@@ -10,9 +10,12 @@ from __future__ import annotations
10
10
  import numpy as np
11
11
  from dataclasses import dataclass
12
12
  from typing import Tuple, Optional
13
+
14
+ import pandas as pd
13
15
  from numba.typed import List
14
16
 
15
17
  import stochvolmodels.pricers.analytic.bsm as bsm
18
+ from stochvolmodels.utils.var_swap_pricer import compute_var_swap_strike
16
19
 
17
20
 
18
21
  @dataclass
@@ -227,6 +230,26 @@ class OptionChain:
227
230
  ask_prices=None if self.ask_prices is None else self.ask_prices[idx])
228
231
  return option_slice
229
232
 
233
+ def get_slice_varswap_strikes(self, floor_with_atm_vols: bool = True) -> pd.Series:
234
+ varswap_strikes = np.zeros_like(self.ttms)
235
+ vols_ttms = self.get_mid_vols()
236
+ for idx, ttm in enumerate(self.ttms):
237
+ mid_prices = bsm.compute_bsm_vanilla_slice_prices(ttm=ttm,
238
+ forward=self.forwards[idx],
239
+ strikes=self.strikes_ttms[idx],
240
+ vols=vols_ttms[idx],
241
+ optiontypes=self.optiontypes_ttms[idx])
242
+ strikes = self.strikes_ttms[idx]
243
+ puts_cond = self.optiontypes_ttms[idx] == 'P'
244
+ puts = pd.Series(mid_prices[puts_cond], index=strikes[puts_cond])
245
+ calls = pd.Series(mid_prices[puts_cond == False], index=strikes[puts_cond == False])
246
+ varswap_strikes[idx] = compute_var_swap_strike(puts=puts, calls=calls, forward=self.forwards[idx], ttm=ttm)
247
+
248
+ if floor_with_atm_vols:
249
+ varswap_strikes = np.maximum(self.get_chain_atm_vols(), varswap_strikes)
250
+
251
+ return pd.Series(varswap_strikes, index=self.ttms)
252
+
230
253
  @classmethod
231
254
  def get_slices_as_chain(cls, option_chain: OptionChain, ids: List[str]) -> OptionChain:
232
255
  """
@@ -0,0 +1,34 @@
1
+ """
2
+ run few unit test to illustrate implementation of log-normal sv model analytics
3
+ """
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+ from stochvolmodels import LogSVPricer, LogSvParams, LogsvModelCalibrationType, ConstraintsType, get_btc_test_chain_data
7
+
8
+ # 1. create instance of pricer
9
+ logsv_pricer = LogSVPricer()
10
+
11
+ # 2. define model params
12
+ params = LogSvParams(sigma0=1.0, theta=1.0, kappa1=5.0, kappa2=5.0, beta=0.2, volvol=2.0)
13
+
14
+ # 3. compute model prices for option slices
15
+ model_prices, vols = logsv_pricer.price_slice(params=params,
16
+ ttm=0.25,
17
+ forward=1.0,
18
+ strikes=np.array([0.8, 0.9, 1.0, 1.1]),
19
+ optiontypes=np.array(['P', 'P', 'C', 'C']))
20
+ print([f"{p:0.4f}, implied vol={v: 0.2%}" for p, v in zip(model_prices, vols)])
21
+
22
+ # 4. calibrate model to test option chain data
23
+ btc_option_chain = get_btc_test_chain_data()
24
+ params0 = LogSvParams(sigma0=1.0, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=2.0)
25
+ btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=btc_option_chain,
26
+ params0=params0,
27
+ model_calibration_type=LogsvModelCalibrationType.PARAMS4,
28
+ constraints_type=ConstraintsType.INVERSE_MARTINGALE)
29
+ print(btc_calibrated_params)
30
+
31
+ # 5. plot model implied vols
32
+ logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
33
+ params=btc_calibrated_params)
34
+ plt.show()
@@ -90,7 +90,6 @@ def run_unit_test(unit_test: UnitTests):
90
90
  nb_path=100000)
91
91
 
92
92
  elif unit_test == UnitTests.PLOT_FIT_TO_BITCOIN_OPTION_CHAIN:
93
-
94
93
  btc_option_chain = sv.get_btc_test_chain_data()
95
94
  btc_calibrated_params = LogSvParams(sigma0=0.8327, theta=1.0139, kappa1=4.8609, kappa2=4.7940, beta=0.1988, volvol=2.3694)
96
95
  logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
@@ -102,7 +101,6 @@ def run_unit_test(unit_test: UnitTests):
102
101
  btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=btc_option_chain,
103
102
  params0=params0,
104
103
  model_calibration_type=LogsvModelCalibrationType.PARAMS4,
105
-
106
104
  constraints_type=sv.ConstraintsType.INVERSE_MARTINGALE)
107
105
  print(btc_calibrated_params)
108
106
  logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
@@ -112,7 +110,7 @@ def run_unit_test(unit_test: UnitTests):
112
110
 
113
111
  if __name__ == '__main__':
114
112
 
115
- unit_test = UnitTests.COMPARE_MODEL_VOLS_TO_MC
113
+ unit_test = UnitTests.COMPUTE_MODEL_PRICES
116
114
 
117
115
  is_run_all_tests = False
118
116
  if is_run_all_tests:
@@ -412,7 +412,7 @@ def run_unit_test(unit_test: UnitTests):
412
412
 
413
413
  elif unit_test == UnitTests.MC_COMPARISION_QVAR:
414
414
  from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar
415
- from stochvolmodels.pricers.logsv_pricer import LogSvParams
415
+ from stochvolmodels import LogSvParams
416
416
  heston_pricer = HestonPricer()
417
417
  ttms = {'1m': 1.0/12.0, '6m': 0.5}
418
418
  option_chain = chains.get_qv_options_test_chain_data()
@@ -36,7 +36,8 @@ def func_a_ode_quadratic_terms(theta: float,
36
36
  phi: np.complex128,
37
37
  psi: np.complex128,
38
38
  is_spot_measure: bool = True,
39
- expansion_order: ExpansionOrder = ExpansionOrder.FIRST
39
+ expansion_order: ExpansionOrder = ExpansionOrder.FIRST,
40
+ vol_backbone_eta: float = 1.0
40
41
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
41
42
  """
42
43
  Matrices for the quadratic form A_t = A.T@M@A + L@A + H
@@ -45,14 +46,15 @@ def func_a_ode_quadratic_terms(theta: float,
45
46
  vartheta2 = beta * beta + volvol * volvol
46
47
  qv = theta * vartheta2
47
48
  qv2 = theta2 * vartheta2
49
+ vol_backbone_eta2 = vol_backbone_eta * vol_backbone_eta
48
50
  if is_spot_measure:
49
51
  lamda = 0
50
52
  kappa2_p = kappa2
51
53
  kappa_p = kappa1 + kappa2 * theta
52
54
  else:
53
- lamda = beta*theta2
54
- kappa2_p = kappa2-beta
55
- kappa_p = kappa1 + kappa2 * theta - 2*beta*theta
55
+ lamda = beta*theta2*vol_backbone_eta
56
+ kappa2_p = kappa2-beta*vol_backbone_eta
57
+ kappa_p = kappa1 + kappa2 * theta - 2*beta*theta*vol_backbone_eta
56
58
 
57
59
  # fill Ms: M should be of same type as L and H for numba, eventhough they are real
58
60
  # utilize that M is symmetric
@@ -83,15 +85,15 @@ def func_a_ode_quadratic_terms(theta: float,
83
85
 
84
86
  # fills Ls
85
87
  L = np.zeros((n, n), dtype=np.complex128)
86
- L[0, 1], L[0, 2] = lamda - theta2 * beta * phi, qv2
87
- L[1, 1], L[1, 2] = -kappa_p - 2.0 * theta * beta * phi, 2.0 * (lamda + qv - theta2 * beta * phi)
88
- L[2, 1], L[2, 2] = -kappa2_p - beta * phi, vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * phi
88
+ L[0, 1], L[0, 2] = lamda - theta2 * beta * vol_backbone_eta * phi, qv2
89
+ L[1, 1], L[1, 2] = -kappa_p - 2.0 * theta * beta * vol_backbone_eta * phi, 2.0 * (lamda + qv - theta2 * beta * vol_backbone_eta * phi)
90
+ L[2, 1], L[2, 2] = -kappa2_p - beta * vol_backbone_eta * phi, vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * vol_backbone_eta * phi
89
91
 
90
92
  if expansion_order == ExpansionOrder.SECOND:
91
93
  L[1, 3] = 3.0*qv2
92
- L[2, 3], L[2, 4] = 3.0 * (2.0 * qv - theta2 * beta * phi), 6.0 * qv2
93
- L[3, 2], L[3, 3], L[3, 4] = -2.0 * (kappa2_p + beta * phi), 3.0 * (vartheta2 - kappa_p - 2.0 * theta * beta * phi), 4.0 * (3.0 * qv - theta2 * beta * phi)
94
- L[4, 3], L[4, 4] = -3.0 * (kappa2_p + beta * phi), 2.0 * (vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * phi)
94
+ L[2, 3], L[2, 4] = 3.0 * (2.0 * qv - theta2 * beta * vol_backbone_eta * phi), 6.0 * qv2
95
+ L[3, 2], L[3, 3], L[3, 4] = -2.0 * (kappa2_p + beta * vol_backbone_eta * phi), 3.0 * (vartheta2 - kappa_p - 2.0 * theta * beta * vol_backbone_eta * phi), 4.0 * (3.0 * qv - theta2 * beta * vol_backbone_eta * phi)
96
+ L[4, 3], L[4, 4] = -3.0 * (kappa2_p + beta * vol_backbone_eta * phi), 2.0 * (vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * vol_backbone_eta * phi)
95
97
 
96
98
  # fill Hs
97
99
  H = np.zeros(n, dtype=np.complex128)
@@ -99,7 +101,7 @@ def func_a_ode_quadratic_terms(theta: float,
99
101
  rhs = (phi * (phi + 1.0) - 2.0 * psi)
100
102
  else:
101
103
  rhs = (phi * (phi - 1.0) - 2.0 * psi)
102
- H[0], H[1], H[2] = 0.5 * theta2 * rhs, theta * rhs, 0.5 * rhs
104
+ H[0], H[1], H[2] = 0.5*theta2 * vol_backbone_eta2 * rhs, theta * vol_backbone_eta2 * rhs, 0.5*vol_backbone_eta2 * rhs
103
105
 
104
106
  return M, L, H
105
107
 
@@ -153,7 +155,8 @@ def solve_ode_for_a(ttm: float,
153
155
  a_t0: Optional[np.ndarray] = None,
154
156
  expansion_order: ExpansionOrder = ExpansionOrder.FIRST,
155
157
  is_stiff_solver: bool = False,
156
- dense_output: bool = False
158
+ dense_output: bool = False,
159
+ vol_backbone_eta: float = 1.0
157
160
  ) -> OdeResult:
158
161
  """
159
162
  solve ode for given phi
@@ -167,7 +170,8 @@ def solve_ode_for_a(ttm: float,
167
170
  phi=phi,
168
171
  psi=psi,
169
172
  expansion_order=expansion_order,
170
- is_spot_measure=is_spot_measure)
173
+ is_spot_measure=is_spot_measure,
174
+ vol_backbone_eta=vol_backbone_eta)
171
175
 
172
176
  if a_t0 is None:
173
177
  a_t0 = np.zeros_like(H, dtype=np.complex128)
@@ -361,7 +365,8 @@ def solve_a_ode_grid(phi_grid: np.ndarray,
361
365
  is_spot_measure: bool = True,
362
366
  a_t0: Optional[np.ndarray] = None,
363
367
  is_stiff_solver: bool = False,
364
- expansion_order: ExpansionOrder = ExpansionOrder.FIRST
368
+ expansion_order: ExpansionOrder = ExpansionOrder.FIRST,
369
+ vol_backbone_eta: float = 1.0
365
370
  ) -> np.ndarray:
366
371
  """
367
372
  solve ode for range phi
@@ -382,7 +387,8 @@ def solve_a_ode_grid(phi_grid: np.ndarray,
382
387
  is_stiff_solver=is_stiff_solver,
383
388
  dense_output=False,
384
389
  expansion_order=expansion_order,
385
- is_spot_measure=is_spot_measure)
390
+ is_spot_measure=is_spot_measure,
391
+ vol_backbone_eta=vol_backbone_eta)
386
392
 
387
393
  a_t1 = np.zeros((phi_grid.shape[0], get_expansion_n(expansion_order)), dtype=np.complex128)
388
394
  for idx, (phi, psi) in enumerate(zip(phi_grid, psi_grid)):
@@ -429,6 +435,7 @@ def compute_logsv_a_mgf_grid(ttm: float,
429
435
  is_stiff_solver: bool = False,
430
436
  is_analytic: bool = False,
431
437
  is_spot_measure: bool = True,
438
+ vol_backbone_eta: float = 1.0,
432
439
  **kwargs
433
440
  ) -> Tuple[np.ndarray, np.ndarray]:
434
441
  """
@@ -471,7 +478,8 @@ def compute_logsv_a_mgf_grid(ttm: float,
471
478
  a_t0=a_t0,
472
479
  is_stiff_solver=is_stiff_solver,
473
480
  expansion_order=expansion_order,
474
- is_spot_measure=is_spot_measure)
481
+ is_spot_measure=is_spot_measure,
482
+ vol_backbone_eta=vol_backbone_eta)
475
483
 
476
484
  y = sigma0 - theta
477
485
  if expansion_order == ExpansionOrder.FIRST:
@@ -0,0 +1,176 @@
1
+ from dataclasses import dataclass, asdict
2
+ from typing import Optional, Dict, Any
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from numpy import linalg as la
7
+ from qis import find_nearest
8
+
9
+ from stochvolmodels import VariableType
10
+ from stochvolmodels.pricers.model_pricer import ModelParams
11
+
12
+
13
+ @dataclass
14
+ class LogSvParams(ModelParams):
15
+ """
16
+ Implementation of model params class
17
+ """
18
+ sigma0: float = 0.2
19
+ theta: float = 0.2
20
+ kappa1: float = 1.0
21
+ kappa2: Optional[float] = 2.5 # Optional is mapped to self.kappa1 / self.theta
22
+ beta: float = -1.0
23
+ volvol: float = 1.0
24
+ vol_backbone: pd.Series = None
25
+
26
+ def __post_init__(self):
27
+ if self.kappa2 is None:
28
+ self.kappa2 = self.kappa1 / self.theta
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ return asdict(self)
32
+
33
+ def to_str(self) -> str:
34
+ return f"sigma0={self.sigma0:0.2f}, theta={self.theta:0.2f}, kappa1={self.kappa1:0.2f}, kappa2={self.kappa2:0.2f}, " \
35
+ f"beta={self.beta:0.2f}, volvol={self.volvol:0.2f}"
36
+
37
+ def set_vol_backbone(self, vol_backbone: pd.Series) -> None:
38
+ self.vol_backbone = vol_backbone
39
+
40
+ def get_vol_backbone_eta(self, tau: float) -> float:
41
+ if self.vol_backbone is not None:
42
+ nearest_tau = find_nearest(a=self.vol_backbone.index.to_numpy(), value=tau, is_equal_or_largest=True)
43
+ vol_backbone_eta = self.vol_backbone.loc[nearest_tau]
44
+ else:
45
+ vol_backbone_eta = 1.0
46
+ return vol_backbone_eta
47
+
48
+ def get_vol_backbone_etas(self, ttms: np.ndarray) -> np.ndarray:
49
+ if self.vol_backbone is not None:
50
+ vol_backbone_etas = np.ones_like(ttms)
51
+ for idx, tau in enumerate(ttms):
52
+ nearest_tau = find_nearest(a=self.vol_backbone.index.to_numpy(), value=tau, is_equal_or_largest=True)
53
+ vol_backbone_etas[idx] = self.vol_backbone.loc[nearest_tau]
54
+ else:
55
+ vol_backbone_etas = np.ones_like(ttms)
56
+ return vol_backbone_etas
57
+
58
+ @property
59
+ def kappa(self) -> float:
60
+ return self.kappa1+self.kappa2*self.theta
61
+
62
+ @property
63
+ def theta2(self) -> float:
64
+ return self.theta*self.theta
65
+
66
+ @property
67
+ def vartheta2(self) -> float:
68
+ return self.beta*self.beta + self.volvol*self.volvol
69
+
70
+ @property
71
+ def gamma(self) -> float:
72
+ """
73
+ assume kappa2 = kappa1 / theta
74
+ """
75
+ return self.kappa1 / self.theta
76
+
77
+ @property
78
+ def eta(self) -> float:
79
+ """
80
+ assume kappa2 = kappa1 / theta
81
+ """
82
+ return self.kappa1 * self.theta / self.vartheta2 - 1.0
83
+
84
+ def get_x_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
85
+ """
86
+ spacial grid to compute density of x
87
+ """
88
+ sigma_t = np.sqrt(ttm * 0.5 * (np.square(self.sigma0) + np.square(self.theta)))
89
+ drift = - 0.5*sigma_t*sigma_t
90
+ stdev = (n_stdevs+1)*sigma_t
91
+ return np.linspace(-stdev+drift, stdev+drift, n)
92
+
93
+ def get_sigma_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
94
+ """
95
+ spacial grid to compute density of sigma
96
+ """
97
+ sigma_t = np.sqrt(0.5*(np.square(self.sigma0) + np.square(self.theta)))
98
+ vvol = 0.5*np.sqrt(self.vartheta2*ttm)
99
+ return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
100
+
101
+ def get_qvar_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
102
+ """
103
+ spacial grid to compute density of i
104
+ """
105
+ sigma_t = np.sqrt(ttm * (np.square(self.sigma0) + np.square(self.theta)))
106
+ vvol = np.sqrt(self.vartheta2)*ttm
107
+ return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
108
+
109
+ def get_variable_space_grid(self, variable_type: VariableType = VariableType.LOG_RETURN,
110
+ ttm: float = 1.0,
111
+ n_stdevs: float = 3,
112
+ n: int = 200
113
+ ) -> np.ndarray:
114
+ if variable_type == VariableType.LOG_RETURN:
115
+ return self.get_x_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
116
+ if variable_type == VariableType.SIGMA:
117
+ return self.get_sigma_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
118
+ elif variable_type == VariableType.Q_VAR:
119
+ return self.get_qvar_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
120
+ else:
121
+ raise NotImplementedError
122
+
123
+ def get_vol_moments_lambda(self,
124
+ n_terms: int = 4
125
+ ) -> np.ndarray:
126
+
127
+ kappa2 = self.kappa2
128
+ kappa = self.kappa
129
+ vartheta2 = self.vartheta2
130
+ theta = self.theta
131
+ theta2 = self.theta2
132
+
133
+ def c(n: int) -> float:
134
+ return 0.5 * vartheta2 * n * (n - 1.0)
135
+
136
+ lambda_m = np.zeros((n_terms, n_terms))
137
+ lambda_m[0, 0] = -kappa
138
+ lambda_m[0, 1] = -kappa2
139
+ lambda_m[1, 0] = 2.0*c(2) * theta
140
+ lambda_m[1, 1] = c(2) - 2.0*kappa
141
+ lambda_m[1, 2] = -2.0*kappa2
142
+
143
+ for n_ in np.arange(2, n_terms):
144
+ n = n_ + 1 # n_ is array counter, n is formula counter
145
+ c_n = c(n)
146
+ lambda_m[n_, n_ - 2] = c_n * theta2
147
+ lambda_m[n_, n_ - 1] = 2.0 * c_n * theta
148
+ lambda_m[n_, n_] = c_n - n*kappa
149
+ if n_ + 1 < n_terms:
150
+ lambda_m[n_, n_ + 1] = -n*kappa2
151
+
152
+ return lambda_m
153
+
154
+ def assert_vol_moments_stability(self, n_terms: int = 4):
155
+ lambda_m = self.get_vol_moments_lambda(n_terms=n_terms)
156
+ w, v = la.eig(lambda_m)
157
+ cond = np.all(np.real(w)<0.0)
158
+ print(f"vol moments stable = {cond}")
159
+
160
+ def print_vol_moments_stability(self, n_terms: int = 4) -> None:
161
+ def c(n: int) -> float:
162
+ return 0.5 * self.vartheta2 * n * (n - 1.0)
163
+
164
+ cond_m2 = c(2) - 2.0*self.kappa
165
+ print(f"con2:\n{cond_m2}")
166
+ cond_m3 = c(3) - 3.0*self.kappa
167
+ print(f"con3:\n{cond_m3}")
168
+ cond_m4 = c(4) - 4.0*self.kappa
169
+ print(f"cond4:\n{cond_m4}")
170
+
171
+ lambda_m = self.get_vol_moments_lambda(n_terms=n_terms)
172
+ print(f"lambda_m:\n{lambda_m}")
173
+
174
+ w, v = la.eig(lambda_m)
175
+ print(f"eigenvalues w:\n{w}")
176
+ print(f"vol moments stable = {np.all(np.real(w)<0.0)}")
@@ -1,7 +1,7 @@
1
1
  """
2
2
  analytics for vol and QV moments computation
3
3
  """
4
-
4
+ # packages
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
  import matplotlib.pyplot as plt
@@ -9,9 +9,9 @@ import seaborn as sns
9
9
  from numpy import linalg as la
10
10
  from scipy import linalg as sla
11
11
  from enum import Enum
12
-
13
- from ...pricers.logsv_pricer import LogSvParams
14
- from ...utils.funcs import set_seed
12
+ # project
13
+ from stochvolmodels.pricers.logsv.logsv_params import LogSvParams
14
+ from stochvolmodels.utils.funcs import set_seed
15
15
 
16
16
 
17
17
  VOLVOL = 1.75
@@ -40,7 +40,7 @@ def compute_analytic_vol_moments(params: LogSvParams,
40
40
  rhs = np.zeros(n_terms)
41
41
  rhs[1] = params.vartheta2*params.theta2
42
42
 
43
- if is_qvar: # need flat boundary condition
43
+ if is_qvar: # need flat boundary condition
44
44
  rhs[-1] = -n_terms*params.kappa2*np.power(y, n_terms+1)
45
45
  else:
46
46
  rhs[-1] = -n_terms*params.kappa2*np.power(y, n_terms+1)
@@ -73,6 +73,9 @@ def compute_analytic_qvar(params: LogSvParams,
73
73
  ttm: float = 1.0,
74
74
  n_terms: int = 4
75
75
  ) -> float:
76
+ """
77
+ compute expected value [ (1/T) int^T_0 sigma^2_t dt]
78
+ """
76
79
  if np.isclose(ttm, 0.0):
77
80
  qvar = np.square(params.sigma0)
78
81
  else:
@@ -113,10 +116,45 @@ def compute_sqrt_qvar_t(params: LogSvParams, t: np.ndarray, n_terms: int = 4) ->
113
116
  return ev
114
117
 
115
118
 
119
+ def fit_model_vol_backbone_to_varswaps(log_sv_params: LogSvParams,
120
+ varswap_strikes: pd.Series,
121
+ n_terms: int = 4,
122
+ verbose: bool = False
123
+ ) -> pd.Series:
124
+ """
125
+ fit model eta so that model reproduces quadratic var
126
+ """
127
+ ttms = varswap_strikes.index.to_numpy()
128
+ market_qvar_dt = ttms * np.square(varswap_strikes.to_numpy())
129
+ # compute model qvars
130
+ model_forwards = np.array([compute_analytic_qvar(params=log_sv_params, ttm=ttm, n_terms=n_terms) for ttm in ttms])
131
+ model_qvar_dt = model_forwards*ttms
132
+ model_eta = np.ones_like(ttms)
133
+ for idx, ttm in enumerate(ttms):
134
+ if idx == 0:
135
+ model_eta[idx] = market_qvar_dt[idx] / model_qvar_dt[idx]
136
+ else:
137
+ model_eta[idx] = (market_qvar_dt[idx]-market_qvar_dt[idx-1]) / (model_qvar_dt[idx]-model_qvar_dt[idx-1])
138
+ # model_eta = np.where(model_eta > 0.0, np.sqrt(model_eta), 1.0)
139
+ model_eta = np.where(model_eta > 0.0, model_eta, 1.0)
140
+ # adhoc adjustemnt for now
141
+ model_eta = np.where(ttms < 0.06, np.sqrt(model_eta), model_eta)
142
+
143
+ model_eta = pd.Series(model_eta, index=ttms)
144
+ if verbose:
145
+ varswap_strikes = np.sqrt(varswap_strikes.to_frame('vars_swap strikes'))
146
+ varswap_strikes['market_qvar_dt'] = market_qvar_dt
147
+ varswap_strikes['model_qvar_dt'] = model_qvar_dt
148
+ varswap_strikes['model_eta'] = model_eta
149
+ print(f"vars_swaps\n{varswap_strikes}")
150
+ return model_eta
151
+
152
+
116
153
  class UnitTests(Enum):
117
154
  VOL_MOMENTS = 1
118
155
  EXPECTED_VOL = 2
119
156
  EXPECTED_QVAR = 3
157
+ VOL_BACKBONE = 4
120
158
 
121
159
 
122
160
  def run_unit_test(unit_test: UnitTests):
@@ -185,12 +223,17 @@ def run_unit_test(unit_test: UnitTests):
185
223
  sns.lineplot(data=analytic_vol_moments, dashes=False, ax=ax)
186
224
  ax.errorbar(x=df.index[::5], y=mc_mean[::5], yerr=mc_std[::5], fmt='o', color='green', capsize=8)
187
225
 
226
+ elif unit_test == UnitTests.VOL_BACKBONE:
227
+ fit_model_vol_backbone_to_varswaps(log_sv_params=params,
228
+ varswap_strikes=pd.Series([1.0, 1.0], index=[1.0 / 12., 2 / 12.0]),
229
+ verbose=True)
230
+
188
231
  plt.show()
189
232
 
190
233
 
191
234
  if __name__ == '__main__':
192
235
 
193
- unit_test = UnitTests.VOL_MOMENTS
236
+ unit_test = UnitTests.VOL_BACKBONE
194
237
 
195
238
  is_run_all_tests = False
196
239
  if is_run_all_tests: