voly 0.0.195__py3-none-any.whl → 0.0.196__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 +107 -100
- voly/models.py +16 -16
- {voly-0.0.195.dist-info → voly-0.0.196.dist-info}/METADATA +1 -1
- {voly-0.0.195.dist-info → voly-0.0.196.dist-info}/RECORD +7 -7
- {voly-0.0.195.dist-info → voly-0.0.196.dist-info}/WHEEL +0 -0
- {voly-0.0.195.dist-info → voly-0.0.196.dist-info}/licenses/LICENSE +0 -0
- {voly-0.0.195.dist-info → voly-0.0.196.dist-info}/top_level.txt +0 -0
voly/core/fit.py
CHANGED
@@ -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
|
5
|
-
|
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
|
-
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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 {
|
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(
|
167
|
-
results_data['mae'].append(
|
168
|
-
results_data['r2'].append(
|
169
|
-
results_data['max_error'].append(
|
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
|
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
|
-
|
186
|
+
for future in futures:
|
187
|
+
future.result()
|
181
188
|
|
182
189
|
# Create results DataFrame
|
183
|
-
results_df = pd.DataFrame(results_data, index=
|
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.
|
219
|
-
w2 = SVIModel.
|
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
|
228
|
-
results_df.
|
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
|
-
|
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
|
-
|
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
|
-
#
|
263
|
-
w_model = np.array([SVIModel.
|
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.
|
275
|
-
w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m)
|
276
|
-
w_double_prime = lambda k: b_scaled * sigma
|
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))
|
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
|
-
|
287
|
-
|
288
|
-
results_df.
|
289
|
-
results_df.
|
290
|
-
results_df.
|
291
|
-
results_df.
|
292
|
-
results_df.
|
293
|
-
results_df.
|
294
|
-
results_df.
|
295
|
-
results_df.
|
296
|
-
results_df.
|
297
|
-
results_df.
|
298
|
-
results_df.
|
299
|
-
results_df.
|
300
|
-
results_df.
|
301
|
-
results_df.
|
302
|
-
results_df.
|
303
|
-
results_df.
|
304
|
-
|
305
|
-
|
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.
|
332
|
-
w2 = SVIModel.
|
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
|
341
|
-
results_df.
|
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"
|
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
|
368
|
-
x_surface: Dictionary mapping maturity
|
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.
|
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
|
|
voly/models.py
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
171
|
-
|
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
|
@@ -2,19 +2,19 @@ voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
|
|
2
2
|
voly/client.py,sha256=0kp2_I-imcJ6uxMcvS0BipS2PgWdFaCPZG5335Vg75Q,14383
|
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=Wop6gZbvAOXSmI0JpYapjVSeJub_i8RLif2PzxRsfFE,7185
|
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=QtEoHz6v_YE-_FX0UwMEpvi623bujKd51KzN6x_owBY,17417
|
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.196.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
17
|
+
voly-0.0.196.dist-info/METADATA,sha256=SNaoFKB8gA3Q17Bgh6vBLeg3LyniNFqX1IOQX7vVWnQ,4115
|
18
|
+
voly-0.0.196.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
19
|
+
voly-0.0.196.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
20
|
+
voly-0.0.196.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|