voly 0.0.182__py3-none-any.whl → 0.0.184__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/fit.py +127 -128
- voly/models.py +2 -2
- {voly-0.0.182.dist-info → voly-0.0.184.dist-info}/METADATA +1 -1
- {voly-0.0.182.dist-info → voly-0.0.184.dist-info}/RECORD +7 -7
- {voly-0.0.182.dist-info → voly-0.0.184.dist-info}/WHEEL +0 -0
- {voly-0.0.182.dist-info → voly-0.0.184.dist-info}/licenses/LICENSE +0 -0
- {voly-0.0.182.dist-info → voly-0.0.184.dist-info}/top_level.txt +0 -0
voly/core/fit.py
CHANGED
@@ -215,8 +215,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
|
|
215
215
|
w1 = SVIModel.svi(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
|
216
216
|
w2 = SVIModel.svi(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
|
217
217
|
if w2 < w1 - 1e-6:
|
218
|
-
logger.warning(
|
219
|
-
|
218
|
+
logger.warning(f"Calendar arbitrage violation at t1={t1:.4f}, t2={t2:.4f}, k={k_val:.4f}: ")
|
219
|
+
logger.warning(f"w1={w1:.6f}, w2={w2:.6f}")
|
220
220
|
calendar_arbitrage_free = False
|
221
221
|
break
|
222
222
|
if not calendar_arbitrage_free:
|
@@ -232,134 +232,133 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame:
|
|
232
232
|
results_df.at[idx, 'calendar_arbitrage_free'] = calendar_arbitrage_free
|
233
233
|
|
234
234
|
# Calendar arbitrage correction
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
idx = results_df.index[j]
|
301
|
-
break
|
302
|
-
|
303
|
-
if idx is not None:
|
304
|
-
results_df.at[idx, 'a'] = float(a_scaled)
|
305
|
-
results_df.at[idx, 'b'] = float(b_scaled)
|
306
|
-
results_df.at[idx, 'm'] = float(m)
|
307
|
-
results_df.at[idx, 'rho'] = float(rho)
|
308
|
-
results_df.at[idx, 'sigma'] = float(sigma)
|
309
|
-
results_df.at[idx, 'nu'] = float(nu)
|
310
|
-
results_df.at[idx, 'psi'] = float(psi)
|
311
|
-
results_df.at[idx, 'p'] = float(p)
|
312
|
-
results_df.at[idx, 'c'] = float(c)
|
313
|
-
results_df.at[idx, 'nu_tilde'] = float(nu_tilde)
|
314
|
-
results_df.at[idx, 'rmse'] = float(rmse)
|
315
|
-
results_df.at[idx, 'mae'] = float(mae)
|
316
|
-
results_df.at[idx, 'r2'] = float(r2)
|
317
|
-
results_df.at[idx, 'max_error'] = float(max_error)
|
318
|
-
results_df.at[idx, 'log_min_strike'] = float(log_min_strike)
|
319
|
-
results_df.at[idx, 'usd_min_strike'] = float(usd_min_strike)
|
320
|
-
results_df.at[idx, 'butterfly_arbitrage_free'] = butterfly_arbitrage_free
|
321
|
-
results_df.at[idx, 'fit_success'] = bool(not np.isnan(a))
|
322
|
-
|
323
|
-
# Calendar arbitrage check (post-correction)
|
324
|
-
logger.info("\nChecking calendar arbitrage (post-correction)...")
|
325
|
-
calendar_arbitrage_free = False # Should be True
|
326
|
-
for i in range(len(sorted_maturities) - 1):
|
327
|
-
mat1, mat2 = sorted_maturities[i], sorted_maturities[i + 1]
|
328
|
-
t1, params1 = params_dict[mat1]
|
329
|
-
t2, params2 = params_dict[mat2]
|
330
|
-
a1, b1, m1, rho1, sigma1 = params1
|
331
|
-
a2, b2, m2, rho2, sigma2 = params2
|
332
|
-
|
333
|
-
if np.isnan(a1) or np.isnan(a2):
|
334
|
-
continue
|
335
|
-
|
336
|
-
group = groups.get_group(mat2)
|
337
|
-
K = group['strikes'].values
|
338
|
-
s = group['index_price'].iloc[0]
|
339
|
-
k_market = np.log(K / s)
|
340
|
-
mask = ~np.isnan(k_market)
|
341
|
-
k_check = np.unique(np.concatenate(
|
342
|
-
[k_market[mask], np.linspace(min(k_market[mask]), max(k_market[mask]), 200)]))
|
343
|
-
|
344
|
-
for k_val in k_check:
|
345
|
-
w1 = SVIModel.svi(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
|
346
|
-
w2 = SVIModel.svi(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
|
347
|
-
if w2 < w1 - 1e-6:
|
348
|
-
logger.warning(
|
349
|
-
f"Calendar arbitrage violation at t1={t1:.4f}, t2={t2:.4f}, k={k_val:.4f}: w1={w1:.6f}, w2={w2:.6f}")
|
350
|
-
calendar_arbitrage_free = False
|
351
|
-
break
|
352
|
-
if not calendar_arbitrage_free:
|
235
|
+
logger.info("\nPerforming calendar arbitrage correction...")
|
236
|
+
for i in range(1, len(sorted_maturities)):
|
237
|
+
mat2 = sorted_maturities[i]
|
238
|
+
mat1 = sorted_maturities[i - 1]
|
239
|
+
t2, params2 = params_dict[mat2]
|
240
|
+
t1, params1 = params_dict[mat1]
|
241
|
+
|
242
|
+
if np.any(np.isnan(params2)) or np.any(np.isnan(params1)):
|
243
|
+
continue
|
244
|
+
|
245
|
+
group = groups.get_group(mat2)
|
246
|
+
s = group['index_price'].iloc[0]
|
247
|
+
K = group['strikes'].values
|
248
|
+
iv = group['mark_iv'].values
|
249
|
+
vega = group['vega'].values if 'vega' in group.columns else np.ones_like(iv)
|
250
|
+
k = np.log(K / s)
|
251
|
+
w = (iv ** 2) * t2
|
252
|
+
mask = ~np.isnan(w) & ~np.isnan(vega) & ~np.isnan(k) & (iv >= 0)
|
253
|
+
k, w, vega, iv = k[mask], w[mask], vega[mask], iv[mask]
|
254
|
+
|
255
|
+
new_params = SVIModel.correct_calendar_arbitrage(
|
256
|
+
params=params2, t=t2, tiv=w, vega=vega, k=k,
|
257
|
+
prev_params=params1, prev_t=t1, k_grid=k_grid
|
258
|
+
)
|
259
|
+
|
260
|
+
params_dict[mat2] = (t2, new_params)
|
261
|
+
|
262
|
+
a, b, m, rho, sigma = new_params
|
263
|
+
a_scaled, b_scaled = a * t2, b * t2
|
264
|
+
nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t2)
|
265
|
+
|
266
|
+
# Recompute fit statistics
|
267
|
+
w_model = np.array([SVIModel.svi(x, a_scaled, b_scaled, m, rho, sigma) for x in k])
|
268
|
+
iv_model = np.sqrt(w_model / t2)
|
269
|
+
iv_market = iv
|
270
|
+
rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
|
271
|
+
mae = mean_absolute_error(iv_market, iv_model)
|
272
|
+
r2 = r2_score(iv_market, iv_model)
|
273
|
+
max_error = np.max(np.abs(iv_market - iv_model))
|
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
|
+
|
279
|
+
# Update butterfly arbitrage check
|
280
|
+
butterfly_arbitrage_free = True
|
281
|
+
k_range = np.linspace(min(k), max(k), 200)
|
282
|
+
w_k = lambda k: SVIModel.svi(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)
|
285
|
+
|
286
|
+
for k_val in k_range:
|
287
|
+
wk = w_k(k_val)
|
288
|
+
wp = w_prime(k_val)
|
289
|
+
wpp = w_double_prime(k_val)
|
290
|
+
g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
|
291
|
+
if g < 0:
|
292
|
+
butterfly_arbitrage_free = False
|
293
|
+
break
|
294
|
+
|
295
|
+
# Find the correct index to update
|
296
|
+
idx = None
|
297
|
+
for j, maturity_name in enumerate(maturity_names):
|
298
|
+
if results_df.iloc[j]['maturity_date'] == mat2:
|
299
|
+
idx = results_df.index[j]
|
353
300
|
break
|
354
301
|
|
355
|
-
|
356
|
-
idx =
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
302
|
+
if idx is not None:
|
303
|
+
results_df.at[idx, 'a'] = float(a_scaled)
|
304
|
+
results_df.at[idx, 'b'] = float(b_scaled)
|
305
|
+
results_df.at[idx, 'm'] = float(m)
|
306
|
+
results_df.at[idx, 'rho'] = float(rho)
|
307
|
+
results_df.at[idx, 'sigma'] = float(sigma)
|
308
|
+
results_df.at[idx, 'nu'] = float(nu)
|
309
|
+
results_df.at[idx, 'psi'] = float(psi)
|
310
|
+
results_df.at[idx, 'p'] = float(p)
|
311
|
+
results_df.at[idx, 'c'] = float(c)
|
312
|
+
results_df.at[idx, 'nu_tilde'] = float(nu_tilde)
|
313
|
+
results_df.at[idx, 'rmse'] = float(rmse)
|
314
|
+
results_df.at[idx, 'mae'] = float(mae)
|
315
|
+
results_df.at[idx, 'r2'] = float(r2)
|
316
|
+
results_df.at[idx, 'max_error'] = float(max_error)
|
317
|
+
results_df.at[idx, 'log_min_strike'] = float(log_min_strike)
|
318
|
+
results_df.at[idx, 'usd_min_strike'] = float(usd_min_strike)
|
319
|
+
results_df.at[idx, 'butterfly_arbitrage_free'] = butterfly_arbitrage_free
|
320
|
+
results_df.at[idx, 'fit_success'] = bool(not np.isnan(a))
|
321
|
+
|
322
|
+
# Calendar arbitrage check (post-correction)
|
323
|
+
logger.info("\nChecking calendar arbitrage (post-correction)...")
|
324
|
+
calendar_arbitrage_free = True
|
325
|
+
for i in range(len(sorted_maturities) - 1):
|
326
|
+
mat1, mat2 = sorted_maturities[i], sorted_maturities[i + 1]
|
327
|
+
t1, params1 = params_dict[mat1]
|
328
|
+
t2, params2 = params_dict[mat2]
|
329
|
+
a1, b1, m1, rho1, sigma1 = params1
|
330
|
+
a2, b2, m2, rho2, sigma2 = params2
|
331
|
+
|
332
|
+
if np.isnan(a1) or np.isnan(a2):
|
333
|
+
continue
|
334
|
+
|
335
|
+
group = groups.get_group(mat2)
|
336
|
+
K = group['strikes'].values
|
337
|
+
s = group['index_price'].iloc[0]
|
338
|
+
k_market = np.log(K / s)
|
339
|
+
mask = ~np.isnan(k_market)
|
340
|
+
k_check = np.unique(np.concatenate(
|
341
|
+
[k_market[mask], np.linspace(min(k_market[mask]), max(k_market[mask]), 200)]))
|
342
|
+
|
343
|
+
for k_val in k_check:
|
344
|
+
w1 = SVIModel.svi(k_val, a1 * t1, b1 * t1, m1, rho1, sigma1)
|
345
|
+
w2 = SVIModel.svi(k_val, a2 * t2, b2 * t2, m2, rho2, sigma2)
|
346
|
+
if w2 < w1 - 1e-6:
|
347
|
+
logger.warning(
|
348
|
+
f"Calendar arbitrage violation at t1={t1:.4f}, t2={t2:.4f}, k={k_val:.4f}: w1={w1:.6f}, w2={w2:.6f}")
|
349
|
+
calendar_arbitrage_free = False
|
350
|
+
break
|
351
|
+
if not calendar_arbitrage_free:
|
352
|
+
break
|
353
|
+
|
354
|
+
for mat in sorted_maturities:
|
355
|
+
idx = None
|
356
|
+
for j, maturity_name in enumerate(maturity_names):
|
357
|
+
if results_df.iloc[j]['maturity_date'] == mat:
|
358
|
+
idx = results_df.index[j]
|
359
|
+
break
|
360
|
+
if idx is not None:
|
361
|
+
results_df.at[idx, 'calendar_arbitrage_free'] = calendar_arbitrage_free
|
363
362
|
|
364
363
|
logger.info("Model fitting complete.")
|
365
364
|
return results_df
|
voly/models.py
CHANGED
@@ -165,10 +165,10 @@ class SVIModel:
|
|
165
165
|
w_current = cls.svi(k_constraint, new_params[0] * t, new_params[1] * t, *new_params[2:])
|
166
166
|
w_prev = cls.svi(k_constraint, a_prev * prev_t, b_prev * prev_t, m_prev, rho_prev, sigma_prev)
|
167
167
|
violation = np.min(w_current - w_prev)
|
168
|
-
|
168
|
+
logger.info(f"Calendar arbitrage correction {'successful' if violation >= -1e-6 else 'failed'} for t={t:.4f}, "
|
169
169
|
f"min margin={violation:.6f}")
|
170
170
|
return new_params
|
171
|
-
|
171
|
+
logger.warning(f"Calendar arbitrage correction failed for t={t:.4f}")
|
172
172
|
return params
|
173
173
|
|
174
174
|
|
@@ -2,19 +2,19 @@ voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
|
|
2
2
|
voly/client.py,sha256=dPyRRmZ_Gvo1zCZMo9eFOx2oaYocmkOt71fzdmOXFyM,14387
|
3
3
|
voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
|
4
4
|
voly/formulas.py,sha256=JnEs6G0wlfRNH6X_YEJMe2RtLH-ryhzufjsim73Bj3c,11176
|
5
|
-
voly/models.py,sha256=
|
5
|
+
voly/models.py,sha256=A7zIUnIoH2rwb2_kp96xfFuOQ8CkGQ-XCx1cOGJSlco,7009
|
6
6
|
voly/core/__init__.py,sha256=bu6fS2I1Pj9fPPnl-zY3L7NqrZSY5Zy6NY2uMUvdhKs,183
|
7
7
|
voly/core/charts.py,sha256=2S-BfCo30aj1_xlNLqF-za5rQWxF_mWKIdtdOe5bgbw,12735
|
8
8
|
voly/core/data.py,sha256=9v9iuE2XdIIlzoRAB7q1ol7YghBzBsPGAiwZ11oDuis,13650
|
9
|
-
voly/core/fit.py,sha256=
|
9
|
+
voly/core/fit.py,sha256=CIayT6HKd-8pc8P76bc2vNivNmF_Uu4fHRNRqNup2R8,17210
|
10
10
|
voly/core/hd.py,sha256=UFAyLncNUHivpPAcko6IK1bC55mudVtdlRFfXp63HXE,14771
|
11
11
|
voly/core/interpolate.py,sha256=JkK172-FXyhesW3hY4pEeuJWG3Bugq7QZXbeKoRpLuo,5305
|
12
12
|
voly/core/rnd.py,sha256=GoC3m1Q46Wnk5tV_mstr-3_aktHeue6BBLh4DQTciW0,13307
|
13
13
|
voly/utils/__init__.py,sha256=E05mWatyC-PDOsCxQV1p5Xi1IgpOomxrNURyCx_gB-w,200
|
14
14
|
voly/utils/density.py,sha256=q0fX4im9TGwMCZ32Hzdv8CNh56KnJo8bmG5w0gVWZH8,5879
|
15
15
|
voly/utils/logger.py,sha256=4-_2bVJmq17Q0d7Rd2mPg1AeR8gxv6EPvcmBDMFWcSM,1744
|
16
|
-
voly-0.0.
|
17
|
-
voly-0.0.
|
18
|
-
voly-0.0.
|
19
|
-
voly-0.0.
|
20
|
-
voly-0.0.
|
16
|
+
voly-0.0.184.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
17
|
+
voly-0.0.184.dist-info/METADATA,sha256=RoKiGHZQ4w4M5KsFMq6hCDmCe8KkTAKCKhcnEDc9C98,4115
|
18
|
+
voly-0.0.184.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
19
|
+
voly-0.0.184.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
20
|
+
voly-0.0.184.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|