stochvolmodels 1.0.9__tar.gz → 1.0.11__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 (29) hide show
  1. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/PKG-INFO +1 -1
  2. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/pyproject.toml +1 -1
  3. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/__init__.py +5 -0
  4. stochvolmodels-1.0.11/stochvolmodels/data/fetch_option_chain.py +176 -0
  5. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/data/option_chain.py +3 -1
  6. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/data/test_option_chain.py +4 -4
  7. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/mc_payoffs.py +3 -1
  8. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/mgf_pricer.py +9 -5
  9. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/affine_expansion.py +2 -2
  10. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv_pricer.py +99 -86
  11. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/model_pricer.py +71 -47
  12. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/utils/plots.py +1 -1
  13. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/LICENSE.txt +0 -0
  14. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/README.md +0 -0
  15. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/data/__init__.py +0 -0
  16. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/__init__.py +0 -0
  17. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/__init__.py +0 -0
  18. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/bsm_pricer.py +0 -0
  19. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/config.py +0 -0
  20. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/normal_pricer.py +0 -0
  21. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
  22. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/heston_pricer.py +0 -0
  23. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/__init__.py +0 -0
  24. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
  25. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/tests/__init__.py +0 -0
  26. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
  27. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/tests/qv_pricer.py +0 -0
  28. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/utils/__init__.py +0 -0
  29. {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/utils/funcs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stochvolmodels
3
- Version: 1.0.9
3
+ Version: 1.0.11
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.9"
3
+ version = "1.0.11"
4
4
  description = "Implementation of stochastic volatility models for option pricing"
5
5
  license = "LICENSE.txt"
6
6
  authors = ["Artur Sepp <artursepp@gmail.com>"]
@@ -62,6 +62,11 @@ from stochvolmodels.pricers.logsv_pricer import (
62
62
 
63
63
  from stochvolmodels.data.option_chain import OptionChain, OptionSlice
64
64
 
65
+ from stochvolmodels.data.fetch_option_chain import (generate_vol_chain_np,
66
+ load_option_chain,
67
+ sample_option_chain_at_times,
68
+ load_price_data)
69
+
65
70
  from stochvolmodels.data.test_option_chain import (
66
71
  get_btc_test_chain_data,
67
72
  get_gld_test_chain_data,
@@ -0,0 +1,176 @@
1
+ """
2
+ this module is using option-chain-analytics package
3
+ to fetch OptionChain data with options data
4
+ see https://pypi.org/project/option-chain-analytics
5
+ """
6
+
7
+ import pandas as pd
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ import qis
11
+ from qis import TimePeriod
12
+ from typing import Dict, Tuple, Optional, Literal
13
+ from numba.typed import List
14
+ from enum import Enum
15
+
16
+ # chain
17
+ from option_chain_analytics import OptionsDataDFs, create_chain_from_from_options_dfs
18
+ from option_chain_analytics.option_chain import SliceColumn, SlicesChain
19
+
20
+ # analytics
21
+ from stochvolmodels.data.option_chain import OptionChain
22
+
23
+
24
+ def generate_vol_chain_np(chain: SlicesChain,
25
+ value_time: pd.Timestamp,
26
+ days_map: Dict[str, int] = {'1w': 7, '1m': 21},
27
+ delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
28
+ is_filtered: bool = True
29
+ ) -> OptionChain:
30
+ """
31
+ given SlicesChain generate OptionChain for calibration inputs
32
+ """
33
+
34
+ ttms, future_prices, discfactors = List(), List(), List()
35
+ optiontypes_ttms, strikes_ttms = List(), List()
36
+ bid_ivs, ask_ivs = List(), List()
37
+ bid_prices, ask_prices = List(), List()
38
+ slice_ids = []
39
+ for label, day in days_map.items():
40
+ next_date = value_time + pd.DateOffset(days=day) # if overlapping next date will be last avilable maturity
41
+ slice_date = chain.get_next_slice_after_date(mat_date=next_date)
42
+ slice_t = chain.expiry_slices[slice_date]
43
+ df = slice_t.get_joint_slice(delta_bounds=delta_bounds, is_filtered=is_filtered)
44
+ if not df.empty:
45
+ slice_ids.append(f"{label}: {slice_t.expiry_id}")
46
+ ttms.append(slice_t.get_ttm())
47
+ future_prices.append(slice_t.get_future_price())
48
+ discfactors.append(1.0)
49
+ strikes_ttms.append(df.index.to_numpy())
50
+ optiontypes_ttms.append(df[SliceColumn.OPTION_TYPE].to_numpy(dtype=str))
51
+ bid_ivs.append(df[SliceColumn.BID_IV].to_numpy())
52
+ ask_ivs.append(df[SliceColumn.ASK_IV].to_numpy())
53
+ bid_prices.append(df[SliceColumn.BID_PRICE].to_numpy())
54
+ ask_prices.append(df[SliceColumn.ASK_PRICE].to_numpy())
55
+
56
+ out = OptionChain(ttms=np.array(ttms),
57
+ forwards=np.array(future_prices),
58
+ discfactors=np.array(discfactors),
59
+ ids=np.array(slice_ids),
60
+ strikes_ttms=strikes_ttms,
61
+ optiontypes_ttms=optiontypes_ttms,
62
+ bid_ivs=bid_ivs,
63
+ ask_ivs=ask_ivs,
64
+ bid_prices=bid_prices,
65
+ ask_prices=ask_prices)
66
+ return out
67
+
68
+
69
+ def load_option_chain(options_data_dfs: OptionsDataDFs,
70
+ value_time: pd.Timestamp = pd.Timestamp('2023-02-06 08:00:00+00:00'),
71
+ days_map: Dict[str, int] = {'1w': 7, '1m': 21},
72
+ delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
73
+ is_filtered: bool = True
74
+ ) -> Optional[OptionChain]:
75
+ chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time)
76
+ if chain is not None:
77
+ option_chain = generate_vol_chain_np(chain=chain,
78
+ value_time=value_time,
79
+ days_map=days_map,
80
+ delta_bounds=delta_bounds,
81
+ is_filtered=is_filtered)
82
+ else:
83
+ option_chain = None
84
+
85
+ return option_chain
86
+
87
+
88
+ def sample_option_chain_at_times(options_data_dfs: OptionsDataDFs,
89
+ time_period: TimePeriod,
90
+ freq: str = 'W-FRI',
91
+ days_map: Dict[str, int] = {'1w': 7, '1m': 21},
92
+ delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
93
+ hour_offset: int = 8
94
+ ) -> Dict[pd.Timestamp, OptionChain]:
95
+ value_times = qis.generate_dates_schedule(time_period=time_period,
96
+ freq=freq,
97
+ hour_offset=hour_offset)
98
+ option_chains = {}
99
+ for value_time in value_times:
100
+ option_chains[value_time] = load_option_chain(options_data_dfs=options_data_dfs,
101
+ value_time=value_time,
102
+ days_map=days_map,
103
+ delta_bounds=delta_bounds,
104
+ is_filtered=True)
105
+ return option_chains
106
+
107
+
108
+ def load_price_data(options_data_dfs: OptionsDataDFs,
109
+ time_period: TimePeriod = None,
110
+ data: Literal['spot', 'perp', 'funding_rate'] = 'spot',
111
+ freq: Optional[str] = 'D' # to do
112
+ ) -> pd.Series:
113
+ #options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker, freq='D', hour_offset=8))
114
+ spot_price = options_data_dfs.get_spot_data()[data]
115
+ if freq is not None:
116
+ spot_price = spot_price.resample(freq).last()
117
+ if time_period is not None:
118
+ spot_price = time_period.locate(spot_price)
119
+ return spot_price
120
+
121
+
122
+ class UnitTests(Enum):
123
+ PRINT_CHAIN_DATA = 1
124
+ GENERATE_VOL_CHAIN_NP = 2
125
+ SAMPLE_CHAIN_AT_TIMES = 3
126
+
127
+
128
+ def run_unit_test(unit_test: UnitTests):
129
+
130
+ ticker = 'BTC' # BTC, ETH
131
+ value_time = pd.Timestamp('2021-10-21 08:00:00+00:00')
132
+ value_time = pd.Timestamp('2023-10-06 08:00:00+00:00')
133
+
134
+ from option_chain_analytics.ts_loaders import ts_data_loader_wrapper
135
+ options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker))
136
+ options_data_dfs.get_start_end_date().print()
137
+ chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time)
138
+
139
+ if unit_test == UnitTests.PRINT_CHAIN_DATA:
140
+ for expiry, eslice in chain.expiry_slices.items():
141
+ eslice.print()
142
+
143
+ elif unit_test == UnitTests.GENERATE_VOL_CHAIN_NP:
144
+ option_chain = generate_vol_chain_np(chain=chain,
145
+ value_time=value_time,
146
+ days_map={'1w': 7},
147
+ delta_bounds=(-0.1, 0.1),
148
+ is_filtered=True)
149
+ option_chain.print()
150
+ skews = option_chain.get_chain_skews(delta=0.35)
151
+ print(skews)
152
+
153
+ elif unit_test == UnitTests.SAMPLE_CHAIN_AT_TIMES:
154
+ time_period = qis.TimePeriod('01Jan2023', '31Jan2023', tz='UTC')
155
+ option_chains = sample_option_chain_at_times(options_data_dfs=options_data_dfs,
156
+ time_period=time_period,
157
+ freq='W-FRI',
158
+ hour_offset=9
159
+ )
160
+ for key, chain in option_chains.items():
161
+ print(f"{key}")
162
+ print(chain)
163
+
164
+ plt.show()
165
+
166
+
167
+ if __name__ == '__main__':
168
+
169
+ unit_test = UnitTests.SAMPLE_CHAIN_AT_TIMES
170
+
171
+ is_run_all_tests = False
172
+ if is_run_all_tests:
173
+ for unit_test in UnitTests:
174
+ run_unit_test(unit_test=unit_test)
175
+ else:
176
+ run_unit_test(unit_test=unit_test)
@@ -85,7 +85,9 @@ class OptionChain:
85
85
  forwards=self.forwards,
86
86
  strikes_ttms=self.strikes_ttms,
87
87
  optiontypes_ttms=self.optiontypes_ttms,
88
- ids=self.ids)
88
+ ids=self.ids,
89
+ bid_ivs=self.bid_ivs,
90
+ ask_ivs=self.ask_ivs)
89
91
  for k, v in this.items():
90
92
  print(f"{k}:\n{v}")
91
93
 
@@ -847,10 +847,10 @@ def get_qv_options_test_chain_data(num_strikes: int = 21) -> OptionChain:
847
847
  """
848
848
  BTC implied vols of 21Oct2021
849
849
  """
850
- ids = array(['1m', '3m', '6m', '12m'])
851
- ttms = array([0.083333333, 0.25, 0.5, 1.0])
852
- forwards = array([1.0, 1.0, 1.0, 1.0])
853
- discfactors = array([1.0, 1.0, 1.0, 1.0])
850
+ ids = array(['1w', '2w', '1m', '3m', '6m', '12m'])
851
+ ttms = array([7.0/365.0, 14.0/365.0, 0.083333333, 0.25, 0.5, 1.0])
852
+ forwards = array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
853
+ discfactors = array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
854
854
  strikes_ttm = np.linspace(0.75, 1.5, num_strikes)
855
855
  strikes_ttms = (strikes_ttm, strikes_ttm, strikes_ttm, strikes_ttm)
856
856
 
@@ -9,7 +9,9 @@ from ...pricers.core.config import VariableType
9
9
 
10
10
 
11
11
  @njit(cache=False, fastmath=True)
12
- def compute_mc_vars_payoff(x0: np.ndarray, sigma0: np.ndarray, qvar0: np.ndarray,
12
+ def compute_mc_vars_payoff(x0: np.ndarray,
13
+ sigma0: np.ndarray,
14
+ qvar0: np.ndarray,
13
15
  ttm: float,
14
16
  forward: float,
15
17
  strikes_ttm: np.ndarray,
@@ -39,8 +39,10 @@ def get_phi_grid(is_spot_measure: bool = True,
39
39
  def get_psi_grid() -> np.ndarray:
40
40
  """
41
41
  for I = QV variable
42
+ need a lot of step for short-dated options
43
+ todo: find a non-uniform grid for short dated options
42
44
  """
43
- p = np.linspace(0, 200, 4000)
45
+ p = np.linspace(0, 4000, 40000)
44
46
  real_p = -0.5
45
47
  psi_grid = real_p + 1j * p
46
48
  return psi_grid
@@ -51,8 +53,8 @@ def get_theta_grid() -> np.ndarray:
51
53
  """
52
54
  for sigma
53
55
  """
54
- p = np.linspace(0, 600, 4000)
55
- real_p = -0.5
56
+ p = np.linspace(0, 600, 5000)
57
+ real_p = 0.0
56
58
  theta_grid = real_p + 1j * p
57
59
  return theta_grid
58
60
 
@@ -303,6 +305,7 @@ def pdf_with_mgf_grid(log_mgf_grid: np.ndarray,
303
305
  transform_var_grid: np.ndarray,
304
306
  space_grid: np.ndarray,
305
307
  shift: float = 0.0,
308
+ scale: float = 1.0,
306
309
  is_simpson: bool = True
307
310
  ) -> np.ndarray:
308
311
  """
@@ -313,8 +316,9 @@ def pdf_with_mgf_grid(log_mgf_grid: np.ndarray,
313
316
  """
314
317
  dp = compute_integration_weights(var_grid=transform_var_grid, is_simpson=is_simpson) / np.pi
315
318
  pdf = np.zeros_like(space_grid)
316
- for idx, x in enumerate(space_grid):
317
- pdf[idx] = np.nansum(np.real(dp * np.exp((x-shift) * transform_var_grid + log_mgf_grid)))
319
+ z = (space_grid - shift) / scale
320
+ for idx, x in enumerate(z):
321
+ pdf[idx] = np.nansum(np.real(dp * np.exp(x * transform_var_grid + log_mgf_grid)))
318
322
  dx = space_grid[1] - space_grid[0]
319
323
  pdf = dx * pdf
320
324
  return pdf
@@ -47,12 +47,12 @@ def func_a_ode_quadratic_terms(theta: float,
47
47
  qv2 = theta2 * vartheta2
48
48
  if is_spot_measure:
49
49
  lamda = 0
50
- kappa_p = kappa1 + kappa2 * theta
51
50
  kappa2_p = kappa2
51
+ kappa_p = kappa1 + kappa2 * theta
52
52
  else:
53
53
  lamda = beta*theta2
54
- kappa_p = kappa1 + kappa2 * theta - 2*beta*theta
55
54
  kappa2_p = kappa2-beta
55
+ kappa_p = kappa1 + kappa2 * theta - 2*beta*theta
56
56
 
57
57
  # fill Ms: M should be of same type as L and H for numba, eventhough they are real
58
58
  # utilize that M is symmetric
@@ -62,6 +62,10 @@ class LogSvParams(ModelParams):
62
62
  def to_dict(self) -> Dict[str, Any]:
63
63
  return asdict(self)
64
64
 
65
+ def to_str(self) -> str:
66
+ return f"sigma0={self.sigma0:0.2f}, theta={self.theta:0.2f}, kappa1={self.kappa1:0.2f}, kappa2={self.kappa2:0.2f}, " \
67
+ f"beta={self.beta:0.2f}, volvol={self.volvol:0.2f}"
68
+
65
69
  @property
66
70
  def kappa(self) -> float:
67
71
  return self.kappa1+self.kappa2*self.theta
@@ -88,24 +92,35 @@ class LogSvParams(ModelParams):
88
92
  """
89
93
  return self.kappa1 * self.theta / self.vartheta2 - 1.0
90
94
 
91
- def get_x_grid(self, ttm: float = 1.0, n_stdevs: int = 3, n: int = 200) -> np.ndarray:
95
+ def get_x_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
96
+ """
97
+ spacial grid to compute density of x
98
+ """
92
99
  sigma_t = np.sqrt(ttm * 0.5 * (np.square(self.sigma0) + np.square(self.theta)))
93
100
  drift = - 0.5*sigma_t*sigma_t
94
101
  stdev = (n_stdevs+1)*sigma_t
95
102
  return np.linspace(-stdev+drift, stdev+drift, n)
96
103
 
97
- def get_sigma_grid(self, ttm: float = 1.0, n_stdevs: int = 3, n: int = 200) -> np.ndarray:
98
- sigma_t = np.sqrt(ttm * 0.5 * (np.square(self.sigma0) + np.square(self.theta)))
99
- vvol = np.sqrt(self.vartheta2/np.abs(2.0*self.kappa1))
104
+ def get_sigma_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
105
+ """
106
+ spacial grid to compute density of sigma
107
+ """
108
+ sigma_t = np.sqrt(0.5*(np.square(self.sigma0) + np.square(self.theta)))
109
+ vvol = 0.5*np.sqrt(self.vartheta2*ttm)
100
110
  return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
101
111
 
102
- def get_qvar_grid(self, ttm: float = 1.0, n_stdevs: int = 3, n: int = 200) -> np.ndarray:
103
- sigma_t = np.sqrt(ttm * 0.5 * (np.square(self.sigma0) + np.square(self.theta)))
104
- vvol = np.sqrt(self.vartheta2/np.abs(2.0*self.kappa1))
112
+ def get_qvar_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
113
+ """
114
+ spacial grid to compute density of i
115
+ """
116
+ sigma_t = np.sqrt(ttm * (np.square(self.sigma0) + np.square(self.theta)))
117
+ vvol = np.sqrt(self.vartheta2)*ttm
105
118
  return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
106
119
 
107
120
  def get_variable_space_grid(self, variable_type: VariableType = VariableType.LOG_RETURN,
108
- ttm: float = 1.0, n_stdevs: int = 3, n: int = 200
121
+ ttm: float = 1.0,
122
+ n_stdevs: float = 3,
123
+ n: int = 200
109
124
  ) -> np.ndarray:
110
125
  if variable_type == VariableType.LOG_RETURN:
111
126
  return self.get_x_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
@@ -177,7 +192,7 @@ LOGSV_BTC_PARAMS = LogSvParams(sigma0=0.8376, theta=1.0413, kappa1=3.1844, kappa
177
192
 
178
193
  class LogSVPricer(ModelPricer):
179
194
 
180
- @timer
195
+ # @timer
181
196
  def price_chain(self,
182
197
  option_chain: OptionChain,
183
198
  params: LogSvParams,
@@ -201,7 +216,10 @@ class LogSVPricer(ModelPricer):
201
216
  def model_mc_price_chain(self,
202
217
  option_chain: OptionChain,
203
218
  params: LogSvParams,
219
+ is_spot_measure: bool = True,
220
+ variable_type: VariableType = VariableType.LOG_RETURN,
204
221
  nb_path: int = 100000,
222
+ nb_steps: Optional[int] = None,
205
223
  **kwargs
206
224
  ) -> (List[np.ndarray], List[np.ndarray]):
207
225
  return logsv_mc_chain_pricer(v0=params.sigma0,
@@ -215,8 +233,18 @@ class LogSVPricer(ModelPricer):
215
233
  discfactors=option_chain.discfactors,
216
234
  strikes_ttms=option_chain.strikes_ttms,
217
235
  optiontypes_ttms=option_chain.optiontypes_ttms,
236
+ is_spot_measure=is_spot_measure,
237
+ variable_type=variable_type,
218
238
  nb_path=nb_path,
219
- **kwargs)
239
+ nb_steps=nb_steps or int(360*np.max(option_chain.ttms))+1)
240
+
241
+ def set_vol_scaler(self, option_chain: OptionChain) -> float:
242
+ """
243
+ use chain vols to set the scaler
244
+ """
245
+ atm0 = option_chain.get_chain_atm_vols()[0]
246
+ ttm0 = option_chain.ttms[0]
247
+ return set_vol_scaler(sigma0=atm0, ttm=ttm0)
220
248
 
221
249
  @timer
222
250
  def calibrate_model_params_to_chain(self,
@@ -233,9 +261,7 @@ class LogSVPricer(ModelPricer):
233
261
  """
234
262
  implementation of model calibration interface with nonlinear constraints
235
263
  """
236
- atm0 = option_chain.get_chain_atm_vols()[0]
237
- ttm0 = option_chain.ttms[0]
238
- vol_scaler = set_vol_scaler(sigma0=atm0, ttm=ttm0)
264
+ vol_scaler = self.set_vol_scaler(option_chain=option_chain)
239
265
 
240
266
  x, market_vols = option_chain.get_chain_data_as_xy()
241
267
  market_vols = to_flat_np_array(market_vols) # market mid quotes
@@ -248,7 +274,46 @@ class LogSVPricer(ModelPricer):
248
274
  else:
249
275
  weights = np.ones_like(market_vols)
250
276
 
251
- # implement different calibrato types
277
+ def parse_model_params(pars: np.ndarray) -> LogSvParams:
278
+ if model_calibration_type == LogsvModelCalibrationType.PARAMS4:
279
+ fit_params = LogSvParams(sigma0=pars[0],
280
+ theta=pars[1],
281
+ kappa1=params0.kappa1,
282
+ kappa2=params0.kappa2,
283
+ beta=pars[2],
284
+ volvol=pars[3])
285
+ elif model_calibration_type == LogsvModelCalibrationType.PARAMS5:
286
+ fit_params = LogSvParams(sigma0=pars[0],
287
+ theta=pars[1],
288
+ kappa1=pars[2],
289
+ kappa2=None,
290
+ beta=pars[3],
291
+ volvol=pars[4])
292
+ else:
293
+ raise NotImplementedError(f"{model_calibration_type}")
294
+ return fit_params
295
+
296
+ def objective(pars: np.ndarray, args: np.ndarray) -> float:
297
+ params = parse_model_params(pars=pars)
298
+ model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, vol_scaler=vol_scaler)
299
+ resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
300
+ return resid
301
+
302
+ # parametric constraints
303
+ def martingale_measure(pars: np.ndarray) -> float:
304
+ params = parse_model_params(pars=pars)
305
+ return params.kappa2 - params.beta
306
+
307
+ def inverse_measure(pars: np.ndarray) -> float:
308
+ params = parse_model_params(pars=pars)
309
+ return params.kappa2 - 2.0 * params.beta
310
+
311
+ def vol_4thmoment_finite(pars: np.ndarray) -> float:
312
+ params = parse_model_params(pars=pars)
313
+ kappa = params.kappa1 + params.kappa2 * params.theta
314
+ return kappa - 1.5 * params.vartheta2
315
+
316
+ # set initial params
252
317
  if model_calibration_type == LogsvModelCalibrationType.PARAMS4:
253
318
  # fit: v0, theta, beta, volvol; kappa1, kappa2 is given with params0
254
319
  p0 = np.array([params0.sigma0, params0.theta, params0.beta, params0.volvol])
@@ -257,29 +322,6 @@ class LogSVPricer(ModelPricer):
257
322
  (params_min.beta, params_max.beta),
258
323
  (params_min.volvol, params_max.volvol))
259
324
 
260
- def objective(pars: np.ndarray, args: np.ndarray) -> float:
261
- v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
262
- params = LogSvParams(sigma0=v0, theta=theta, kappa1=params0.kappa1,
263
- kappa2=params0.kappa2, beta=beta, volvol=volvol)
264
- model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params,
265
- vol_scaler=vol_scaler)
266
- resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
267
- return resid
268
-
269
- def martingale_measure(pars: np.ndarray) -> float:
270
- v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
271
- return params0.kappa2 - beta
272
-
273
- def inverse_measure(pars: np.ndarray) -> float:
274
- v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
275
- return params0.kappa2 - 2.0 * beta
276
-
277
- def vol_4thmoment_finite(pars: np.ndarray) -> float:
278
- v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
279
- vartheta2 = beta * beta + volvol * volvol
280
- kappa = params0.kappa1 + params0.kappa2 * theta
281
- return kappa - 1.5 * vartheta2
282
-
283
325
  elif model_calibration_type == LogsvModelCalibrationType.PARAMS5:
284
326
  # fit: v0, theta, kappa1, beta, volvol; kappa2 is mapped as kappa1 / theta
285
327
  p0 = np.array([params0.sigma0, params0.theta, params0.kappa1, params0.beta, params0.volvol])
@@ -289,28 +331,6 @@ class LogSVPricer(ModelPricer):
289
331
  (params_min.beta, params_max.beta),
290
332
  (params_min.volvol, params_max.volvol))
291
333
 
292
- def objective(pars: np.ndarray, args: np.ndarray) -> float:
293
- v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
294
- params = LogSvParams(sigma0=v0, theta=theta, kappa1=kappa1, kappa2=None, beta=beta, volvol=volvol)
295
- model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, vol_scaler=vol_scaler)
296
- resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
297
- return resid
298
-
299
- def martingale_measure(pars: np.ndarray) -> float:
300
- v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
301
- return kappa1 / theta - beta
302
-
303
- def inverse_measure(pars: np.ndarray) -> float:
304
- v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
305
- return kappa1 / theta - 2.0*beta
306
-
307
- def vol_4thmoment_finite(pars: np.ndarray) -> float:
308
- v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
309
- vartheta2 = beta*beta + volvol*volvol
310
- kappa2 = kappa1 / theta
311
- kappa = kappa1 + kappa2 * theta
312
- return kappa - 1.5*vartheta2
313
-
314
334
  else:
315
335
  raise NotImplementedError(f"{model_calibration_type}")
316
336
 
@@ -349,26 +369,7 @@ class LogSVPricer(ModelPricer):
349
369
  else:
350
370
  res = minimize(objective, p0, args=None, method='SLSQP', bounds=bounds, options=options)
351
371
 
352
- popt = res.x
353
-
354
- if model_calibration_type == LogsvModelCalibrationType.PARAMS4:
355
- fit_params = LogSvParams(sigma0=popt[0],
356
- theta=popt[1],
357
- kappa1=params0.kappa1,
358
- kappa2=params0.kappa2,
359
- beta=popt[2],
360
- volvol=popt[3])
361
-
362
- elif model_calibration_type == LogsvModelCalibrationType.PARAMS5:
363
- fit_params = LogSvParams(sigma0=popt[0],
364
- theta=popt[1],
365
- kappa1=popt[2],
366
- kappa2=None,
367
- beta=popt[3],
368
- volvol=popt[4])
369
-
370
- else:
371
- raise NotImplementedError(f"{model_calibration_type}")
372
+ fit_params = parse_model_params(pars=res.x)
372
373
 
373
374
  return fit_params
374
375
 
@@ -379,12 +380,14 @@ class LogSVPricer(ModelPricer):
379
380
  ttm: float = 1.0,
380
381
  nb_path: int = 100000,
381
382
  is_spot_measure: bool = True,
382
- nb_steps: int = 360,
383
+ nb_steps: int = None,
384
+ year_days: int = 360,
383
385
  **kwargs
384
386
  ) -> Tuple[np.ndarray, np.ndarray]:
385
387
  """
386
388
  simulate vols in dt_path grid
387
389
  """
390
+ nb_steps = nb_steps or int(np.ceil(year_days * ttm))
388
391
  sigma_t, grid_t = simulate_vol_paths(ttm=ttm,
389
392
  v0=params.sigma0,
390
393
  theta=params.theta,
@@ -487,7 +490,8 @@ def logsv_chain_pricer(params: LogSvParams,
487
490
  is_spot_measure: bool = True,
488
491
  expansion_order: ExpansionOrder = ExpansionOrder.SECOND,
489
492
  variable_type: VariableType = VariableType.LOG_RETURN,
490
- vol_scaler: float = None
493
+ vol_scaler: float = None,
494
+ **kwargs
491
495
  ) -> List[np.ndarray]:
492
496
  """
493
497
  wrapper to price option chain on variable_type
@@ -590,19 +594,24 @@ def logsv_pdfs(params: LogSvParams,
590
594
  if variable_type == VariableType.LOG_RETURN:
591
595
  transform_var_grid = phi_grid
592
596
  shift = 0.0
593
- elif variable_type == VariableType.Q_VAR:
597
+ scale = 1.0
598
+ elif variable_type == VariableType.Q_VAR: # scaled by ttm
594
599
  transform_var_grid = psi_grid
595
600
  shift = 0.0
601
+ scale = 1.0 / ttm
596
602
  elif variable_type == VariableType.SIGMA:
597
603
  transform_var_grid = theta_grid
598
604
  shift = params.theta
605
+ scale = 1.0
599
606
  else:
600
607
  raise NotImplementedError
601
608
 
602
609
  pdf = mgfp.pdf_with_mgf_grid(log_mgf_grid=log_mgf_grid,
603
610
  transform_var_grid=transform_var_grid,
604
611
  space_grid=space_grid,
605
- shift=shift)
612
+ shift=shift,
613
+ scale=scale)
614
+ pdf = pdf / scale
606
615
  return pdf
607
616
 
608
617
 
@@ -620,9 +629,9 @@ def logsv_mc_chain_pricer(ttms: np.ndarray,
620
629
  volvol: float,
621
630
  is_spot_measure: bool = True,
622
631
  nb_path: int = 100000,
632
+ nb_steps: int = 360,
623
633
  variable_type: VariableType = VariableType.LOG_RETURN
624
634
  ) -> Tuple[List[np.ndarray], List[np.ndarray]]:
625
-
626
635
  # starting values
627
636
  x0 = np.zeros(nb_path)
628
637
  qvar0 = np.zeros(nb_path)
@@ -643,6 +652,7 @@ def logsv_mc_chain_pricer(ttms: np.ndarray,
643
652
  beta=beta,
644
653
  volvol=volvol,
645
654
  nb_path=nb_path,
655
+ nb_steps=nb_steps,
646
656
  is_spot_measure=is_spot_measure)
647
657
  ttm0 = ttm
648
658
  option_prices, option_std = compute_mc_vars_payoff(x0=x0, sigma0=sigma0, qvar0=qvar0,
@@ -714,7 +724,9 @@ def simulate_logsv_x_vol_terminal(ttm: float,
714
724
  nb_path: int = 100000,
715
725
  nb_steps: int = 360
716
726
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
717
-
727
+ """
728
+ mc simulator for terminal values of log-return, vol sigma0, and qvar for log sv model
729
+ """
718
730
  if x0.shape[0] == 1: # initial value
719
731
  x0 = x0*np.zeros(nb_path)
720
732
  else:
@@ -744,9 +756,10 @@ def simulate_logsv_x_vol_terminal(ttm: float,
744
756
  for t_, (w0, w1) in enumerate(zip(W0, W1)):
745
757
  sigma0_2dt = sigma0 * sigma0 * dt
746
758
  x0 = x0 + alpha * 0.5 * sigma0_2dt + sigma0 * w0
747
- qvar0 = qvar0 + sigma0_2dt
748
759
  vol_var = vol_var + ((kappa1 * theta / sigma0 - kappa1) + kappa2*(theta-sigma0) + adj*sigma0 - 0.5*vartheta2) * dt + beta*w0+volvol*w1
749
760
  sigma0 = np.exp(vol_var)
761
+ qvar0 = qvar0 + 0.5*(sigma0_2dt+sigma0 * sigma0 * dt)
762
+
750
763
 
751
764
  return x0, sigma0, qvar0
752
765
 
@@ -9,6 +9,7 @@ market options data is passed using data container ChainData
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import string
12
13
  import pandas as pd
13
14
  import numpy as np
14
15
  import matplotlib.pyplot as plt
@@ -18,6 +19,7 @@ from abc import ABC, abstractmethod
18
19
  from scipy import stats
19
20
  from dataclasses import dataclass, asdict
20
21
  from typing import Tuple, Optional, Dict
22
+ import qis as qis
21
23
 
22
24
  from stochvolmodels.pricers.core.config import VariableType
23
25
  from stochvolmodels.data.option_chain import OptionChain, OptionSlice
@@ -58,12 +60,13 @@ class ModelPricer(ABC):
58
60
  def compute_chain_prices_with_vols(self,
59
61
  option_chain: OptionChain,
60
62
  params: ModelParams,
63
+ variable_type: VariableType = VariableType.LOG_RETURN,
61
64
  **kwargs
62
65
  ) -> Tuple[List[np.ndarray], List[np.ndarray]]:
63
66
  """
64
67
  price chain and compute model vols
65
68
  """
66
- model_prices = self.price_chain(option_chain=option_chain, params=params, **kwargs)
69
+ model_prices = self.price_chain(option_chain=option_chain, params=params, variable_type=variable_type, **kwargs)
67
70
  model_ivols = option_chain.compute_model_ivols_from_chain_data(model_prices=model_prices)
68
71
  return model_prices, model_ivols
69
72
 
@@ -80,7 +83,10 @@ class ModelPricer(ABC):
80
83
  **kwargs)
81
84
  return model_ivols
82
85
 
83
- def model_mc_price_chain(self, option_chain: OptionChain, params: ModelParams, **kwargs
86
+ def model_mc_price_chain(self,
87
+ option_chain: OptionChain, params: ModelParams,
88
+ variable_type: VariableType = VariableType.LOG_RETURN,
89
+ **kwargs
84
90
  ) -> Tuple[List[np.ndarray], List[np.ndarray]]:
85
91
  """
86
92
  abstract method for pricing chain data using simulation of model dynamics
@@ -161,6 +167,7 @@ class ModelPricer(ABC):
161
167
  def compute_mc_chain_implied_vols(self,
162
168
  option_chain: OptionChain,
163
169
  params: ModelParams,
170
+ variable_type: VariableType = VariableType.LOG_RETURN,
164
171
  nb_path: int = 100000,
165
172
  **kwargs
166
173
  ) -> Tuple[List[np.ndarray], ...]:
@@ -169,6 +176,7 @@ class ModelPricer(ABC):
169
176
  """
170
177
  model_prices_ttms, option_std_ttms = self.model_mc_price_chain(option_chain=option_chain,
171
178
  params=params,
179
+ variable_type=variable_type,
172
180
  nb_path=nb_path,
173
181
  **kwargs)
174
182
  std_factor = 1.96
@@ -307,6 +315,7 @@ class ModelPricer(ABC):
307
315
  is_log_strike_xaxis: bool = False,
308
316
  headers: Optional[List[str]] = None,
309
317
  xvar_format: str = None,
318
+ figsize: Tuple[float, float] = plot.FIGSIZE,
310
319
  **kwargs
311
320
  ) -> plt.Figure:
312
321
  """
@@ -316,19 +325,19 @@ class ModelPricer(ABC):
316
325
  model_ivols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, **kwargs)
317
326
 
318
327
  num_slices = len(option_chain.ttms)
319
- if num_slices == 1:
320
- with sns.axes_style('darkgrid'):
321
- fig, ax = plt.subplots(1, 1, figsize=plot.FIGSIZE, tight_layout=True)
322
- axs = np.array([[ax, np.nan], [np.nan, np.nan]])
323
- elif num_slices == 2:
324
- with sns.axes_style('darkgrid'):
325
- fig, axs = plt.subplots(2, 1, figsize=plot.FIGSIZE, tight_layout=True)
326
- axs = np.array([[axs[0], np.nan], [axs[1], np.nan]])
327
- elif num_slices in [3, 4]:
328
- with sns.axes_style('darkgrid'):
329
- fig, axs = plt.subplots(2, 2, figsize=plot.FIGSIZE, tight_layout=True)
330
- else:
331
- raise NotImplementedError
328
+ with sns.axes_style('darkgrid'):
329
+ if num_slices == 1:
330
+ fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
331
+ axs = [ax]
332
+ elif num_slices == 2:
333
+ fig, axs = plt.subplots(1, 2, figsize=figsize, tight_layout=True)
334
+ elif num_slices == 3:
335
+ fig, axs = plt.subplots(1, 3, figsize=figsize, tight_layout=True)
336
+ elif num_slices == 4:
337
+ fig, axs = plt.subplots(2, 2, figsize=figsize, tight_layout=True)
338
+ axs = qis.to_flat_list(axs)
339
+ else:
340
+ raise NotImplementedError
332
341
 
333
342
  atm_vols = option_chain.get_chain_atm_vols()
334
343
  for idx, ttm in enumerate(option_chain.ttms):
@@ -367,7 +376,7 @@ class ModelPricer(ABC):
367
376
  strike_name=strike_name,
368
377
  xvar_format=xvar_format,
369
378
  x_rotation=0,
370
- ax=axs[idx % 2][idx // 2],
379
+ ax=axs[idx],
371
380
  **kwargs)
372
381
  return fig
373
382
 
@@ -377,8 +386,9 @@ class ModelPricer(ABC):
377
386
  is_log_strike_xaxis: bool = False,
378
387
  variable_type: VariableType = VariableType.LOG_RETURN,
379
388
  nb_path: int = 100000,
389
+ figsize: Tuple[float, float] = plot.FIGSIZE,
380
390
  **kwargs
381
- ) -> None:
391
+ ) -> plt.Figure:
382
392
  """
383
393
  comparision of model implied vols computed old_analytics vs mc pricer
384
394
  optimized for 2*2 figure
@@ -391,11 +401,20 @@ class ModelPricer(ABC):
391
401
  nb_path=nb_path,
392
402
  **kwargs)
393
403
 
404
+ num_slices = len(option_chain.ttms)
394
405
  with sns.axes_style('darkgrid'):
395
- if len(option_chain.ttms) > 1:
396
- fig, axs = plt.subplots(2, len(option_chain.ttms) // 2, figsize=plot.FIGSIZE, tight_layout=True)
406
+ if num_slices == 1:
407
+ fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
408
+ axs = [ax]
409
+ elif num_slices == 2:
410
+ fig, axs = plt.subplots(1, 2, figsize=figsize, tight_layout=True)
411
+ elif num_slices == 3:
412
+ fig, axs = plt.subplots(1, 3, figsize=figsize, tight_layout=True)
413
+ elif num_slices == 4:
414
+ fig, axs = plt.subplots(2, 2, figsize=figsize, tight_layout=True)
415
+ axs = qis.to_flat_list(axs)
397
416
  else:
398
- fig, axs = plt.subplots(1, 1, figsize=plot.FIGSIZE, tight_layout=True)
417
+ raise NotImplementedError
399
418
 
400
419
  for idx, ttm in enumerate(option_chain.ttms):
401
420
  if is_log_strike_xaxis:
@@ -419,10 +438,6 @@ class ModelPricer(ABC):
419
438
  else:
420
439
  title = f"{ttm=:0.2f}"
421
440
 
422
- if len(option_chain.ttms) > 1:
423
- ax = axs[idx % 2][idx // 2]
424
- else:
425
- ax = axs
426
441
  plot.vol_slice_fit(bid_vol=pd.Series(mc_ivols_down[idx], index=strikes),
427
442
  ask_vol=pd.Series(mc_ivols_up[idx], index=strikes),
428
443
  model_vols=model_vol_t,
@@ -432,18 +447,19 @@ class ModelPricer(ABC):
432
447
  strike_name=strike_name,
433
448
  xvar_format=xvar_format,
434
449
  x_rotation=0,
435
- ax=ax,
450
+ ax=axs[idx],
436
451
  **kwargs)
452
+ return fig
437
453
 
438
454
  def plot_comp_mma_inverse_options_with_mc(self,
439
455
  option_chain: OptionChain,
440
456
  params: ModelParams,
441
457
  variable_type: VariableType = VariableType.LOG_RETURN,
442
458
  nb_path: int = 100000,
443
- headers: Optional[List[str]] = ('(A)', '(B)', '(C)', '(D)'), # optimized for 2*2 figure
444
459
  is_log_strike_xaxis: bool = False,
445
460
  is_plot_vols: bool = True,
446
461
  figsize: Tuple[float, float] = plot.FIGSIZE,
462
+ xvar_format: str = '{:0,.2f}',
447
463
  **kwargs
448
464
  ) -> plt.Figure:
449
465
  """
@@ -458,18 +474,19 @@ class ModelPricer(ABC):
458
474
  variable_type=variable_type,
459
475
  **kwargs)
460
476
 
461
- model_prices_inv, model_ivols_inv= self.compute_chain_prices_with_vols(option_chain=option_chain, params=params,
462
- is_spot_measure=False,
463
- variable_type=variable_type,
464
- **kwargs)
477
+ model_prices_inv, model_ivols_inv = self.compute_chain_prices_with_vols(option_chain=option_chain, params=params,
478
+ is_spot_measure=False,
479
+ variable_type=variable_type,
480
+ **kwargs)
465
481
 
466
482
  # we perform MC simulation in MMA measure
467
- mc_kwargs = update_kwargs(kwargs, dict(is_spot_measure=True, variable_type=variable_type))
468
483
  model_prices_ttms, model_prices_ttms_ups, model_prices_ttms_downs, \
469
484
  mc_ivols, mc_ivols_up, mc_ivols_down, mc_stdev_ttms = self.compute_mc_chain_implied_vols(option_chain=option_chain,
470
485
  params=params,
471
486
  nb_path=nb_path,
472
- **mc_kwargs)
487
+ variable_type=variable_type,
488
+ is_spot_measure=True,
489
+ **kwargs)
473
490
 
474
491
  if is_plot_vols:
475
492
  model_datas = {mma_label: model_ivols_mma, inverse_lable: model_ivols_inv}
@@ -480,27 +497,34 @@ class ModelPricer(ABC):
480
497
  mc_data = model_prices_ttms
481
498
  mc_data_lower, mc_data_upper = model_prices_ttms_downs, model_prices_ttms_ups
482
499
 
483
- if option_chain.ttms.size < 4:
484
- nrows, ncols = 1, option_chain.ttms.size
485
- else:
486
- nrows, ncols = 2, option_chain.ttms.size//2
487
-
500
+ num_slices = len(option_chain.ttms)
488
501
  with sns.axes_style('darkgrid'):
489
- fig, axs = plt.subplots(nrows, ncols, figsize=figsize, tight_layout=True)
502
+ if num_slices == 1:
503
+ fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
504
+ axs = [ax]
505
+ elif num_slices == 2:
506
+ fig, axs = plt.subplots(1, 2, figsize=figsize, tight_layout=True)
507
+ elif num_slices == 3:
508
+ fig, axs = plt.subplots(1, 3, figsize=figsize, tight_layout=True)
509
+ elif num_slices == 4:
510
+ fig, axs = plt.subplots(2, 2, figsize=figsize, tight_layout=True)
511
+ axs = qis.to_flat_list(axs)
512
+ else:
513
+ raise NotImplementedError
490
514
 
491
515
  for idx, ttm in enumerate(option_chain.ttms):
492
516
  if is_log_strike_xaxis:
493
517
  strikes = np.log(option_chain.strikes_ttms[idx] / option_chain.forwards[idx])
494
- xvar_format = '{:0.2f}'
495
518
  strike_name = 'log-strike'
496
519
  else:
497
520
  strikes = option_chain.strikes_ttms[idx]
498
521
  if variable_type == VariableType.LOG_RETURN:
499
- xvar_format = '{:0,.2f}'
500
522
  strike_name = 'strike'
523
+ elif variable_type == VariableType.Q_VAR:
524
+ strikes = option_chain.strikes_ttms[idx] / option_chain.forwards[idx]
525
+ strike_name = 'QVAR strike %'
501
526
  else:
502
- xvar_format = '{:0.2f}'
503
- strike_name = 'QVAR strike'
527
+ raise NotImplementedError
504
528
 
505
529
  model_vols = {}
506
530
  for key, model_data in model_datas.items():
@@ -509,10 +533,8 @@ class ModelPricer(ABC):
509
533
  model_vols = pd.DataFrame.from_dict(model_vols, orient='columns')
510
534
 
511
535
  if option_chain.ids is not None:
512
- if headers is not None:
513
- title = f"{headers[idx]} slice - {option_chain.ids[idx]}"
514
- else:
515
- title = f"slice - {option_chain.ids[idx]}"
536
+ title = f"{string.ascii_uppercase[idx]}) slice - {option_chain.ids[idx]}"
537
+
516
538
  else:
517
539
  title = f"{ttm=:0.2f}"
518
540
 
@@ -520,6 +542,8 @@ class ModelPricer(ABC):
520
542
  fp=0.5 * (mc_data_lower[idx] + mc_data_upper[idx]))
521
543
  if is_log_strike_xaxis:
522
544
  atm_points = {'ATM': (0.0, atm_vol)}
545
+ elif variable_type == VariableType.Q_VAR:
546
+ atm_points = {'ATM': (1.0, atm_vol)}
523
547
  else:
524
548
  atm_points = {'ATM': (option_chain.forwards[idx], atm_vol)}
525
549
 
@@ -535,6 +559,6 @@ class ModelPricer(ABC):
535
559
  atm_points=atm_points,
536
560
  ylabel='Implied vols' if is_plot_vols else 'Model prices',
537
561
  yvar_format='{:.0%}' if is_plot_vols else '{:.2f}',
538
- ax=axs[idx] if axs.ndim == 1 else axs[idx % 2][idx // 2],
562
+ ax=axs[idx],
539
563
  **kwargs)
540
564
  return fig
@@ -133,7 +133,7 @@ def vol_slice_fit(bid_vol: pd.Series,
133
133
  x_rotation: int = 0,
134
134
  ax: plt.Subplot = None,
135
135
  **kwargs
136
- ) -> plt.Figure:
136
+ ) -> Optional[plt.Figure]:
137
137
 
138
138
  if ax is None:
139
139
  fig, ax = plt.subplots(1, 1, figsize=(8, 8))