voly 0.0.156__tar.gz → 0.0.158__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.
Files changed (26) hide show
  1. {voly-0.0.156/src/voly.egg-info → voly-0.0.158}/PKG-INFO +1 -1
  2. {voly-0.0.156 → voly-0.0.158}/pyproject.toml +2 -2
  3. {voly-0.0.156 → voly-0.0.158}/src/voly/client.py +5 -2
  4. {voly-0.0.156 → voly-0.0.158}/src/voly/core/data.py +103 -0
  5. {voly-0.0.156 → voly-0.0.158}/src/voly/core/hd.py +199 -18
  6. {voly-0.0.156 → voly-0.0.158}/src/voly/formulas.py +0 -1
  7. {voly-0.0.156 → voly-0.0.158/src/voly.egg-info}/PKG-INFO +1 -1
  8. {voly-0.0.156 → voly-0.0.158}/LICENSE +0 -0
  9. {voly-0.0.156 → voly-0.0.158}/README.md +0 -0
  10. {voly-0.0.156 → voly-0.0.158}/setup.cfg +0 -0
  11. {voly-0.0.156 → voly-0.0.158}/setup.py +0 -0
  12. {voly-0.0.156 → voly-0.0.158}/src/voly/__init__.py +0 -0
  13. {voly-0.0.156 → voly-0.0.158}/src/voly/core/__init__.py +0 -0
  14. {voly-0.0.156 → voly-0.0.158}/src/voly/core/charts.py +0 -0
  15. {voly-0.0.156 → voly-0.0.158}/src/voly/core/fit.py +0 -0
  16. {voly-0.0.156 → voly-0.0.158}/src/voly/core/interpolate.py +0 -0
  17. {voly-0.0.156 → voly-0.0.158}/src/voly/core/rnd.py +0 -0
  18. {voly-0.0.156 → voly-0.0.158}/src/voly/exceptions.py +0 -0
  19. {voly-0.0.156 → voly-0.0.158}/src/voly/models.py +0 -0
  20. {voly-0.0.156 → voly-0.0.158}/src/voly/utils/__init__.py +0 -0
  21. {voly-0.0.156 → voly-0.0.158}/src/voly/utils/density.py +0 -0
  22. {voly-0.0.156 → voly-0.0.158}/src/voly/utils/logger.py +0 -0
  23. {voly-0.0.156 → voly-0.0.158}/src/voly.egg-info/SOURCES.txt +0 -0
  24. {voly-0.0.156 → voly-0.0.158}/src/voly.egg-info/dependency_links.txt +0 -0
  25. {voly-0.0.156 → voly-0.0.158}/src/voly.egg-info/requires.txt +0 -0
  26. {voly-0.0.156 → voly-0.0.158}/src/voly.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.156
3
+ Version: 0.0.158
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voly"
7
- version = "0.0.156"
7
+ version = "0.0.158"
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.156"
63
+ python_version = "0.0.158"
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 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
  )
@@ -232,6 +232,109 @@ def process_option_chain(df: pd.DataFrame, currency: str) -> pd.DataFrame:
232
232
  return df
233
233
 
234
234
 
235
+ @catch_exception
236
+ def process_order_book_depth(option_chain, max_depth=5):
237
+ """
238
+ Process the order book depth data to enhance option pricing and weighting.
239
+
240
+ Args:
241
+ option_chain: DataFrame containing option data with 'bids' and 'asks' columns
242
+ max_depth: Maximum number of levels to consider from the order book
243
+
244
+ Returns:
245
+ Enhanced option_chain with additional columns for depth analysis
246
+ """
247
+ # Create new columns for depth analysis
248
+ option_chain['vwap_bid'] = float('nan')
249
+ option_chain['vwap_ask'] = float('nan')
250
+ option_chain['vwap_mid'] = float('nan')
251
+ option_chain['depth_bid_qty'] = float('nan')
252
+ option_chain['depth_ask_qty'] = float('nan')
253
+ option_chain['vwap_bid_iv'] = float('nan')
254
+ option_chain['vwap_ask_iv'] = float('nan')
255
+ option_chain['vwap_mid_iv'] = float('nan')
256
+ option_chain['depth_liquidity'] = float('nan')
257
+
258
+ for idx, row in option_chain.iterrows():
259
+ s = row['underlying_price']
260
+ k = row['strikes']
261
+ t = row['t']
262
+ r = row['interest_rate'] if 'interest_rate' in row else 0.0
263
+ option_type = 'C' if row['option_type'] == 'call' else 'P'
264
+
265
+ # Process bid side
266
+ if 'bids' in row and isinstance(row['bids'], list) and len(row['bids']) > 0:
267
+ # Clean up the bid data
268
+ clean_bids = []
269
+ for bid in row['bids'][:max_depth]: # Limit to max_depth levels
270
+ if len(bid) >= 3:
271
+ # Extract price and quantity, removing 'new' if present
272
+ price = float(bid[1]) if bid[0] == 'new' else float(bid[0])
273
+ qty = float(bid[2]) if bid[0] == 'new' else float(bid[1])
274
+ clean_bids.append((price, qty))
275
+
276
+ if clean_bids:
277
+ # Calculate volume-weighted average price
278
+ total_qty = sum(qty for _, qty in clean_bids)
279
+ vwap_bid = sum(price * qty for price, qty in clean_bids) / total_qty if total_qty > 0 else float('nan')
280
+
281
+ # Calculate IV for VWAP
282
+ try:
283
+ vwap_bid_iv = voly.iv(vwap_bid * s, s, k, r, t, option_type)
284
+ except:
285
+ vwap_bid_iv = float('nan')
286
+
287
+ option_chain.at[idx, 'vwap_bid'] = vwap_bid
288
+ option_chain.at[idx, 'depth_bid_qty'] = total_qty
289
+ option_chain.at[idx, 'vwap_bid_iv'] = vwap_bid_iv
290
+
291
+ # Process ask side
292
+ if 'asks' in row and isinstance(row['asks'], list) and len(row['asks']) > 0:
293
+ # Clean up the ask data
294
+ clean_asks = []
295
+ for ask in row['asks'][:max_depth]: # Limit to max_depth levels
296
+ if len(ask) >= 3:
297
+ # Extract price and quantity, removing 'new' if present
298
+ price = float(ask[1]) if ask[0] == 'new' else float(ask[0])
299
+ qty = float(ask[2]) if ask[0] == 'new' else float(ask[1])
300
+ clean_asks.append((price, qty))
301
+
302
+ if clean_asks:
303
+ # Calculate volume-weighted average price
304
+ total_qty = sum(qty for _, qty in clean_asks)
305
+ vwap_ask = sum(price * qty for price, qty in clean_asks) / total_qty if total_qty > 0 else float('nan')
306
+
307
+ # Calculate IV for VWAP
308
+ try:
309
+ vwap_ask_iv = voly.iv(vwap_ask * s, s, k, r, t, option_type)
310
+ except:
311
+ vwap_ask_iv = float('nan')
312
+
313
+ option_chain.at[idx, 'vwap_ask'] = vwap_ask
314
+ option_chain.at[idx, 'depth_ask_qty'] = total_qty
315
+ option_chain.at[idx, 'vwap_ask_iv'] = vwap_ask_iv
316
+
317
+ # Calculate mid VWAP if both bid and ask are available
318
+ if not np.isnan(option_chain.at[idx, 'vwap_bid']) and not np.isnan(option_chain.at[idx, 'vwap_ask']):
319
+ vwap_mid = (option_chain.at[idx, 'vwap_bid'] + option_chain.at[idx, 'vwap_ask']) / 2
320
+
321
+ # Calculate IV for mid VWAP
322
+ try:
323
+ vwap_mid_iv = voly.iv(vwap_mid * s, s, k, r, t, option_type)
324
+ except:
325
+ vwap_mid_iv = float('nan')
326
+
327
+ option_chain.at[idx, 'vwap_mid'] = vwap_mid
328
+ option_chain.at[idx, 'vwap_mid_iv'] = vwap_mid_iv
329
+
330
+ # Calculate depth liquidity (sum of bid and ask quantities)
331
+ bid_qty = option_chain.at[idx, 'depth_bid_qty'] if not np.isnan(option_chain.at[idx, 'depth_bid_qty']) else 0
332
+ ask_qty = option_chain.at[idx, 'depth_ask_qty'] if not np.isnan(option_chain.at[idx, 'depth_ask_qty']) else 0
333
+ option_chain.at[idx, 'depth_liquidity'] = bid_qty + ask_qty
334
+
335
+ return option_chain
336
+
337
+
235
338
  @catch_exception
236
339
  async def fetch_option_chain(exchange: str = 'deribit',
237
340
  currency: str = 'BTC',
@@ -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
125
-
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:]
160
+ # Get scaled returns
161
+ scaled_returns, dte_returns = calculate_historical_returns(df_hist, n_periods)
132
162
 
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,147 @@ 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
+ # Apply Girsanov adjustment to shift to risk-neutral measure
234
+ expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
235
+ adjustment = mu_scaled - expected_risk_neutral_mean
236
+ mu_rn = mu_scaled - adjustment
237
+
238
+ # Scale parameter for t-distribution
239
+ # In scipy's t-distribution, the scale parameter is different from normal std
240
+ # For t-distribution: variance = (df/(df-2)) * scale^2
241
+ # So: scale = sqrt(variance * (df-2)/df)
242
+ scale = sigma_scaled * np.sqrt((df - 2) / df) if df > 2 else sigma_scaled
243
+
244
+ # Calculate PDF using t-distribution in log-moneyness domain
245
+ pdf_lm = student_t.pdf(LM, df=df, loc=mu_rn, scale=scale)
246
+
247
+ # Normalize the PDF
248
+ pdf_lm = normalize_density(pdf_lm, dx)
249
+
250
+ # Transform to other domains
251
+ pdfs = transform_to_domains(pdf_lm, domains)
252
+
253
+ return pdfs
254
+
255
+
256
+ @catch_exception
257
+ def calculate_kde_hd(df_hist: pd.DataFrame,
258
+ t: float,
259
+ r: float,
260
+ n_periods: int,
261
+ domains: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
262
+ """
263
+ Calculate historical density using Kernel Density Estimation (KDE) based on historical returns.
264
+
265
+ Parameters:
266
+ -----------
267
+ df_hist : pd.DataFrame
268
+ Historical price data
269
+ t : float
270
+ Time to maturity in years
271
+ r : float
272
+ Risk-free rate
273
+ n_periods : int
274
+ Number of periods to scale returns
275
+ domains : Dict[str, np.ndarray]
276
+ Domain arrays
277
+
278
+ Returns:
279
+ --------
280
+ Dict[str, np.ndarray]
281
+ Dictionary of PDFs in different domains
282
+ """
283
+ # Extract log-moneyness domain
284
+ LM = domains['log_moneyness']
285
+ dx = domains['dx']
286
+
287
+ # Get scaled returns
288
+ scaled_returns, dte_returns = calculate_historical_returns(df_hist, n_periods)
289
+
290
+ # Calculate parameters (for Girsanov adjustment)
291
+ mu_scaled = np.mean(scaled_returns)
292
+ sigma_scaled = np.std(scaled_returns)
293
+
294
+ # Apply Girsanov adjustment to shift to risk-neutral measure
295
+ expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
296
+ adjustment = mu_scaled - expected_risk_neutral_mean
297
+
298
+ # Shift the returns to be risk-neutral
299
+ rn_returns = scaled_returns - adjustment + expected_risk_neutral_mean
300
+
301
+ # Fit KDE model using scipy's gaussian_kde with Scott's rule for bandwidth
302
+ kde = stats.gaussian_kde(rn_returns, bw_method='scott')
303
+
304
+ # Evaluate KDE at points in log-moneyness domain
305
+ pdf_lm = kde(LM)
306
+
307
+ # Normalize the PDF
308
+ pdf_lm = normalize_density(pdf_lm, dx)
309
+
310
+ # Transform to other domains
311
+ pdfs = transform_to_domains(pdf_lm, domains)
312
+
313
+ return pdfs
314
+
315
+
154
316
  @catch_exception
155
317
  def get_hd_surface(model_results: pd.DataFrame,
156
318
  df_hist: pd.DataFrame,
157
319
  domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
158
320
  return_domain: str = 'log_moneyness',
321
+ method: str = 'normal',
159
322
  centered: bool = False) -> Dict[str, Any]:
160
323
  """
161
- Generate historical density surface using normal distributions.
324
+ Generate historical density surface using various distribution methods.
162
325
 
163
326
  Parameters:
164
327
  -----------
@@ -170,6 +333,8 @@ def get_hd_surface(model_results: pd.DataFrame,
170
333
  (min_log_moneyness, max_log_moneyness, num_points)
171
334
  return_domain : str
172
335
  Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes')
336
+ method : str
337
+ Method for estimating density ('normal', 'student_t', 'kde')
173
338
  centered : bool
174
339
  Whether to center distributions at their modes (peaks)
175
340
 
@@ -192,6 +357,22 @@ def get_hd_surface(model_results: pd.DataFrame,
192
357
  if return_domain not in valid_domains:
193
358
  raise VolyError(f"Invalid return_domain: {return_domain}. Must be one of {valid_domains}")
194
359
 
360
+ # Validate method
361
+ valid_methods = ['normal', 'student_t', 'kde']
362
+ if method not in valid_methods:
363
+ raise VolyError(f"Invalid method: {method}. Must be one of {valid_methods}")
364
+
365
+ # Select calculation function based on method
366
+ if method == 'student_t':
367
+ calculate_hd = calculate_student_t_hd
368
+ logger.info("Using Student's t-distribution for historical density")
369
+ elif method == 'kde':
370
+ calculate_hd = calculate_kde_hd
371
+ logger.info("Using Kernel Density Estimation (KDE) for historical density")
372
+ else: # default to normal
373
+ calculate_hd = calculate_normal_hd
374
+ logger.info("Using normal distribution for historical density")
375
+
195
376
  # Determine granularity from data (minutes between data points)
196
377
  time_diff = (df_hist.index[1] - df_hist.index[0]).total_seconds() / 60
197
378
  minutes_per_period = max(1, int(time_diff))
@@ -217,8 +398,8 @@ def get_hd_surface(model_results: pd.DataFrame,
217
398
  # Prepare domains
218
399
  domains = prepare_domains(domain_params, s)
219
400
 
220
- # Calculate density
221
- pdfs = calculate_normal_hd(
401
+ # Calculate density using the selected method
402
+ pdfs = calculate_hd(
222
403
  df_hist=df_hist,
223
404
  t=t,
224
405
  r=r,
@@ -239,11 +420,11 @@ def get_hd_surface(model_results: pd.DataFrame,
239
420
  all_moments[i] = moments
240
421
 
241
422
  except Exception as e:
242
- logger.warning(f"Failed to calculate HD for maturity {i}: {str(e)}")
423
+ logger.warning(f"Failed to calculate HD for maturity {i} using {method} method: {str(e)}")
243
424
 
244
425
  # Check if we have any valid results
245
426
  if not pdf_surface:
246
- raise VolyError("No valid densities could be calculated. Check your input data.")
427
+ raise VolyError(f"No valid densities could be calculated using {method} method. Check your input data.")
247
428
 
248
429
  # Center distributions if requested
249
430
  if centered:
@@ -253,7 +434,7 @@ def get_hd_surface(model_results: pd.DataFrame,
253
434
  # Create DataFrame with moments
254
435
  moments = pd.DataFrame(all_moments).T
255
436
 
256
- logger.info("Historical density calculation complete using normal distribution")
437
+ logger.info(f"Historical density calculation complete using {method} distribution")
257
438
 
258
439
  return {
259
440
  'pdf_surface': pdf_surface,
@@ -251,7 +251,6 @@ def iv(option_price: float, s: float, K: float, r: float, t: float,
251
251
  K=K,
252
252
  t=t,
253
253
  r=r,
254
- q=0.0, # Assume zero dividend yield
255
254
  flag=flag
256
255
  )
257
256
  return iv_value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.156
3
+ Version: 0.0.158
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
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