voly 0.0.61__py3-none-any.whl → 0.0.63__py3-none-any.whl

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.
voly/client.py CHANGED
@@ -339,7 +339,7 @@ class VolyClient:
339
339
 
340
340
  # Fit the model
341
341
  fit_results, fit_performance = fit_model(
342
- market_data=market_data,
342
+ option_chain=option_chain,
343
343
  model_name=model_name,
344
344
  initial_params=initial_params,
345
345
  param_bounds=param_bounds
@@ -352,32 +352,34 @@ class VolyClient:
352
352
 
353
353
  @staticmethod
354
354
  def get_iv_surface(fit_results: Dict[str, Any],
355
- moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
355
+ log_moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
356
356
  ) -> Dict[str, Any]:
357
357
  """
358
358
  Generate implied volatility surface using optimized SVI parameters.
359
359
 
360
360
  Parameters:
361
- - param_matrix: Matrix of optimized SVI parameters from fit_results
362
- - moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
361
+ - fit_results: DataFrame from fit_model()
362
+ - log_moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
363
+ - return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'strikes', 'delta')
363
364
 
364
365
  Returns:
365
- - Tuple of (moneyness_array, iv_surface)
366
+ - Tuple of (iv_surface, x_surface)
366
367
  """
367
368
  # Generate the surface
368
- moneyness_array, iv_surface = get_iv_surface(
369
+ iv_surface, x_surface = get_iv_surface(
369
370
  fit_results=fit_results,
370
- moneyness_params=moneyness_params
371
+ log_moneyness_params=log_moneyness_params,
372
+ return_domain=return_domain
371
373
  )
372
374
 
373
375
  return {
374
- 'moneyness_array': moneyness_array,
375
- 'iv_surface': iv_surface
376
+ 'iv_surface': iv_surface,
377
+ 'x_surface': x_surface
376
378
  }
377
379
 
378
380
  @staticmethod
379
381
  def plot_model(fit_results: Dict[str, Any],
380
- market_data: pd.DataFrame = None,
382
+ option_chain: pd.DataFrame = None,
381
383
  moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
382
384
  ) -> Dict[str, go.Figure]:
383
385
  """
@@ -385,7 +387,7 @@ class VolyClient:
385
387
 
386
388
  Parameters:
387
389
  - fit_results: Dictionary with fitting results from fit_model()
388
- - market_data: Optional market data for comparison
390
+ - option_chain: Optional market data for comparison
389
391
  - moneyness_params: Grid of log-moneyness values
390
392
 
391
393
  Returns:
@@ -401,7 +403,7 @@ class VolyClient:
401
403
  fit_performance = fit_results['fit_performance']
402
404
 
403
405
  # Plot volatility smiles
404
- plots['smiles'] = plot_all_smiles(moneyness_array, iv_surface, market_data)
406
+ plots['smiles'] = plot_all_smiles(moneyness_array, iv_surface, option_chain)
405
407
 
406
408
  # Plot 3D surface
407
409
  plots['surface_3d'] = plot_3d_surface(moneyness_array, iv_surface)
voly/core/charts.py CHANGED
@@ -21,7 +21,7 @@ pio.renderers.default = "browser"
21
21
  @catch_exception
22
22
  def plot_volatility_smile(moneyness_array: np.ndarray,
23
23
  iv_array: np.ndarray,
24
- market_data: pd.DataFrame = None,
24
+ option_chain: pd.DataFrame = None,
25
25
  maturity: Optional[str] = None) -> go.Figure:
26
26
  """Plot volatility smile for a single expiry."""
27
27
  fig = go.Figure()
@@ -38,8 +38,8 @@ def plot_volatility_smile(moneyness_array: np.ndarray,
38
38
  )
39
39
 
40
40
  # Add market data if provided
41
- if market_data is not None and maturity is not None:
42
- maturity_data = market_data[market_data['maturity_name'] == maturity]
41
+ if option_chain is not None and maturity is not None:
42
+ maturity_data = option_chain[option_chain['maturity_name'] == maturity]
43
43
 
44
44
  if not maturity_data.empty:
45
45
  # Add bid and ask IVs
@@ -72,13 +72,13 @@ def plot_volatility_smile(moneyness_array: np.ndarray,
72
72
  @catch_exception
73
73
  def plot_all_smiles(moneyness_array: np.ndarray,
74
74
  iv_surface: Dict[str, np.ndarray],
75
- market_data: Optional[pd.DataFrame] = None) -> List[go.Figure]:
75
+ option_chain: Optional[pd.DataFrame] = None) -> List[go.Figure]:
76
76
  """Plot volatility smiles for all expiries."""
77
77
  return [
78
78
  plot_volatility_smile(
79
79
  moneyness_array=moneyness_array,
80
80
  iv_array=iv_surface[maturity],
81
- market_data=market_data,
81
+ option_chain=option_chain,
82
82
  maturity=maturity
83
83
  )
84
84
  for maturity in iv_surface.keys()
voly/core/fit.py CHANGED
@@ -11,6 +11,7 @@ from typing import List, Tuple, Dict, Optional, Union, Any
11
11
  from scipy.optimize import least_squares
12
12
  from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
13
13
  from voly.utils.logger import logger, catch_exception
14
+ from voly.formulas import get_x_domain
14
15
  from voly.exceptions import VolyError
15
16
  from voly.models import SVIModel
16
17
  import warnings
@@ -19,17 +20,17 @@ warnings.filterwarnings("ignore")
19
20
 
20
21
 
21
22
  @catch_exception
22
- def calculate_residuals(params: List[float], ytm: float, market_data: pd.DataFrame,
23
+ def calculate_residuals(params: List[float], ytm: float, option_chain: pd.DataFrame,
23
24
  model: Any = SVIModel) -> np.ndarray:
24
25
  """Calculate residuals between market and model implied volatilities."""
25
- maturity_data = market_data[market_data['ytm'] == ytm]
26
+ maturity_data = option_chain[option_chain['ytm'] == ytm]
26
27
  w = np.array([model.svi(x, *params) for x in maturity_data['log_moneyness']])
27
28
  iv_actual = maturity_data['mark_iv'].values
28
29
  return iv_actual - np.sqrt(w / ytm)
29
30
 
30
31
 
31
32
  @catch_exception
32
- def fit_model(market_data: pd.DataFrame,
33
+ def fit_model(option_chain: pd.DataFrame,
33
34
  model_name: str = 'svi',
34
35
  initial_params: Optional[List[float]] = None,
35
36
  param_bounds: Optional[Tuple] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
@@ -56,8 +57,8 @@ def fit_model(market_data: pd.DataFrame,
56
57
  'rmse', 'mae', 'r2', 'max_error', 'n_points']
57
58
 
58
59
  # Get unique maturities and sort them
59
- unique_ytms = sorted(market_data['ytm'].unique())
60
- maturity_names = [market_data[market_data['ytm'] == ytm]['maturity_name'].iloc[0] for ytm in unique_ytms]
60
+ unique_ytms = sorted(option_chain['ytm'].unique())
61
+ maturity_names = [option_chain[option_chain['ytm'] == ytm]['maturity_name'].iloc[0] for ytm in unique_ytms]
61
62
 
62
63
  # Create empty DataFrames
63
64
  fit_results_df = pd.DataFrame(index=results_index, columns=maturity_names)
@@ -68,7 +69,7 @@ def fit_model(market_data: pd.DataFrame,
68
69
 
69
70
  for ytm in unique_ytms:
70
71
  # Get data for this maturity
71
- maturity_data = market_data[market_data['ytm'] == ytm]
72
+ maturity_data = option_chain[option_chain['ytm'] == ytm]
72
73
  maturity_name = maturity_data['maturity_name'].iloc[0]
73
74
  dtm = maturity_data['dtm'].iloc[0]
74
75
 
@@ -79,7 +80,7 @@ def fit_model(market_data: pd.DataFrame,
79
80
  result = least_squares(
80
81
  calculate_residuals,
81
82
  initial_params,
82
- args=(ytm, market_data, SVIModel),
83
+ args=(ytm, option_chain, SVIModel),
83
84
  bounds=param_bounds,
84
85
  max_nfev=1000
85
86
  )
@@ -136,19 +137,30 @@ def fit_model(market_data: pd.DataFrame,
136
137
 
137
138
  @catch_exception
138
139
  def get_iv_surface(fit_results: pd.DataFrame,
139
- log_moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
140
- ) -> Tuple[np.ndarray, Dict[str, np.ndarray]]:
141
- """Generate implied volatility surface using optimized SVI parameters."""
142
- # Extract moneyness parameters
140
+ log_moneyness_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
141
+ return_domain: str = 'log_moneyness') -> Tuple[Dict[str, np.ndarray], np.ndarray]:
142
+ """
143
+ Generate implied volatility surface using optimized SVI parameters.
144
+
145
+ Parameters:
146
+ - fit_results: DataFrame from fit_model()
147
+ - log_moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
148
+ - return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'strikes', 'delta')
149
+
150
+ Returns:
151
+ - Tuple of (iv_surface, x_surface)
152
+ iv_surface: Dictionary mapping maturity names to IV arrays
153
+ x_surface: Dictionary mapping maturity names to requested x domain arrays
154
+ """
155
+
156
+ # Generate implied volatility surface in log-moneyness domain
143
157
  min_m, max_m, num_points = log_moneyness_params
158
+ log_moneyness_array = np.linspace(min_m, max_m, num=num_points)
144
159
 
145
- # Generate moneyness array and initialize surface dictionary
146
- moneyness_array = np.linspace(min_m, max_m, num=num_points)
147
160
  iv_surface = {}
148
-
149
- # Generate implied volatility for each maturity
161
+ x_surface = {}
150
162
  for maturity in fit_results.columns:
151
- # Extract parameters and YTM
163
+ # Calculate SVI total implied variance and convert to IV
152
164
  params = [
153
165
  fit_results.loc['a', maturity],
154
166
  fit_results.loc['b', maturity],
@@ -158,8 +170,19 @@ def get_iv_surface(fit_results: pd.DataFrame,
158
170
  ]
159
171
  ytm = fit_results.loc['ytm', maturity]
160
172
 
161
- # Calculate SVI total implied variance and convert to IV
162
- w_svi = np.array([SVIModel.svi(x, *params) for x in moneyness_array])
163
- iv_surface[maturity] = np.sqrt(w_svi / ytm)
173
+ w_svi = np.array([SVIModel.svi(x, *params) for x in log_moneyness_array])
174
+ iv_array = np.sqrt(w_svi / ytm)
175
+ iv_surface[maturity] = iv_array
176
+
177
+ x_domain = get_x_domain(
178
+ log_moneyness_params=log_moneyness_params,
179
+ return_domain=return_domain,
180
+ s=fit_results.loc['s', maturity],
181
+ r=fit_results.loc['r', maturity],
182
+ iv_array=iv_array,
183
+ ytm=fit_results.loc['ytm', maturity]
184
+ )
185
+
186
+ x_surface[maturity] = x_domain
164
187
 
165
- return moneyness_array, iv_surface
188
+ return iv_surface, x_surface
voly/formulas.py CHANGED
@@ -7,7 +7,7 @@ from scipy.stats import norm
7
7
  from typing import Tuple, Dict, Union, List, Optional
8
8
  from voly.utils.logger import catch_exception
9
9
  from voly.models import SVIModel
10
- from math import exp
10
+ from typing import Tuple
11
11
 
12
12
 
13
13
  @catch_exception
@@ -276,3 +276,86 @@ def iv(option_price: float, s: float, k: float, r: float, t: float,
276
276
 
277
277
  # If we reach here, we didn't converge
278
278
  return np.nan
279
+
280
+
281
+ @catch_exception
282
+ def get_x_domain(log_moneyness_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
283
+ return_domain: str = 'log_moneyness',
284
+ s: float = None,
285
+ r: float = None,
286
+ iv_array: np.ndarray = None,
287
+ ytm: float = None) -> np.ndarray:
288
+ """
289
+ Compute the x-domain for a given return type (log-moneyness, moneyness, strikes, or delta).
290
+
291
+ Parameters:
292
+ -----------
293
+ log_moneyness_params : Tuple[float, float, int],
294
+ Parameters for log-moneyness domain: (min_log_moneyness, max_log_moneyness, num_points).
295
+ Default is (-1.5, 1.5, 1000).
296
+ return_domain : str, optional
297
+ The desired domain to return. Options are 'log_moneyness', 'moneyness', 'strikes', or 'delta'.
298
+ Default is 'log_moneyness'.
299
+ s : float, optional
300
+ Spot price of the underlying asset. Required for 'strikes' and 'delta' domains.
301
+ r : float, optional
302
+ Risk-free interest rate. Required for 'delta' domain.
303
+ iv_array : np.ndarray, optional
304
+ Array of implied volatilities. Required for 'delta' domain.
305
+ ytm : float, optional
306
+ Time to maturity in years. Required for 'delta' domain.
307
+
308
+ Returns:
309
+ --------
310
+ np.ndarray
311
+ The x-domain array corresponding to the specified return_domain.
312
+
313
+ Raises:
314
+ -------
315
+ ValueError
316
+ If required parameters are missing for the specified return_domain.
317
+ """
318
+
319
+ # Extract log-moneyness parameters
320
+ min_m, max_m, num_points = log_moneyness_params
321
+
322
+ # Generate the base log-moneyness array
323
+ log_moneyness = np.linspace(min_m, max_m, num_points)
324
+
325
+ # Handle different return domains
326
+ if return_domain == 'log_moneyness':
327
+ return log_moneyness
328
+
329
+ elif return_domain == 'moneyness':
330
+ return np.exp(log_moneyness)
331
+
332
+ elif return_domain == 'strikes':
333
+ if s is None:
334
+ raise ValueError("Spot price 's' is required for return_domain='strikes'.")
335
+ return s / np.exp(log_moneyness) # K = S/exp(log_moneyness)
336
+
337
+ elif return_domain == 'delta':
338
+ # Check for required parameters
339
+ required_params = {'s': s, 'r': r, 'iv_array': iv_array, 'ytm': ytm}
340
+ missing_params = [param for param, value in required_params.items() if value is None]
341
+ if missing_params:
342
+ raise ValueError(f"The following parameters are required for return_domain='delta': {missing_params}")
343
+
344
+ if len(iv_array) != len(log_moneyness):
345
+ raise ValueError(
346
+ f"iv_array must have the same length as the log-moneyness array ({len(log_moneyness)}).")
347
+
348
+ # Compute strikes
349
+ strikes = s / np.exp(log_moneyness)
350
+
351
+ # Compute deltas
352
+ delta_values = np.array([
353
+ delta(s, k, r, vol, ytm, 'call')
354
+ for k, vol in zip(strikes, iv_array)
355
+ ])
356
+
357
+ return delta_values
358
+
359
+ else:
360
+ raise ValueError(
361
+ f"Invalid return_domain: {return_domain}. Must be one of ['log_moneyness', 'moneyness', 'strikes', 'delta'].")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.61
3
+ Version: 0.0.63
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -1,18 +1,18 @@
1
1
  voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
2
- voly/client.py,sha256=JzxVZSmb71zcB7JGicYZM-NA_gC7Jedu2SOYImxXSNY,20452
2
+ voly/client.py,sha256=E_3bSDBQt5Kv5-NnU1noDFaj75llkBV3Wt2AxT270-k,20577
3
3
  voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
4
- voly/formulas.py,sha256=Xgaq4lx1fNzRfu9W84fMNeH6GRJ0FNFNUUUYn5ffjjE,8843
4
+ voly/formulas.py,sha256=gl4L_KtSd8ZJTAc6_RroJSDwOS8z6gcnpARG7oJFxdk,11897
5
5
  voly/models.py,sha256=LXXIlpXZQEfXTuCngxC8Hd3bWtw6wdXDCSGxTLmHM-c,3659
6
6
  voly/core/__init__.py,sha256=bu6fS2I1Pj9fPPnl-zY3L7NqrZSY5Zy6NY2uMUvdhKs,183
7
- voly/core/charts.py,sha256=kWjd3sfvTalfuElt7vSuRPtYuWpMoJ9-L2Z9Mj-mOMQ,24374
7
+ voly/core/charts.py,sha256=0fYD748wHUBtuOcRufzfDwjlhmLf6g081KH-zquIj0A,24381
8
8
  voly/core/data.py,sha256=e8qBArubNqPkrfuIYB_q2WhRf7TKzA4Z3FhMC-xyLEE,8862
9
- voly/core/fit.py,sha256=YClTiODlH9CBAqv5VNBlE0k6qD22Hwic91PnTWstyj4,6490
9
+ voly/core/fit.py,sha256=N1wey5NYeInIPKfa5xC0P4eBVhoTf0wwu7cYMmgv-Xg,7310
10
10
  voly/core/interpolate.py,sha256=ztVIePJZOh-CIbn69wkh1JW2rKywNe2FEewRN0zcSAo,8185
11
11
  voly/core/rnd.py,sha256=8FTU-Qp9epW9yE4XSOdiFGIRXrGyXqF6mVgZn1NMvxk,11813
12
12
  voly/utils/__init__.py,sha256=E05mWatyC-PDOsCxQV1p5Xi1IgpOomxrNURyCx_gB-w,200
13
13
  voly/utils/logger.py,sha256=4-_2bVJmq17Q0d7Rd2mPg1AeR8gxv6EPvcmBDMFWcSM,1744
14
- voly-0.0.61.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
15
- voly-0.0.61.dist-info/METADATA,sha256=4yMzNptN7516PzN07OVCD5eFTJYuL7gWFfm9o3l35Y8,4092
16
- voly-0.0.61.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
17
- voly-0.0.61.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
18
- voly-0.0.61.dist-info/RECORD,,
14
+ voly-0.0.63.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
15
+ voly-0.0.63.dist-info/METADATA,sha256=mB6YOpRgoriXvhchIXltVimIlJDsWYSmo1uAqjVLsME,4092
16
+ voly-0.0.63.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
17
+ voly-0.0.63.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
18
+ voly-0.0.63.dist-info/RECORD,,
File without changes
File without changes