voly 0.0.156__tar.gz → 0.0.157__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.156/src/voly.egg-info → voly-0.0.157}/PKG-INFO +1 -1
- {voly-0.0.156 → voly-0.0.157}/pyproject.toml +2 -2
- {voly-0.0.156 → voly-0.0.157}/src/voly/client.py +5 -2
- {voly-0.0.156 → voly-0.0.157}/src/voly/core/hd.py +201 -18
- {voly-0.0.156 → voly-0.0.157/src/voly.egg-info}/PKG-INFO +1 -1
- {voly-0.0.156 → voly-0.0.157}/LICENSE +0 -0
- {voly-0.0.156 → voly-0.0.157}/README.md +0 -0
- {voly-0.0.156 → voly-0.0.157}/setup.cfg +0 -0
- {voly-0.0.156 → voly-0.0.157}/setup.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/__init__.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/core/__init__.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/core/charts.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/core/data.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/core/fit.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/core/rnd.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/exceptions.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/formulas.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/models.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/utils/density.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly/utils/logger.py +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.156 → voly-0.0.157}/src/voly.egg-info/requires.txt +0 -0
- {voly-0.0.156 → voly-0.0.157}/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.157"
|
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.157"
|
64
64
|
warn_return_any = true
|
65
65
|
warn_unused_configs = true
|
66
66
|
disallow_untyped_defs = true
|
@@ -364,26 +364,29 @@ class VolyClient:
|
|
364
364
|
df_hist: pd.DataFrame,
|
365
365
|
domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
|
366
366
|
return_domain: str = 'log_moneyness',
|
367
|
+
method: str = 'normal',
|
367
368
|
centered: bool = False) -> Dict[str, Any]:
|
368
369
|
"""
|
369
|
-
Generate historical density surface using
|
370
|
+
Generate historical density surface using various distribution methods.
|
370
371
|
|
371
372
|
Parameters:
|
372
373
|
- model_results: DataFrame with model parameters and maturities
|
373
374
|
- df_hist: DataFrame with historical price data
|
374
375
|
- domain_params: Tuple of (min_log_moneyness, max_log_moneyness, num_points)
|
375
376
|
- return_domain: Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes')
|
377
|
+
- method: Method for density estimation ('normal', 'student_t', 'kde')
|
376
378
|
- centered: Whether to center distributions at their modes (peaks)
|
377
379
|
|
378
380
|
Returns:
|
379
381
|
- Dictionary with pdf_surface, cdf_surface, x_surface, and moments
|
380
382
|
"""
|
381
|
-
logger.info("Calculating HD surface")
|
383
|
+
logger.info(f"Calculating HD surface using {method} method")
|
382
384
|
|
383
385
|
return get_hd_surface(
|
384
386
|
model_results=model_results,
|
385
387
|
df_hist=df_hist,
|
386
388
|
domain_params=domain_params,
|
387
389
|
return_domain=return_domain,
|
390
|
+
method=method,
|
388
391
|
centered=centered
|
389
392
|
)
|
@@ -9,6 +9,7 @@ import pandas as pd
|
|
9
9
|
import datetime as dt
|
10
10
|
from typing import Dict, Tuple, Any, Optional, List
|
11
11
|
from scipy import stats
|
12
|
+
from scipy.stats import t as student_t
|
12
13
|
from voly.utils.logger import logger, catch_exception
|
13
14
|
from voly.exceptions import VolyError
|
14
15
|
from voly.core.rnd import get_all_moments
|
@@ -89,6 +90,42 @@ def get_historical_data(currency: str,
|
|
89
90
|
return df_hist
|
90
91
|
|
91
92
|
|
93
|
+
@catch_exception
|
94
|
+
def calculate_historical_returns(df_hist: pd.DataFrame, n_periods: int) -> Tuple[np.ndarray, np.ndarray]:
|
95
|
+
"""
|
96
|
+
Calculate historical returns and scale them appropriately.
|
97
|
+
|
98
|
+
Parameters:
|
99
|
+
-----------
|
100
|
+
df_hist : pd.DataFrame
|
101
|
+
Historical price data
|
102
|
+
n_periods : int
|
103
|
+
Number of periods to scale returns
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
--------
|
107
|
+
Tuple[np.ndarray, np.ndarray]
|
108
|
+
(scaled_returns, raw_returns) tuple
|
109
|
+
"""
|
110
|
+
# Calculate log returns
|
111
|
+
raw_returns = np.log(df_hist['close'] / df_hist['close'].shift(1)).dropna().values
|
112
|
+
|
113
|
+
# Filter historical data based on n_periods
|
114
|
+
if len(raw_returns) < n_periods:
|
115
|
+
logger.warning(f"Not enough historical data, using all {len(raw_returns)} points available")
|
116
|
+
dte_returns = raw_returns
|
117
|
+
else:
|
118
|
+
dte_returns = raw_returns[-n_periods:]
|
119
|
+
|
120
|
+
# Calculate scaling factor
|
121
|
+
scaling_factor = np.sqrt(n_periods)
|
122
|
+
|
123
|
+
# Scale returns for the maturity
|
124
|
+
scaled_returns = dte_returns * scaling_factor
|
125
|
+
|
126
|
+
return scaled_returns, dte_returns
|
127
|
+
|
128
|
+
|
92
129
|
@catch_exception
|
93
130
|
def calculate_normal_hd(df_hist: pd.DataFrame,
|
94
131
|
t: float,
|
@@ -120,19 +157,12 @@ def calculate_normal_hd(df_hist: pd.DataFrame,
|
|
120
157
|
LM = domains['log_moneyness']
|
121
158
|
dx = domains['dx']
|
122
159
|
|
123
|
-
#
|
124
|
-
|
160
|
+
# Get scaled returns
|
161
|
+
scaled_returns, dte_returns = calculate_historical_returns(df_hist, n_periods)
|
125
162
|
|
126
|
-
#
|
127
|
-
|
128
|
-
|
129
|
-
dte_returns = returns
|
130
|
-
else:
|
131
|
-
dte_returns = returns[-n_periods:]
|
132
|
-
|
133
|
-
# Calculate scaled parameters for normal distribution
|
134
|
-
mu_scaled = np.mean(dte_returns) * np.sqrt(n_periods)
|
135
|
-
sigma_scaled = np.std(dte_returns) * np.sqrt(n_periods)
|
163
|
+
# Calculate parameters for normal distribution
|
164
|
+
mu_scaled = np.mean(scaled_returns)
|
165
|
+
sigma_scaled = np.std(scaled_returns)
|
136
166
|
|
137
167
|
# Apply Girsanov adjustment to shift to risk-neutral measure
|
138
168
|
expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
|
@@ -151,14 +181,149 @@ def calculate_normal_hd(df_hist: pd.DataFrame,
|
|
151
181
|
return pdfs
|
152
182
|
|
153
183
|
|
184
|
+
@catch_exception
|
185
|
+
def calculate_student_t_hd(df_hist: pd.DataFrame,
|
186
|
+
t: float,
|
187
|
+
r: float,
|
188
|
+
n_periods: int,
|
189
|
+
domains: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
190
|
+
"""
|
191
|
+
Calculate historical density using Student's t-distribution based on historical returns.
|
192
|
+
|
193
|
+
Parameters:
|
194
|
+
-----------
|
195
|
+
df_hist : pd.DataFrame
|
196
|
+
Historical price data
|
197
|
+
t : float
|
198
|
+
Time to maturity in years
|
199
|
+
r : float
|
200
|
+
Risk-free rate
|
201
|
+
n_periods : int
|
202
|
+
Number of periods to scale returns
|
203
|
+
domains : Dict[str, np.ndarray]
|
204
|
+
Domain arrays
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
--------
|
208
|
+
Dict[str, np.ndarray]
|
209
|
+
Dictionary of PDFs in different domains
|
210
|
+
"""
|
211
|
+
# Extract log-moneyness domain
|
212
|
+
LM = domains['log_moneyness']
|
213
|
+
dx = domains['dx']
|
214
|
+
|
215
|
+
# Get scaled returns
|
216
|
+
scaled_returns, dte_returns = calculate_historical_returns(df_hist, n_periods)
|
217
|
+
|
218
|
+
# Calculate parameters for t-distribution
|
219
|
+
mu_scaled = np.mean(scaled_returns)
|
220
|
+
sigma_scaled = np.std(scaled_returns)
|
221
|
+
|
222
|
+
# Estimate excess kurtosis and calculate degrees of freedom
|
223
|
+
kurtosis = stats.kurtosis(dte_returns, fisher=True)
|
224
|
+
|
225
|
+
# Convert kurtosis to degrees of freedom (df)
|
226
|
+
# For t-distribution: kurtosis = 6/(df-4) for df > 4
|
227
|
+
# Solve for df: df = 6/kurtosis + 4
|
228
|
+
if kurtosis > 0:
|
229
|
+
df = min(max(6 / kurtosis + 4, 3), 30) # Bound between 3 and 30
|
230
|
+
else:
|
231
|
+
df = 5 # Default value if kurtosis calculation fails
|
232
|
+
|
233
|
+
logger.info(f"Estimated degrees of freedom for t-distribution: {df:.2f}")
|
234
|
+
|
235
|
+
# Apply Girsanov adjustment to shift to risk-neutral measure
|
236
|
+
expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
|
237
|
+
adjustment = mu_scaled - expected_risk_neutral_mean
|
238
|
+
mu_rn = mu_scaled - adjustment
|
239
|
+
|
240
|
+
# Scale parameter for t-distribution
|
241
|
+
# In scipy's t-distribution, the scale parameter is different from normal std
|
242
|
+
# For t-distribution: variance = (df/(df-2)) * scale^2
|
243
|
+
# So: scale = sqrt(variance * (df-2)/df)
|
244
|
+
scale = sigma_scaled * np.sqrt((df - 2) / df) if df > 2 else sigma_scaled
|
245
|
+
|
246
|
+
# Calculate PDF using t-distribution in log-moneyness domain
|
247
|
+
pdf_lm = student_t.pdf(LM, df=df, loc=mu_rn, scale=scale)
|
248
|
+
|
249
|
+
# Normalize the PDF
|
250
|
+
pdf_lm = normalize_density(pdf_lm, dx)
|
251
|
+
|
252
|
+
# Transform to other domains
|
253
|
+
pdfs = transform_to_domains(pdf_lm, domains)
|
254
|
+
|
255
|
+
return pdfs
|
256
|
+
|
257
|
+
|
258
|
+
@catch_exception
|
259
|
+
def calculate_kde_hd(df_hist: pd.DataFrame,
|
260
|
+
t: float,
|
261
|
+
r: float,
|
262
|
+
n_periods: int,
|
263
|
+
domains: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
264
|
+
"""
|
265
|
+
Calculate historical density using Kernel Density Estimation (KDE) based on historical returns.
|
266
|
+
|
267
|
+
Parameters:
|
268
|
+
-----------
|
269
|
+
df_hist : pd.DataFrame
|
270
|
+
Historical price data
|
271
|
+
t : float
|
272
|
+
Time to maturity in years
|
273
|
+
r : float
|
274
|
+
Risk-free rate
|
275
|
+
n_periods : int
|
276
|
+
Number of periods to scale returns
|
277
|
+
domains : Dict[str, np.ndarray]
|
278
|
+
Domain arrays
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
--------
|
282
|
+
Dict[str, np.ndarray]
|
283
|
+
Dictionary of PDFs in different domains
|
284
|
+
"""
|
285
|
+
# Extract log-moneyness domain
|
286
|
+
LM = domains['log_moneyness']
|
287
|
+
dx = domains['dx']
|
288
|
+
|
289
|
+
# Get scaled returns
|
290
|
+
scaled_returns, dte_returns = calculate_historical_returns(df_hist, n_periods)
|
291
|
+
|
292
|
+
# Calculate parameters (for Girsanov adjustment)
|
293
|
+
mu_scaled = np.mean(scaled_returns)
|
294
|
+
sigma_scaled = np.std(scaled_returns)
|
295
|
+
|
296
|
+
# Apply Girsanov adjustment to shift to risk-neutral measure
|
297
|
+
expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
|
298
|
+
adjustment = mu_scaled - expected_risk_neutral_mean
|
299
|
+
|
300
|
+
# Shift the returns to be risk-neutral
|
301
|
+
rn_returns = scaled_returns - adjustment + expected_risk_neutral_mean
|
302
|
+
|
303
|
+
# Fit KDE model using scipy's gaussian_kde with Scott's rule for bandwidth
|
304
|
+
kde = stats.gaussian_kde(rn_returns, bw_method='scott')
|
305
|
+
|
306
|
+
# Evaluate KDE at points in log-moneyness domain
|
307
|
+
pdf_lm = kde(LM)
|
308
|
+
|
309
|
+
# Normalize the PDF
|
310
|
+
pdf_lm = normalize_density(pdf_lm, dx)
|
311
|
+
|
312
|
+
# Transform to other domains
|
313
|
+
pdfs = transform_to_domains(pdf_lm, domains)
|
314
|
+
|
315
|
+
return pdfs
|
316
|
+
|
317
|
+
|
154
318
|
@catch_exception
|
155
319
|
def get_hd_surface(model_results: pd.DataFrame,
|
156
320
|
df_hist: pd.DataFrame,
|
157
321
|
domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
|
158
322
|
return_domain: str = 'log_moneyness',
|
323
|
+
method: str = 'normal',
|
159
324
|
centered: bool = False) -> Dict[str, Any]:
|
160
325
|
"""
|
161
|
-
Generate historical density surface using
|
326
|
+
Generate historical density surface using various distribution methods.
|
162
327
|
|
163
328
|
Parameters:
|
164
329
|
-----------
|
@@ -170,6 +335,8 @@ def get_hd_surface(model_results: pd.DataFrame,
|
|
170
335
|
(min_log_moneyness, max_log_moneyness, num_points)
|
171
336
|
return_domain : str
|
172
337
|
Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes')
|
338
|
+
method : str
|
339
|
+
Method for estimating density ('normal', 'student_t', 'kde')
|
173
340
|
centered : bool
|
174
341
|
Whether to center distributions at their modes (peaks)
|
175
342
|
|
@@ -192,6 +359,22 @@ def get_hd_surface(model_results: pd.DataFrame,
|
|
192
359
|
if return_domain not in valid_domains:
|
193
360
|
raise VolyError(f"Invalid return_domain: {return_domain}. Must be one of {valid_domains}")
|
194
361
|
|
362
|
+
# Validate method
|
363
|
+
valid_methods = ['normal', 'student_t', 'kde']
|
364
|
+
if method not in valid_methods:
|
365
|
+
raise VolyError(f"Invalid method: {method}. Must be one of {valid_methods}")
|
366
|
+
|
367
|
+
# Select calculation function based on method
|
368
|
+
if method == 'student_t':
|
369
|
+
calculate_hd = calculate_student_t_hd
|
370
|
+
logger.info("Using Student's t-distribution for historical density")
|
371
|
+
elif method == 'kde':
|
372
|
+
calculate_hd = calculate_kde_hd
|
373
|
+
logger.info("Using Kernel Density Estimation (KDE) for historical density")
|
374
|
+
else: # default to normal
|
375
|
+
calculate_hd = calculate_normal_hd
|
376
|
+
logger.info("Using normal distribution for historical density")
|
377
|
+
|
195
378
|
# Determine granularity from data (minutes between data points)
|
196
379
|
time_diff = (df_hist.index[1] - df_hist.index[0]).total_seconds() / 60
|
197
380
|
minutes_per_period = max(1, int(time_diff))
|
@@ -217,8 +400,8 @@ def get_hd_surface(model_results: pd.DataFrame,
|
|
217
400
|
# Prepare domains
|
218
401
|
domains = prepare_domains(domain_params, s)
|
219
402
|
|
220
|
-
# Calculate density
|
221
|
-
pdfs =
|
403
|
+
# Calculate density using the selected method
|
404
|
+
pdfs = calculate_hd(
|
222
405
|
df_hist=df_hist,
|
223
406
|
t=t,
|
224
407
|
r=r,
|
@@ -239,11 +422,11 @@ def get_hd_surface(model_results: pd.DataFrame,
|
|
239
422
|
all_moments[i] = moments
|
240
423
|
|
241
424
|
except Exception as e:
|
242
|
-
logger.warning(f"Failed to calculate HD for maturity {i}: {str(e)}")
|
425
|
+
logger.warning(f"Failed to calculate HD for maturity {i} using {method} method: {str(e)}")
|
243
426
|
|
244
427
|
# Check if we have any valid results
|
245
428
|
if not pdf_surface:
|
246
|
-
raise VolyError("No valid densities could be calculated. Check your input data.")
|
429
|
+
raise VolyError(f"No valid densities could be calculated using {method} method. Check your input data.")
|
247
430
|
|
248
431
|
# Center distributions if requested
|
249
432
|
if centered:
|
@@ -253,7 +436,7 @@ def get_hd_surface(model_results: pd.DataFrame,
|
|
253
436
|
# Create DataFrame with moments
|
254
437
|
moments = pd.DataFrame(all_moments).T
|
255
438
|
|
256
|
-
logger.info("Historical density calculation complete using
|
439
|
+
logger.info(f"Historical density calculation complete using {method} distribution")
|
257
440
|
|
258
441
|
return {
|
259
442
|
'pdf_surface': pdf_surface,
|
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
|