voly 0.0.193__tar.gz → 0.0.194__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.
- {voly-0.0.193/src/voly.egg-info → voly-0.0.194}/PKG-INFO +1 -1
- {voly-0.0.193 → voly-0.0.194}/pyproject.toml +2 -2
- {voly-0.0.193 → voly-0.0.194}/src/voly/core/fit.py +65 -79
- {voly-0.0.193 → voly-0.0.194/src/voly.egg-info}/PKG-INFO +1 -1
- {voly-0.0.193 → voly-0.0.194}/LICENSE +0 -0
- {voly-0.0.193 → voly-0.0.194}/README.md +0 -0
- {voly-0.0.193 → voly-0.0.194}/setup.cfg +0 -0
- {voly-0.0.193 → voly-0.0.194}/setup.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/__init__.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/client.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/core/__init__.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/core/charts.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/core/data.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/core/hd.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/core/rnd.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/exceptions.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/formulas.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/models.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/utils/density.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly/utils/logger.py +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly.egg-info/requires.txt +0 -0
- {voly-0.0.193 → voly-0.0.194}/src/voly.egg-info/top_level.txt +0 -0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "voly"
|
7
|
-
version = "0.0.
|
7
|
+
version = "0.0.194"
|
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.
|
63
|
+
python_version = "0.0.194"
|
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,
|
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
|
-
|
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,
|
75
|
+
def process_maturity(maturity, maturity_data):
|
75
76
|
"""Process single maturity for SVI calibration."""
|
76
|
-
|
77
|
-
duplicated_iv =
|
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 =
|
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
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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(
|
179
|
-
results_data['mae'].append(
|
180
|
-
results_data['r2'].append(
|
181
|
-
results_data['max_error'].append(
|
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
|
173
|
+
return maturity_name
|
186
174
|
|
187
|
-
# Parallel processing of maturities
|
188
|
-
|
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,
|
191
|
-
for maturity,
|
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)
|
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
|
-
|
226
|
-
K =
|
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
|
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
|
-
|
256
|
-
K =
|
257
|
-
iv =
|
258
|
-
vega =
|
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
|
-
#
|
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 =
|
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
|
-
|
338
|
-
K =
|
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(
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|