voly 0.0.1__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/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Voly - Options & volatility research package
3
+
4
+ A package for options data analysis, volatility modeling,
5
+ and risk-neutral density estimation.
6
+ """
7
+
8
+ from voly.client import VolyClient
9
+
10
+ __all__ = ["VolyClient"]
voly/client.py ADDED
@@ -0,0 +1,540 @@
1
+ """
2
+ Main client interface for the Voly package.
3
+
4
+ This module provides the VolyClient class, which serves as the main
5
+ entry point for users to interact with the package functionality.
6
+ """
7
+
8
+ import asyncio
9
+ import pandas as pd
10
+ import numpy as np
11
+ from typing import Dict, List, Tuple, Optional, Union, Any, Callable
12
+ import plotly.graph_objects as go
13
+
14
+ from voly.utils.logger import logger, catch_exception, setup_file_logging
15
+ from voly.exceptions import VolyError, ValidationError, DataError
16
+ from voly.models import SVIModel
17
+ from voly.formulas import (
18
+ bs, delta, gamma, vega, theta, rho, vanna, volga, charm, greeks, iv, implied_underlying
19
+ )
20
+ from voly.core.data import fetch_option_chain, process_option_chain
21
+ from voly.core.fit import fit_model
22
+ from voly.core.rnd import calculate_rnd, calculate_pdf, calculate_cdf, calculate_strike_probability
23
+ from voly.core.interpolate import interpolate_model
24
+ from voly.core.charts import (
25
+ plot_volatility_smile, plot_3d_surface, plot_parameters, plot_fit_statistics,
26
+ plot_rnd, plot_pdf, plot_cdf, plot_rnd_all_expiries, plot_rnd_3d,
27
+ plot_rnd_statistics, generate_all_plots, plot_interpolated_surface
28
+ )
29
+
30
+
31
+ class VolyClient:
32
+ def __init__(self, enable_file_logging: bool = False, logs_dir: str = "logs/"):
33
+ """
34
+ Initialize the Voly client.
35
+
36
+ Parameters:
37
+ - enable_file_logging: Whether to enable file-based logging
38
+ - logs_dir: Directory for log files if file logging is enabled
39
+ """
40
+ if enable_file_logging:
41
+ setup_file_logging(logs_dir)
42
+
43
+ logger.info("VolyClient initialized")
44
+ self._loop = None # For async operations
45
+
46
+ def _get_event_loop(self):
47
+ """Get or create an event loop for async operations"""
48
+ try:
49
+ self._loop = asyncio.get_event_loop()
50
+ except RuntimeError:
51
+ self._loop = asyncio.new_event_loop()
52
+ asyncio.set_event_loop(self._loop)
53
+ return self._loop
54
+
55
+ # -------------------------------------------------------------------------
56
+ # Data Fetching and Processing
57
+ # -------------------------------------------------------------------------
58
+
59
+ def get_option_chain(self, exchange: str = 'deribit',
60
+ currency: str = 'BTC',
61
+ depth: bool = False) -> pd.DataFrame:
62
+ """
63
+ Fetch option chain data from the specified exchange.
64
+
65
+ Parameters:
66
+ - exchange: Exchange to fetch data from (currently only 'deribit' is supported)
67
+ - currency: Currency to fetch options for (e.g., 'BTC', 'ETH')
68
+ - depth: Whether to include full order book depth
69
+
70
+ Returns:
71
+ - Processed option chain data as a pandas DataFrame
72
+ """
73
+ logger.info(f"Fetching option chain data from {exchange} for {currency}")
74
+
75
+ loop = self._get_event_loop()
76
+
77
+ try:
78
+ option_chain = loop.run_until_complete(
79
+ fetch_option_chain(exchange, currency, depth)
80
+ )
81
+ return option_chain
82
+ except VolyError as e:
83
+ logger.error(f"Error fetching option chain: {str(e)}")
84
+ raise
85
+
86
+ # -------------------------------------------------------------------------
87
+ # Black-Scholes and Greeks Calculations
88
+ # -------------------------------------------------------------------------
89
+
90
+ @staticmethod
91
+ def bs(s: float, k: float, r: float, vol: float, t: float,
92
+ option_type: str = 'call') -> float:
93
+ """
94
+ Calculate Black-Scholes option price.
95
+
96
+ Parameters:
97
+ - s: Underlying price
98
+ - k: Strike price
99
+ - r: Risk-free rate
100
+ - vol: Volatility
101
+ - t: Time to expiry in years
102
+ - option_type: 'call' or 'put'
103
+
104
+ Returns:
105
+ - Option price
106
+ """
107
+ return bs(s, k, r, vol, t, option_type)
108
+
109
+ @staticmethod
110
+ def delta(s: float, k: float, r: float, vol: float, t: float,
111
+ option_type: str = 'call') -> float:
112
+ """
113
+ Calculate option delta.
114
+
115
+ Parameters:
116
+ - s: Underlying price
117
+ - k: Strike price
118
+ - r: Risk-free rate
119
+ - vol: Volatility
120
+ - t: Time to expiry in years
121
+ - option_type: 'call' or 'put'
122
+
123
+ Returns:
124
+ - Delta value
125
+ """
126
+ return delta(s, k, r, vol, t, option_type)
127
+
128
+ @staticmethod
129
+ def gamma(s: float, k: float, r: float, vol: float, t: float) -> float:
130
+ """
131
+ Calculate option gamma.
132
+
133
+ Parameters:
134
+ - s: Underlying price
135
+ - k: Strike price
136
+ - r: Risk-free rate
137
+ - vol: Volatility
138
+ - t: Time to expiry in years
139
+
140
+ Returns:
141
+ - Gamma value
142
+ """
143
+ return gamma(s, k, r, vol, t)
144
+
145
+ @staticmethod
146
+ def vega(s: float, k: float, r: float, vol: float, t: float) -> float:
147
+ """
148
+ Calculate option vega.
149
+
150
+ Parameters:
151
+ - s: Underlying price
152
+ - k: Strike price
153
+ - r: Risk-free rate
154
+ - vol: Volatility
155
+ - t: Time to expiry in years
156
+
157
+ Returns:
158
+ - Vega value (for 1% change in volatility)
159
+ """
160
+ return vega(s, k, r, vol, t)
161
+
162
+ @staticmethod
163
+ def theta(s: float, k: float, r: float, vol: float, t: float,
164
+ option_type: str = 'call') -> float:
165
+ """
166
+ Calculate option theta.
167
+
168
+ Parameters:
169
+ - s: Underlying price
170
+ - k: Strike price
171
+ - r: Risk-free rate
172
+ - vol: Volatility
173
+ - t: Time to expiry in years
174
+ - option_type: 'call' or 'put'
175
+
176
+ Returns:
177
+ - Theta value (per day)
178
+ """
179
+ return theta(s, k, r, vol, t, option_type)
180
+
181
+ @staticmethod
182
+ def rho(s: float, k: float, r: float, vol: float, t: float,
183
+ option_type: str = 'call') -> float:
184
+ """
185
+ Calculate option rho.
186
+
187
+ Parameters:
188
+ - s: Underlying price
189
+ - k: Strike price
190
+ - r: Risk-free rate
191
+ - vol: Volatility
192
+ - t: Time to expiry in years
193
+ - option_type: 'call' or 'put'
194
+
195
+ Returns:
196
+ - Rho value (for 1% change in interest rate)
197
+ """
198
+ return rho(s, k, r, vol, t, option_type)
199
+
200
+ @staticmethod
201
+ def vanna(s: float, k: float, r: float, vol: float, t: float) -> float:
202
+ """
203
+ Calculate option vanna.
204
+
205
+ Parameters:
206
+ - s: Underlying price
207
+ - k: Strike price
208
+ - r: Risk-free rate
209
+ - vol: Volatility
210
+ - t: Time to expiry in years
211
+
212
+ Returns:
213
+ - Vanna value
214
+ """
215
+ return vanna(s, k, r, vol, t)
216
+
217
+ @staticmethod
218
+ def volga(s: float, k: float, r: float, vol: float, t: float) -> float:
219
+ """
220
+ Calculate option volga (vomma).
221
+
222
+ Parameters:
223
+ - s: Underlying price
224
+ - k: Strike price
225
+ - r: Risk-free rate
226
+ - vol: Volatility
227
+ - t: Time to expiry in years
228
+
229
+ Returns:
230
+ - Volga value
231
+ """
232
+ return volga(s, k, r, vol, t)
233
+
234
+ @staticmethod
235
+ def charm(s: float, k: float, r: float, vol: float, t: float,
236
+ option_type: str = 'call') -> float:
237
+ """
238
+ Calculate option charm (delta decay).
239
+
240
+ Parameters:
241
+ - s: Underlying price
242
+ - k: Strike price
243
+ - r: Risk-free rate
244
+ - vol: Volatility
245
+ - t: Time to expiry in years
246
+ - option_type: 'call' or 'put'
247
+
248
+ Returns:
249
+ - Charm value (per day)
250
+ """
251
+ return charm(s, k, r, vol, t, option_type)
252
+
253
+ @staticmethod
254
+ def greeks(s: float, k: float, r: float, vol: float, t: float,
255
+ option_type: str = 'call') -> Dict[str, float]:
256
+ """
257
+ Calculate all option Greeks.
258
+
259
+ Parameters:
260
+ - s: Underlying price
261
+ - k: Strike price
262
+ - r: Risk-free rate
263
+ - vol: Volatility
264
+ - t: Time to expiry in years
265
+ - option_type: 'call' or 'put'
266
+
267
+ Returns:
268
+ - Dictionary with all Greeks (price, delta, gamma, vega, theta, rho, vanna, volga, charm)
269
+ """
270
+ return greeks(s, k, r, vol, t, option_type)
271
+
272
+ @staticmethod
273
+ def iv(option_price: float, s: float, k: float, r: float, t: float,
274
+ option_type: str = 'call') -> float:
275
+ """
276
+ Calculate implied volatility.
277
+
278
+ Parameters:
279
+ - option_price: Market price of the option
280
+ - s: Underlying price
281
+ - k: Strike price
282
+ - r: Risk-free rate
283
+ - t: Time to expiry in years
284
+ - option_type: 'call' or 'put'
285
+
286
+ Returns:
287
+ - Implied volatility
288
+ """
289
+ return iv(option_price, s, k, r, vol=None, t=t, option_type=option_type)
290
+
291
+ # -------------------------------------------------------------------------
292
+ # Model Fitting
293
+ # -------------------------------------------------------------------------
294
+
295
+ @staticmethod
296
+ def fit_model(market_data: pd.DataFrame,
297
+ model_type: str = 'svi',
298
+ moneyness_range: Tuple[float, float] = (-2, 2),
299
+ num_points: int = 500,
300
+ plot: bool = False) -> Dict[str, Any]:
301
+ """
302
+ Fit a volatility model to market data.
303
+
304
+ Parameters:
305
+ - market_data: DataFrame with market data
306
+ - model_type: Type of model to fit (default: 'svi')
307
+ - moneyness_range: (min, max) range for moneyness grid
308
+ - num_points: Number of points for moneyness grid
309
+ - plot: Whether to generate and return plots
310
+
311
+ Returns:
312
+ - Dictionary with fitting results and optional plots
313
+ """
314
+ logger.info(f"Fitting {model_type.upper()} model to market data")
315
+
316
+ # Fit the model
317
+ fit_results = fit_model(
318
+ market_data=market_data,
319
+ model_type=model_type,
320
+ moneyness_range=moneyness_range,
321
+ num_points=num_points
322
+ )
323
+
324
+ # Generate plots if requested
325
+ if plot:
326
+ logger.info("Generating model fitting plots")
327
+ plots = generate_all_plots(fit_results, market_data=market_data)
328
+ fit_results['plots'] = plots
329
+
330
+ return fit_results
331
+
332
+ # -------------------------------------------------------------------------
333
+ # Risk-Neutral Density (RND)
334
+ # -------------------------------------------------------------------------
335
+
336
+ @staticmethod
337
+ def rnd(fit_results: Dict[str, Any],
338
+ maturity: Optional[str] = None,
339
+ spot_price: float = 1.0,
340
+ plot: bool = False) -> Dict[str, Any]:
341
+ """
342
+ Calculate risk-neutral density from fitted model.
343
+
344
+ Parameters:
345
+ - fit_results: Dictionary with fitting results from fit_model()
346
+ - maturity: Optional maturity name to calculate RND for a specific expiry
347
+ - spot_price: Current spot price
348
+ - plot: Whether to generate and return plots
349
+
350
+ Returns:
351
+ - Dictionary with RND results and optional plots
352
+ """
353
+ logger.info("Calculating risk-neutral density")
354
+
355
+ # Calculate RND
356
+ rnd_results = calculate_rnd(fit_results, maturity, spot_price)
357
+
358
+ # Generate plots if requested
359
+ if plot:
360
+ logger.info("Generating RND plots")
361
+ plots = generate_all_plots(fit_results, rnd_results)
362
+ rnd_results['plots'] = plots
363
+
364
+ return rnd_results
365
+
366
+ @staticmethod
367
+ def pdf(rnd_results: Dict[str, Any],
368
+ maturity: Optional[str] = None,
369
+ plot: bool = False) -> Tuple[np.ndarray, np.ndarray]:
370
+ """
371
+ Calculate probability density function (PDF) from RND results.
372
+
373
+ Parameters:
374
+ - rnd_results: Dictionary with RND results from rnd()
375
+ - maturity: Optional maturity name for a specific expiry
376
+ - plot: Whether to generate and return a plot
377
+
378
+ Returns:
379
+ - Tuple of (prices, pdf_values) and optional plot
380
+ """
381
+ logger.info("Calculating PDF from RND")
382
+
383
+ # Extract required data
384
+ moneyness_grid = rnd_results['moneyness_grid']
385
+ rnd_surface = rnd_results['rnd_surface']
386
+ spot_price = rnd_results['spot_price']
387
+
388
+ # Select maturity
389
+ if maturity is None:
390
+ # Use first maturity if not specified
391
+ maturity = list(rnd_surface.keys())[0]
392
+ elif maturity not in rnd_surface:
393
+ raise ValidationError(f"Maturity '{maturity}' not found in RND results")
394
+
395
+ # Get RND values for the selected maturity
396
+ rnd_values = rnd_surface[maturity]
397
+
398
+ # Calculate PDF
399
+ prices, pdf_values = calculate_pdf(moneyness_grid, rnd_values, spot_price)
400
+
401
+ result = (prices, pdf_values)
402
+
403
+ # Generate plot if requested
404
+ if plot:
405
+ logger.info(f"Generating PDF plot for {maturity}")
406
+ pdf_plot = plot_pdf(
407
+ moneyness_grid, rnd_values, spot_price,
408
+ title=f"Probability Density Function - {maturity}"
409
+ )
410
+ result = (prices, pdf_values, pdf_plot)
411
+
412
+ return result
413
+
414
+ @staticmethod
415
+ def cdf(rnd_results: Dict[str, Any],
416
+ maturity: Optional[str] = None,
417
+ plot: bool = False) -> Tuple[np.ndarray, np.ndarray]:
418
+ """
419
+ Calculate cumulative distribution function (CDF) from RND results.
420
+
421
+ Parameters:
422
+ - rnd_results: Dictionary with RND results from rnd()
423
+ - maturity: Optional maturity name for a specific expiry
424
+ - plot: Whether to generate and return a plot
425
+
426
+ Returns:
427
+ - Tuple of (prices, cdf_values) and optional plot
428
+ """
429
+ logger.info("Calculating CDF from RND")
430
+
431
+ # Extract required data
432
+ moneyness_grid = rnd_results['moneyness_grid']
433
+ rnd_surface = rnd_results['rnd_surface']
434
+ spot_price = rnd_results['spot_price']
435
+
436
+ # Select maturity
437
+ if maturity is None:
438
+ # Use first maturity if not specified
439
+ maturity = list(rnd_surface.keys())[0]
440
+ elif maturity not in rnd_surface:
441
+ raise ValidationError(f"Maturity '{maturity}' not found in RND results")
442
+
443
+ # Get RND values for the selected maturity
444
+ rnd_values = rnd_surface[maturity]
445
+
446
+ # Calculate CDF
447
+ prices, cdf_values = calculate_cdf(moneyness_grid, rnd_values, spot_price)
448
+
449
+ result = (prices, cdf_values)
450
+
451
+ # Generate plot if requested
452
+ if plot:
453
+ logger.info(f"Generating CDF plot for {maturity}")
454
+ cdf_plot = plot_cdf(
455
+ moneyness_grid, rnd_values, spot_price,
456
+ title=f"Cumulative Distribution Function - {maturity}"
457
+ )
458
+ result = (prices, cdf_values, cdf_plot)
459
+
460
+ return result
461
+
462
+ @staticmethod
463
+ def probability(rnd_results: Dict[str, Any],
464
+ target_price: float,
465
+ maturity: Optional[str] = None,
466
+ direction: str = 'above') -> float:
467
+ """
468
+ Calculate the probability of price being above or below a target price.
469
+
470
+ Parameters:
471
+ - rnd_results: Dictionary with RND results from rnd()
472
+ - target_price: Target price level
473
+ - maturity: Optional maturity name for a specific expiry
474
+ - direction: 'above' or 'below'
475
+
476
+ Returns:
477
+ - Probability (0 to 1)
478
+ """
479
+ if direction not in ['above', 'below']:
480
+ raise ValidationError("Direction must be 'above' or 'below'")
481
+
482
+ # Extract required data
483
+ moneyness_grid = rnd_results['moneyness_grid']
484
+ rnd_surface = rnd_results['rnd_surface']
485
+ spot_price = rnd_results['spot_price']
486
+
487
+ # Select maturity
488
+ if maturity is None:
489
+ # Use first maturity if not specified
490
+ maturity = list(rnd_surface.keys())[0]
491
+ elif maturity not in rnd_surface:
492
+ raise ValidationError(f"Maturity '{maturity}' not found in RND results")
493
+
494
+ # Get RND values for the selected maturity
495
+ rnd_values = rnd_surface[maturity]
496
+
497
+ # Calculate probability
498
+ prob = calculate_strike_probability(
499
+ target_price, moneyness_grid, rnd_values, spot_price, direction
500
+ )
501
+
502
+ return prob
503
+
504
+ # -------------------------------------------------------------------------
505
+ # Interpolation
506
+ # -------------------------------------------------------------------------
507
+
508
+ @staticmethod
509
+ def interpolate(fit_results: Dict[str, Any],
510
+ specific_days: Optional[List[int]] = None,
511
+ num_points: int = 10,
512
+ method: str = 'cubic',
513
+ plot: bool = False) -> Dict[str, Any]:
514
+ """
515
+ Interpolate a fitted model to specific days to expiry.
516
+
517
+ Parameters:
518
+ - fit_results: Dictionary with fitting results from fit_model()
519
+ - specific_days: Optional list of specific days to include (e.g., [7, 30, 90, 180])
520
+ - num_points: Number of points for regular grid if specific_days is None
521
+ - method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
522
+ - plot: Whether to generate and return a plot
523
+
524
+ Returns:
525
+ - Dictionary with interpolation results and optional plot
526
+ """
527
+ logger.info(f"Interpolating model with {method} method")
528
+
529
+ # Interpolate the model
530
+ interp_results = interpolate_model(
531
+ fit_results, specific_days, num_points, method
532
+ )
533
+
534
+ # Generate plot if requested
535
+ if plot:
536
+ logger.info("Generating interpolated surface plot")
537
+ interp_plot = plot_interpolated_surface(interp_results)
538
+ interp_results['plot'] = interp_plot
539
+
540
+ return interp_results
voly/core/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ Core functionality for the Voly package.
3
+
4
+ This module contains the core functions for data fetching,
5
+ model fitting, surface interpolation, and risk-neutral density
6
+ estimation.
7
+ """
8
+
9
+ from voly.core.data import get_deribit_data, process_option_chain
10
+ from voly.core.fit import optimize_svi_parameters, create_parameters_matrix
11
+ from voly.core.rnd import calculate_risk_neutral_density