voly 0.0.156__py3-none-any.whl → 0.0.157__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/client.py CHANGED
@@ -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 normal distributions.
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
  )
voly/core/hd.py CHANGED
@@ -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
- # Calculate log returns
124
- returns = np.log(df_hist['close'] / df_hist['close'].shift(1)).dropna().values
160
+ # Get scaled returns
161
+ scaled_returns, dte_returns = calculate_historical_returns(df_hist, n_periods)
125
162
 
126
- # Filter historical data based on n_periods
127
- if len(returns) < n_periods:
128
- logger.warning(f"Not enough historical data, using all {len(returns)} points available")
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 normal distributions.
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 = calculate_normal_hd(
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 normal distribution")
439
+ logger.info(f"Historical density calculation complete using {method} distribution")
257
440
 
258
441
  return {
259
442
  'pdf_surface': pdf_surface,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.156
3
+ Version: 0.0.157
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
2
- voly/client.py,sha256=ZhHpGSTDsU62FA18EUlkXeWZOul4QSR2hV2P_eyVMwM,14577
2
+ voly/client.py,sha256=-yE1_cBvjkK-BO_kKCYtn4WPbNOhAzT0hsfykU5LvQQ,14761
3
3
  voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
4
4
  voly/formulas.py,sha256=G_soRiPwQlHy6milOAj6TdmBWr-fNZpMvm0joXAMZ90,10767
5
5
  voly/models.py,sha256=o-pHujGfr5Gn8ItckMzLI4Q8yaX9FQaV8UjCxv2zgTY,3364
@@ -7,14 +7,14 @@ voly/core/__init__.py,sha256=bu6fS2I1Pj9fPPnl-zY3L7NqrZSY5Zy6NY2uMUvdhKs,183
7
7
  voly/core/charts.py,sha256=E21OZB5lTY4YL2flgaFJ6s5g3_ExtAQT2zryZZxLPyM,12735
8
8
  voly/core/data.py,sha256=pDeuYhP0GX4RbtlqByvsE3rfHcIkix0BU5MLW8sKIeI,8935
9
9
  voly/core/fit.py,sha256=Tb9eeG7e_2dQTcqt6aqEwFrZdy6jR9rSNqe6tzOdVhQ,9245
10
- voly/core/hd.py,sha256=gQgph4JFv6xbgm82uUf_ek7JTBMkN09CdQilpJ_lPbM,8919
10
+ voly/core/hd.py,sha256=kAQO2ft6vmobW60mokhoZbzVElYC_wy1OeBXhmeCtAg,14850
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.156.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
17
- voly-0.0.156.dist-info/METADATA,sha256=B6HFS29vi8kBKECuW0KrUgUYAWkw522hD3Bfs55CN1A,4115
18
- voly-0.0.156.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
19
- voly-0.0.156.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
20
- voly-0.0.156.dist-info/RECORD,,
16
+ voly-0.0.157.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
17
+ voly-0.0.157.dist-info/METADATA,sha256=5RTJLbGrsh2EQQ4jTBqIGoYF6UDI4_4UnRHsSEDSMM8,4115
18
+ voly-0.0.157.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
19
+ voly-0.0.157.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
20
+ voly-0.0.157.dist-info/RECORD,,
File without changes