voly 0.0.16__tar.gz → 0.0.18__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.
- {voly-0.0.16/src/voly.egg-info → voly-0.0.18}/PKG-INFO +1 -1
- {voly-0.0.16 → voly-0.0.18}/pyproject.toml +2 -2
- {voly-0.0.16 → voly-0.0.18}/src/voly/client.py +45 -7
- {voly-0.0.16 → voly-0.0.18}/src/voly/core/charts.py +20 -100
- {voly-0.0.16 → voly-0.0.18}/src/voly/core/fit.py +16 -15
- {voly-0.0.16 → voly-0.0.18/src/voly.egg-info}/PKG-INFO +1 -1
- {voly-0.0.16 → voly-0.0.18}/LICENSE +0 -0
- {voly-0.0.16 → voly-0.0.18}/README.md +0 -0
- {voly-0.0.16 → voly-0.0.18}/setup.cfg +0 -0
- {voly-0.0.16 → voly-0.0.18}/setup.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/__init__.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/core/__init__.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/core/data.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/core/rnd.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/exceptions.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/formulas.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/models.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly/utils/logger.py +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly.egg-info/requires.txt +0 -0
- {voly-0.0.16 → voly-0.0.18}/src/voly.egg-info/top_level.txt +0 -0
- {voly-0.0.16 → voly-0.0.18}/tests/test_client.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voly"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.18"
|
|
8
8
|
description = "Options & volatility research package"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -60,7 +60,7 @@ line_length = 100
|
|
|
60
60
|
multi_line_output = 3
|
|
61
61
|
|
|
62
62
|
[tool.mypy]
|
|
63
|
-
python_version = "0.0.
|
|
63
|
+
python_version = "0.0.18"
|
|
64
64
|
warn_return_any = true
|
|
65
65
|
warn_unused_configs = true
|
|
66
66
|
disallow_untyped_defs = true
|
|
@@ -323,23 +323,22 @@ class VolyClient:
|
|
|
323
323
|
|
|
324
324
|
@staticmethod
|
|
325
325
|
def get_surface(param_matrix: pd.DataFrame,
|
|
326
|
-
|
|
326
|
+
moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
|
|
327
|
+
) -> Tuple[np.ndarray, Dict[float, np.ndarray], np.ndarray]:
|
|
327
328
|
"""
|
|
328
329
|
Generate implied volatility surface using optimized SVI parameters.
|
|
329
330
|
|
|
330
331
|
Parameters:
|
|
331
|
-
- param_matrix: Matrix of optimized SVI parameters from fit_results
|
|
332
|
-
-
|
|
332
|
+
- param_matrix: Matrix of optimized SVI parameters from fit_results
|
|
333
|
+
- moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
|
|
333
334
|
|
|
334
335
|
Returns:
|
|
335
|
-
-
|
|
336
|
+
- Tuple of (moneyness_grid, iv_surface)
|
|
336
337
|
"""
|
|
337
|
-
logger.info("Generating implied volatility surface")
|
|
338
|
-
|
|
339
338
|
# Generate the surface
|
|
340
339
|
moneyness_grid, iv_surface = get_surface(
|
|
341
340
|
param_matrix=param_matrix,
|
|
342
|
-
|
|
341
|
+
moneyness_params=moneyness_params
|
|
343
342
|
)
|
|
344
343
|
|
|
345
344
|
return {
|
|
@@ -347,6 +346,45 @@ class VolyClient:
|
|
|
347
346
|
'iv_surface': iv_surface
|
|
348
347
|
}
|
|
349
348
|
|
|
349
|
+
@staticmethod
|
|
350
|
+
def plot_model(fit_results: Dict[str, Any],
|
|
351
|
+
market_data: pd.DataFrame = None,
|
|
352
|
+
moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
|
|
353
|
+
) -> Dict[str, go.Figure]:
|
|
354
|
+
"""
|
|
355
|
+
Generate all plots for the fitted model and RND results.
|
|
356
|
+
|
|
357
|
+
Parameters:
|
|
358
|
+
- fit_results: Dictionary with fitting results from fit_model()
|
|
359
|
+
- market_data: Optional market data for comparison
|
|
360
|
+
- moneyness_params: Grid of log-moneyness values
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
- Dictionary of plot figures
|
|
364
|
+
"""
|
|
365
|
+
plots = {}
|
|
366
|
+
|
|
367
|
+
moneyness_grid, iv_surface = get_surface(fit_results['raw_params_matrix'], moneyness_params)
|
|
368
|
+
|
|
369
|
+
# Extract data from fit results
|
|
370
|
+
raw_param_matrix = fit_results['raw_param_matrix']
|
|
371
|
+
jw_param_matrix = fit_results.get('jw_param_matrix')
|
|
372
|
+
fit_performance = fit_results.get('fit_performance')
|
|
373
|
+
|
|
374
|
+
# Plot volatility smiles
|
|
375
|
+
plots['smiles'] = plot_all_smiles(moneyness_grid, iv_surface, market_data)
|
|
376
|
+
|
|
377
|
+
# Plot 3D surface
|
|
378
|
+
plots['surface_3d'] = plot_3d_surface(moneyness_grid, iv_surface)
|
|
379
|
+
|
|
380
|
+
# Plot parameters
|
|
381
|
+
plots['raw_params'], plots['jw_params'] = plot_parameters(raw_param_matrix, jw_param_matrix)
|
|
382
|
+
|
|
383
|
+
# Plot fit statistics if available
|
|
384
|
+
plots['fit_performance'] = plot_fit_performance(fit_performance)
|
|
385
|
+
|
|
386
|
+
return plots
|
|
387
|
+
|
|
350
388
|
# -------------------------------------------------------------------------
|
|
351
389
|
# Risk-Neutral Density (RND)
|
|
352
390
|
# -------------------------------------------------------------------------
|
|
@@ -8,11 +8,11 @@ risk-neutral densities, and model fitting results.
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
10
10
|
from typing import Dict, List, Tuple, Optional, Union, Any
|
|
11
|
+
from voly.utils.logger import logger, catch_exception
|
|
12
|
+
from voly.models import SVIModel
|
|
11
13
|
import plotly.graph_objects as go
|
|
12
14
|
from plotly.subplots import make_subplots
|
|
13
15
|
import plotly.io as pio
|
|
14
|
-
from voly.utils.logger import logger, catch_exception
|
|
15
|
-
from voly.models import SVIModel
|
|
16
16
|
|
|
17
17
|
# Set default renderer to browser for interactive plots
|
|
18
18
|
pio.renderers.default = "browser"
|
|
@@ -243,7 +243,7 @@ def plot_parameters(raw_param_matrix: pd.DataFrame,
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
@catch_exception
|
|
246
|
-
def plot_fit_statistics(
|
|
246
|
+
def plot_fit_statistics(fit_performance: pd.DataFrame) -> go.Figure:
|
|
247
247
|
"""
|
|
248
248
|
Plot the fitting accuracy statistics.
|
|
249
249
|
|
|
@@ -260,17 +260,17 @@ def plot_fit_statistics(stats_df: pd.DataFrame) -> go.Figure:
|
|
|
260
260
|
)
|
|
261
261
|
|
|
262
262
|
# Create custom tick labels with maturity name, DTE, and YTE
|
|
263
|
-
tick_labels = [f"{m} (DTE: {d:.1f}
|
|
264
|
-
zip(
|
|
263
|
+
tick_labels = [f"{m} (DTE: {d:.1f})" for m, d in
|
|
264
|
+
zip(fit_performance['Maturity'], fit_performance['DTE'])]
|
|
265
265
|
|
|
266
266
|
# Get x-axis values for plotting (use indices for positioning)
|
|
267
|
-
x_indices = list(range(len(
|
|
267
|
+
x_indices = list(range(len(fit_performance)))
|
|
268
268
|
|
|
269
269
|
# Plot RMSE
|
|
270
270
|
fig.add_trace(
|
|
271
271
|
go.Scatter(
|
|
272
272
|
x=x_indices,
|
|
273
|
-
y=
|
|
273
|
+
y=fit_performance['RMSE'] * 100, # Convert to percentage
|
|
274
274
|
mode='lines+markers',
|
|
275
275
|
name='RMSE',
|
|
276
276
|
line=dict(width=2),
|
|
@@ -284,7 +284,7 @@ def plot_fit_statistics(stats_df: pd.DataFrame) -> go.Figure:
|
|
|
284
284
|
fig.add_trace(
|
|
285
285
|
go.Scatter(
|
|
286
286
|
x=x_indices,
|
|
287
|
-
y=
|
|
287
|
+
y=fit_performance['MAE'] * 100, # Convert to percentage
|
|
288
288
|
mode='lines+markers',
|
|
289
289
|
name='MAE',
|
|
290
290
|
line=dict(width=2),
|
|
@@ -298,7 +298,7 @@ def plot_fit_statistics(stats_df: pd.DataFrame) -> go.Figure:
|
|
|
298
298
|
fig.add_trace(
|
|
299
299
|
go.Scatter(
|
|
300
300
|
x=x_indices,
|
|
301
|
-
y=
|
|
301
|
+
y=fit_performance['R²'],
|
|
302
302
|
mode='lines+markers',
|
|
303
303
|
name='R²',
|
|
304
304
|
line=dict(width=2),
|
|
@@ -312,7 +312,7 @@ def plot_fit_statistics(stats_df: pd.DataFrame) -> go.Figure:
|
|
|
312
312
|
fig.add_trace(
|
|
313
313
|
go.Scatter(
|
|
314
314
|
x=x_indices,
|
|
315
|
-
y=
|
|
315
|
+
y=fit_performance['Max Error'] * 100, # Convert to percentage
|
|
316
316
|
mode='lines+markers',
|
|
317
317
|
name='Max Error',
|
|
318
318
|
line=dict(width=2),
|
|
@@ -327,7 +327,7 @@ def plot_fit_statistics(stats_df: pd.DataFrame) -> go.Figure:
|
|
|
327
327
|
for col in range(1, 3):
|
|
328
328
|
fig.update_xaxes(
|
|
329
329
|
tickvals=x_indices,
|
|
330
|
-
ticktext=
|
|
330
|
+
ticktext=fit_performance['Maturity'],
|
|
331
331
|
tickangle=45,
|
|
332
332
|
row=row, col=col
|
|
333
333
|
)
|
|
@@ -349,29 +349,27 @@ def plot_fit_statistics(stats_df: pd.DataFrame) -> go.Figure:
|
|
|
349
349
|
|
|
350
350
|
|
|
351
351
|
@catch_exception
|
|
352
|
-
def plot_3d_surface(
|
|
353
|
-
|
|
354
|
-
iv_surface: Dict[float, np.ndarray],
|
|
355
|
-
interpolate: bool = True,
|
|
356
|
-
title: str = 'Implied Volatility Surface') -> go.Figure:
|
|
352
|
+
def plot_3d_surface(moneyness_grid: np.ndarray,
|
|
353
|
+
iv_surface: Dict[float, np.ndarray]) -> go.Figure:
|
|
357
354
|
"""
|
|
358
355
|
Plot 3D implied volatility surface.
|
|
359
356
|
|
|
360
357
|
Parameters:
|
|
361
|
-
-
|
|
362
|
-
- expiries: Expiry times in years
|
|
358
|
+
- log_moneyness_grid: grid of log_moneyness values
|
|
363
359
|
- iv_surface: Dictionary mapping expiry times to IV arrays
|
|
364
|
-
- interpolate: Whether to interpolate the surface
|
|
365
360
|
- title: Plot title
|
|
366
361
|
|
|
367
362
|
Returns:
|
|
368
363
|
- Plotly figure
|
|
369
364
|
"""
|
|
365
|
+
|
|
366
|
+
yte_values = list(iv_surface.keys())
|
|
367
|
+
|
|
370
368
|
# Convert implied volatility surface to array
|
|
371
|
-
z_array = np.array([iv_surface[t] for t in
|
|
369
|
+
z_array = np.array([iv_surface[t] for t in yte_values])
|
|
372
370
|
|
|
373
371
|
# Create mesh grid
|
|
374
|
-
X, Y = np.meshgrid(
|
|
372
|
+
X, Y = np.meshgrid(moneyness_grid, yte_values)
|
|
375
373
|
Z = z_array * 100 # Convert to percentage
|
|
376
374
|
|
|
377
375
|
# Create 3D surface plot
|
|
@@ -383,7 +381,7 @@ def plot_3d_surface(moneyness: np.ndarray,
|
|
|
383
381
|
|
|
384
382
|
# Update layout
|
|
385
383
|
fig.update_layout(
|
|
386
|
-
title=
|
|
384
|
+
title='Implied Volatility 3D Surface',
|
|
387
385
|
template='plotly_dark',
|
|
388
386
|
scene=dict(
|
|
389
387
|
xaxis_title='Log Moneyness',
|
|
@@ -904,81 +902,3 @@ def plot_interpolated_surface(
|
|
|
904
902
|
|
|
905
903
|
return fig
|
|
906
904
|
|
|
907
|
-
|
|
908
|
-
@catch_exception
|
|
909
|
-
def generate_all_plots(fit_results: Dict[str, Any],
|
|
910
|
-
rnd_results: Optional[Dict[str, Any]] = None,
|
|
911
|
-
market_data: Optional[pd.DataFrame] = None) -> Dict[str, go.Figure]:
|
|
912
|
-
"""
|
|
913
|
-
Generate all plots for the fitted model and RND results.
|
|
914
|
-
|
|
915
|
-
Parameters:
|
|
916
|
-
- fit_results: Dictionary with fitting results from fit_model()
|
|
917
|
-
- rnd_results: Optional dictionary with RND results from calculate_rnd()
|
|
918
|
-
- market_data: Optional market data for comparison
|
|
919
|
-
|
|
920
|
-
Returns:
|
|
921
|
-
- Dictionary of plot figures
|
|
922
|
-
"""
|
|
923
|
-
plots = {}
|
|
924
|
-
|
|
925
|
-
# Extract data from fit results
|
|
926
|
-
moneyness_grid = fit_results['moneyness_grid']
|
|
927
|
-
iv_surface = fit_results['iv_surface']
|
|
928
|
-
raw_param_matrix = fit_results['raw_param_matrix']
|
|
929
|
-
jw_param_matrix = fit_results.get('jw_param_matrix')
|
|
930
|
-
stats_df = fit_results.get('stats_df')
|
|
931
|
-
unique_expiries = fit_results['unique_expiries']
|
|
932
|
-
|
|
933
|
-
# Plot volatility smiles
|
|
934
|
-
logger.info("Generating volatility smile plots...")
|
|
935
|
-
plots['smiles'] = plot_all_smiles(moneyness_grid, iv_surface, market_data)
|
|
936
|
-
|
|
937
|
-
# Plot 3D surface
|
|
938
|
-
logger.info("Generating 3D volatility surface plot...")
|
|
939
|
-
plots['surface_3d'] = plot_3d_surface(moneyness_grid, unique_expiries, iv_surface)
|
|
940
|
-
|
|
941
|
-
# Plot parameters
|
|
942
|
-
logger.info("Generating parameter plots...")
|
|
943
|
-
plots['raw_params'], plots['jw_params'] = plot_parameters(raw_param_matrix, jw_param_matrix)
|
|
944
|
-
|
|
945
|
-
# Plot fit statistics if available
|
|
946
|
-
if stats_df is not None:
|
|
947
|
-
logger.info("Generating fit statistics plot...")
|
|
948
|
-
plots['fit_stats'] = plot_fit_statistics(stats_df)
|
|
949
|
-
|
|
950
|
-
# Plot RND results if available
|
|
951
|
-
if rnd_results is not None:
|
|
952
|
-
logger.info("Generating RND plots...")
|
|
953
|
-
|
|
954
|
-
# Extract RND data
|
|
955
|
-
rnd_surface = rnd_results['rnd_surface']
|
|
956
|
-
rnd_statistics = rnd_results['rnd_statistics']
|
|
957
|
-
rnd_probabilities = rnd_results['rnd_probabilities']
|
|
958
|
-
spot_price = rnd_results['spot_price']
|
|
959
|
-
|
|
960
|
-
# Plot RND for each expiry
|
|
961
|
-
plots['rnd'] = {}
|
|
962
|
-
for maturity_name, rnd_values in rnd_surface.items():
|
|
963
|
-
plots['rnd'][maturity_name] = plot_rnd(
|
|
964
|
-
moneyness_grid, rnd_values, spot_price,
|
|
965
|
-
title=f"Risk-Neutral Density - {maturity_name}"
|
|
966
|
-
)
|
|
967
|
-
|
|
968
|
-
# Plot all RNDs in one figure
|
|
969
|
-
plots['rnd_all'] = plot_rnd_all_expiries(moneyness_grid, rnd_surface, raw_param_matrix, spot_price)
|
|
970
|
-
|
|
971
|
-
# Plot 3D RND surface
|
|
972
|
-
plots['rnd_3d'] = plot_rnd_3d(moneyness_grid, rnd_surface, raw_param_matrix, spot_price)
|
|
973
|
-
|
|
974
|
-
# Plot RND statistics
|
|
975
|
-
plots['rnd_stats'], plots['rnd_probs'] = plot_rnd_statistics(rnd_statistics, rnd_probabilities)
|
|
976
|
-
|
|
977
|
-
# Plot PDF and CDF for the first expiry
|
|
978
|
-
first_maturity = list(rnd_surface.keys())[0]
|
|
979
|
-
first_rnd = rnd_surface[first_maturity]
|
|
980
|
-
|
|
981
|
-
plots['pdf'] = plot_pdf(moneyness_grid, first_rnd, spot_price)
|
|
982
|
-
plots['cdf'] = plot_cdf(moneyness_grid, first_rnd, spot_price)
|
|
983
|
-
|
|
984
|
-
return plots
|
|
@@ -156,7 +156,7 @@ def fit_svi_parameters(market_data: pd.DataFrame,
|
|
|
156
156
|
else:
|
|
157
157
|
logger.warning(f'Optimization for {maturity_name}: {RED}FAILED{RESET}')
|
|
158
158
|
|
|
159
|
-
logger.info('
|
|
159
|
+
logger.info('-------------------------------------')
|
|
160
160
|
|
|
161
161
|
# Create DataFrame with all fit performance data
|
|
162
162
|
fit_performance = pd.DataFrame(fit_data)
|
|
@@ -265,34 +265,35 @@ def fit_model(market_data: pd.DataFrame,
|
|
|
265
265
|
|
|
266
266
|
|
|
267
267
|
@catch_exception
|
|
268
|
-
def get_surface(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
) -> Tuple[np.ndarray, Dict[float, np.ndarray], np.ndarray]:
|
|
268
|
+
def get_surface(param_matrix: pd.DataFrame,
|
|
269
|
+
moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
|
|
270
|
+
) -> Tuple[np.ndarray, Dict[float, np.ndarray], np.ndarray]:
|
|
272
271
|
"""
|
|
273
272
|
Generate implied volatility surface using optimized SVI parameters.
|
|
274
273
|
|
|
275
274
|
Parameters:
|
|
276
275
|
- param_matrix: Matrix of optimized SVI parameters from fit_results
|
|
277
|
-
-
|
|
276
|
+
- moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
|
|
278
277
|
|
|
279
278
|
Returns:
|
|
280
|
-
- Tuple of (moneyness_grid, iv_surface
|
|
279
|
+
- Tuple of (moneyness_grid, iv_surface)
|
|
281
280
|
"""
|
|
281
|
+
iv_surface = {}
|
|
282
|
+
|
|
282
283
|
# Extract moneyness parameters
|
|
283
|
-
min_m, max_m, num_points =
|
|
284
|
+
min_m, max_m, num_points = moneyness_params
|
|
284
285
|
|
|
285
286
|
# Generate moneyness grid
|
|
286
|
-
|
|
287
|
-
implied_volatility_surface = {}
|
|
287
|
+
moneyness_grid = np.linspace(min_m, max_m, num=num_points)
|
|
288
288
|
|
|
289
289
|
# Get YTE values from the parameter matrix attributes
|
|
290
|
-
yte_values =
|
|
290
|
+
yte_values = fit_results['fit_performance']['YTE']
|
|
291
|
+
maturity_values = fit_results['fit_performance']['Maturity']
|
|
291
292
|
|
|
292
293
|
# Generate implied volatility for each expiry
|
|
293
|
-
for
|
|
294
|
-
svi_params = param_matrix[
|
|
294
|
+
for maturity, yte in zip(maturity_values, yte_values):
|
|
295
|
+
svi_params = param_matrix[maturity].values
|
|
295
296
|
w_svi = [SVIModel.svi(x, *svi_params) for x in moneyness_values]
|
|
296
|
-
|
|
297
|
+
iv_surface[yte] = np.sqrt(np.array(w_svi) / yte)
|
|
297
298
|
|
|
298
|
-
return
|
|
299
|
+
return moneyness_grid, iv_surface
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|