voly 0.0.195__tar.gz → 0.0.196__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 (26) hide show
  1. {voly-0.0.195/src/voly.egg-info → voly-0.0.196}/PKG-INFO +1 -1
  2. {voly-0.0.195 → voly-0.0.196}/pyproject.toml +2 -2
  3. {voly-0.0.195 → voly-0.0.196}/src/voly/core/fit.py +107 -100
  4. {voly-0.0.195 → voly-0.0.196}/src/voly/models.py +16 -16
  5. {voly-0.0.195 → voly-0.0.196/src/voly.egg-info}/PKG-INFO +1 -1
  6. {voly-0.0.195 → voly-0.0.196}/LICENSE +0 -0
  7. {voly-0.0.195 → voly-0.0.196}/README.md +0 -0
  8. {voly-0.0.195 → voly-0.0.196}/setup.cfg +0 -0
  9. {voly-0.0.195 → voly-0.0.196}/setup.py +0 -0
  10. {voly-0.0.195 → voly-0.0.196}/src/voly/__init__.py +0 -0
  11. {voly-0.0.195 → voly-0.0.196}/src/voly/client.py +0 -0
  12. {voly-0.0.195 → voly-0.0.196}/src/voly/core/__init__.py +0 -0
  13. {voly-0.0.195 → voly-0.0.196}/src/voly/core/charts.py +0 -0
  14. {voly-0.0.195 → voly-0.0.196}/src/voly/core/data.py +0 -0
  15. {voly-0.0.195 → voly-0.0.196}/src/voly/core/hd.py +0 -0
  16. {voly-0.0.195 → voly-0.0.196}/src/voly/core/interpolate.py +0 -0
  17. {voly-0.0.195 → voly-0.0.196}/src/voly/core/rnd.py +0 -0
  18. {voly-0.0.195 → voly-0.0.196}/src/voly/exceptions.py +0 -0
  19. {voly-0.0.195 → voly-0.0.196}/src/voly/formulas.py +0 -0
  20. {voly-0.0.195 → voly-0.0.196}/src/voly/utils/__init__.py +0 -0
  21. {voly-0.0.195 → voly-0.0.196}/src/voly/utils/density.py +0 -0
  22. {voly-0.0.195 → voly-0.0.196}/src/voly/utils/logger.py +0 -0
  23. {voly-0.0.195 → voly-0.0.196}/src/voly.egg-info/SOURCES.txt +0 -0
  24. {voly-0.0.195 → voly-0.0.196}/src/voly.egg-info/dependency_links.txt +0 -0
  25. {voly-0.0.195 → voly-0.0.196}/src/voly.egg-info/requires.txt +0 -0
  26. {voly-0.0.195 → voly-0.0.196}/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.195
3
+ Version: 0.0.196
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.195"
7
+ version = "0.0.196"
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.195"
63
+ python_version = "0.0.196"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -1,8 +1,8 @@
1
1
  """
2
2
  Model fitting and calibration module for the Voly package.
3
3
 
4
- This module handles fitting volatility models to market data and
5
- calculating fitting statistics.
4
+ This module handles fitting volatility models to market data, calculating fitting statistics,
5
+ and generating visualizations.
6
6
  """
7
7
 
8
8
  import numpy as np
@@ -16,20 +16,25 @@ from voly.models import SVIModel
16
16
  from concurrent.futures import ThreadPoolExecutor
17
17
  import warnings
18
18
  import time
19
+ import plotly.graph_objects as go
20
+ from plotly.subplots import make_subplots
19
21
 
20
22
  warnings.filterwarnings("ignore")
21
23
 
22
24
 
23
25
  @catch_exception
24
- def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
26
+ def fit_model(option_chain: pd.DataFrame, num_points: int = 2000) -> Tuple[pd.DataFrame, Dict]:
25
27
  """
26
- Fit a volatility model to market data with parallel processing.
28
+ Fit a volatility model to market data with parallel processing and generate visualizations.
27
29
 
28
30
  Parameters:
29
31
  - option_chain: DataFrame with market data
32
+ - num_points: Number of points for k_grid and plotting
30
33
 
31
34
  Returns:
32
- - DataFrame with all fit results and performance metrics as columns, maturity_names as index
35
+ - Tuple of (results_df, params_dict)
36
+ results_df: DataFrame with all fit results and performance metrics as columns, maturity_dates as index
37
+ params_dict: Dictionary mapping maturity_dates to (t, params)
33
38
  """
34
39
  # Start overall timer
35
40
  start_total = time.time()
@@ -38,7 +43,6 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
38
43
  column_dtypes = {
39
44
  's': float,
40
45
  't': float,
41
- 'r': float,
42
46
  'maturity_date': 'datetime64[ns]',
43
47
  'a': float,
44
48
  'b': float,
@@ -55,22 +59,18 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
55
59
  'fit_success': bool,
56
60
  'butterfly_arbitrage_free': bool,
57
61
  'calendar_arbitrage_free': bool,
58
- 'loss': float,
59
62
  'rmse': float,
60
63
  'mae': float,
61
64
  'r2': float,
62
65
  'max_error': float,
66
+ 'loss': float,
63
67
  'n_points': int
64
68
  }
65
69
 
66
70
  s = option_chain['index_price'].iloc[0]
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
- maturity_dates = [option_chain[option_chain['t'] == t]['maturity_date'].iloc[0] for t in unique_ts]
70
71
  maturity_data_groups = option_chain.groupby('maturity_date')
71
72
  params_dict = {}
72
73
  results_data = {col: [] for col in column_dtypes.keys()}
73
- num_points = 2000 # Number of points for k_grid
74
74
 
75
75
  def process_maturity(maturity, maturity_data):
76
76
  """Process single maturity for SVI calibration."""
@@ -94,7 +94,6 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
94
94
  # Combine cleaned duplicates and unique rows
95
95
  maturity_data = pd.concat([unique_iv, cleaned_duplicated_iv])
96
96
  maturity_date = maturity_data['maturity_date'].iloc[0]
97
- maturity_name = maturity_data['maturity_name'].iloc[0]
98
97
 
99
98
  t = maturity_data['t'].iloc[0]
100
99
  K = maturity_data['strikes'].values
@@ -108,45 +107,53 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
108
107
  params = [np.nan] * 5
109
108
  loss = np.inf
110
109
  nu = psi = p = c = nu_tilde = np.nan
110
+ rmse = mae = r2 = max_error = np.nan
111
111
  butterfly_arbitrage_free = True
112
- r = maturity_data['interest_rate'].iloc[0] if 'interest_rate' in maturity_data.columns else 0
113
112
  log_min_strike = usd_min_strike = np.nan
114
113
 
115
114
  if len(k) > 5:
116
115
  params, loss = SVIModel.fit(tiv=w, vega=vega, k=k, tau=t)
117
- a, b, m, rho, sigma = params
118
- a_scaled, b_scaled = a * t, b * t
119
- params_dict[maturity_date] = (t, params)
120
- nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t)
121
- log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
122
- usd_min_strike = np.exp(log_min_strike) * s
123
-
124
- # Butterfly arbitrage check
125
- k_range = np.linspace(min(k), max(k), num_points)
126
- w_k = lambda k: SVIModel.svi(k, a_scaled, b_scaled, m, rho, sigma)
127
- w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m) ** 2 + sigma ** 2))
128
- w_double_prime = lambda k: b_scaled * sigma ** 2 / ((k - m) ** 2 + sigma ** 2) ** (3 / 2)
129
- for k_val in k_range:
130
- wk = w_k(k_val)
131
- wp = w_prime(k_val)
132
- wpp = w_double_prime(k_val)
133
- g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
134
- if g < 0:
135
- butterfly_arbitrage_free = False
136
- break
137
- else:
138
- params = [np.nan] * 5
139
- loss = np.inf
116
+ if not np.isnan(params[0]):
117
+ params_dict[maturity_date] = (t, params)
118
+ a, b, m, rho, sigma = params
119
+ a_scaled, b_scaled = a * t, b * t
120
+ nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t)
121
+
122
+ # Compute fit statistics
123
+ w_model = np.array([SVIModel.svi_raw(x, a_scaled, b_scaled, m, rho, sigma) for x in k])
124
+ iv_model = np.sqrt(w_model / t)
125
+ iv_market = iv
126
+ rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
127
+ mae = mean_absolute_error(iv_market, iv_model)
128
+ r2 = r2_score(iv_market, iv_model)
129
+ max_error = np.max(np.abs(iv_market - iv_model))
130
+
131
+ # Compute min strike
132
+ log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
133
+ usd_min_strike = np.exp(log_min_strike) * s
134
+
135
+ # Butterfly arbitrage check
136
+ k_range = np.linspace(min(k), max(k), num_points)
137
+ w_k = lambda k: SVIModel.svi_raw(k, a_scaled, b_scaled, m, rho, sigma)
138
+ w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m)**2 + sigma**2))
139
+ w_double_prime = lambda k: b_scaled * sigma**2 / ((k - m)**2 + sigma**2)**(3/2)
140
+ for k_val in k_range:
141
+ wk = w_k(k_val)
142
+ wp = w_prime(k_val)
143
+ wpp = w_double_prime(k_val)
144
+ g = (1 - (k_val * wp) / (2 * wk))**2 - (wp**2) / 4 * (1 / wk + 1/4) + wpp / 2
145
+ if g < 0:
146
+ butterfly_arbitrage_free = False
147
+ break
140
148
 
141
149
  # Log result
142
150
  GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
143
151
  status = f'{GREEN}SUCCESS{RESET}' if not np.isnan(params[0]) else f'{RED}FAILED{RESET}'
144
- logger.info(f'Optimization for {maturity}: {status}')
152
+ logger.info(f'Optimization for {maturity_date}: {status}')
145
153
 
146
154
  # Store results
147
155
  results_data['s'].append(float(s))
148
156
  results_data['t'].append(float(t))
149
- results_data['r'].append(float(r))
150
157
  results_data['maturity_date'].append(maturity_date)
151
158
  results_data['a'].append(float(a_scaled) if not np.isnan(params[0]) else np.nan)
152
159
  results_data['b'].append(float(b_scaled) if not np.isnan(params[0]) else np.nan)
@@ -163,24 +170,24 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
163
170
  results_data['fit_success'].append(bool(not np.isnan(params[0])))
164
171
  results_data['butterfly_arbitrage_free'].append(butterfly_arbitrage_free)
165
172
  results_data['calendar_arbitrage_free'].append(True) # Updated after check
166
- results_data['rmse'].append(np.nan)
167
- results_data['mae'].append(np.nan)
168
- results_data['r2'].append(np.nan)
169
- results_data['max_error'].append(np.nan)
173
+ results_data['rmse'].append(float(rmse))
174
+ results_data['mae'].append(float(mae))
175
+ results_data['r2'].append(float(r2))
176
+ results_data['max_error'].append(float(max_error))
170
177
  results_data['loss'].append(float(loss))
171
178
  results_data['n_points'].append(int(len(k)))
172
179
 
173
- return maturity_name
180
+ return maturity_date
174
181
 
175
182
  # Parallel processing of maturities
176
- logger.info("\nStarting parallel processing of maturities...")
177
183
  with ThreadPoolExecutor() as executor:
178
184
  futures = [executor.submit(process_maturity, maturity, maturity_data)
179
185
  for maturity, maturity_data in maturity_data_groups]
180
- maturity_names = [future.result() for future in futures]
186
+ for future in futures:
187
+ future.result()
181
188
 
182
189
  # Create results DataFrame
183
- results_df = pd.DataFrame(results_data, index=maturity_names)
190
+ results_df = pd.DataFrame(results_data, index=results_data['maturity_date'])
184
191
 
185
192
  # Convert columns to appropriate types
186
193
  for col, dtype in column_dtypes.items():
@@ -194,7 +201,6 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
194
201
  results_df = results_df.sort_values(by='t')
195
202
 
196
203
  # Calendar arbitrage check (pre-correction)
197
- logger.info("\nChecking calendar arbitrage (pre-correction)...")
198
204
  k_grid = np.linspace(-2, 2, num_points)
199
205
  sorted_maturities = sorted(params_dict.keys(), key=lambda x: params_dict[x][0])
200
206
  calendar_arbitrage_free = True
@@ -215,8 +221,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
215
221
  k_check = np.unique(np.concatenate([k_market[mask], np.linspace(min(k_market[mask]), max(k_market[mask]), num_points)]))
216
222
 
217
223
  for k_val in k_check:
218
- w1 = SVIModel.svi(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
219
- w2 = SVIModel.svi(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
224
+ w1 = SVIModel.svi_raw(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
225
+ w2 = SVIModel.svi_raw(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
220
226
  if w2 < w1 - 1e-6:
221
227
  logger.warning(f"Calendar arbitrage violation at t1={t1:.4f}, t2={t2:.4f}, k={k_val:.4f}: w1={w1:.6f}, w2={w2:.6f}")
222
228
  calendar_arbitrage_free = False
@@ -224,11 +230,11 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
224
230
  if not calendar_arbitrage_free:
225
231
  break
226
232
 
227
- for mat in results_df['maturity_date']:
228
- results_df.loc[results_df['maturity_date'] == mat, 'calendar_arbitrage_free'] = calendar_arbitrage_free
233
+ for mat in sorted_maturities:
234
+ results_df.at[mat, 'calendar_arbitrage_free'] = calendar_arbitrage_free
229
235
 
230
236
  # Calendar arbitrage correction
231
- logger.info("Performing calendar arbitrage correction...")
237
+ start_correction = time.time()
232
238
  for i in range(1, len(sorted_maturities)):
233
239
  mat2 = sorted_maturities[i]
234
240
  mat1 = sorted_maturities[i - 1]
@@ -252,15 +258,13 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
252
258
  prev_params=params1, prev_t=t1, k_grid=k_grid
253
259
  )
254
260
 
261
+ params_dict[mat2] = (t2, new_params)
255
262
  a, b, m, rho, sigma = new_params
256
263
  a_scaled, b_scaled = a * t2, b * t2
257
- params_dict[mat2] = (t2, new_params)
258
- nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t2)
259
- log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
260
- usd_min_strike = np.exp(log_min_strike) * s
264
+ nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, sigma, rho, m, t2)
261
265
 
262
- # Compute fit statistics
263
- w_model = np.array([SVIModel.svi(x, a_scaled, b_scaled, m, rho, sigma) for x in k])
266
+ # Recompute fit statistics
267
+ w_model = np.array([SVIModel.svi_raw(x, a_scaled, b_scaled, m, rho, sigma) for x in k])
264
268
  iv_model = np.sqrt(w_model / t2)
265
269
  iv_market = iv
266
270
  rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
@@ -268,47 +272,49 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
268
272
  r2 = r2_score(iv_market, iv_model)
269
273
  max_error = np.max(np.abs(iv_market - iv_model))
270
274
 
275
+ # Recompute min strike
276
+ log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
277
+ usd_min_strike = np.exp(log_min_strike) * s
278
+
271
279
  # Update butterfly arbitrage check
272
280
  butterfly_arbitrage_free = True
273
281
  k_range = np.linspace(min(k), max(k), num_points)
274
- w_k = lambda k: SVIModel.svi(k, a_scaled, b_scaled, m, rho, sigma)
275
- w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m) ** 2 + sigma ** 2))
276
- w_double_prime = lambda k: b_scaled * sigma ** 2 / ((k - m) ** 2 + sigma ** 2) ** (3 / 2)
282
+ w_k = lambda k: SVIModel.svi_raw(k, a_scaled, b_scaled, m, rho, sigma)
283
+ w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m)**2 + sigma**2))
284
+ w_double_prime = lambda k: b_scaled * sigma**2 / ((k - m)**2 + sigma**2)**(3/2)
277
285
  for k_val in k_range:
278
286
  wk = w_k(k_val)
279
287
  wp = w_prime(k_val)
280
288
  wpp = w_double_prime(k_val)
281
- g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
289
+ g = (1 - (k_val * wp) / (2 * wk))**2 - (wp**2) / 4 * (1 / wk + 1/4) + wpp / 2
282
290
  if g < 0:
283
291
  butterfly_arbitrage_free = False
284
292
  break
285
293
 
286
- # Update results_df using maturity_name
287
- mat_name = maturity_data['maturity_name'].iloc[0]
288
- results_df.loc[mat_name, 'a'] = float(a_scaled)
289
- results_df.loc[mat_name, 'b'] = float(b_scaled)
290
- results_df.loc[mat_name, 'm'] = float(m)
291
- results_df.loc[mat_name, 'rho'] = float(rho)
292
- results_df.loc[mat_name, 'sigma'] = float(sigma)
293
- results_df.loc[mat_name, 'nu'] = float(nu)
294
- results_df.loc[mat_name, 'psi'] = float(psi)
295
- results_df.loc[mat_name, 'p'] = float(p)
296
- results_df.loc[mat_name, 'c'] = float(c)
297
- results_df.loc[mat_name, 'nu_tilde'] = float(nu_tilde)
298
- results_df.loc[mat_name, 'rmse'] = float(rmse)
299
- results_df.loc[mat_name, 'mae'] = float(mae)
300
- results_df.loc[mat_name, 'r2'] = float(r2)
301
- results_df.loc[mat_name, 'max_error'] = float(max_error)
302
- results_df.loc[mat_name, 'log_min_strike'] = float(log_min_strike)
303
- results_df.loc[mat_name, 'usd_min_strike'] = float(usd_min_strike)
304
- results_df.loc[mat_name, 'butterfly_arbitrage_free'] = butterfly_arbitrage_free
305
- results_df.loc[mat_name, 'fit_success'] = bool(not np.isnan(a))
306
- else:
307
- logger.warning(f"Skipping parameter update for maturity {mat2} due to invalid parameters")
308
- mat_name = maturity_data['maturity_name'].iloc[0]
309
- results_df.loc[mat_name, 'fit_success'] = False
294
+ results_df.at[mat2, 'a'] = float(a_scaled)
295
+ results_dfKILLat[mat2, 'b'] = float(b_scaled)
296
+ results_df.at[mat2, 'm'] = float(m)
297
+ results_df.at[mat2, 'rho'] = float(rho)
298
+ results_df.at[mat2, 'sigma'] = float(sigma)
299
+ results_df.at[mat2, 'nu'] = float(nu)
300
+ results_df.at[mat2, 'psi'] = float(psi)
301
+ results_df.at[mat2, 'p'] = float(p)
302
+ results_df.at[mat2, 'c'] = float(c)
303
+ results_df.at[mat2, 'nu_tilde'] = float(nu_tilde)
304
+ results_df.at[mat2, 'rmse'] = float(rmse)
305
+ results_df.at[mat2, 'mae'] = float(mae)
306
+ results_df.at[mat2, 'r2'] = float(r2)
307
+ results_df.at[mat2, 'max_error'] = float(max_error)
308
+ results_df.at[mat2, 'log_min_strike'] = float(log_min_strike)
309
+ results_df.at[mat2, 'usd_min_strike'] = float(usd_min_strike)
310
+ results_df.at[mat2, 'butterfly_arbitrage_free'] = butterfly_arbitrage_free
311
+ results_df.at[mat2, 'fit_success'] = bool(not np.isnan(a))
312
+ end_correction = time.time()
313
+ logger.info(f"Calendar arbitrage correction completed in {end_correction - start_correction:.4f} seconds")
310
314
 
311
315
  # Calendar arbitrage check (post-correction)
316
+ logger.info("\nChecking calendar arbitrage (post-correction)...")
317
+ start_post_check = time.time()
312
318
  calendar_arbitrage_free = True
313
319
  for i in range(len(sorted_maturities) - 1):
314
320
  mat1, mat2 = sorted_maturities[i], sorted_maturities[i + 1]
@@ -324,28 +330,29 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
324
330
  K = maturity_data['strikes'].values
325
331
  k_market = np.log(K / s)
326
332
  mask = ~np.isnan(k_market)
327
- k_check = np.unique(np.concatenate(
328
- [k_market[mask], np.linspace(min(k_market[mask]), max(k_market[mask]), num_points)]))
333
+ k_check = np.unique(np.concatenate([k_market[mask], np.linspace(min(k_market[mask]), max(k_market[mask]), num_points)]))
329
334
 
330
335
  for k_val in k_check:
331
- w1 = SVIModel.svi(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
332
- w2 = SVIModel.svi(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
336
+ w1 = SVIModel.svi_raw(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
337
+ w2 = SVIModel.svi_raw(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
333
338
  if w2 < w1 - 1e-6:
334
339
  logger.warning(f"Calendar arbitrage violation at t1={t1:.4f}, t2={t2:.4f}, k={k_val:.4f}: w1={w1:.6f}, w2={w2:.6f}")
335
340
  calendar_arbitrage_free = False
336
341
  break
337
342
  if not calendar_arbitrage_free:
338
343
  break
344
+ end_post_check = time.time()
345
+ logger.info(f"Post-correction calendar arbitrage check completed in {end_post_check - start_post_check:.4f} seconds")
339
346
 
340
- for mat in results_df['maturity_date']:
341
- results_df.loc[results_df['maturity_date'] == mat, 'calendar_arbitrage_free'] = calendar_arbitrage_free
347
+ for mat in sorted_maturities:
348
+ results_df.at[mat, 'calendar_arbitrage_free'] = calendar_arbitrage_free
342
349
 
343
350
  # End overall timer and print total time
344
351
  end_total = time.time()
345
- logger.info(f"Total execution time for model fit: {end_total - start_total:.4f} seconds")
352
+ logger.info(f"\nTotal execution time for fit_model: {end_total - start_total:.4f} seconds")
346
353
 
347
- logger.info("Model fitting complete.")
348
- return results_df
354
+ logger.info("Model fitting and visualization complete.")
355
+ return results_df, params_dict
349
356
 
350
357
 
351
358
  @catch_exception
@@ -364,11 +371,11 @@ def get_iv_surface(model_results: pd.DataFrame,
364
371
 
365
372
  Returns:
366
373
  - Tuple of (iv_surface, x_surface)
367
- iv_surface: Dictionary mapping maturity/dtm names to IV arrays
368
- x_surface: Dictionary mapping maturity/dtm names to requested x domain arrays
374
+ iv_surface: Dictionary mapping maturity to IV arrays
375
+ x_surface: Dictionary mapping maturity to requested x domain arrays
369
376
  """
370
377
  # Check if required columns are present
371
- required_columns = ['a', 'b', 'm', 'rho', 'sigma', 't']
378
+ required_columns = ['a', 'b', 'm', 'rho', 'sigma', 't', 's']
372
379
  missing_columns = [col for col in required_columns if col not in model_results.columns]
373
380
  if missing_columns:
374
381
  raise VolyError(f"Required columns missing in model_results: {missing_columns}")
@@ -390,11 +397,11 @@ def get_iv_surface(model_results: pd.DataFrame,
390
397
  model_results.loc[i, 'sigma']
391
398
  ]
392
399
  s = model_results.loc[i, 's']
393
- r = model_results.loc[i, 'r']
394
400
  t = model_results.loc[i, 't']
401
+ r = model_results.loc[i, 'r'] if 'r' in model_results.columns else 0
395
402
 
396
403
  # Calculate implied volatility
397
- w = np.array([SVIModel.svi(x, *params) for x in LM])
404
+ w = np.array([SVIModel.svi_raw(x, *params) for x in LM])
398
405
  o = np.sqrt(w / t)
399
406
  iv_surface[i] = o
400
407
 
@@ -35,6 +35,7 @@ class SVIModel:
35
35
 
36
36
  @staticmethod
37
37
  def svi(k, a, b, m, rho, sigma):
38
+ """Compute SVI total implied variance."""
38
39
  assert b >= 0 and abs(rho) <= 1 and sigma >= 0 and a + b * sigma * np.sqrt(1 - rho ** 2) >= 0
39
40
  return a + b * (rho * (k - m) + np.sqrt((k - m) ** 2 + sigma ** 2))
40
41
 
@@ -46,7 +47,7 @@ class SVIModel:
46
47
  @staticmethod
47
48
  def raw_to_jw_params(a: float, b: float, m: float, rho: float, sigma: float, t: float) -> Tuple[
48
49
  float, float, float, float, float]:
49
- """Convert raw SVI parameters to Jump-Wing parameters."""
50
+ """Convert raw SVI to Jump-Wing parameters."""
50
51
  nu = (a + b * ((-rho) * m + np.sqrt(m ** 2 + sigma ** 2))) / t
51
52
  psi = (1 / np.sqrt(nu * t)) * (b / 2) * (rho - (m / np.sqrt(m ** 2 + sigma ** 2)))
52
53
  p = (1 / np.sqrt(nu * t)) * b * (1 - rho)
@@ -54,8 +55,15 @@ class SVIModel:
54
55
  nu_tilde = (1 / t) * (a + b * sigma * np.sqrt(1 - rho ** 2))
55
56
  return nu, psi, p, c, nu_tilde
56
57
 
58
+ @staticmethod
59
+ def loss(tiv, vega, y, c, d, a):
60
+ """Compute weighted mean squared error for calibration."""
61
+ diff = tiv - (a + d * y + c * np.sqrt(y * y + 1))
62
+ return (vega * diff * diff).mean()
63
+
57
64
  @classmethod
58
65
  def calibration(cls, tiv, vega, k, m, sigma):
66
+ """Calibrate c, d, a parameters given m and sigma."""
59
67
  sigma = max(sigma, 0.001)
60
68
  vega = vega / vega.max() if vega.max() > 0 else np.ones_like(vega)
61
69
  y = (k - m) / sigma
@@ -80,16 +88,11 @@ class SVIModel:
80
88
  loss = cls.loss(tiv, vega, y, c, d, a)
81
89
  return c, d, a, loss
82
90
 
83
- @staticmethod
84
- def loss(tiv, vega, y, c, d, a):
85
- diff = tiv - (a + d * y + c * np.sqrt(y * y + 1))
86
- return (vega * diff * diff).mean()
87
-
88
91
  @classmethod
89
92
  def fit(cls, tiv, vega, k, tau=1.0):
93
+ """Fit SVI model."""
90
94
  if len(k) <= 5:
91
95
  return [np.nan] * 5, np.inf
92
-
93
96
  vega = vega / vega.max() if vega.max() > 0 else np.ones_like(vega)
94
97
  m_init = np.mean(k)
95
98
  sigma_init = max(0.1, np.std(k) * 0.1)
@@ -100,7 +103,7 @@ class SVIModel:
100
103
  return loss
101
104
 
102
105
  result = minimize(score, [sigma_init, m_init], bounds=[(0.001, None), (None, None)],
103
- tol=1e-16, method="SLSQP", options={'maxfun': 5000})
106
+ tol=1e-16, method="SLSQP", options={'maxfun': 5000})
104
107
 
105
108
  sigma, m = result.x
106
109
  c, d, a_calib, loss = cls.calibration(tiv, vega, k, m, sigma)
@@ -114,9 +117,7 @@ class SVIModel:
114
117
  a_svi = a_calib / tau
115
118
  rho_svi = b_svi = 0
116
119
 
117
- # Validate parameters
118
- params = [a_svi, b_svi, m, rho_svi, sigma]
119
- return params, loss
120
+ return [a_svi, b_svi, m, rho_svi, sigma], loss
120
121
 
121
122
  @classmethod
122
123
  def correct_calendar_arbitrage(cls, params, t, tiv, vega, k, prev_params, prev_t, k_grid):
@@ -162,13 +163,12 @@ class SVIModel:
162
163
 
163
164
  if result.success:
164
165
  new_params = result.x
165
- a, b, m, rho, sigma = new_params
166
- a_scaled, b_scaled = a * t, b * t
167
- w_current = cls.svi(k_constraint, a_scaled, b_scaled, m, rho, sigma)
166
+ w_current = cls.svi(k_constraint, new_params[0] * t, new_params[1] * t, *new_params[2:])
168
167
  w_prev = cls.svi(k_constraint, a_prev * prev_t, b_prev * prev_t, m_prev, rho_prev, sigma_prev)
169
168
  violation = np.min(w_current - w_prev)
170
- logger.info(f"Calendar arbitrage correction {'successful' if violation >= -1e-6 else 'failed'} for t={t:.4f}, "
171
- f"min margin={violation:.6f}")
169
+ logger.info(
170
+ f"Calendar arbitrage correction {'successful' if violation >= -1e-6 else 'failed'} for t={t:.4f}, "
171
+ f"min margin={violation:.6f}")
172
172
  return new_params
173
173
  logger.warning(f"Calendar arbitrage correction failed for t={t:.4f}")
174
174
  return params
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.195
3
+ Version: 0.0.196
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
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