voly 0.0.182__py3-none-any.whl → 0.0.183__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 +125 -126
- {voly-0.0.182.dist-info → voly-0.0.183.dist-info}/METADATA +1 -1
- {voly-0.0.182.dist-info → voly-0.0.183.dist-info}/RECORD +6 -6
- {voly-0.0.182.dist-info → voly-0.0.183.dist-info}/WHEEL +0 -0
- {voly-0.0.182.dist-info → voly-0.0.183.dist-info}/licenses/LICENSE +0 -0
- {voly-0.0.182.dist-info → voly-0.0.183.dist-info}/top_level.txt +0 -0
voly/core/fit.py
CHANGED
@@ -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
|
@@ -6,15 +6,15 @@ voly/models.py,sha256=wxqf9T4D2ORO7g3KcwxUL0E78fG69W29xqer3ccoUXo,6994
|
|
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=P4axtSrrdTIoXpGe2GsdP4bJi8e8t_S_yda3St_TEoI,17195
|
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.183.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
17
|
+
voly-0.0.183.dist-info/METADATA,sha256=D4HB2YlkiMk6vpiczAcgNTPU8AsxEZZMCR0nZZLplFM,4115
|
18
|
+
voly-0.0.183.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
19
|
+
voly-0.0.183.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
20
|
+
voly-0.0.183.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|