voly 0.0.166__tar.gz → 0.0.167__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 (28) hide show
  1. {voly-0.0.166/src/voly.egg-info → voly-0.0.167}/PKG-INFO +1 -1
  2. {voly-0.0.166 → voly-0.0.167}/pyproject.toml +2 -2
  3. {voly-0.0.166 → voly-0.0.167}/src/voly/client.py +6 -11
  4. {voly-0.0.166 → voly-0.0.167}/src/voly/core/charts.py +1 -1
  5. {voly-0.0.166 → voly-0.0.167}/src/voly/core/data.py +1 -1
  6. voly-0.0.167/src/voly/core/fit.py +427 -0
  7. voly-0.0.167/src/voly/models.py +217 -0
  8. {voly-0.0.166 → voly-0.0.167/src/voly.egg-info}/PKG-INFO +1 -1
  9. voly-0.0.166/src/voly/core/fit.py +0 -241
  10. voly-0.0.166/src/voly/models.py +0 -81
  11. {voly-0.0.166 → voly-0.0.167}/LICENSE +0 -0
  12. {voly-0.0.166 → voly-0.0.167}/README.md +0 -0
  13. {voly-0.0.166 → voly-0.0.167}/setup.cfg +0 -0
  14. {voly-0.0.166 → voly-0.0.167}/setup.py +0 -0
  15. {voly-0.0.166 → voly-0.0.167}/src/voly/__init__.py +0 -0
  16. {voly-0.0.166 → voly-0.0.167}/src/voly/core/__init__.py +0 -0
  17. {voly-0.0.166 → voly-0.0.167}/src/voly/core/hd.py +0 -0
  18. {voly-0.0.166 → voly-0.0.167}/src/voly/core/interpolate.py +0 -0
  19. {voly-0.0.166 → voly-0.0.167}/src/voly/core/rnd.py +0 -0
  20. {voly-0.0.166 → voly-0.0.167}/src/voly/exceptions.py +0 -0
  21. {voly-0.0.166 → voly-0.0.167}/src/voly/formulas.py +0 -0
  22. {voly-0.0.166 → voly-0.0.167}/src/voly/utils/__init__.py +0 -0
  23. {voly-0.0.166 → voly-0.0.167}/src/voly/utils/density.py +0 -0
  24. {voly-0.0.166 → voly-0.0.167}/src/voly/utils/logger.py +0 -0
  25. {voly-0.0.166 → voly-0.0.167}/src/voly.egg-info/SOURCES.txt +0 -0
  26. {voly-0.0.166 → voly-0.0.167}/src/voly.egg-info/dependency_links.txt +0 -0
  27. {voly-0.0.166 → voly-0.0.167}/src/voly.egg-info/requires.txt +0 -0
  28. {voly-0.0.166 → voly-0.0.167}/src/voly.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.166
3
+ Version: 0.0.167
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voly"
7
- version = "0.0.166"
7
+ version = "0.0.167"
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.166"
63
+ python_version = "0.0.167"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -165,29 +165,24 @@ class VolyClient:
165
165
 
166
166
  @staticmethod
167
167
  def fit_model(option_chain: pd.DataFrame,
168
- model_name: str = 'svi',
169
- initial_params: Optional[List[float]] = None,
170
- param_bounds: Optional[Tuple] = None) -> Dict[str, Any]:
168
+ domain_params: Tuple[float, float, int] = (-2, 2, 500)
169
+ ) -> Dict[str, Any]:
171
170
  """
172
171
  Fit a volatility model to market data.
173
172
 
174
173
  Parameters:
175
174
  - option_chain: DataFrame with option market data
176
- - model_name: Name of model to fit (default: 'svi')
177
- - initial_params: Optional initial parameters for optimization
178
- - param_bounds: Optional parameter bounds for optimization
175
+ - domain_params: Tuple of (min, max, num_points) for the moneyness grid
179
176
 
180
177
  Returns:
181
- - Tuple of (fit_results, fit_performance)
178
+ - Dataframe with fit_results
182
179
  """
183
- logger.info(f"Fitting {model_name.upper()} model to market data")
180
+ logger.info(f"Fitting model to market data.")
184
181
 
185
182
  # Fit the model
186
183
  fit_results = fit_model(
187
184
  option_chain=option_chain,
188
- model_name=model_name,
189
- initial_params=initial_params,
190
- param_bounds=param_bounds
185
+ domain_params=domain_params
191
186
  )
192
187
 
193
188
  return fit_results
@@ -139,7 +139,7 @@ def plot_raw_parameters(fit_results: pd.DataFrame) -> go.Figure:
139
139
  - Plotly figure
140
140
  """
141
141
  # Select parameters to plot
142
- param_names = ['a', 'b', 'sigma', 'rho', 'm']
142
+ param_names = ['a', 'b', 'm', 'rho', 'sigma']
143
143
 
144
144
  # Create subplots
145
145
  fig = make_subplots(
@@ -259,7 +259,7 @@ def process_order_book_depth(option_chain, max_depth=5):
259
259
  s = row['underlying_price']
260
260
  k = row['strikes']
261
261
  t = row['t']
262
- r = row['interest_rate'] if 'interest_rate' in row else 0.0
262
+ r = row['r'] if 'r' in row else 0.0
263
263
  option_type = 'C' if row['option_type'] == 'call' else 'P'
264
264
 
265
265
  # Process bid side
@@ -0,0 +1,427 @@
1
+ """
2
+ Model fitting and calibration module for the Voly package.
3
+
4
+ This module handles fitting volatility models to market data and
5
+ calculating fitting statistics.
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.optimize import least_squares
12
+ from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
13
+ from voly.utils.logger import logger, catch_exception
14
+ from voly.formulas import get_domain
15
+ from voly.exceptions import VolyError
16
+ from voly.models import SVIModel
17
+ import warnings
18
+
19
+ warnings.filterwarnings("ignore")
20
+
21
+
22
+ @catch_exception
23
+ def fit_model(option_chain: pd.DataFrame,
24
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000)) -> pd.DataFrame:
25
+ """
26
+ Fit Voly's volatility model to market data.
27
+
28
+ Parameters:
29
+ - option_chain: DataFrame with market data
30
+ - domain_params : Tuple[float, float, int]
31
+ (min_log_moneyness, max_log_moneyness, num_points)
32
+
33
+ Returns:
34
+ - DataFrame with all fit results and performance metrics as columns, maturity_names as index
35
+ """
36
+
37
+ # Define column names and their data types
38
+ column_dtypes = {
39
+ 's': float,
40
+ 't': float,
41
+ 'r': float,
42
+ 'maturity_date': 'datetime64[ns]',
43
+ 'a': float,
44
+ 'b': float,
45
+ 'm': float,
46
+ 'rho': float,
47
+ 'sigma': float,
48
+ 'nu': float,
49
+ 'psi': float,
50
+ 'p': float,
51
+ 'c': float,
52
+ 'nu_tilde': float,
53
+ 'log_min_strike': float,
54
+ 'usd_min_strike': float,
55
+ 'fit_success': bool,
56
+ 'butterfly_arbitrage_free': bool,
57
+ 'calendar_arbitrage_free': bool,
58
+ 'loss': float,
59
+ 'rmse': float,
60
+ 'mae': float,
61
+ 'r2': float,
62
+ 'max_error': float,
63
+ 'n_points': int
64
+ }
65
+
66
+ # Get unique maturities and sort them
67
+ unique_ts = sorted(option_chain['t'].unique())
68
+ maturity_names = [option_chain[option_chain['t'] == t]['maturity_name'].iloc[0] for t in unique_ts]
69
+
70
+ # Store results in a dictionary first
71
+ results_data = {col: [] for col in column_dtypes.keys()}
72
+
73
+ # ANSI color codes for terminal output
74
+ GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
75
+
76
+ s = option_chain['index_price'].iloc[-1]
77
+
78
+ # Dictionary to track fit results by maturity for arbitrage checks
79
+ fit_params_dict = {}
80
+
81
+ # First pass: Fit each maturity
82
+ for t in unique_ts:
83
+ # Get data for this maturity
84
+ maturity_data = option_chain[option_chain['t'] == t]
85
+ maturity_name = maturity_data['maturity_name'].iloc[0]
86
+ maturity_date = maturity_data['maturity_date'].iloc[0]
87
+
88
+ logger.info(f"Optimizing for {maturity_name}...")
89
+
90
+ # Extract data for fitting
91
+ k = maturity_data['log_moneyness'].values
92
+ iv = maturity_data['mark_iv'].values
93
+ vega = maturity_data['vega'].values if 'vega' in maturity_data else np.ones_like(iv)
94
+
95
+ # Apply mask to filter out invalid data
96
+ mask = ~np.isnan(iv) & ~np.isnan(k) & (iv > 0)
97
+ k_masked, iv_masked, vega_masked = k[mask], iv[mask], vega[mask]
98
+
99
+ # Check if we have enough valid points
100
+ if len(k_masked) <= 5:
101
+ logger.warning(f"Not enough valid data points for {maturity_name}, skipping.")
102
+ params = [np.nan] * 5
103
+ loss = np.inf
104
+ else:
105
+ # Calculate total implied variance (w = iv² * t)
106
+ w = (iv_masked ** 2) * t
107
+
108
+ # Fit using the improved SVI method
109
+ params, loss = SVIModel.fit(tiv=w, vega=vega_masked, k=k_masked, tau=t)
110
+
111
+ # Store the parameters for this maturity
112
+ fit_params_dict[maturity_date] = (t, params)
113
+
114
+ # Extract parameters (will be nan if fit failed)
115
+ a, b, m, rho, sigma = params
116
+
117
+ # Calculate statistics
118
+ fit_success = not np.isnan(a)
119
+ butterfly_arbitrage_free = True
120
+ calendar_arbitrage_free = True # Will check later
121
+
122
+ # Initialize default metrics
123
+ rmse = mae = r2 = max_error = np.nan
124
+ nu = psi = p = c = nu_tilde = np.nan
125
+ log_min_strike = usd_min_strike = np.nan
126
+ a_scaled = b_scaled = np.nan
127
+
128
+ if fit_success:
129
+ # Scale a and b by t
130
+ a_scaled, b_scaled = a * t, b * t
131
+
132
+ # Calculate Jump-Wing parameters
133
+ nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t)
134
+
135
+ # Calculate model predictions for statistics
136
+ w_model = np.array([SVIModel.svi(x, a_scaled, b_scaled, m, rho, sigma) for x in k_masked])
137
+ iv_model = np.sqrt(w_model / t)
138
+ iv_market = iv_masked
139
+
140
+ # Calculate statistics
141
+ rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
142
+ mae = mean_absolute_error(iv_market, iv_model)
143
+ r2 = r2_score(iv_market, iv_model)
144
+ max_error = np.max(np.abs(iv_market - iv_model))
145
+
146
+ # Calculate minimum strike
147
+ log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
148
+ usd_min_strike = s * np.exp(-log_min_strike) # Convert from log_moneyness to strike
149
+
150
+ # Check butterfly arbitrage
151
+ k_range = np.linspace(min(k_masked), max(k_masked), domain_params[2])
152
+ for k_val in k_range:
153
+ wk = SVIModel.svi(k_val, a_scaled, b_scaled, m, rho, sigma)
154
+ wp = SVIModel.svi_d(k_val, a_scaled, b_scaled, m, rho, sigma)
155
+ wpp = SVIModel.svi_dd(k_val, a_scaled, b_scaled, m, rho, sigma)
156
+ g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
157
+ if g < 0:
158
+ butterfly_arbitrage_free = False
159
+ break
160
+
161
+ r = maturity_data['r'].iloc[0] if 'r' in maturity_data.columns else 0
162
+
163
+ # Store values in the results dictionary with proper types
164
+ results_data['s'].append(float(s))
165
+ results_data['t'].append(float(t))
166
+ results_data['r'].append(float(r))
167
+ results_data['maturity_date'].append(maturity_date)
168
+ results_data['a'].append(float(a_scaled) if fit_success else np.nan)
169
+ results_data['b'].append(float(b_scaled) if fit_success else np.nan)
170
+ results_data['sigma'].append(float(sigma) if fit_success else np.nan)
171
+ results_data['m'].append(float(m) if fit_success else np.nan)
172
+ results_data['rho'].append(float(rho) if fit_success else np.nan)
173
+ results_data['nu'].append(float(nu))
174
+ results_data['psi'].append(float(psi))
175
+ results_data['p'].append(float(p))
176
+ results_data['c'].append(float(c))
177
+ results_data['nu_tilde'].append(float(nu_tilde))
178
+ results_data['log_min_strike'].append(float(log_min_strike))
179
+ results_data['usd_min_strike'].append(float(usd_min_strike))
180
+ results_data['fit_success'].append(bool(fit_success))
181
+ results_data['butterfly_arbitrage_free'].append(bool(butterfly_arbitrage_free))
182
+ results_data['calendar_arbitrage_free'].append(bool(True)) # Will update in second pass
183
+ results_data['loss'].append(float(loss))
184
+ results_data['rmse'].append(float(rmse))
185
+ results_data['mae'].append(float(mae))
186
+ results_data['r2'].append(float(r2))
187
+ results_data['max_error'].append(float(max_error))
188
+ results_data['n_points'].append(int(len(k_masked)))
189
+
190
+ # Log result
191
+ status = f'{GREEN}SUCCESS{RESET}' if fit_success else f'{RED}FAILED{RESET}'
192
+ logger.info(f'Optimization for {maturity_name}: {status}')
193
+ logger.info('-------------------------------------')
194
+
195
+ # Create DataFrame with proper types
196
+ results_df = pd.DataFrame(results_data, index=maturity_names)
197
+
198
+ # Convert columns to appropriate types
199
+ for col, dtype in column_dtypes.items():
200
+ if col in results_df.columns:
201
+ try:
202
+ results_df[col] = results_df[col].astype(dtype)
203
+ except (ValueError, TypeError) as e:
204
+ logger.warning(f"Could not convert column {col} to {dtype}: {e}")
205
+
206
+ # Second pass: Check and correct for calendar arbitrage
207
+ logger.info("Checking for calendar arbitrage...")
208
+ sorted_maturities = sorted(fit_params_dict.keys())
209
+ k_grid = np.linspace(domain_params[0], domain_params[1], domain_params[2]) # Grid for arbitrage checking
210
+
211
+ # Check calendar arbitrage before correction
212
+ calendar_arbitrage_free = True
213
+ for i in range(len(sorted_maturities) - 1):
214
+ mat1, mat2 = sorted_maturities[i], sorted_maturities[i + 1]
215
+ t1, params1 = fit_params_dict[mat1]
216
+ t2, params2 = fit_params_dict[mat2]
217
+ a1, b1, sigma1, rho1, m1 = params1
218
+ a2, b2, sigma2, rho2, m2 = params2
219
+
220
+ if np.isnan(a1) or np.isnan(a2):
221
+ continue
222
+
223
+ # Check arbitrage on a grid of points
224
+ for k_val in k_grid:
225
+ w1 = SVIModel.svi(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
226
+ w2 = SVIModel.svi(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
227
+ if w2 < w1 - 1e-6:
228
+ calendar_arbitrage_free = False
229
+ logger.warning(
230
+ f"Calendar arbitrage detected between {sorted_maturities[i]} and {sorted_maturities[i + 1]}")
231
+ break
232
+
233
+ # Update results with calendar arbitrage status
234
+ for i, maturity_name in enumerate(maturity_names):
235
+ idx = results_df.index[i]
236
+ results_df.at[idx, 'calendar_arbitrage_free'] = calendar_arbitrage_free
237
+
238
+ # Correct calendar arbitrage if needed
239
+ if not calendar_arbitrage_free:
240
+ logger.info("Correcting calendar arbitrage...")
241
+ for i in range(1, len(sorted_maturities)):
242
+ mat1 = sorted_maturities[i - 1]
243
+ mat2 = sorted_maturities[i]
244
+ t1, params1 = fit_params_dict[mat1]
245
+ t2, params2 = fit_params_dict[mat2]
246
+
247
+ if np.isnan(params1[0]) or np.isnan(params2[0]):
248
+ continue
249
+
250
+ # Find the index in maturity_names that corresponds to mat2
251
+ maturity_idx = None
252
+ for j, maturity_name in enumerate(maturity_names):
253
+ if results_df.iloc[j]['maturity_date'] == mat2:
254
+ maturity_idx = j
255
+ break
256
+
257
+ if maturity_idx is None:
258
+ continue
259
+
260
+ # Get data for correction
261
+ idx = results_df.index[maturity_idx]
262
+ maturity_data = option_chain[option_chain['maturity_name'] == idx]
263
+ k = maturity_data['log_moneyness'].values
264
+ iv = maturity_data['mark_iv'].values
265
+ vega = maturity_data['vega'].values if 'vega' in maturity_data else np.ones_like(iv)
266
+
267
+ # Apply mask to filter out invalid data
268
+ mask = ~np.isnan(iv) & ~np.isnan(k) & (iv > 0)
269
+ k_masked, iv_masked, vega_masked = k[mask], iv[mask], vega[mask]
270
+
271
+ if len(k_masked) <= 5:
272
+ continue
273
+
274
+ # Calculate total implied variance
275
+ w = (iv_masked ** 2) * t2
276
+
277
+ # Apply calendar arbitrage correction
278
+ new_params = SVIModel.correct_calendar_arbitrage(
279
+ params=params2, t=t2, tiv=w, vega=vega_masked, k=k_masked,
280
+ prev_params=params1, prev_t=t1, k_grid=k_grid
281
+ )
282
+
283
+ # Update the parameters dictionary
284
+ fit_params_dict[mat2] = (t2, new_params)
285
+
286
+ # Extract corrected parameters
287
+ a, b, m, rho, sigma = new_params
288
+
289
+ # Calculate scaled parameters and Jump-Wing parameters
290
+ a_scaled, b_scaled = a * t2, b * t2
291
+ nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t2)
292
+
293
+ # Calculate model predictions for statistics
294
+ w_model = np.array([SVIModel.svi(x, a_scaled, b_scaled, m, rho, sigma) for x in k_masked])
295
+ iv_model = np.sqrt(w_model / t2)
296
+ iv_market = iv_masked
297
+
298
+ # Calculate statistics
299
+ rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
300
+ mae = mean_absolute_error(iv_market, iv_model)
301
+ r2 = r2_score(iv_market, iv_model)
302
+ max_error = np.max(np.abs(iv_market - iv_model))
303
+
304
+ # Calculate minimum strike
305
+ log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
306
+ usd_min_strike = s * np.exp(-log_min_strike)
307
+
308
+ # Check butterfly arbitrage
309
+ butterfly_arbitrage_free = True
310
+ k_range = np.linspace(min(k_masked), max(k_masked), domain_params[2])
311
+ for k_val in k_range:
312
+ wk = SVIModel.svi(k_val, a_scaled, b_scaled, m, rho, sigma)
313
+ wp = SVIModel.svi_d(k_val, a_scaled, b_scaled, m, rho, sigma)
314
+ wpp = SVIModel.svi_dd(k_val, a_scaled, b_scaled, m, rho, sigma)
315
+ g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
316
+ if g < 0:
317
+ butterfly_arbitrage_free = False
318
+ break
319
+
320
+ # Update results in the DataFrame
321
+ results_df.at[idx, 'a'] = float(a_scaled)
322
+ results_df.at[idx, 'b'] = float(b_scaled)
323
+ results_df.at[idx, 'm'] = float(m)
324
+ results_df.at[idx, 'rho'] = float(rho)
325
+ results_df.at[idx, 'sigma'] = float(sigma)
326
+ results_df.at[idx, 'nu'] = float(nu)
327
+ results_df.at[idx, 'psi'] = float(psi)
328
+ results_df.at[idx, 'p'] = float(p)
329
+ results_df.at[idx, 'c'] = float(c)
330
+ results_df.at[idx, 'nu_tilde'] = float(nu_tilde)
331
+ results_df.at[idx, 'log_min_strike'] = float(log_min_strike)
332
+ results_df.at[idx, 'usd_min_strike'] = float(usd_min_strike)
333
+ results_df.at[idx, 'butterfly_arbitrage_free'] = bool(butterfly_arbitrage_free)
334
+ results_df.at[idx, 'rmse'] = float(rmse)
335
+ results_df.at[idx, 'mae'] = float(mae)
336
+ results_df.at[idx, 'r2'] = float(r2)
337
+ results_df.at[idx, 'max_error'] = float(max_error)
338
+
339
+ logger.info(
340
+ f"SVI parameters for {idx}: a={a_scaled:.4f}, b={b_scaled:.4f}, sigma={sigma:.4f}, rho={rho:.4f}, m={m:.4f}")
341
+
342
+ # Check calendar arbitrage after correction
343
+ calendar_arbitrage_free = True
344
+ for i in range(len(sorted_maturities) - 1):
345
+ mat1, mat2 = sorted_maturities[i], sorted_maturities[i + 1]
346
+ t1, params1 = fit_params_dict[mat1]
347
+ t2, params2 = fit_params_dict[mat2]
348
+ a1, b1, sigma1, rho1, m1 = params1
349
+ a2, b2, sigma2, rho2, m2 = params2
350
+
351
+ if np.isnan(a1) or np.isnan(a2):
352
+ continue
353
+
354
+ # Check arbitrage on a grid of points
355
+ for k_val in k_grid:
356
+ w1 = SVIModel.svi(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
357
+ w2 = SVIModel.svi(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
358
+ if w2 < w1 - 1e-6:
359
+ calendar_arbitrage_free = False
360
+ logger.warning(f"Calendar arbitrage still detected between {mat1} and {mat2} after correction")
361
+ break
362
+
363
+ # Update results with final calendar arbitrage status
364
+ for i, maturity_name in enumerate(maturity_names):
365
+ maturity_date = results_df.iloc[i]['maturity_date']
366
+ idx = results_df.index[i]
367
+ results_df.at[idx, 'calendar_arbitrage_free'] = calendar_arbitrage_free
368
+
369
+ logger.info("Model fitting complete.")
370
+ return results_df
371
+
372
+
373
+ @catch_exception
374
+ def get_iv_surface(model_results: pd.DataFrame,
375
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
376
+ return_domain: str = 'log_moneyness') -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]:
377
+ """
378
+ Generate implied volatility surface using optimized SVI parameters.
379
+
380
+ Works with both regular fit_results and interpolated_results dataframes.
381
+
382
+ Parameters:
383
+ - model_results: DataFrame from fit_model() or interpolate_model(). Maturity names or DTM as Index
384
+ - domain_params: Tuple of (min, max, num_points) for the log-moneyness array
385
+ - return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
386
+
387
+ Returns:
388
+ - Tuple of (iv_surface, x_surface)
389
+ iv_surface: Dictionary mapping maturity/dtm names to IV arrays
390
+ x_surface: Dictionary mapping maturity/dtm names to requested x domain arrays
391
+ """
392
+ # Check if required columns are present
393
+ required_columns = ['a', 'b', 'm', 'rho', 'sigma', 't']
394
+ missing_columns = [col for col in required_columns if col not in model_results.columns]
395
+ if missing_columns:
396
+ raise VolyError(f"Required columns missing in model_results: {missing_columns}")
397
+
398
+ # Generate implied volatility surface in log-moneyness domain
399
+ LM = np.linspace(domain_params[0], domain_params[1], domain_params[2])
400
+
401
+ iv_surface = {}
402
+ x_surface = {}
403
+
404
+ # Process each maturity/dtm
405
+ for i in model_results.index:
406
+ # Calculate SVI total implied variance and convert to IV
407
+ params = [
408
+ model_results.loc[i, 'a'],
409
+ model_results.loc[i, 'b'],
410
+ model_results.loc[i, 'm'],
411
+ model_results.loc[i, 'rho'],
412
+ model_results.loc[i, 'sigma']
413
+ ]
414
+ s = model_results.loc[i, 's']
415
+ r = model_results.loc[i, 'r']
416
+ t = model_results.loc[i, 't']
417
+
418
+ # Calculate implied volatility
419
+ w = np.array([SVIModel.svi(x, *params) for x in LM])
420
+ o = np.sqrt(w / t)
421
+ iv_surface[i] = o
422
+
423
+ # Calculate x domain for this maturity/dtm
424
+ x = get_domain(domain_params, s, r, o, t, return_domain)
425
+ x_surface[i] = x
426
+
427
+ return iv_surface, x_surface
@@ -0,0 +1,217 @@
1
+ """
2
+ Volatility models for the Voly package.
3
+ """
4
+
5
+ import numpy as np
6
+ from numpy.linalg import solve
7
+ from scipy.optimize import minimize
8
+ from typing import Tuple, Dict, List, Optional, Union
9
+
10
+
11
+ class SVIModel:
12
+ """
13
+ Stochastic Volatility Inspired (SVI) model.
14
+
15
+ This class provides methods for calculating implied volatility using the
16
+ SVI parameterization, as well as its derivatives and related functions.
17
+ """
18
+
19
+ # Parameter names for reference
20
+ PARAM_NAMES = ['a', 'b', 'm', 'rho', 'sigma']
21
+ JW_PARAM_NAMES = ['nu', 'psi', 'p', 'c', 'nu_tilde']
22
+
23
+ # Parameter descriptions for documentation
24
+ PARAM_DESCRIPTIONS = {
25
+ 'a': 'Base level of total implied variance',
26
+ 'b': 'Volatility skewness/smile modulation (controls wing slopes)',
27
+ 'sigma': 'Convexity control of the volatility smile (reduces ATM curvature)',
28
+ 'rho': 'Skewness/slope of the volatility smile (-1 to 1, rotates smile)',
29
+ 'm': 'Horizontal shift of the smile peak',
30
+ 'nu': 'ATM variance (level of ATM volatility)',
31
+ 'psi': 'ATM volatility skew (affects the gradient of the curve at ATM point)',
32
+ 'p': 'Slope of put wing (left side of curve)',
33
+ 'c': 'Slope of call wing (right side of curve)',
34
+ 'nu_tilde': 'Minimum implied total variance',
35
+ }
36
+
37
+ @staticmethod
38
+ def svi_raw(k, a, b, m, rho, sigma):
39
+ assert b >= 0, 'b must be non-negative'
40
+ assert abs(rho) <= 1, '|rho| must be <= 1'
41
+ assert sigma >= 0, 'sigma must be non-negative'
42
+ assert a + b * sigma * sqrt(1 - rho ** 2) >= 0, 'a + b*sigma*sqrt(1-rho^2) must be non-negative'
43
+ return a + b * (rho * (k - m) + sqrt((k - m) ** 2 + sigma ** 2))
44
+
45
+ @staticmethod
46
+ def svi(LM: float, a: float, b: float, sigma: float, rho: float, m: float) -> float:
47
+ return a + b * (rho * (LM - m) + np.sqrt((LM - m) ** 2 + sigma ** 2))
48
+
49
+ @staticmethod
50
+ def raw_to_jw_params(a: float, b: float, m: float, rho: float, sigma: float, t: float) -> Tuple[
51
+ float, float, float, float, float]:
52
+ nu = (a + b * ((-rho) * m + np.sqrt(m ** 2 + sigma ** 2))) / t
53
+ psi = (1 / np.sqrt(nu * t)) * (b / 2) * (rho - (m / np.sqrt(m ** 2 + sigma ** 2)))
54
+ p = (1 / np.sqrt(nu * t)) * b * (1 - rho)
55
+ c = (1 / np.sqrt(nu * t)) * b * (1 + rho)
56
+ nu_tilde = (1 / t) * (a + b * sigma * np.sqrt(1 - rho ** 2))
57
+ return nu, psi, p, c, nu_tilde
58
+
59
+ @classmethod
60
+ def calibration(cls, tiv, vega, k, m, sigma):
61
+ """
62
+ Calibrate SVI parameters using a more stable approach.
63
+
64
+ Parameters:
65
+ - tiv: Total implied variance values
66
+ - vega: Option vega values (for weighting)
67
+ - k: Log-moneyness values
68
+ - m: Horizontal shift parameter
69
+ - sigma: Convexity parameter
70
+
71
+ Returns:
72
+ - c, d, a: Calibrated parameters
73
+ - loss: Calibration loss value
74
+ """
75
+ sigma = max(sigma, 0.001)
76
+ vega = vega / vega.max() if vega.max() > 0 else np.ones_like(vega)
77
+ y = (k - m) / sigma
78
+ w = vega.mean()
79
+ y1 = (vega * y).mean()
80
+ y2 = (vega * y * y).mean()
81
+ y3 = (vega * np.sqrt(y * y + 1)).mean()
82
+ y4 = (vega * y * np.sqrt(y * y + 1)).mean()
83
+ y5 = (vega * (y * y + 1)).mean()
84
+ vy2 = (vega * tiv * np.sqrt(y * y + 1)).mean()
85
+ vy = (vega * tiv * y).mean()
86
+ v = (vega * tiv).mean()
87
+
88
+ matrix = [[y5, y4, y3], [y4, y2, y1], [y3, y1, w]]
89
+ vector = [vy2, vy, v]
90
+ c, d, a = solve(np.array(matrix), np.array(vector))
91
+
92
+ c = np.clip(c, 0, 4 * sigma)
93
+ a = max(a, 1e-6)
94
+ d = np.clip(d, -min(c, 4 * sigma - c), min(c, 4 * sigma - c))
95
+
96
+ loss = cls.loss(tiv, vega, y, c, d, a)
97
+ return c, d, a, loss
98
+
99
+ @staticmethod
100
+ def loss(tiv, vega, y, c, d, a):
101
+ """Calculate weighted loss for SVI calibration."""
102
+ diff = tiv - (a + d * y + c * np.sqrt(y * y + 1))
103
+ return (vega * diff * diff).mean()
104
+
105
+ @classmethod
106
+ def fit(cls, tiv, vega, k, tau=1.0):
107
+ """
108
+ Fit SVI model to market data using a more stable two-step approach.
109
+
110
+ Parameters:
111
+ - tiv: Total implied variance values
112
+ - vega: Option vega values (for weighting)
113
+ - k: Log-moneyness values
114
+ - tau: Time to expiry in years
115
+
116
+ Returns:
117
+ - params: [a, b, m, rho, sigma] parameters
118
+ - loss: Fitting loss value
119
+ """
120
+ if len(k) <= 5:
121
+ return [np.nan] * 5, np.inf
122
+
123
+ vega = vega / vega.max() if vega.max() > 0 else np.ones_like(vega)
124
+ m_init = np.mean(k)
125
+ sigma_init = max(0.1, np.std(k) * 0.1)
126
+
127
+ def score(params):
128
+ sigma, m = params
129
+ c, d, a_calib, loss = cls.calibration(tiv, vega, k, m, sigma)
130
+ return loss
131
+
132
+ result = minimize(score, [sigma_init, m_init], bounds=[(0.001, None), (None, None)],
133
+ tol=1e-16, method="Nelder-Mead", options={'maxfun': 5000})
134
+
135
+ sigma, m = result.x
136
+ c, d, a_calib, loss = cls.calibration(tiv, vega, k, m, sigma)
137
+ a_calib = max(a_calib, 1e-6)
138
+
139
+ if c != 0:
140
+ a_svi = a_calib / tau
141
+ rho_svi = d / c
142
+ b_svi = c / (sigma * tau)
143
+ else:
144
+ a_svi = a_calib / tau
145
+ rho_svi = b_svi = 0
146
+
147
+ return [a_svi, b_svi, m, rho_svi, sigma], loss
148
+
149
+ @classmethod
150
+ def correct_calendar_arbitrage(cls, params, t, tiv, vega, k, prev_params, prev_t, domain_params):
151
+ """
152
+ Correct calendar arbitrage by ensuring the current SVI surface stays above the previous one.
153
+
154
+ Parameters:
155
+ - params: Current SVI parameters [a, b, sigma, rho, m]
156
+ - t: Current time to expiry
157
+ - tiv: Current total implied variance values
158
+ - vega: Current vega values
159
+ - k: Current log-moneyness values
160
+ - prev_params: Previous SVI parameters
161
+ - prev_t: Previous time to expiry
162
+ - k_grid: Grid of log-moneyness values for arbitrage checking
163
+
164
+ Returns:
165
+ - New arbitrage-free parameters
166
+ """
167
+
168
+ if np.any(np.isnan(params)) or np.any(np.isnan(prev_params)):
169
+ return params
170
+
171
+ a_init, b_init, sigma_init, rho_init, m_init = params
172
+ a_prev, b_prev, sigma_prev, rho_prev, m_prev = prev_params
173
+ k_constraint = np.unique(np.concatenate([k, np.linspace(min(k), max(k), domain_params[2])]))
174
+
175
+ def objective(x):
176
+ a, b, sigma, rho, m = x
177
+ w_model = cls.svi(k, a * t, b * t, sigma, rho, m)
178
+ fit_loss = ((w_model - tiv) ** 2 * vega).mean()
179
+ param_deviation = sum(((x[i] - params[i]) / max(abs(params[i]), 1e-6)) ** 2
180
+ for i in range(len(params)))
181
+ return fit_loss + 0.01 * param_deviation
182
+
183
+ def calendar_constraint(x):
184
+ a, b, sigma, rho, m = x
185
+ w_current = cls.svi(k_constraint, a * t, b * t, sigma, rho, m)
186
+ w_prev = cls.svi(k_constraint, a_prev * prev_t, b_prev * prev_t, sigma_prev, rho_prev, m_prev)
187
+ return w_current - w_prev
188
+
189
+ bounds = [
190
+ (max(a_init * 0.8, 1e-6), a_init * 1.2),
191
+ (max(b_init * 0.8, 0), b_init * 1.2),
192
+ (max(sigma_init * 0.8, 1e-6), sigma_init * 1.2),
193
+ (max(rho_init - 0.05, -1), min(rho_init + 0.05, 1)),
194
+ (m_init - 0.05, m_init + 0.05)
195
+ ]
196
+
197
+ constraints = [
198
+ {'type': 'ineq', 'fun': calendar_constraint},
199
+ {'type': 'ineq', 'fun': lambda x: x[0] + x[1] * x[2] * np.sqrt(1 - x[3] ** 2)}
200
+ ]
201
+
202
+ result = minimize(
203
+ objective, [a_init, b_init, sigma_init, rho_init, m_init],
204
+ bounds=bounds, constraints=constraints, method='SLSQP',
205
+ options={'disp': False, 'maxiter': 1000, 'ftol': 1e-8}
206
+ )
207
+
208
+ if result.success:
209
+ return result.x
210
+
211
+ return params
212
+
213
+
214
+ # Models dictionary for easy access
215
+ MODELS = {
216
+ 'svi': SVIModel,
217
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.166
3
+ Version: 0.0.167
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -1,241 +0,0 @@
1
- """
2
- Model fitting and calibration module for the Voly package.
3
-
4
- This module handles fitting volatility models to market data and
5
- calculating fitting statistics.
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.optimize import least_squares
12
- from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
13
- from voly.utils.logger import logger, catch_exception
14
- from voly.formulas import get_domain
15
- from voly.exceptions import VolyError
16
- from voly.models import SVIModel
17
- import warnings
18
-
19
- warnings.filterwarnings("ignore")
20
-
21
-
22
- @catch_exception
23
- def calculate_residuals(params: List[float], t: float, option_chain: pd.DataFrame,
24
- model: Any = SVIModel) -> np.ndarray:
25
- """Calculate residuals between market and model implied volatilities."""
26
- maturity_data = option_chain[option_chain['t'] == t]
27
- w = np.array([model.svi(x, *params) for x in maturity_data['log_moneyness']])
28
- iv_actual = maturity_data['mark_iv'].values
29
- return iv_actual - np.sqrt(w / t)
30
-
31
-
32
- @catch_exception
33
- def fit_model(option_chain: pd.DataFrame,
34
- model_name: str = 'svi',
35
- initial_params: Optional[List[float]] = None,
36
- param_bounds: Optional[Tuple] = None) -> pd.DataFrame:
37
- """
38
- Fit a volatility model to market data.
39
-
40
- Parameters:
41
- - option_chain: DataFrame with market data
42
- - model_name: Type of model to fit (default: 'svi')
43
- - initial_params: Optional initial parameters for optimization (default: model's defaults)
44
- - param_bounds: Optional parameter bounds for optimization (default: model's defaults)
45
-
46
- Returns:
47
- - DataFrame with all fit results and performance metrics as columns, maturity_names as index
48
- """
49
- if model_name.lower() != 'svi':
50
- raise VolyError(f"Model type '{model_name}' is not supported. Currently only 'svi' is available.")
51
-
52
- # Use defaults if not provided
53
- initial_params = initial_params or SVIModel.DEFAULT_INITIAL_PARAMS
54
- param_bounds = param_bounds or SVIModel.DEFAULT_PARAM_BOUNDS
55
-
56
- # Define column names and their data types
57
- column_dtypes = {
58
- 's': float,
59
- 'u': float,
60
- 't': float,
61
- 'r': float,
62
- 'oi': float,
63
- 'volume': float,
64
- 'maturity_date': 'datetime64[ns]',
65
- 'a': float,
66
- 'b': float,
67
- 'sigma': float,
68
- 'rho': float,
69
- 'm': float,
70
- 'nu': float,
71
- 'psi': float,
72
- 'p': float,
73
- 'c': float,
74
- 'nu_tilde': float,
75
- 'fit_success': bool,
76
- 'cost': float,
77
- 'optimality': float,
78
- 'rmse': float,
79
- 'mae': float,
80
- 'r2': float,
81
- 'max_error': float,
82
- 'n_points': int
83
- }
84
-
85
- # Get unique maturities and sort them
86
- unique_ts = sorted(option_chain['t'].unique())
87
- maturity_names = [option_chain[option_chain['t'] == t]['maturity_name'].iloc[0] for t in unique_ts]
88
-
89
- # Store results in a dictionary first
90
- results_data = {col: [] for col in column_dtypes.keys()}
91
-
92
- # ANSI color codes for terminal output
93
- GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
94
-
95
- s = option_chain['index_price'].iloc[-1]
96
-
97
- for t in unique_ts:
98
- # Get data for this maturity
99
- maturity_data = option_chain[option_chain['t'] == t]
100
- maturity_name = maturity_data['maturity_name'].iloc[0]
101
-
102
- logger.info(f"Optimizing for {maturity_name}...")
103
-
104
- # Optimize SVI parameters
105
- try:
106
- result = least_squares(
107
- calculate_residuals,
108
- initial_params,
109
- args=(t, option_chain, SVIModel),
110
- bounds=param_bounds,
111
- max_nfev=1000
112
- )
113
- except Exception as e:
114
- raise VolyError(f"Optimization failed for {maturity_name}: {str(e)}")
115
-
116
- # Extract raw parameters
117
- a, b, sigma, rho, m = result.x
118
-
119
- # Calculate model predictions for statistics
120
- w = np.array([SVIModel.svi(x, *result.x) for x in maturity_data['log_moneyness']])
121
- iv_model = np.sqrt(w / t)
122
- iv_market = maturity_data['mark_iv'].values
123
-
124
- # Calculate statistics
125
- rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
126
- mae = mean_absolute_error(iv_market, iv_model)
127
- r2 = r2_score(iv_market, iv_model)
128
- max_error = np.max(np.abs(iv_market - iv_model))
129
-
130
- # Get or calculate additional required data
131
- u = maturity_data['underlying_price'].iloc[0]
132
-
133
- # Aggregate open interest and volume
134
- oi = maturity_data['open_interest'].sum() if 'open_interest' in maturity_data.columns else 0
135
- volume = maturity_data['volume'].sum() if 'volume' in maturity_data.columns else 0
136
- r = maturity_data['interest_rate'].iloc[0] if 'interest_rate' in maturity_data.columns else 0
137
-
138
- # Calculate Jump-Wing parameters
139
- nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a, b, sigma, rho, m, t)
140
-
141
- # Store values in the results dictionary with proper types
142
- results_data['s'].append(float(s))
143
- results_data['u'].append(float(u))
144
- results_data['t'].append(float(t))
145
- results_data['r'].append(float(r))
146
- results_data['oi'].append(float(oi))
147
- results_data['volume'].append(float(volume))
148
- results_data['maturity_date'].append(maturity_data['maturity_date'].iloc[0])
149
- results_data['a'].append(float(a))
150
- results_data['b'].append(float(b))
151
- results_data['sigma'].append(float(sigma))
152
- results_data['m'].append(float(m))
153
- results_data['rho'].append(float(rho))
154
- results_data['nu'].append(float(nu))
155
- results_data['psi'].append(float(psi))
156
- results_data['p'].append(float(p))
157
- results_data['c'].append(float(c))
158
- results_data['nu_tilde'].append(float(nu_tilde))
159
- results_data['fit_success'].append(bool(result.success))
160
- results_data['cost'].append(float(result.cost))
161
- results_data['optimality'].append(float(result.optimality))
162
- results_data['rmse'].append(float(rmse))
163
- results_data['mae'].append(float(mae))
164
- results_data['r2'].append(float(r2))
165
- results_data['max_error'].append(float(max_error))
166
- results_data['n_points'].append(int(len(maturity_data)))
167
-
168
- # Log result
169
- status = f'{GREEN}SUCCESS{RESET}' if result.success else f'{RED}FAILED{RESET}'
170
- logger.info(f'Optimization for {maturity_name}: {status}')
171
- logger.info('-------------------------------------')
172
-
173
- # Create DataFrame with proper types
174
- results_df = pd.DataFrame(results_data, index=maturity_names)
175
-
176
- # Convert columns to appropriate types
177
- for col, dtype in column_dtypes.items():
178
- if col in results_df.columns:
179
- try:
180
- results_df[col] = results_df[col].astype(dtype)
181
- except (ValueError, TypeError) as e:
182
- logger.warning(f"Could not convert column {col} to {dtype}: {e}")
183
-
184
- return results_df
185
-
186
-
187
- @catch_exception
188
- def get_iv_surface(model_results: pd.DataFrame,
189
- domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
190
- return_domain: str = 'log_moneyness') -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]:
191
- """
192
- Generate implied volatility surface using optimized SVI parameters.
193
-
194
- Works with both regular fit_results and interpolated_results dataframes.
195
-
196
- Parameters:
197
- - model_results: DataFrame from fit_model() or interpolate_model(). Maturity names or DTM as Index
198
- - domain_params: Tuple of (min, max, num_points) for the log-moneyness array
199
- - return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
200
-
201
- Returns:
202
- - Tuple of (iv_surface, x_surface)
203
- iv_surface: Dictionary mapping maturity/dtm names to IV arrays
204
- x_surface: Dictionary mapping maturity/dtm names to requested x domain arrays
205
- """
206
- # Check if required columns are present
207
- required_columns = ['a', 'b', 'sigma', 'rho', 'm', 't']
208
- missing_columns = [col for col in required_columns if col not in model_results.columns]
209
- if missing_columns:
210
- raise VolyError(f"Required columns missing in model_results: {missing_columns}")
211
-
212
- # Generate implied volatility surface in log-moneyness domain
213
- LM = np.linspace(domain_params[0], domain_params[1], domain_params[2])
214
-
215
- iv_surface = {}
216
- x_surface = {}
217
-
218
- # Process each maturity/dtm
219
- for i in model_results.index:
220
- # Calculate SVI total implied variance and convert to IV
221
- params = [
222
- model_results.loc[i, 'a'],
223
- model_results.loc[i, 'b'],
224
- model_results.loc[i, 'sigma'],
225
- model_results.loc[i, 'rho'],
226
- model_results.loc[i, 'm']
227
- ]
228
- s = model_results.loc[i, 's']
229
- r = model_results.loc[i, 'r']
230
- t = model_results.loc[i, 't']
231
-
232
- # Calculate implied volatility
233
- w = np.array([SVIModel.svi(x, *params) for x in LM])
234
- o = np.sqrt(w / t)
235
- iv_surface[i] = o
236
-
237
- # Calculate x domain for this maturity/dtm
238
- x = get_domain(domain_params, s, r, o, t, return_domain)
239
- x_surface[i] = x
240
-
241
- return iv_surface, x_surface
@@ -1,81 +0,0 @@
1
- """
2
- Volatility models for the Voly package.
3
- """
4
-
5
- import numpy as np
6
- from typing import Tuple, Dict, List, Optional, Union
7
-
8
-
9
- class SVIModel:
10
- """
11
- Stochastic Volatility Inspired (SVI) model.
12
-
13
- This class provides methods for calculating implied volatility using the
14
- SVI parameterization, as well as its derivatives and related functions.
15
- """
16
-
17
- # Default initial parameters and bounds
18
- DEFAULT_INITIAL_PARAMS = [0.04, 0.1, 0.2, -0.5, 0.01]
19
- DEFAULT_PARAM_BOUNDS = ([-np.inf, 0, 0, -1, -np.inf], [np.inf, np.inf, np.inf, 1, np.inf])
20
-
21
- # Parameter names for reference
22
- PARAM_NAMES = ['a', 'b', 'sigma', 'rho', 'm']
23
- JW_PARAM_NAMES = ['nu', 'psi', 'p', 'c', 'nu_tilde']
24
-
25
- # Parameter descriptions for documentation
26
- PARAM_DESCRIPTIONS = {
27
- 'a': 'Base level of total implied variance',
28
- 'b': 'Volatility skewness/smile modulation (controls wing slopes)',
29
- 'sigma': 'Convexity control of the volatility smile (reduces ATM curvature)',
30
- 'rho': 'Skewness/slope of the volatility smile (-1 to 1, rotates smile)',
31
- 'm': 'Horizontal shift of the smile peak',
32
- 'nu': 'ATM variance (level of ATM volatility)',
33
- 'psi': 'ATM volatility skew (affects the gradient of the curve at ATM point)',
34
- 'p': 'Slope of put wing (left side of curve)',
35
- 'c': 'Slope of call wing (right side of curve)',
36
- 'nu_tilde': 'Minimum implied total variance',
37
- }
38
-
39
- @staticmethod
40
- def svi(LM: float, a: float, b: float, sigma: float, rho: float, m: float) -> float:
41
- return a + b * (rho * (LM - m) + np.sqrt((LM - m) ** 2 + sigma ** 2))
42
-
43
- @staticmethod
44
- def svi_d(LM: float, a: float, b: float, sigma: float, rho: float, m: float) -> float:
45
- return b * (rho + ((LM - m) / np.sqrt((LM - m) ** 2 + sigma ** 2)))
46
-
47
- @staticmethod
48
- def svi_dd(LM: float, a: float, b: float, sigma: float, rho: float, m: float) -> float:
49
- return b * LM ** 2 / ((LM - m) ** 2 + sigma ** 2) ** 1.5
50
-
51
- @staticmethod
52
- def svi_min_strike(sigma: float, rho: float, m: float) -> float:
53
- return m - ((sigma * rho) / np.sqrt(1 - rho ** 2))
54
-
55
- @staticmethod
56
- def raw_to_jw_params(a: float, b: float, sigma: float, rho: float, m: float, t: float) -> Tuple[float, float, float, float, float]:
57
- nu = (a + b * ((-rho) * m + np.sqrt(m ** 2 + sigma ** 2))) / t
58
- psi = (1 / np.sqrt(nu * t)) * (b / 2) * (rho - (m / np.sqrt(m ** 2 + sigma ** 2)))
59
- p = (1 / np.sqrt(nu * t)) * b * (1 - rho)
60
- c = (1 / np.sqrt(nu * t)) * b * (1 + rho)
61
- nu_tilde = (1 / t) * (a + b * sigma * np.sqrt(1 - rho ** 2))
62
- return nu, psi, p, c, nu_tilde
63
-
64
- @staticmethod
65
- def jw_to_raw_params(nu: float, psi: float, p: float, c: float, nu_tilde: float, t: float) -> Tuple[float, float, float, float, float]:
66
- w = nu * t
67
- b = (c + p) / 2
68
- rho = (c - p) / (c + p)
69
- beta = rho - ((2 * w * psi) / b)
70
- alpha = np.sign(beta) * (np.sqrt((1 / (beta ** 2)) - 1))
71
- m = (((nu ** 2) - (nu_tilde ** 2)) * t) / (
72
- b * ((-rho) + (np.sign(alpha) * np.sqrt(1 + alpha ** 2)) - (alpha * np.sqrt(1 - rho ** 2))))
73
- sigma = alpha * m
74
- a = ((nu_tilde ** 2) * t) - (b * sigma * np.sqrt(1 - rho ** 2))
75
- return a, b, sigma, rho, m
76
-
77
-
78
- # Models dictionary for easy access
79
- MODELS = {
80
- 'svi': SVIModel,
81
- }
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