voly 0.0.127__tar.gz → 0.0.129__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.127/src/voly.egg-info → voly-0.0.129}/PKG-INFO +1 -1
- {voly-0.0.127 → voly-0.0.129}/pyproject.toml +2 -2
- {voly-0.0.127 → voly-0.0.129}/src/voly/client.py +0 -37
- {voly-0.0.127 → voly-0.0.129}/src/voly/core/hd.py +0 -166
- {voly-0.0.127 → voly-0.0.129}/src/voly/formulas.py +1 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/models.py +1 -0
- {voly-0.0.127 → voly-0.0.129/src/voly.egg-info}/PKG-INFO +1 -1
- {voly-0.0.127 → voly-0.0.129}/LICENSE +0 -0
- {voly-0.0.127 → voly-0.0.129}/README.md +0 -0
- {voly-0.0.127 → voly-0.0.129}/setup.cfg +0 -0
- {voly-0.0.127 → voly-0.0.129}/setup.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/__init__.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/core/__init__.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/core/charts.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/core/data.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/core/fit.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/core/rnd.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/exceptions.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly/utils/logger.py +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.127 → voly-0.0.129}/src/voly.egg-info/requires.txt +0 -0
- {voly-0.0.127 → voly-0.0.129}/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.129"
|
|
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.129"
|
|
64
64
|
warn_return_any = true
|
|
65
65
|
warn_unused_configs = true
|
|
66
66
|
disallow_untyped_defs = true
|
|
@@ -368,40 +368,3 @@ class VolyClient:
|
|
|
368
368
|
'x_surface': x_surface,
|
|
369
369
|
'moments': moments
|
|
370
370
|
}
|
|
371
|
-
|
|
372
|
-
@staticmethod
|
|
373
|
-
def get_rv_surface(model_results: pd.DataFrame,
|
|
374
|
-
pdf_surface: Dict[str, np.ndarray],
|
|
375
|
-
x_surface: Dict[str, np.ndarray],
|
|
376
|
-
domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
|
|
377
|
-
return_domain: str = 'log_moneyness') -> Dict[str, Any]:
|
|
378
|
-
"""
|
|
379
|
-
Transform historical density surface into a volatility surface.
|
|
380
|
-
|
|
381
|
-
Parameters:
|
|
382
|
-
- model_results: DataFrame from fit_model() with maturity information
|
|
383
|
-
- pdf_surface: Dictionary mapping maturity names to historical density arrays
|
|
384
|
-
- x_surface: Dictionary mapping maturity names to x domain arrays
|
|
385
|
-
- domain_params: Tuple of (min, max, num_points) for the log-moneyness grid
|
|
386
|
-
- return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
|
|
387
|
-
|
|
388
|
-
Returns:
|
|
389
|
-
- Dictionary with implied volatility surface information
|
|
390
|
-
"""
|
|
391
|
-
|
|
392
|
-
logger.info("Calculating realized volatility surface")
|
|
393
|
-
|
|
394
|
-
# Generate the surface
|
|
395
|
-
fit_results, rv_surface, x_surface = get_rv_surface(
|
|
396
|
-
model_results=model_results,
|
|
397
|
-
pdf_surface=pdf_surface,
|
|
398
|
-
x_surface=x_surface,
|
|
399
|
-
domain_params=domain_params,
|
|
400
|
-
return_domain=return_domain
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
'fit_results': fit_results,
|
|
405
|
-
'rv_surface': rv_surface,
|
|
406
|
-
'x_surface': x_surface
|
|
407
|
-
}
|
|
@@ -205,169 +205,3 @@ def get_hd_surface(model_results: pd.DataFrame,
|
|
|
205
205
|
moments = pd.DataFrame(all_moments).T
|
|
206
206
|
|
|
207
207
|
return pdf_surface, cdf_surface, x_surface, moments
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
@catch_exception
|
|
211
|
-
def get_rv_surface(model_results: pd.DataFrame,
|
|
212
|
-
pdf_surface: Dict[str, np.ndarray],
|
|
213
|
-
x_surface: Dict[str, np.ndarray],
|
|
214
|
-
domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
|
|
215
|
-
return_domain: str = 'log_moneyness') -> Tuple[
|
|
216
|
-
Dict[str, np.ndarray], Dict[str, np.ndarray], pd.DataFrame]:
|
|
217
|
-
|
|
218
|
-
# Check if required columns are present
|
|
219
|
-
required_columns = ['s', 't', 'r']
|
|
220
|
-
missing_columns = [col for col in required_columns if col not in model_results.columns]
|
|
221
|
-
if missing_columns:
|
|
222
|
-
raise VolyError(f"Required columns missing in model_results: {missing_columns}")
|
|
223
|
-
|
|
224
|
-
rv_surface = {}
|
|
225
|
-
new_x_surface = {}
|
|
226
|
-
all_params = {}
|
|
227
|
-
|
|
228
|
-
# Check if pdf_surface is empty
|
|
229
|
-
if not pdf_surface:
|
|
230
|
-
logger.warning("Historical density surface is empty.")
|
|
231
|
-
return {}, {}, pd.DataFrame()
|
|
232
|
-
|
|
233
|
-
# Process each maturity
|
|
234
|
-
for i in model_results.index:
|
|
235
|
-
if i not in pdf_surface:
|
|
236
|
-
logger.warning(f"No historical density available for maturity {i}, skipping.")
|
|
237
|
-
continue
|
|
238
|
-
|
|
239
|
-
# Get parameters for this maturity
|
|
240
|
-
s = model_results.loc[i, 's']
|
|
241
|
-
r = model_results.loc[i, 'r']
|
|
242
|
-
t = model_results.loc[i, 't']
|
|
243
|
-
|
|
244
|
-
# Get historical density for this maturity
|
|
245
|
-
pdf = pdf_surface[i]
|
|
246
|
-
x = x_surface[i]
|
|
247
|
-
|
|
248
|
-
# Calculate x_domain grids
|
|
249
|
-
LM = get_domain(domain_params, s, r, None, t, 'log_moneyness')
|
|
250
|
-
M = get_domain(domain_params, s, r, None, t, 'moneyness')
|
|
251
|
-
R = get_domain(domain_params, s, r, None, t, 'returns')
|
|
252
|
-
K = get_domain(domain_params, s, r, None, t, 'log_moneyness')
|
|
253
|
-
|
|
254
|
-
# Recover call prices from the PDF
|
|
255
|
-
c_recovered = np.zeros_like(LM)
|
|
256
|
-
for j, lm_k in enumerate(LM):
|
|
257
|
-
mask = LM >= lm_k
|
|
258
|
-
if np.any(mask):
|
|
259
|
-
integrand = s * (np.exp(LM[mask]) - np.exp(lm_k)) * pdf[mask]
|
|
260
|
-
c_recovered[j] = np.exp(-r * t) * np.trapz(integrand, LM[mask])
|
|
261
|
-
|
|
262
|
-
# Ensure call prices are at least the intrinsic value
|
|
263
|
-
intrinsic_values = np.maximum(s - K, 0)
|
|
264
|
-
c_recovered = np.maximum(c_recovered, intrinsic_values)
|
|
265
|
-
|
|
266
|
-
# Determine min_lm and max_lm based on days to expiry (DTE)
|
|
267
|
-
dte = t * 365.25
|
|
268
|
-
if dte <= 30:
|
|
269
|
-
min_lm, max_lm = -0.3, 0.3
|
|
270
|
-
elif dte <= 90:
|
|
271
|
-
min_lm, max_lm = -0.6, 0.6
|
|
272
|
-
else:
|
|
273
|
-
min_lm, max_lm = -0.9, 0.9
|
|
274
|
-
|
|
275
|
-
# Generate key log-moneyness points
|
|
276
|
-
key_lm_points = generate_lm_points(min_lm, max_lm)
|
|
277
|
-
|
|
278
|
-
# Find the indices of the key log-moneyness points
|
|
279
|
-
key_indices = [np.argmin(np.abs(LM - lm)) for lm in key_lm_points]
|
|
280
|
-
key_lm_actual = LM[key_indices] # Actual log moneyness values
|
|
281
|
-
key_strikes = K[key_indices] # Corresponding strikes
|
|
282
|
-
|
|
283
|
-
# Extract call prices at key log-moneyness points
|
|
284
|
-
key_call_prices = c_recovered[key_indices]
|
|
285
|
-
|
|
286
|
-
# Compute IV at key log-moneyness points using our own iv function
|
|
287
|
-
key_ivs = []
|
|
288
|
-
for j, idx in enumerate(key_indices):
|
|
289
|
-
call_price = key_call_prices[j]
|
|
290
|
-
strike = key_strikes[j]
|
|
291
|
-
if call_price <= 0:
|
|
292
|
-
iv_value = 0.01 # Minimum IV of 1%
|
|
293
|
-
else:
|
|
294
|
-
try:
|
|
295
|
-
iv_value = iv(option_price=call_price, s=s, K=strike, r=r, t=t, option_type='call')
|
|
296
|
-
iv_value = max(0.01, min(iv_value, 3.0)) # Clamp between 1% and 300%
|
|
297
|
-
except Exception as e:
|
|
298
|
-
logger.warning(f"IV calculation failed for strike {strike}: {str(e)}")
|
|
299
|
-
iv_value = 0.01 # Fallback to 1%
|
|
300
|
-
key_ivs.append(iv_value)
|
|
301
|
-
key_ivs = np.array(key_ivs)
|
|
302
|
-
|
|
303
|
-
# Create a synthetic option chain for SVI fitting
|
|
304
|
-
# Convert to DataFrame columns that fit_model expects
|
|
305
|
-
synthetic_chain = pd.DataFrame({
|
|
306
|
-
'maturity_name': [i] * len(key_strikes),
|
|
307
|
-
'maturity_date': pd.Timestamp.now() + pd.Timedelta(days=int(t * 365.25)),
|
|
308
|
-
'index_price': s,
|
|
309
|
-
'underlying_price': s,
|
|
310
|
-
'strike': key_strikes,
|
|
311
|
-
'log_moneyness': key_lm_actual,
|
|
312
|
-
'mark_iv': key_ivs,
|
|
313
|
-
't': t,
|
|
314
|
-
'r': r,
|
|
315
|
-
'option_type': 'call'
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
# Fit the SVI model to the recovered IVs
|
|
319
|
-
fit_results_rv = fit_model(option_chain=synthetic_chain, model_name='svi')
|
|
320
|
-
|
|
321
|
-
# Get the parameters for this maturity
|
|
322
|
-
a = fit_results_rv.loc[i, 'a']
|
|
323
|
-
b = fit_results_rv.loc[i, 'b']
|
|
324
|
-
sigma = fit_results_rv.loc[i, 'sigma']
|
|
325
|
-
rho = fit_results_rv.loc[i, 'rho']
|
|
326
|
-
m = fit_results_rv.loc[i, 'm']
|
|
327
|
-
|
|
328
|
-
# Store the parameters
|
|
329
|
-
params = {
|
|
330
|
-
's': s,
|
|
331
|
-
'r': r,
|
|
332
|
-
't': t,
|
|
333
|
-
'a': a,
|
|
334
|
-
'b': b,
|
|
335
|
-
'sigma': sigma,
|
|
336
|
-
'rho': rho,
|
|
337
|
-
'm': m
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(params['a'], params['b'], params['sigma'], params['rho'], params['m'], params['t'])
|
|
341
|
-
params.update({
|
|
342
|
-
'nu': nu,
|
|
343
|
-
'psi': psi,
|
|
344
|
-
'p': p,
|
|
345
|
-
'c': c,
|
|
346
|
-
'nu_tilde': nu_tilde,
|
|
347
|
-
})
|
|
348
|
-
all_params[i] = params
|
|
349
|
-
|
|
350
|
-
# Calculate implied volatility using SVI model
|
|
351
|
-
w = np.array([SVIModel.svi(lm, a, b, sigma, rho, m) for lm in LM])
|
|
352
|
-
o_recovered = np.sqrt(w / t)
|
|
353
|
-
|
|
354
|
-
# Store results
|
|
355
|
-
rv_surface[i] = o_recovered
|
|
356
|
-
|
|
357
|
-
if return_domain == 'log_moneyness':
|
|
358
|
-
x = LM
|
|
359
|
-
elif return_domain == 'moneyness':
|
|
360
|
-
x = M
|
|
361
|
-
elif return_domain == 'returns':
|
|
362
|
-
x = R
|
|
363
|
-
elif return_domain == 'strikes':
|
|
364
|
-
x = K
|
|
365
|
-
elif return_domain == 'delta':
|
|
366
|
-
x = get_domain(domain_params, s, r, o_recovered, t, 'delta')
|
|
367
|
-
|
|
368
|
-
new_x_surface[i] = x
|
|
369
|
-
|
|
370
|
-
# Create a DataFrame with parameters
|
|
371
|
-
fit_results = pd.DataFrame(all_params).T
|
|
372
|
-
x_surface = new_x_surface
|
|
373
|
-
return fit_results, rv_surface, x_surface
|
|
@@ -43,6 +43,7 @@ def d1(s: float, K: float, r: float, o: float, t: float, option_type: str = 'cal
|
|
|
43
43
|
return np.nan
|
|
44
44
|
return (np.log(s / K) + (r + o ** 2 / 2) * t) / (o * np.sqrt(t))
|
|
45
45
|
|
|
46
|
+
|
|
46
47
|
@catch_exception
|
|
47
48
|
@vectorize_inputs
|
|
48
49
|
def d2(s: float, K: float, r: float, o: float, t: float, option_type: str = 'call') -> float:
|
|
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
|