voly 0.0.1__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/__init__.py +10 -0
- voly/client.py +540 -0
- voly/core/__init__.py +11 -0
- voly/core/charts.py +984 -0
- voly/core/data.py +312 -0
- voly/core/fit.py +331 -0
- voly/core/interpolate.py +221 -0
- voly/core/rnd.py +389 -0
- voly/exceptions.py +3 -0
- voly/formulas.py +243 -0
- voly/models.py +86 -0
- voly/utils/__init__.py +8 -0
- voly/utils/logger.py +72 -0
- voly-0.0.1.dist-info/LICENSE +21 -0
- voly-0.0.1.dist-info/METADATA +132 -0
- voly-0.0.1.dist-info/RECORD +18 -0
- voly-0.0.1.dist-info/WHEEL +5 -0
- voly-0.0.1.dist-info/top_level.txt +1 -0
voly/core/interpolate.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Surface interpolation module for the Voly package.
|
|
3
|
+
|
|
4
|
+
This module handles interpolation of implied volatility surfaces
|
|
5
|
+
across both moneyness and time dimensions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from typing import List, Tuple, Dict, Optional, Union, Any
|
|
11
|
+
from scipy.interpolate import interp1d, pchip_interpolate
|
|
12
|
+
from voly.utils.logger import logger, catch_exception
|
|
13
|
+
from voly.exceptions import InterpolationError
|
|
14
|
+
from voly.models import SVIModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@catch_exception
|
|
18
|
+
def interpolate_surface_time(
|
|
19
|
+
moneyness_grid: np.ndarray,
|
|
20
|
+
expiries: np.ndarray,
|
|
21
|
+
surface_values: np.ndarray,
|
|
22
|
+
target_expiries: np.ndarray,
|
|
23
|
+
method: str = 'cubic'
|
|
24
|
+
) -> np.ndarray:
|
|
25
|
+
"""
|
|
26
|
+
Interpolate the surface across the time dimension.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
- moneyness_grid: Array of log-moneyness values
|
|
30
|
+
- expiries: Array of expiry times (in years)
|
|
31
|
+
- surface_values: 2D array of values to interpolate (rows=expiries, cols=moneyness)
|
|
32
|
+
- target_expiries: Array of target expiry times to interpolate to
|
|
33
|
+
- method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
- 2D array of interpolated values (rows=target_expiries, cols=moneyness)
|
|
37
|
+
"""
|
|
38
|
+
if len(expiries) < 2:
|
|
39
|
+
raise InterpolationError("At least two expiries are required for time interpolation")
|
|
40
|
+
|
|
41
|
+
# Initialize the output array
|
|
42
|
+
interpolated_surface = np.zeros((len(target_expiries), len(moneyness_grid)))
|
|
43
|
+
|
|
44
|
+
# For each moneyness point, interpolate across time
|
|
45
|
+
for i in range(len(moneyness_grid)):
|
|
46
|
+
if method == 'pchip':
|
|
47
|
+
# Use PCHIP (Piecewise Cubic Hermite Interpolating Polynomial)
|
|
48
|
+
interpolated_values = pchip_interpolate(expiries, surface_values[:, i], target_expiries)
|
|
49
|
+
else:
|
|
50
|
+
# Use regular interpolation (linear, cubic, etc.)
|
|
51
|
+
interp_func = interp1d(expiries, surface_values[:, i], kind=method, bounds_error=False,
|
|
52
|
+
fill_value='extrapolate')
|
|
53
|
+
interpolated_values = interp_func(target_expiries)
|
|
54
|
+
|
|
55
|
+
interpolated_surface[:, i] = interpolated_values
|
|
56
|
+
|
|
57
|
+
return interpolated_surface
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@catch_exception
|
|
61
|
+
def create_target_expiries(
|
|
62
|
+
min_dte: float,
|
|
63
|
+
max_dte: float,
|
|
64
|
+
num_points: int = 10,
|
|
65
|
+
specific_days: Optional[List[int]] = None
|
|
66
|
+
) -> np.ndarray:
|
|
67
|
+
"""
|
|
68
|
+
Create a grid of target expiry days for interpolation.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
- min_dte: Minimum days to expiry
|
|
72
|
+
- max_dte: Maximum days to expiry
|
|
73
|
+
- num_points: Number of points for regular grid
|
|
74
|
+
- specific_days: Optional list of specific days to include (e.g., [7, 30, 90, 180])
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
- Array of target expiry times in years
|
|
78
|
+
"""
|
|
79
|
+
# Create regular grid in days
|
|
80
|
+
if specific_days is not None:
|
|
81
|
+
# Filter specific days to be within range
|
|
82
|
+
days = np.array([d for d in specific_days if min_dte <= d <= max_dte])
|
|
83
|
+
if len(days) == 0:
|
|
84
|
+
logger.warning("No specific days within range, using regular grid")
|
|
85
|
+
days = np.linspace(min_dte, max_dte, num_points)
|
|
86
|
+
else:
|
|
87
|
+
# Use regular grid
|
|
88
|
+
days = np.linspace(min_dte, max_dte, num_points)
|
|
89
|
+
|
|
90
|
+
# Convert to years
|
|
91
|
+
years = days / 365.25
|
|
92
|
+
|
|
93
|
+
return years
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@catch_exception
|
|
97
|
+
def interpolate_svi_parameters(
|
|
98
|
+
raw_param_matrix: pd.DataFrame,
|
|
99
|
+
target_expiries_years: np.ndarray,
|
|
100
|
+
method: str = 'cubic'
|
|
101
|
+
) -> pd.DataFrame:
|
|
102
|
+
"""
|
|
103
|
+
Interpolate SVI parameters across time.
|
|
104
|
+
|
|
105
|
+
Parameters:
|
|
106
|
+
- raw_param_matrix: Matrix of SVI parameters with maturity names as columns
|
|
107
|
+
- target_expiries_years: Array of target expiry times (in years)
|
|
108
|
+
- method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
- Matrix of interpolated SVI parameters
|
|
112
|
+
"""
|
|
113
|
+
# Get expiry times in years from the parameter matrix
|
|
114
|
+
yte_values = raw_param_matrix.attrs['yte_values']
|
|
115
|
+
dte_values = raw_param_matrix.attrs['dte_values']
|
|
116
|
+
|
|
117
|
+
# Sort maturity names by DTE
|
|
118
|
+
maturity_names = sorted(yte_values.keys(), key=lambda x: dte_values[x])
|
|
119
|
+
|
|
120
|
+
# Extract expiry times in order
|
|
121
|
+
expiry_years = np.array([yte_values[m] for m in maturity_names])
|
|
122
|
+
|
|
123
|
+
# Check if we have enough points for interpolation
|
|
124
|
+
if len(expiry_years) < 2:
|
|
125
|
+
raise InterpolationError("At least two expiries are required for interpolation")
|
|
126
|
+
|
|
127
|
+
# Create new parameter matrix for interpolated values
|
|
128
|
+
interp_param_matrix = pd.DataFrame(
|
|
129
|
+
columns=[f"t{i:.2f}" for i in target_expiries_years],
|
|
130
|
+
index=SVIModel.PARAM_NAMES
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# For each SVI parameter, interpolate across time
|
|
134
|
+
for param in SVIModel.PARAM_NAMES:
|
|
135
|
+
param_values = np.array([raw_param_matrix.loc[param, m] for m in maturity_names])
|
|
136
|
+
|
|
137
|
+
if method == 'pchip':
|
|
138
|
+
interpolated_values = pchip_interpolate(expiry_years, param_values, target_expiries_years)
|
|
139
|
+
else:
|
|
140
|
+
interp_func = interp1d(expiry_years, param_values, kind=method, bounds_error=False,
|
|
141
|
+
fill_value='extrapolate')
|
|
142
|
+
interpolated_values = interp_func(target_expiries_years)
|
|
143
|
+
|
|
144
|
+
# Store interpolated values
|
|
145
|
+
for i, t in enumerate(target_expiries_years):
|
|
146
|
+
interp_param_matrix.loc[param, f"t{t:.2f}"] = interpolated_values[i]
|
|
147
|
+
|
|
148
|
+
# Create matching DTE values for convenience
|
|
149
|
+
interp_dte_values = target_expiries_years * 365.25
|
|
150
|
+
|
|
151
|
+
# Store YTE and DTE as attributes in the DataFrame for reference
|
|
152
|
+
interp_param_matrix.attrs['yte_values'] = {f"t{t:.2f}": t for t in target_expiries_years}
|
|
153
|
+
interp_param_matrix.attrs['dte_values'] = {f"t{t:.2f}": t * 365.25 for t in target_expiries_years}
|
|
154
|
+
|
|
155
|
+
return interp_param_matrix
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@catch_exception
|
|
159
|
+
def interpolate_model(
|
|
160
|
+
fit_results: Dict[str, Any],
|
|
161
|
+
specific_days: Optional[List[int]] = None,
|
|
162
|
+
num_points: int = 10,
|
|
163
|
+
method: str = 'cubic'
|
|
164
|
+
) -> Dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Interpolate a fitted model to specific days to expiry.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
- fit_results: Dictionary with fitting results from fit_model()
|
|
170
|
+
- specific_days: Optional list of specific days to include (e.g., [7, 30, 90, 180])
|
|
171
|
+
- num_points: Number of points for regular grid if specific_days is None
|
|
172
|
+
- method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
- Dictionary with interpolation results
|
|
176
|
+
"""
|
|
177
|
+
# Extract required data from fit results
|
|
178
|
+
raw_param_matrix = fit_results['raw_param_matrix']
|
|
179
|
+
moneyness_grid = fit_results['moneyness_grid']
|
|
180
|
+
|
|
181
|
+
# Get min and max DTE from the original data
|
|
182
|
+
dte_values = list(raw_param_matrix.attrs['dte_values'].values())
|
|
183
|
+
min_dte = min(dte_values)
|
|
184
|
+
max_dte = max(dte_values)
|
|
185
|
+
|
|
186
|
+
# Create target expiry grid
|
|
187
|
+
target_expiries_years = create_target_expiries(min_dte, max_dte, num_points, specific_days)
|
|
188
|
+
|
|
189
|
+
# Interpolate SVI parameters
|
|
190
|
+
interp_param_matrix = interpolate_svi_parameters(raw_param_matrix, target_expiries_years, method)
|
|
191
|
+
|
|
192
|
+
# Generate implied volatility surface from interpolated parameters
|
|
193
|
+
iv_surface = {}
|
|
194
|
+
for maturity_name, yte in interp_param_matrix.attrs['yte_values'].items():
|
|
195
|
+
svi_params = interp_param_matrix[maturity_name].values
|
|
196
|
+
w_svi = [SVIModel.svi(x, *svi_params) for x in moneyness_grid]
|
|
197
|
+
iv_surface[yte] = np.sqrt(np.array(w_svi) / yte)
|
|
198
|
+
|
|
199
|
+
# Convert to Jump-Wing parameters for the interpolated maturities
|
|
200
|
+
jw_param_matrix = pd.DataFrame(
|
|
201
|
+
columns=interp_param_matrix.columns,
|
|
202
|
+
index=SVIModel.JW_PARAM_NAMES
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
for maturity_name, yte in interp_param_matrix.attrs['yte_values'].items():
|
|
206
|
+
a, b, sigma, rho, m = interp_param_matrix[maturity_name].values
|
|
207
|
+
nu, psi, p, c, nu_tilde = SVIModel.svi_jw_params(a, b, sigma, rho, m, yte)
|
|
208
|
+
jw_param_matrix[maturity_name] = [nu, psi, p, c, nu_tilde]
|
|
209
|
+
|
|
210
|
+
# Copy attributes from param matrix
|
|
211
|
+
jw_param_matrix.attrs = interp_param_matrix.attrs.copy()
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
'moneyness_grid': moneyness_grid,
|
|
215
|
+
'target_expiries_years': target_expiries_years,
|
|
216
|
+
'target_expiries_days': target_expiries_years * 365.25,
|
|
217
|
+
'interp_param_matrix': interp_param_matrix,
|
|
218
|
+
'interp_jw_param_matrix': jw_param_matrix,
|
|
219
|
+
'iv_surface': iv_surface,
|
|
220
|
+
'method': method
|
|
221
|
+
}
|
voly/core/rnd.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module handles calculating risk-neutral densities from
|
|
3
|
+
fitted volatility models and converting to probability functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from typing import Dict, List, Tuple, Optional, Union, Any
|
|
9
|
+
from voly.utils.logger import logger, catch_exception
|
|
10
|
+
from voly.exceptions import RNDError
|
|
11
|
+
from voly.models import SVIModel
|
|
12
|
+
from voly.formulas import rnd_base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@catch_exception
|
|
16
|
+
def calculate_risk_neutral_density(
|
|
17
|
+
log_moneyness: np.ndarray,
|
|
18
|
+
params: List[float],
|
|
19
|
+
model: Any = SVIModel
|
|
20
|
+
) -> np.ndarray:
|
|
21
|
+
"""
|
|
22
|
+
Calculate the risk-neutral density (RND) from model parameters.
|
|
23
|
+
|
|
24
|
+
Parameters:
|
|
25
|
+
- log_moneyness: Array of log-moneyness values
|
|
26
|
+
- params: Model parameters (e.g., SVI parameters [a, b, sigma, rho, m])
|
|
27
|
+
- model: Model class to use (default: SVIModel)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
- Array of RND values corresponding to each log-moneyness value
|
|
31
|
+
"""
|
|
32
|
+
# Adjust parameter order to match function signatures
|
|
33
|
+
a, b, sigma, rho, m = params
|
|
34
|
+
|
|
35
|
+
# Calculate total variance
|
|
36
|
+
total_var = np.array([model.svi(x, a, b, sigma, rho, m) for x in log_moneyness])
|
|
37
|
+
|
|
38
|
+
# Calculate risk-neutral density using the base RND function
|
|
39
|
+
rnd_values = np.array([rnd_base(x, var) for x, var in zip(log_moneyness, total_var)])
|
|
40
|
+
|
|
41
|
+
return rnd_values
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@catch_exception
|
|
45
|
+
def calculate_rnd_for_all_expiries(
|
|
46
|
+
moneyness_grid: np.ndarray,
|
|
47
|
+
param_matrix: pd.DataFrame,
|
|
48
|
+
model: Any = SVIModel
|
|
49
|
+
) -> Dict[str, np.ndarray]:
|
|
50
|
+
"""
|
|
51
|
+
Calculate RND for all expiries in the parameter matrix.
|
|
52
|
+
|
|
53
|
+
Parameters:
|
|
54
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
55
|
+
- param_matrix: Matrix containing model parameters for each expiry
|
|
56
|
+
- model: Model class to use (default: SVIModel)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
- Dictionary mapping maturity names to RND arrays
|
|
60
|
+
"""
|
|
61
|
+
rnd_surface = {}
|
|
62
|
+
|
|
63
|
+
# Get YTE values from the parameter matrix attributes
|
|
64
|
+
yte_values = param_matrix.attrs['yte_values']
|
|
65
|
+
|
|
66
|
+
# Calculate RND for each expiry
|
|
67
|
+
for maturity_name, yte in yte_values.items():
|
|
68
|
+
params = param_matrix[maturity_name].values
|
|
69
|
+
|
|
70
|
+
# Calculate RND
|
|
71
|
+
rnd_values = calculate_risk_neutral_density(
|
|
72
|
+
moneyness_grid,
|
|
73
|
+
params,
|
|
74
|
+
yte,
|
|
75
|
+
model=model
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
rnd_surface[maturity_name] = rnd_values
|
|
79
|
+
|
|
80
|
+
return rnd_surface
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@catch_exception
|
|
84
|
+
def calculate_probability_thresholds(
|
|
85
|
+
moneyness_grid: np.ndarray,
|
|
86
|
+
rnd_values: np.ndarray,
|
|
87
|
+
thresholds=None
|
|
88
|
+
) -> Dict[str, float]:
|
|
89
|
+
"""
|
|
90
|
+
Calculate probabilities at specific log-moneyness thresholds.
|
|
91
|
+
|
|
92
|
+
Parameters:
|
|
93
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
94
|
+
- rnd_values: Risk-neutral density values
|
|
95
|
+
- thresholds: Log-moneyness thresholds to calculate probabilities for
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
- Dictionary mapping thresholds to their probabilities
|
|
99
|
+
"""
|
|
100
|
+
# Calculate step size for integration
|
|
101
|
+
if thresholds is None:
|
|
102
|
+
thresholds = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]
|
|
103
|
+
|
|
104
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
105
|
+
|
|
106
|
+
# Normalize the RND for proper probability
|
|
107
|
+
total_density = np.sum(rnd_values) * dx
|
|
108
|
+
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
109
|
+
|
|
110
|
+
# Calculate cumulative distribution function (CDF)
|
|
111
|
+
cdf = np.cumsum(normalized_rnd) * dx
|
|
112
|
+
|
|
113
|
+
# Initialize probability results dictionary
|
|
114
|
+
result = {}
|
|
115
|
+
|
|
116
|
+
# Calculate probabilities for each threshold
|
|
117
|
+
for threshold in thresholds:
|
|
118
|
+
# Find the nearest index
|
|
119
|
+
idx = np.abs(moneyness_grid - threshold).argmin()
|
|
120
|
+
|
|
121
|
+
# Get exact threshold value (may be slightly different from requested)
|
|
122
|
+
actual_threshold = moneyness_grid[idx]
|
|
123
|
+
|
|
124
|
+
# Calculate probability P(X ≤ threshold)
|
|
125
|
+
if idx < len(cdf):
|
|
126
|
+
prob = cdf[idx]
|
|
127
|
+
else:
|
|
128
|
+
prob = 1.0
|
|
129
|
+
|
|
130
|
+
# Calculate probability of exceeding positive thresholds
|
|
131
|
+
# and probability of going below negative thresholds
|
|
132
|
+
if threshold >= 0:
|
|
133
|
+
# P(X > threshold) = 1 - P(X ≤ threshold)
|
|
134
|
+
result[f"p_above_{threshold}"] = 1.0 - prob
|
|
135
|
+
else:
|
|
136
|
+
# P(X < threshold) = P(X ≤ threshold)
|
|
137
|
+
result[f"p_below_{threshold}"] = prob
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@catch_exception
|
|
143
|
+
def calculate_moments(
|
|
144
|
+
moneyness_grid: np.ndarray,
|
|
145
|
+
rnd_values: np.ndarray) -> Dict[str, float]:
|
|
146
|
+
"""
|
|
147
|
+
Calculate statistical moments (mean, variance, skewness, kurtosis) of the RND.
|
|
148
|
+
|
|
149
|
+
Parameters:
|
|
150
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
151
|
+
- rnd_values: Array of RND values
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
- Dictionary with statistical moments
|
|
155
|
+
"""
|
|
156
|
+
# Calculate total probability (for normalization)
|
|
157
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
158
|
+
total_prob = np.sum(rnd_values) * dx
|
|
159
|
+
|
|
160
|
+
# Normalize the RND if needed
|
|
161
|
+
normalized_rnd = rnd_values / total_prob if total_prob > 0 else rnd_values
|
|
162
|
+
|
|
163
|
+
# Calculate moments in percentage terms
|
|
164
|
+
# First, convert log-moneyness to percentage returns
|
|
165
|
+
returns_pct = (np.exp(moneyness_grid) - 1) * 100 # Convert to percentage returns
|
|
166
|
+
|
|
167
|
+
# Calculate mean (expected return in %)
|
|
168
|
+
mean_pct = np.sum(returns_pct * normalized_rnd) * dx
|
|
169
|
+
|
|
170
|
+
# Calculate variance (in % squared)
|
|
171
|
+
centered_returns = returns_pct - mean_pct
|
|
172
|
+
variance_pct = np.sum(centered_returns ** 2 * normalized_rnd) * dx
|
|
173
|
+
std_dev_pct = np.sqrt(variance_pct)
|
|
174
|
+
|
|
175
|
+
# Skewness and kurtosis are unitless
|
|
176
|
+
skewness = np.sum(centered_returns ** 3 * normalized_rnd) * dx / (std_dev_pct ** 3) if std_dev_pct > 0 else 0
|
|
177
|
+
kurtosis = np.sum(centered_returns ** 4 * normalized_rnd) * dx / (variance_pct ** 2) if variance_pct > 0 else 0
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"mean_pct": mean_pct, # Mean return in percentage
|
|
181
|
+
"variance_pct": variance_pct, # Variance in percentage squared
|
|
182
|
+
"std_dev_pct": std_dev_pct, # Standard deviation in percentage
|
|
183
|
+
"skewness": skewness, # Unitless
|
|
184
|
+
"kurtosis": kurtosis, # Unitless
|
|
185
|
+
"excess_kurtosis": kurtosis - 3 # Unitless
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@catch_exception
|
|
190
|
+
def analyze_rnd_statistics(
|
|
191
|
+
moneyness_grid: np.ndarray,
|
|
192
|
+
rnd_surface: Dict[str, np.ndarray],
|
|
193
|
+
param_matrix: pd.DataFrame) -> pd.DataFrame:
|
|
194
|
+
"""
|
|
195
|
+
Analyze RND statistics for all expiries.
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
199
|
+
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
200
|
+
- param_matrix: Matrix containing model parameters
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
- DataFrame with RND statistics for each expiry
|
|
204
|
+
"""
|
|
205
|
+
# Get maturity information
|
|
206
|
+
dte_values = param_matrix.attrs['dte_values']
|
|
207
|
+
yte_values = param_matrix.attrs['yte_values']
|
|
208
|
+
|
|
209
|
+
# Initialize data dictionary
|
|
210
|
+
data = {
|
|
211
|
+
"maturity_name": [],
|
|
212
|
+
"dte": [],
|
|
213
|
+
"yte": [],
|
|
214
|
+
"mean_pct": [],
|
|
215
|
+
"std_dev_pct": [],
|
|
216
|
+
"skewness": [],
|
|
217
|
+
"excess_kurtosis": []
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Calculate moments for each expiry
|
|
221
|
+
for maturity_name, rnd in rnd_surface.items():
|
|
222
|
+
moments = calculate_moments(moneyness_grid, rnd)
|
|
223
|
+
|
|
224
|
+
data["maturity_name"].append(maturity_name)
|
|
225
|
+
data["dte"].append(dte_values[maturity_name])
|
|
226
|
+
data["yte"].append(yte_values[maturity_name])
|
|
227
|
+
data["mean_pct"].append(moments["mean_pct"])
|
|
228
|
+
data["std_dev_pct"].append(moments["std_dev_pct"])
|
|
229
|
+
data["skewness"].append(moments["skewness"])
|
|
230
|
+
data["excess_kurtosis"].append(moments["excess_kurtosis"])
|
|
231
|
+
|
|
232
|
+
# Create DataFrame and sort by DTE
|
|
233
|
+
stats_df = pd.DataFrame(data)
|
|
234
|
+
stats_df = stats_df.sort_values(by="dte")
|
|
235
|
+
|
|
236
|
+
return stats_df
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@catch_exception
|
|
240
|
+
def calculate_pdf(
|
|
241
|
+
moneyness_grid: np.ndarray,
|
|
242
|
+
rnd_values: np.ndarray,
|
|
243
|
+
spot_price: float = 1.0
|
|
244
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
245
|
+
"""
|
|
246
|
+
Calculate probability density function (PDF) from RND values.
|
|
247
|
+
|
|
248
|
+
Parameters:
|
|
249
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
250
|
+
- rnd_values: Array of RND values
|
|
251
|
+
- spot_price: Spot price of the underlying
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
- Tuple of (prices, pdf_values) for plotting
|
|
255
|
+
"""
|
|
256
|
+
# Calculate step size for normalization
|
|
257
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
258
|
+
|
|
259
|
+
# Normalize the RND
|
|
260
|
+
total_density = np.sum(rnd_values) * dx
|
|
261
|
+
pdf_values = rnd_values / total_density if total_density > 0 else rnd_values
|
|
262
|
+
|
|
263
|
+
# Convert log-moneyness to actual prices
|
|
264
|
+
prices = spot_price * np.exp(moneyness_grid)
|
|
265
|
+
|
|
266
|
+
return prices, pdf_values
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@catch_exception
|
|
270
|
+
def calculate_cdf(
|
|
271
|
+
moneyness_grid: np.ndarray,
|
|
272
|
+
rnd_values: np.ndarray,
|
|
273
|
+
spot_price: float = 1.0
|
|
274
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
275
|
+
"""
|
|
276
|
+
Calculate cumulative distribution function (CDF) from RND values.
|
|
277
|
+
|
|
278
|
+
Parameters:
|
|
279
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
280
|
+
- rnd_values: Array of RND values
|
|
281
|
+
- spot_price: Spot price of the underlying
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
- Tuple of (prices, cdf_values) for plotting
|
|
285
|
+
"""
|
|
286
|
+
# Calculate step size for normalization
|
|
287
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
288
|
+
|
|
289
|
+
# Normalize the RND
|
|
290
|
+
total_density = np.sum(rnd_values) * dx
|
|
291
|
+
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
292
|
+
|
|
293
|
+
# Calculate CDF
|
|
294
|
+
cdf_values = np.cumsum(normalized_rnd) * dx
|
|
295
|
+
|
|
296
|
+
# Convert log-moneyness to actual prices
|
|
297
|
+
prices = spot_price * np.exp(moneyness_grid)
|
|
298
|
+
|
|
299
|
+
return prices, cdf_values
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@catch_exception
|
|
303
|
+
def calculate_strike_probability(
|
|
304
|
+
target_price: float,
|
|
305
|
+
moneyness_grid: np.ndarray,
|
|
306
|
+
rnd_values: np.ndarray,
|
|
307
|
+
spot_price: float,
|
|
308
|
+
direction: str = 'above'
|
|
309
|
+
) -> float:
|
|
310
|
+
"""
|
|
311
|
+
Calculate probability of price being above or below a target price.
|
|
312
|
+
|
|
313
|
+
Parameters:
|
|
314
|
+
- target_price: Target price level
|
|
315
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
316
|
+
- rnd_values: Array of RND values
|
|
317
|
+
- spot_price: Current spot price
|
|
318
|
+
- direction: 'above' or 'below'
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
- Probability (0 to 1)
|
|
322
|
+
"""
|
|
323
|
+
# Convert target price to log-moneyness
|
|
324
|
+
target_moneyness = np.log(target_price / spot_price)
|
|
325
|
+
|
|
326
|
+
# Calculate CDF
|
|
327
|
+
_, cdf_values = calculate_cdf(moneyness_grid, rnd_values, spot_price)
|
|
328
|
+
|
|
329
|
+
# Find the nearest index to target moneyness
|
|
330
|
+
target_idx = np.abs(moneyness_grid - target_moneyness).argmin()
|
|
331
|
+
|
|
332
|
+
# Get probability
|
|
333
|
+
if target_idx < len(cdf_values):
|
|
334
|
+
cdf_at_target = cdf_values[target_idx]
|
|
335
|
+
else:
|
|
336
|
+
cdf_at_target = 1.0
|
|
337
|
+
|
|
338
|
+
# Return probability based on direction
|
|
339
|
+
if direction.lower() == 'above':
|
|
340
|
+
return 1.0 - cdf_at_target
|
|
341
|
+
else: # below
|
|
342
|
+
return cdf_at_target
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@catch_exception
|
|
346
|
+
def calculate_rnd(
|
|
347
|
+
fit_results: Dict[str, Any],
|
|
348
|
+
maturity: Optional[str] = None) -> Dict[str, Any]:
|
|
349
|
+
"""
|
|
350
|
+
Calculate risk-neutral density from fit results.
|
|
351
|
+
|
|
352
|
+
Parameters:
|
|
353
|
+
- fit_results: Dictionary with fitting results from fit_model()
|
|
354
|
+
- maturity: Optional maturity name to calculate RND for a specific expiry
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
- Dictionary with RND results
|
|
358
|
+
"""
|
|
359
|
+
# Extract required data from fit results
|
|
360
|
+
raw_param_matrix = fit_results['raw_param_matrix']
|
|
361
|
+
moneyness_grid = fit_results['moneyness_grid']
|
|
362
|
+
|
|
363
|
+
# Calculate RND for all expiries or just the specified one
|
|
364
|
+
if maturity is not None:
|
|
365
|
+
# Validate maturity
|
|
366
|
+
if maturity not in raw_param_matrix.columns:
|
|
367
|
+
raise RNDError(f"Maturity '{maturity}' not found in fit results")
|
|
368
|
+
|
|
369
|
+
# Just calculate for the specified maturity
|
|
370
|
+
yte = raw_param_matrix.attrs['yte_values'][maturity]
|
|
371
|
+
params = raw_param_matrix[maturity].values
|
|
372
|
+
rnd_values = calculate_risk_neutral_density(moneyness_grid, params, yte)
|
|
373
|
+
rnd_surface = {maturity: rnd_values}
|
|
374
|
+
else:
|
|
375
|
+
# Calculate for all maturities
|
|
376
|
+
rnd_surface = calculate_rnd_for_all_expiries(moneyness_grid, raw_param_matrix)
|
|
377
|
+
|
|
378
|
+
# Calculate statistics
|
|
379
|
+
rnd_statistics = analyze_rnd_statistics(moneyness_grid, rnd_surface, raw_param_matrix)
|
|
380
|
+
|
|
381
|
+
# Calculate probabilities
|
|
382
|
+
rnd_probabilities = analyze_rnd_probabilities(moneyness_grid, rnd_surface, raw_param_matrix)
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
'moneyness_grid': moneyness_grid,
|
|
386
|
+
'rnd_surface': rnd_surface,
|
|
387
|
+
'rnd_statistics': rnd_statistics,
|
|
388
|
+
'rnd_probabilities': rnd_probabilities
|
|
389
|
+
}
|
voly/exceptions.py
ADDED