voly 0.0.193__tar.gz → 0.0.195__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.193/src/voly.egg-info → voly-0.0.195}/PKG-INFO +1 -1
  2. {voly-0.0.193 → voly-0.0.195}/pyproject.toml +2 -2
  3. {voly-0.0.193 → voly-0.0.195}/src/voly/core/fit.py +65 -79
  4. {voly-0.0.193 → voly-0.0.195}/src/voly/models.py +8 -6
  5. {voly-0.0.193 → voly-0.0.195/src/voly.egg-info}/PKG-INFO +1 -1
  6. {voly-0.0.193 → voly-0.0.195}/LICENSE +0 -0
  7. {voly-0.0.193 → voly-0.0.195}/README.md +0 -0
  8. {voly-0.0.193 → voly-0.0.195}/setup.cfg +0 -0
  9. {voly-0.0.193 → voly-0.0.195}/setup.py +0 -0
  10. {voly-0.0.193 → voly-0.0.195}/src/voly/__init__.py +0 -0
  11. {voly-0.0.193 → voly-0.0.195}/src/voly/client.py +0 -0
  12. {voly-0.0.193 → voly-0.0.195}/src/voly/core/__init__.py +0 -0
  13. {voly-0.0.193 → voly-0.0.195}/src/voly/core/charts.py +0 -0
  14. {voly-0.0.193 → voly-0.0.195}/src/voly/core/data.py +0 -0
  15. {voly-0.0.193 → voly-0.0.195}/src/voly/core/hd.py +0 -0
  16. {voly-0.0.193 → voly-0.0.195}/src/voly/core/interpolate.py +0 -0
  17. {voly-0.0.193 → voly-0.0.195}/src/voly/core/rnd.py +0 -0
  18. {voly-0.0.193 → voly-0.0.195}/src/voly/exceptions.py +0 -0
  19. {voly-0.0.193 → voly-0.0.195}/src/voly/formulas.py +0 -0
  20. {voly-0.0.193 → voly-0.0.195}/src/voly/utils/__init__.py +0 -0
  21. {voly-0.0.193 → voly-0.0.195}/src/voly/utils/density.py +0 -0
  22. {voly-0.0.193 → voly-0.0.195}/src/voly/utils/logger.py +0 -0
  23. {voly-0.0.193 → voly-0.0.195}/src/voly.egg-info/SOURCES.txt +0 -0
  24. {voly-0.0.193 → voly-0.0.195}/src/voly.egg-info/dependency_links.txt +0 -0
  25. {voly-0.0.193 → voly-0.0.195}/src/voly.egg-info/requires.txt +0 -0
  26. {voly-0.0.193 → voly-0.0.195}/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.193
3
+ Version: 0.0.195
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.193"
7
+ version = "0.0.195"
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.193"
63
+ python_version = "0.0.195"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -29,7 +29,7 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
29
29
  - option_chain: DataFrame with market data
30
30
 
31
31
  Returns:
32
- - DataFrame with all fit results and performance metrics as columns, maturity_dates as index
32
+ - DataFrame with all fit results and performance metrics as columns, maturity_names as index
33
33
  """
34
34
  # Start overall timer
35
35
  start_total = time.time()
@@ -66,15 +66,16 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
66
66
  s = option_chain['index_price'].iloc[0]
67
67
  unique_ts = sorted(option_chain['t'].unique())
68
68
  maturity_names = [option_chain[option_chain['t'] == t]['maturity_name'].iloc[0] for t in unique_ts]
69
- groups = option_chain.groupby('maturity_date')
69
+ maturity_dates = [option_chain[option_chain['t'] == t]['maturity_date'].iloc[0] for t in unique_ts]
70
+ maturity_data_groups = option_chain.groupby('maturity_date')
70
71
  params_dict = {}
71
72
  results_data = {col: [] for col in column_dtypes.keys()}
72
73
  num_points = 2000 # Number of points for k_grid
73
74
 
74
- def process_maturity(maturity, group):
75
+ def process_maturity(maturity, maturity_data):
75
76
  """Process single maturity for SVI calibration."""
76
- group = group[group['option_type'] == 'C']
77
- duplicated_iv = group[group.duplicated('mark_iv', keep=False)]
77
+ maturity_data = maturity_data[maturity_data['option_type'] == 'C']
78
+ duplicated_iv = maturity_data[maturity_data.duplicated('mark_iv', keep=False)]
78
79
 
79
80
  # For each duplicated IV, keep the row closest to log_moneyness=0
80
81
  def keep_closest_to_zero(subgroup):
@@ -88,17 +89,17 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
88
89
  )
89
90
 
90
91
  # Get rows with unique mark_iv (no duplicates)
91
- unique_iv = group.drop_duplicates('mark_iv', keep=False)
92
+ unique_iv = maturity_data.drop_duplicates('mark_iv', keep=False)
92
93
 
93
94
  # Combine cleaned duplicates and unique rows
94
95
  maturity_data = pd.concat([unique_iv, cleaned_duplicated_iv])
95
96
  maturity_date = maturity_data['maturity_date'].iloc[0]
97
+ maturity_name = maturity_data['maturity_name'].iloc[0]
96
98
 
97
-
98
- t = group['t'].iloc[0]
99
- K = group['strikes'].values
100
- iv = group['mark_iv'].values
101
- vega = group['vega'].values if 'vega' in group.columns else np.ones_like(iv)
99
+ t = maturity_data['t'].iloc[0]
100
+ K = maturity_data['strikes'].values
101
+ iv = maturity_data['mark_iv'].values
102
+ vega = maturity_data['vega'].values if 'vega' in maturity_data.columns else np.ones_like(iv)
102
103
  k = np.log(K / s)
103
104
  w = (iv ** 2) * t
104
105
  mask = ~np.isnan(w) & ~np.isnan(vega) & ~np.isnan(k) & (iv >= 0)
@@ -107,48 +108,35 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
107
108
  params = [np.nan] * 5
108
109
  loss = np.inf
109
110
  nu = psi = p = c = nu_tilde = np.nan
110
- rmse = mae = r2 = max_error = np.nan
111
111
  butterfly_arbitrage_free = True
112
112
  r = maturity_data['interest_rate'].iloc[0] if 'interest_rate' in maturity_data.columns else 0
113
113
  log_min_strike = usd_min_strike = np.nan
114
114
 
115
115
  if len(k) > 5:
116
116
  params, loss = SVIModel.fit(tiv=w, vega=vega, k=k, tau=t)
117
- if not np.isnan(params[0]):
118
- params_dict[maturity_date] = (t, params)
119
- a, b, m, rho, sigma = params
120
- a_scaled, b_scaled = a * t, b * t
121
-
122
- # Transform to Jump-Wing parameters
123
- nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t)
124
-
125
- # Compute fit statistics
126
- w_model = np.array([SVIModel.svi(x, a_scaled, b_scaled, m, rho, sigma) for x in k])
127
- iv_model = np.sqrt(w_model / t)
128
- iv_market = iv
129
- rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
130
- mae = mean_absolute_error(iv_market, iv_model)
131
- r2 = r2_score(iv_market, iv_model)
132
- max_error = np.max(np.abs(iv_market - iv_model))
133
-
134
- # Compute min strike
135
- log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
136
- usd_min_strike = np.exp(log_min_strike) * s
137
-
138
- # Butterfly arbitrage check
139
- k_range = np.linspace(min(k), max(k), num_points)
140
- w_k = lambda k: SVIModel.svi(k, a_scaled, b_scaled, m, rho, sigma)
141
- w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m) ** 2 + sigma ** 2))
142
- w_double_prime = lambda k: b_scaled * sigma ** 2 / ((k - m) ** 2 + sigma ** 2) ** (3 / 2)
143
-
144
- for k_val in k_range:
145
- wk = w_k(k_val)
146
- wp = w_prime(k_val)
147
- wpp = w_double_prime(k_val)
148
- g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
149
- if g < 0:
150
- butterfly_arbitrage_free = False
151
- break
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
152
140
 
153
141
  # Log result
154
142
  GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
@@ -175,24 +163,21 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
175
163
  results_data['fit_success'].append(bool(not np.isnan(params[0])))
176
164
  results_data['butterfly_arbitrage_free'].append(butterfly_arbitrage_free)
177
165
  results_data['calendar_arbitrage_free'].append(True) # Updated after check
178
- results_data['rmse'].append(float(rmse))
179
- results_data['mae'].append(float(mae))
180
- results_data['r2'].append(float(r2))
181
- results_data['max_error'].append(float(max_error))
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)
182
170
  results_data['loss'].append(float(loss))
183
171
  results_data['n_points'].append(int(len(k)))
184
172
 
185
- return maturity
173
+ return maturity_name
186
174
 
187
- # Parallel processing of maturities with timer
188
- start_parallel = time.time()
175
+ # Parallel processing of maturities
176
+ logger.info("\nStarting parallel processing of maturities...")
189
177
  with ThreadPoolExecutor() as executor:
190
- futures = [executor.submit(process_maturity, maturity, group)
191
- for maturity, group in groups]
192
- for future in futures:
193
- future.result()
194
- end_parallel = time.time()
195
- logger.info(f"Processing completed in {end_parallel - start_parallel:.4f} seconds")
178
+ futures = [executor.submit(process_maturity, maturity, maturity_data)
179
+ for maturity, maturity_data in maturity_data_groups]
180
+ maturity_names = [future.result() for future in futures]
196
181
 
197
182
  # Create results DataFrame
198
183
  results_df = pd.DataFrame(results_data, index=maturity_names)
@@ -208,7 +193,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
208
193
  # Sort by time to maturity
209
194
  results_df = results_df.sort_values(by='t')
210
195
 
211
- # Calendar arbitrage check (pre-correction) with timer
196
+ # Calendar arbitrage check (pre-correction)
197
+ logger.info("\nChecking calendar arbitrage (pre-correction)...")
212
198
  k_grid = np.linspace(-2, 2, num_points)
213
199
  sorted_maturities = sorted(params_dict.keys(), key=lambda x: params_dict[x][0])
214
200
  calendar_arbitrage_free = True
@@ -222,8 +208,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
222
208
  if np.isnan(a1) or np.isnan(a2):
223
209
  continue
224
210
 
225
- group = groups.get_group(mat2)
226
- K = group['strikes'].values
211
+ maturity_data = maturity_data_groups.get_group(mat2)
212
+ K = maturity_data['strikes'].values
227
213
  k_market = np.log(K / s)
228
214
  mask = ~np.isnan(k_market)
229
215
  k_check = np.unique(np.concatenate([k_market[mask], np.linspace(min(k_market[mask]), max(k_market[mask]), num_points)]))
@@ -241,7 +227,7 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
241
227
  for mat in results_df['maturity_date']:
242
228
  results_df.loc[results_df['maturity_date'] == mat, 'calendar_arbitrage_free'] = calendar_arbitrage_free
243
229
 
244
- # Calendar arbitrage correction with timer
230
+ # Calendar arbitrage correction
245
231
  logger.info("Performing calendar arbitrage correction...")
246
232
  for i in range(1, len(sorted_maturities)):
247
233
  mat2 = sorted_maturities[i]
@@ -252,10 +238,10 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
252
238
  if np.any(np.isnan(params2)) or np.any(np.isnan(params1)):
253
239
  continue
254
240
 
255
- group = groups.get_group(mat2)
256
- K = group['strikes'].values
257
- iv = group['mark_iv'].values
258
- vega = group['vega'].values if 'vega' in group.columns else np.ones_like(iv)
241
+ maturity_data = maturity_data_groups.get_group(mat2)
242
+ K = maturity_data['strikes'].values
243
+ iv = maturity_data['mark_iv'].values
244
+ vega = maturity_data['vega'].values if 'vega' in maturity_data.columns else np.ones_like(iv)
259
245
  k = np.log(K / s)
260
246
  w = (iv ** 2) * t2
261
247
  mask = ~np.isnan(w) & ~np.isnan(vega) & ~np.isnan(k) & (iv >= 0)
@@ -266,13 +252,14 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
266
252
  prev_params=params1, prev_t=t1, k_grid=k_grid
267
253
  )
268
254
 
269
- params_dict[mat2] = (t2, new_params)
270
-
271
255
  a, b, m, rho, sigma = new_params
272
256
  a_scaled, b_scaled = a * t2, b * t2
257
+ params_dict[mat2] = (t2, new_params)
273
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
274
261
 
275
- # Recompute fit statistics
262
+ # Compute fit statistics
276
263
  w_model = np.array([SVIModel.svi(x, a_scaled, b_scaled, m, rho, sigma) for x in k])
277
264
  iv_model = np.sqrt(w_model / t2)
278
265
  iv_market = iv
@@ -281,17 +268,12 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
281
268
  r2 = r2_score(iv_market, iv_model)
282
269
  max_error = np.max(np.abs(iv_market - iv_model))
283
270
 
284
- # Recompute min strike
285
- log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
286
- usd_min_strike = np.exp(log_min_strike) * s
287
-
288
271
  # Update butterfly arbitrage check
289
272
  butterfly_arbitrage_free = True
290
273
  k_range = np.linspace(min(k), max(k), num_points)
291
274
  w_k = lambda k: SVIModel.svi(k, a_scaled, b_scaled, m, rho, sigma)
292
275
  w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m) ** 2 + sigma ** 2))
293
276
  w_double_prime = lambda k: b_scaled * sigma ** 2 / ((k - m) ** 2 + sigma ** 2) ** (3 / 2)
294
-
295
277
  for k_val in k_range:
296
278
  wk = w_k(k_val)
297
279
  wp = w_prime(k_val)
@@ -302,7 +284,7 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
302
284
  break
303
285
 
304
286
  # Update results_df using maturity_name
305
- mat_name = group['maturity_name'].iloc[0]
287
+ mat_name = maturity_data['maturity_name'].iloc[0]
306
288
  results_df.loc[mat_name, 'a'] = float(a_scaled)
307
289
  results_df.loc[mat_name, 'b'] = float(b_scaled)
308
290
  results_df.loc[mat_name, 'm'] = float(m)
@@ -321,6 +303,10 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
321
303
  results_df.loc[mat_name, 'usd_min_strike'] = float(usd_min_strike)
322
304
  results_df.loc[mat_name, 'butterfly_arbitrage_free'] = butterfly_arbitrage_free
323
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
324
310
 
325
311
  # Calendar arbitrage check (post-correction)
326
312
  calendar_arbitrage_free = True
@@ -334,8 +320,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
334
320
  if np.isnan(a1) or np.isnan(a2):
335
321
  continue
336
322
 
337
- group = groups.get_group(mat2)
338
- K = group['strikes'].values
323
+ maturity_data = maturity_data_groups.get_group(mat2)
324
+ K = maturity_data['strikes'].values
339
325
  k_market = np.log(K / s)
340
326
  mask = ~np.isnan(k_market)
341
327
  k_check = np.unique(np.concatenate(
@@ -90,8 +90,6 @@ class SVIModel:
90
90
  if len(k) <= 5:
91
91
  return [np.nan] * 5, np.inf
92
92
 
93
- from scipy.optimize import minimize
94
-
95
93
  vega = vega / vega.max() if vega.max() > 0 else np.ones_like(vega)
96
94
  m_init = np.mean(k)
97
95
  sigma_init = max(0.1, np.std(k) * 0.1)
@@ -102,7 +100,7 @@ class SVIModel:
102
100
  return loss
103
101
 
104
102
  result = minimize(score, [sigma_init, m_init], bounds=[(0.001, None), (None, None)],
105
- tol=1e-16, method="SLSQP", options={'maxfun': 5000})
103
+ tol=1e-16, method="SLSQP", options={'maxfun': 5000})
106
104
 
107
105
  sigma, m = result.x
108
106
  c, d, a_calib, loss = cls.calibration(tiv, vega, k, m, sigma)
@@ -116,7 +114,9 @@ class SVIModel:
116
114
  a_svi = a_calib / tau
117
115
  rho_svi = b_svi = 0
118
116
 
119
- return [a_svi, b_svi, m, rho_svi, sigma], loss
117
+ # Validate parameters
118
+ params = [a_svi, b_svi, m, rho_svi, sigma]
119
+ return params, loss
120
120
 
121
121
  @classmethod
122
122
  def correct_calendar_arbitrage(cls, params, t, tiv, vega, k, prev_params, prev_t, k_grid):
@@ -162,11 +162,13 @@ class SVIModel:
162
162
 
163
163
  if result.success:
164
164
  new_params = result.x
165
- w_current = cls.svi(k_constraint, new_params[0] * t, new_params[1] * t, *new_params[2:])
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
168
  w_prev = cls.svi(k_constraint, a_prev * prev_t, b_prev * prev_t, m_prev, rho_prev, sigma_prev)
167
169
  violation = np.min(w_current - w_prev)
168
170
  logger.info(f"Calendar arbitrage correction {'successful' if violation >= -1e-6 else 'failed'} for t={t:.4f}, "
169
- f"min margin={violation:.6f}")
171
+ f"min margin={violation:.6f}")
170
172
  return new_params
171
173
  logger.warning(f"Calendar arbitrage correction failed for t={t:.4f}")
172
174
  return params
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.193
3
+ Version: 0.0.195
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