voly 0.0.85__py3-none-any.whl → 0.0.87__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/core/rnd.py CHANGED
@@ -9,359 +9,280 @@ from typing import Dict, List, Tuple, Optional, Union, Any
9
9
  from voly.utils.logger import logger, catch_exception
10
10
  from voly.exceptions import VolyError
11
11
  from voly.models import SVIModel
12
+ from voly.formulas import get_all_domains
12
13
 
13
14
 
15
+ # Breeden-Litzenberger Method
14
16
  @catch_exception
15
- def rnd(moneyness_array: float, total_var: float) -> float:
16
- return np.exp(-(moneyness_array ** 2) / (2 * total_var)) / (np.sqrt(2 * np.pi * total_var))
17
-
18
-
17
+ def breeden(domain_params, s, o, r, t, return_domain):
18
+ LM = get_domain(domain_params, s, o, r, t, 'log_moneyness')
19
+ M = get_domain(domain_params, s, o, r, t, 'moneyness')
20
+ R = get_domain(domain_params, s, o, r, t, 'returns')
21
+ K = get_domain(domain_params, s, o, r, t, 'strikes')
22
+ D = get_domain(domain_params, s, o, r, t, 'delta')
23
+
24
+ c = voly.bs(s, K, r, o, t, option_type='call')
25
+ c1 = np.gradient(c, K)
26
+ c2 = np.gradient(c1, K)
27
+
28
+ rnd_k = np.maximum(np.exp(r * t) * c2, 0)
29
+ rnd_lm = rnd_k * K
30
+
31
+ dx = LM[1] - LM[0]
32
+ total_area = np.sum(rnd_lm * dx)
33
+ pdf_lm = rnd_lm / total_area
34
+ pdf_k = pdf_lm / K
35
+ pdf_m = pdf_k * s
36
+ pdf_r = pdf_lm / (1 + R)
37
+
38
+ n_d1 = stats.norm.pdf(voly.d1(s, K, r, o, t, option_type='call'))
39
+ dd_dK = n_d1 / (o * np.sqrt(t) * K)
40
+ pdf_d = pdf_k / dd_dK
41
+
42
+ cdf = np.cumsum(pdf_lm) * dx
43
+ cdf = cdf / cdf[-1]
44
+
45
+ if return_domain == 'log_moneyness':
46
+ x = LM
47
+ pdf = pdf_lm
48
+ moments = get_all_moments(x, pdf)
49
+ return pdf, cdf, x, moments
50
+ elif return_domain == 'moneyness':
51
+ x = M
52
+ pdf = pdf_m
53
+ moments = get_all_moments(x, pdf)
54
+ return pdf, cdf, x, moments
55
+ elif return_domain == 'returns':
56
+ x = R
57
+ pdf = pdf_r
58
+ moments = get_all_moments(x, pdf)
59
+ return pdf, cdf, x, moments
60
+ elif return_domain == 'strikes':
61
+ x = K
62
+ pdf = pdf_k
63
+ moments = get_all_moments(x, pdf)
64
+ return pdf, cdf, x, moments
65
+ elif return_domain == 'delta':
66
+ sort_idx = np.argsort(D)
67
+ x = D[sort_idx]
68
+ pdf = pdf_d[sort_idx]
69
+ moments = get_all_moments(x, pdf)
70
+ return pdf, cdf, x, moments
71
+
72
+
73
+ # Rookley's Method
19
74
  @catch_exception
20
- def get_rnd_surface(fit_results: Dict[str, Any],
21
- moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
22
- ) -> Dict[str, Any]:
23
- """
24
- Calculate RND for all expiries using the SVI parameter matrix.
25
-
26
- Parameters:
27
- - moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
28
- - fit_results: results from fit_model()
29
-
30
- Returns:
31
- - moneyness_array, rnd_surface
32
- """
33
- rnd_surface = {}
34
-
35
- # Extract moneyness parameters
36
- min_m, max_m, num_points = moneyness_params
37
-
38
- # Generate moneyness grid
39
- moneyness_array = np.linspace(min_m, max_m, num=num_points)
40
-
41
- # Get YTE values from the fit results attributes
42
- yte_values = fit_results['fit_performance']['YTE']
43
- maturity_values = fit_results['fit_performance']['Maturity']
44
- param_matrix = fit_results['raw_param_matrix']
45
-
46
- # Generate rnd for each expiry
47
- for maturity, yte in zip(maturity_values, yte_values):
48
- svi_params_list = list(param_matrix[maturity].values)
49
- a, b, sigma, rho, m = svi_params_list
50
-
51
- # Calculate total variance
52
- total_var = np.array([SVIModel.svi(x, a, b, sigma, rho, m) for x in moneyness_array])
53
-
54
- # Calculate risk-neutral density using the base RND function
55
- rnd_values = np.array([rnd(x, var) for x, var in zip(moneyness_array, total_var)])
56
- rnd_surface[maturity] = rnd_values
57
-
58
- return moneyness_array, rnd_surface
59
-
60
-
61
- @catch_exception
62
- def calculate_probability_thresholds(
63
- moneyness_grid: np.ndarray,
64
- rnd_values: np.ndarray,
65
- thresholds=None
66
- ) -> Dict[str, float]:
67
- """
68
- Calculate probabilities at specific log-moneyness thresholds.
69
-
70
- Parameters:
71
- - moneyness_grid: Grid of log-moneyness values
72
- - rnd_values: Risk-neutral density values
73
- - thresholds: Log-moneyness thresholds to calculate probabilities for
74
-
75
- Returns:
76
- - Dictionary mapping thresholds to their probabilities
77
- """
78
- # Calculate step size for integration
79
- if thresholds is None:
80
- thresholds = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]
81
-
82
- dx = moneyness_grid[1] - moneyness_grid[0]
83
-
84
- # Normalize the RND for proper probability
85
- total_density = np.sum(rnd_values) * dx
86
- normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
87
-
88
- # Calculate cumulative distribution function (CDF)
89
- cdf = np.cumsum(normalized_rnd) * dx
90
-
91
- # Initialize probability results dictionary
92
- result = {}
93
-
94
- # Calculate probabilities for each threshold
95
- for threshold in thresholds:
96
- # Find the nearest index
97
- idx = np.abs(moneyness_grid - threshold).argmin()
98
-
99
- # Get exact threshold value (may be slightly different from requested)
100
- actual_threshold = moneyness_grid[idx]
101
-
102
- # Calculate probability P(X ≤ threshold)
103
- if idx < len(cdf):
104
- prob = cdf[idx]
105
- else:
106
- prob = 1.0
107
-
108
- # Calculate probability of exceeding positive thresholds
109
- # and probability of going below negative thresholds
110
- if threshold >= 0:
111
- # P(X > threshold) = 1 - P(X ≤ threshold)
112
- result[f"p_above_{threshold}"] = 1.0 - prob
113
- else:
114
- # P(X < threshold) = P(X ≤ threshold)
115
- result[f"p_below_{threshold}"] = prob
116
-
117
- return result
118
-
119
-
120
- @catch_exception
121
- def calculate_moments(
122
- moneyness_grid: np.ndarray,
123
- rnd_values: np.ndarray) -> Dict[str, float]:
124
- """
125
- Calculate statistical moments (mean, variance, skewness, kurtosis) of the RND.
126
-
127
- Parameters:
128
- - moneyness_grid: Grid of log-moneyness values
129
- - rnd_values: Array of RND values
130
-
131
- Returns:
132
- - Dictionary with statistical moments
133
- """
134
- # Calculate total probability (for normalization)
135
- dx = moneyness_grid[1] - moneyness_grid[0]
136
- total_prob = np.sum(rnd_values) * dx
137
-
138
- # Normalize the RND if needed
139
- normalized_rnd = rnd_values / total_prob if total_prob > 0 else rnd_values
140
-
141
- # Calculate moments in percentage terms
142
- # First, convert log-moneyness to percentage returns
143
- returns_pct = (np.exp(moneyness_grid) - 1) * 100 # Convert to percentage returns
144
-
145
- # Calculate mean (expected return in %)
146
- mean_pct = np.sum(returns_pct * normalized_rnd) * dx
147
-
148
- # Calculate variance (in % squared)
149
- centered_returns = returns_pct - mean_pct
150
- variance_pct = np.sum(centered_returns ** 2 * normalized_rnd) * dx
151
- std_dev_pct = np.sqrt(variance_pct)
152
-
153
- # Skewness and kurtosis are unitless
154
- skewness = np.sum(centered_returns ** 3 * normalized_rnd) * dx / (std_dev_pct ** 3) if std_dev_pct > 0 else 0
155
- kurtosis = np.sum(centered_returns ** 4 * normalized_rnd) * dx / (variance_pct ** 2) if variance_pct > 0 else 0
156
-
157
- return {
158
- "mean_pct": mean_pct, # Mean return in percentage
159
- "variance_pct": variance_pct, # Variance in percentage squared
160
- "std_dev_pct": std_dev_pct, # Standard deviation in percentage
161
- "skewness": skewness, # Unitless
162
- "kurtosis": kurtosis, # Unitless
163
- "excess_kurtosis": kurtosis - 3 # Unitless
164
- }
75
+ def rookley(domain_params, s, o, r, t, return_domain):
76
+ LM = get_domain(domain_params, s, o, r, t, 'log_moneyness')
77
+ M = get_domain(domain_params, s, o, r, t, 'moneyness')
78
+ R = get_domain(domain_params, s, o, r, t, 'returns')
79
+ K = get_domain(domain_params, s, o, r, t, 'strikes')
80
+ D = get_domain(domain_params, s, o, r, t, 'delta')
81
+
82
+ o1 = np.gradient(o, M)
83
+ o2 = np.gradient(o1, M)
84
+
85
+ st = np.sqrt(t)
86
+ rt = r * t
87
+ ert = np.exp(rt)
88
+
89
+ d1 = (np.log(M) + (r + 1 / 2 * o ** 2) * t) / (o * st)
90
+ d2 = d1 - o * st
91
+
92
+ del_d1_M = 1 / (M * o * st)
93
+ del_d2_M = del_d1_M
94
+ del_d1_o = -(np.log(M) + rt) / (o ** 2 * st) + st / 2
95
+ del_d2_o = -(np.log(M) + rt) / (o ** 2 * st) - st / 2
96
+
97
+ d_d1_M = del_d1_M + del_d1_o * o1
98
+ d_d2_M = del_d2_M + del_d2_o * o1
99
+
100
+ dd_d1_M = (
101
+ -(1 / (M * o * st)) * (1 / M + o1 / o)
102
+ + o2 * (st / 2 - (np.log(M) + rt) / (o ** 2 * st))
103
+ + o1 * (2 * o1 * (np.log(M) + rt) / (o ** 3 * st) - 1 / (M * o ** 2 * st))
104
+ )
105
+ dd_d2_M = (
106
+ -(1 / (M * o * st)) * (1 / M + o1 / o)
107
+ - o2 * (st / 2 + (np.log(M) + rt) / (o ** 2 * st))
108
+ + o1 * (2 * o1 * (np.log(M) + rt) / (o ** 3 * st) - 1 / (M * o ** 2 * st))
109
+ )
110
+
111
+ d_c_M = stats.norm.pdf(d1) * d_d1_M - 1 / ert * stats.norm.pdf(d2) / M * d_d2_M + 1 / ert * stats.norm.cdf(d2) / (
112
+ M ** 2)
113
+ dd_c_M = (
114
+ stats.norm.pdf(d1) * (dd_d1_M - d1 * (d_d1_M) ** 2)
115
+ - stats.norm.pdf(d2) / (ert * M) * (dd_d2_M - 2 / M * d_d2_M - d2 * (d_d2_M) ** 2)
116
+ - 2 * stats.norm.cdf(d2) / (ert * M ** 3)
117
+ )
118
+
119
+ dd_c_K = dd_c_M * (M / K) ** 2 + 2 * d_c_M * (M / K ** 2)
120
+
121
+ rnd_k = np.maximum(ert * s * dd_c_K, 0)
122
+ rnd_lm = rnd_k * K
123
+
124
+ dx = LM[1] - LM[0]
125
+ total_area = np.sum(rnd_lm * dx)
126
+ pdf_lm = rnd_lm / total_area
127
+ pdf_k = pdf_lm / K
128
+ pdf_m = pdf_k * s
129
+ pdf_r = pdf_lm / (1 + R)
130
+
131
+ n_d1 = stats.norm.pdf(voly.d1(s, K, r, o, t, option_type='call'))
132
+ dd_dK = n_d1 / (o * np.sqrt(t) * K)
133
+ pdf_d = pdf_k / dd_dK
134
+
135
+ cdf = np.cumsum(pdf_lm) * dx
136
+ cdf = cdf / cdf[-1]
137
+
138
+ if return_domain == 'log_moneyness':
139
+ x = LM
140
+ pdf = pdf_lm
141
+ moments = get_all_moments(x, pdf)
142
+ return pdf, cdf, x, moments
143
+ elif return_domain == 'moneyness':
144
+ x = M
145
+ pdf = pdf_m
146
+ moments = get_all_moments(x, pdf)
147
+ return pdf, cdf, x, moments
148
+ elif return_domain == 'returns':
149
+ x = R
150
+ pdf = pdf_r
151
+ moments = get_all_moments(x, pdf)
152
+ return pdf, cdf, moments
153
+ elif return_domain == 'strikes':
154
+ x = K
155
+ pdf = pdf_k
156
+ moments = get_all_moments(x, pdf)
157
+ return pdf, cdf, x, moments
158
+ elif return_domain == 'delta':
159
+ sort_idx = np.argsort(D)
160
+ x = D[sort_idx]
161
+ pdf = pdf_d[sort_idx]
162
+ moments = get_all_moments(x, pdf)
163
+ return pdf, cdf, x, moments
165
164
 
166
165
 
167
166
  @catch_exception
168
- def analyze_rnd_statistics(
169
- moneyness_grid: np.ndarray,
170
- rnd_surface: Dict[str, np.ndarray],
171
- param_matrix: pd.DataFrame) -> pd.DataFrame:
172
- """
173
- Analyze RND statistics for all expiries.
174
-
175
- Parameters:
176
- - moneyness_grid: Grid of log-moneyness values
177
- - rnd_surface: Dictionary mapping maturity names to RND arrays
178
- - param_matrix: Matrix containing model parameters
179
-
180
- Returns:
181
- - DataFrame with RND statistics for each expiry
182
- """
183
- # Get maturity information
184
- dte_values = param_matrix.attrs['dte_values']
185
- yte_values = param_matrix.attrs['yte_values']
186
-
187
- # Initialize data dictionary
188
- data = {
189
- "maturity_name": [],
190
- "dte": [],
191
- "yte": [],
192
- "mean_pct": [],
193
- "std_dev_pct": [],
194
- "skewness": [],
195
- "excess_kurtosis": []
167
+ def get_all_moments(x, pdf):
168
+ mean = np.trapz(x * pdf, x) # E[X]
169
+ median = x[np.searchsorted(np.cumsum(pdf * np.diff(x, prepend=x[0])), 0.5)] # Median (50th percentile)
170
+ mode = x[np.argmax(pdf)] # Mode (peak of PDF)
171
+ variance = np.trapz((x - mean) ** 2 * pdf, x) # Var[X] = E[(X - μ)^2]
172
+ std_dev = np.sqrt(variance) # Standard deviation
173
+ skewness = np.trapz((x - mean) ** 3 * pdf, x) / std_dev ** 3 # Skewness
174
+ kurtosis = np.trapz((x - mean) ** 4 * pdf, x) / std_dev ** 4 # Kurtosis
175
+ excess_kurtosis = kurtosis - 3 # Excess kurtosis (relative to normal dist.)
176
+ q25 = x[np.searchsorted(np.cumsum(pdf * np.diff(x, prepend=x[0])), 0.25)] # 25th percentile
177
+ q75 = x[np.searchsorted(np.cumsum(pdf * np.diff(x, prepend=x[0])), 0.75)] # 75th percentile
178
+ iqr = q75 - q25 # Inter-quartile range
179
+ entropy = -np.trapz(pdf * np.log(pdf + 1e-10), x) # Differential entropy (avoid log(0))
180
+
181
+ # Full Z-score areas
182
+ dx = np.diff(x, prepend=x[0])
183
+ z = (x - mean) / std_dev
184
+ o1p = np.sum(pdf[(z > 0) & (z < 1)] * dx[(z > 0) & (z < 1)])
185
+ o2p = np.sum(pdf[(z >= 1) & (z < 2)] * dx[(z >= 1) & (z < 2)])
186
+ o3p = np.sum(pdf[(z >= 2) & (z < 3)] * dx[(z >= 2) & (z < 3)])
187
+ o4p = np.sum(pdf[z >= 3] * dx[z >= 3])
188
+ o1n = np.sum(pdf[(z < 0) & (z > -1)] * dx[(z < 0) & (z > -1)])
189
+ o2n = np.sum(pdf[(z <= -1) & (z > -2)] * dx[(z <= -1) & (z > -2)])
190
+ o3n = np.sum(pdf[(z <= -2) & (z > -3)] * dx[(z <= -2) & (z > -3)])
191
+ o4n = np.sum(pdf[z <= -3] * dx[z <= -3])
192
+
193
+ moments = {
194
+ 'mean': mean,
195
+ 'median': median,
196
+ 'mode': mode,
197
+ 'variance': variance,
198
+ 'std_dev': std_dev,
199
+ 'skewness': skewness,
200
+ 'kurtosis': kurtosis,
201
+ 'excess_kurtosis': excess_kurtosis,
202
+ 'q25': q25,
203
+ 'q75': q75,
204
+ 'iqr': iqr,
205
+ 'entropy': entropy,
206
+ 'o1p': o1p,
207
+ 'o2p': o2p,
208
+ 'o3p': o3p,
209
+ 'o4p': o4p,
210
+ 'o1n': o1n,
211
+ 'o2n': o2n,
212
+ 'o3n': o3n,
213
+ 'o4n': o4n
196
214
  }
197
-
198
- # Calculate moments for each expiry
199
- for maturity_name, rnd in rnd_surface.items():
200
- moments = calculate_moments(moneyness_grid, rnd)
201
-
202
- data["maturity_name"].append(maturity_name)
203
- data["dte"].append(dte_values[maturity_name])
204
- data["yte"].append(yte_values[maturity_name])
205
- data["mean_pct"].append(moments["mean_pct"])
206
- data["std_dev_pct"].append(moments["std_dev_pct"])
207
- data["skewness"].append(moments["skewness"])
208
- data["excess_kurtosis"].append(moments["excess_kurtosis"])
209
-
210
- # Create DataFrame and sort by DTE
211
- stats_df = pd.DataFrame(data)
212
- stats_df = stats_df.sort_values(by="dte")
213
-
214
- return stats_df
215
+ return moments
215
216
 
216
217
 
217
218
  @catch_exception
218
- def calculate_pdf(
219
- moneyness_grid: np.ndarray,
220
- rnd_values: np.ndarray,
221
- spot_price: float = 1.0
222
- ) -> Tuple[np.ndarray, np.ndarray]:
219
+ def get_rnd_surface(model_results: pd.DataFrame,
220
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
221
+ return_domain: str = 'log_moneyness',
222
+ method: str = 'rookley') -> Tuple[
223
+ Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray], pd.DataFrame]:
223
224
  """
224
- Calculate probability density function (PDF) from RND values.
225
+ Generate RND surface from vol smile parameters.
225
226
 
226
- Parameters:
227
- - moneyness_grid: Grid of log-moneyness values
228
- - rnd_values: Array of RND values
229
- - spot_price: Spot price of the underlying
230
-
231
- Returns:
232
- - Tuple of (prices, pdf_values) for plotting
233
- """
234
- # Calculate step size for normalization
235
- dx = moneyness_grid[1] - moneyness_grid[0]
236
-
237
- # Normalize the RND
238
- total_density = np.sum(rnd_values) * dx
239
- pdf_values = rnd_values / total_density if total_density > 0 else rnd_values
240
-
241
- # Convert log-moneyness to actual prices
242
- prices = spot_price * np.exp(moneyness_grid)
243
-
244
- return prices, pdf_values
245
-
246
-
247
- @catch_exception
248
- def calculate_cdf(
249
- moneyness_grid: np.ndarray,
250
- rnd_values: np.ndarray,
251
- spot_price: float = 1.0
252
- ) -> Tuple[np.ndarray, np.ndarray]:
253
- """
254
- Calculate cumulative distribution function (CDF) from RND values.
227
+ Works with both regular fit_results and interpolated_results dataframes.
255
228
 
256
229
  Parameters:
257
- - moneyness_grid: Grid of log-moneyness values
258
- - rnd_values: Array of RND values
259
- - spot_price: Spot price of the underlying
230
+ - model_results: DataFrame from fit_model() or interpolate_model(). Maturity names or DTM as Index
231
+ - domain_params: Tuple of (min, max, num_points) for the x-domain grid
232
+ - return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
233
+ - method: 'rookley' or 'breeden'
260
234
 
261
235
  Returns:
262
- - Tuple of (prices, cdf_values) for plotting
236
+ - Tuple containing:
237
+ - pdf_surface: Dictionary mapping maturity/dtm names to PDF arrays of their requested domain
238
+ - cdf_surface: Dictionary mapping maturity/dtm names to CDF arrays
239
+ - x_surface: Dictionary mapping maturity/dtm names to requested x domain arrays
240
+ - moments_df: DataFrame with moments of the distributions using model_results index
263
241
  """
264
- # Calculate step size for normalization
265
- dx = moneyness_grid[1] - moneyness_grid[0]
266
-
267
- # Normalize the RND
268
- total_density = np.sum(rnd_values) * dx
269
- normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
270
-
271
- # Calculate CDF
272
- cdf_values = np.cumsum(normalized_rnd) * dx
273
-
274
- # Convert log-moneyness to actual prices
275
- prices = spot_price * np.exp(moneyness_grid)
276
-
277
- return prices, cdf_values
278
-
279
-
280
- @catch_exception
281
- def calculate_strike_probability(
282
- target_price: float,
283
- moneyness_grid: np.ndarray,
284
- rnd_values: np.ndarray,
285
- spot_price: float,
286
- direction: str = 'above'
287
- ) -> float:
288
- """
289
- Calculate probability of price being above or below a target price.
290
-
291
- Parameters:
292
- - target_price: Target price level
293
- - moneyness_grid: Grid of log-moneyness values
294
- - rnd_values: Array of RND values
295
- - spot_price: Current spot price
296
- - direction: 'above' or 'below'
297
-
298
- Returns:
299
- - Probability (0 to 1)
300
- """
301
- # Convert target price to log-moneyness
302
- target_moneyness = np.log(target_price / spot_price)
303
-
304
- # Calculate CDF
305
- _, cdf_values = calculate_cdf(moneyness_grid, rnd_values, spot_price)
306
-
307
- # Find the nearest index to target moneyness
308
- target_idx = np.abs(moneyness_grid - target_moneyness).argmin()
309
-
310
- # Get probability
311
- if target_idx < len(cdf_values):
312
- cdf_at_target = cdf_values[target_idx]
313
- else:
314
- cdf_at_target = 1.0
242
+ # Check if required columns are present
243
+ required_columns = ['s', 'a', 'b', 'sigma', 'm', 'rho', 't', 'r']
244
+ missing_columns = [col for col in required_columns if col not in model_results.columns]
245
+ if missing_columns:
246
+ raise VolyError(f"Required columns missing in model_results: {missing_columns}")
247
+
248
+ pdf_surface = {}
249
+ cdf_surface = {}
250
+ x_surface = {}
251
+ all_moments = {}
252
+
253
+ # Process each maturity/dtm
254
+ for i in model_results.index:
255
+ # Calculate SVI total implied variance and convert to IV
256
+ params = [
257
+ model_results.loc[i, 'a'],
258
+ model_results.loc[i, 'b'],
259
+ model_results.loc[i, 'sigma'],
260
+ model_results.loc[i, 'rho'],
261
+ model_results.loc[i, 'm']
262
+ ]
263
+ s = model_results.loc[i, 's']
264
+ r = model_results.loc[i, 'r']
265
+ t = model_results.loc[i, 't']
266
+
267
+ # Calculate implied volatility
268
+ LM = np.linspace(domain_params[0], domain_params[1], domain_params[2])
269
+ w = np.array([SVIModel.svi(x, *params) for x in LM])
270
+ o = np.sqrt(w / t)
271
+
272
+ if method == 'rookley':
273
+ pdf, cdf, x, moments = rookley(domain_params, s, o, r, t, return_domain)
274
+ else:
275
+ pdf, cdf, x, moments = breeden(domain_params, s, o, r, t, return_domain)
315
276
 
316
- # Return probability based on direction
317
- if direction.lower() == 'above':
318
- return 1.0 - cdf_at_target
319
- else: # below
320
- return cdf_at_target
277
+ pdf_surface[i] = pdf
278
+ cdf_surface[i] = cdf
279
+ x_surface[i] = x
280
+ all_moments[i] = moments
321
281
 
282
+ # Create a DataFrame with moments using the same index as model_results
283
+ moments = pd.DataFrame(all_moments).T
322
284
 
323
- @catch_exception
324
- def calculate_rnd(
325
- fit_results: Dict[str, Any],
326
- maturity: Optional[str] = None) -> Dict[str, Any]:
327
- """
328
- Calculate risk-neutral density from fit results.
285
+ # Ensure the index matches the model_results index
286
+ moments.index = model_results.index
329
287
 
330
- Parameters:
331
- - fit_results: Dictionary with fitting results from fit_model()
332
- - maturity: Optional maturity name to calculate RND for a specific expiry
333
-
334
- Returns:
335
- - Dictionary with RND results
336
- """
337
- # Extract required data from fit results
338
- raw_param_matrix = fit_results['raw_param_matrix']
339
- moneyness_grid = fit_results['moneyness_grid']
340
-
341
- # Calculate RND for all expiries or just the specified one
342
- if maturity is not None:
343
- # Validate maturity
344
- if maturity not in raw_param_matrix.columns:
345
- raise VolyError(f"Maturity '{maturity}' not found in fit results")
346
-
347
- # Just calculate for the specified maturity
348
- yte = raw_param_matrix.attrs['yte_values'][maturity]
349
- params = raw_param_matrix[maturity].values
350
- rnd_values = calculate_risk_neutral_density(moneyness_grid, params, yte)
351
- rnd_surface = {maturity: rnd_values}
352
- else:
353
- # Calculate for all maturities
354
- rnd_surface = calculate_rnd_for_all_expiries(moneyness_grid, raw_param_matrix)
355
-
356
- # Calculate statistics
357
- rnd_statistics = analyze_rnd_statistics(moneyness_grid, rnd_surface, raw_param_matrix)
358
-
359
- # Calculate probabilities
360
- rnd_probabilities = analyze_rnd_probabilities(moneyness_grid, rnd_surface, raw_param_matrix)
361
-
362
- return {
363
- 'moneyness_grid': moneyness_grid,
364
- 'rnd_surface': rnd_surface,
365
- 'rnd_statistics': rnd_statistics,
366
- 'rnd_probabilities': rnd_probabilities
367
- }
288
+ return pdf_surface, cdf_surface, x_surface, moments