voly 0.0.111__tar.gz → 0.0.113__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.111/src/voly.egg-info → voly-0.0.113}/PKG-INFO +1 -1
  2. {voly-0.0.111 → voly-0.0.113}/pyproject.toml +2 -2
  3. {voly-0.0.111 → voly-0.0.113}/src/voly/client.py +62 -2
  4. voly-0.0.113/src/voly/core/hd.py +371 -0
  5. {voly-0.0.111 → voly-0.0.113}/src/voly/formulas.py +17 -89
  6. {voly-0.0.111 → voly-0.0.113/src/voly.egg-info}/PKG-INFO +1 -1
  7. voly-0.0.111/src/voly/core/hd.py +0 -70
  8. {voly-0.0.111 → voly-0.0.113}/LICENSE +0 -0
  9. {voly-0.0.111 → voly-0.0.113}/README.md +0 -0
  10. {voly-0.0.111 → voly-0.0.113}/setup.cfg +0 -0
  11. {voly-0.0.111 → voly-0.0.113}/setup.py +0 -0
  12. {voly-0.0.111 → voly-0.0.113}/src/voly/__init__.py +0 -0
  13. {voly-0.0.111 → voly-0.0.113}/src/voly/core/__init__.py +0 -0
  14. {voly-0.0.111 → voly-0.0.113}/src/voly/core/charts.py +0 -0
  15. {voly-0.0.111 → voly-0.0.113}/src/voly/core/data.py +0 -0
  16. {voly-0.0.111 → voly-0.0.113}/src/voly/core/fit.py +0 -0
  17. {voly-0.0.111 → voly-0.0.113}/src/voly/core/interpolate.py +0 -0
  18. {voly-0.0.111 → voly-0.0.113}/src/voly/core/rnd.py +0 -0
  19. {voly-0.0.111 → voly-0.0.113}/src/voly/exceptions.py +0 -0
  20. {voly-0.0.111 → voly-0.0.113}/src/voly/models.py +0 -0
  21. {voly-0.0.111 → voly-0.0.113}/src/voly/utils/__init__.py +0 -0
  22. {voly-0.0.111 → voly-0.0.113}/src/voly/utils/logger.py +0 -0
  23. {voly-0.0.111 → voly-0.0.113}/src/voly.egg-info/SOURCES.txt +0 -0
  24. {voly-0.0.111 → voly-0.0.113}/src/voly.egg-info/dependency_links.txt +0 -0
  25. {voly-0.0.111 → voly-0.0.113}/src/voly.egg-info/requires.txt +0 -0
  26. {voly-0.0.111 → voly-0.0.113}/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.111
3
+ Version: 0.0.113
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.111"
7
+ version = "0.0.113"
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.111"
63
+ python_version = "0.0.113"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -15,12 +15,12 @@ from voly.utils.logger import logger, catch_exception, setup_file_logging
15
15
  from voly.exceptions import VolyError
16
16
  from voly.models import SVIModel
17
17
  from voly.formulas import (
18
- d1, d2, bs, delta, gamma, vega, theta, rho, vanna, volga, charm, greeks, iv, pdf_to_calls
18
+ d1, d2, bs, delta, gamma, vega, theta, rho, vanna, volga, charm, greeks, iv
19
19
  )
20
20
  from voly.core.data import fetch_option_chain, process_option_chain
21
21
  from voly.core.fit import fit_model, get_iv_surface
22
22
  from voly.core.rnd import get_rnd_surface
23
- from voly.core.hd import get_historical_data
23
+ from voly.core.hd import get_historical_data, get_hd_surface, get_iv_surface
24
24
  from voly.core.interpolate import interpolate_model
25
25
  from voly.core.charts import (
26
26
  plot_all_smiles, plot_raw_parameters, plot_jw_parameters, plot_fit_performance, plot_3d_surface,
@@ -337,3 +337,63 @@ class VolyClient:
337
337
  )
338
338
 
339
339
  return df_hist
340
+
341
+ @staticmethod
342
+ def get_hd_surface(model_results: pd.DataFrame,
343
+ df_hist: pd.DataFrame,
344
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
345
+ return_domain: str = 'log_moneyness') -> Dict[str, Any]:
346
+
347
+ logger.info("Calculating historical density surface")
348
+
349
+ # Generate the surface
350
+ pdf_surface, cdf_surface, x_surface, moments = get_hd_surface(
351
+ model_results=model_results,
352
+ df_hist=df_hist,
353
+ domain_params=domain_params,
354
+ return_domain=return_domain
355
+ )
356
+
357
+ return {
358
+ 'pdf_surface': pdf_surface,
359
+ 'cdf_surface': cdf_surface,
360
+ 'x_surface': x_surface,
361
+ 'moments': moments
362
+ }
363
+
364
+ @staticmethod
365
+ def get_rv_surface(model_results: pd.DataFrame,
366
+ pdf_surface: Dict[str, np.ndarray],
367
+ x_surface: Dict[str, np.ndarray],
368
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
369
+ return_domain: str = 'log_moneyness') -> Dict[str, Any]:
370
+ """
371
+ Transform historical density surface into a volatility surface.
372
+
373
+ Parameters:
374
+ - model_results: DataFrame from fit_model() with maturity information
375
+ - pdf_surface: Dictionary mapping maturity names to historical density arrays
376
+ - x_surface: Dictionary mapping maturity names to x domain arrays
377
+ - domain_params: Tuple of (min, max, num_points) for the log-moneyness grid
378
+ - return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
379
+
380
+ Returns:
381
+ - Dictionary with implied volatility surface information
382
+ """
383
+
384
+ logger.info("Calculating realized volatility surface")
385
+
386
+ # Generate the surface
387
+ fit_results, iv_surface, x_surface = get_rv_surface(
388
+ model_results=model_results,
389
+ pdf_surface=pdf_surface,
390
+ x_surface=x_surface,
391
+ domain_params=domain_params,
392
+ return_domain=return_domain
393
+ )
394
+
395
+ return {
396
+ 'fit_results': fit_results,
397
+ 'iv_surface': iv_surface,
398
+ 'x_surface': x_surface
399
+ }
@@ -0,0 +1,371 @@
1
+ """
2
+ This module handles calculating historical densities from
3
+ time series of prices and converting them to implied volatility smiles.
4
+ """
5
+
6
+ import ccxt
7
+ import pandas as pd
8
+ import datetime as dt
9
+ from scipy import stats
10
+ from voly.utils.logger import logger, catch_exception
11
+ from voly.exceptions import VolyError
12
+ from voly.core.rnd import get_all_moments
13
+ from voly.formulas import iv
14
+ from voly.core.models import SVIModel, raw_to_jw_params
15
+ from voly.core.fit import fit_model
16
+
17
+
18
+ @catch_exception
19
+ def get_historical_data(currency, lookback_days, granularity, exchange_name):
20
+ """
21
+ Fetch historical OHLCV data for a cryptocurrency.
22
+
23
+ Parameters:
24
+ ----------
25
+ currency : str
26
+ The cryptocurrency to fetch data for (e.g., 'BTC', 'ETH').
27
+ lookback_days : str
28
+ The lookback period in days, formatted as '90d', '30d', etc.
29
+ granularity : str
30
+ The time interval for data points (e.g., '15m', '1h', '1d').
31
+ exchange_name : str
32
+ The exchange to fetch data from (default: 'binance').
33
+
34
+ Returns:
35
+ -------
36
+ df_hist : pandas.DataFrame containing the historical price data with OHLCV columns.
37
+ """
38
+
39
+ try:
40
+ # Get the exchange class from ccxt
41
+ exchange_class = getattr(ccxt, exchange_name.lower())
42
+ exchange = exchange_class({'enableRateLimit': True})
43
+ except (AttributeError, TypeError):
44
+ raise VolyError(f"Exchange '{exchange_name}' not found in ccxt. Please check the exchange name.")
45
+
46
+ # Form the trading pair symbol
47
+ symbol = currency + '/USDT'
48
+
49
+ # Convert lookback_days to timestamp
50
+ if lookback_days.endswith('d'):
51
+ days_ago = int(lookback_days[:-1])
52
+ date_start = (dt.datetime.now() - dt.timedelta(days=days_ago)).strftime('%Y-%m-%d %H:%M:%S')
53
+ else:
54
+ raise VolyError("lookback_days should be in format '90d', '30d', etc.")
55
+
56
+ from_ts = exchange.parse8601(date_start)
57
+ ohlcv_list = []
58
+ ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
59
+ ohlcv_list.append(ohlcv)
60
+ while True:
61
+ from_ts = ohlcv[-1][0]
62
+ new_ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
63
+ ohlcv.extend(new_ohlcv)
64
+ if len(new_ohlcv) != 1000:
65
+ break
66
+
67
+ # Convert to DataFrame
68
+ df_hist = pd.DataFrame(ohlcv, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
69
+ df_hist['date'] = pd.to_datetime(df_hist['date'], unit='ms')
70
+ df_hist.set_index('date', inplace=True)
71
+ df_hist = df_hist.sort_index(ascending=True)
72
+
73
+ print(f"Data fetched successfully: {len(df_hist)} rows from {df_hist.index[0]} to {df_hist.index[-1]}")
74
+
75
+ return df_hist
76
+
77
+
78
+ def generate_lm_points(min_lm, max_lm):
79
+ if min_lm >= max_lm:
80
+ raise ValueError("min_lm must be less than max_lm")
81
+
82
+ max_transformed = np.sqrt(max_lm) if max_lm > 0 else 0
83
+ min_transformed = -np.sqrt(-min_lm) if min_lm < 0 else 0
84
+
85
+ transformed_points = np.arange(min_transformed, max_transformed + 0.05, 0.05)
86
+ lm_points = np.sign(transformed_points) * transformed_points ** 2
87
+
88
+ lm_points = np.unique(np.round(lm_points, decimals=2))
89
+ lm_points = sorted(lm_points)
90
+
91
+ return lm_points
92
+
93
+
94
+ @catch_exception
95
+ def get_hd_surface(model_results: pd.DataFrame,
96
+ df_hist: pd.DataFrame,
97
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
98
+ return_domain: str = 'log_moneyness') -> Tuple[
99
+ Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray], pd.DataFrame]:
100
+
101
+ # Check if required columns are present
102
+ required_columns = ['s', 't', 'r']
103
+ missing_columns = [col for col in required_columns if col not in model_results.columns]
104
+ if missing_columns:
105
+ raise VolyError(f"Required columns missing in model_results: {missing_columns}")
106
+
107
+ # Determine granularity from df_hist
108
+ if len(df_hist) > 1:
109
+ # Calculate minutes between consecutive timestamps
110
+ minutes_diff = (df_hist.index[1] - df_hist.index[0]).total_seconds() / 60
111
+ minutes_per_period = int(minutes_diff)
112
+ else:
113
+ VolyError("Cannot determine granularity from df_hist.")
114
+ return
115
+
116
+ pdf_surface = {}
117
+ cdf_surface = {}
118
+ x_surface = {}
119
+ all_moments = {}
120
+
121
+ # Process each maturity
122
+ for i in model_results.index:
123
+ # Get parameters for this maturity
124
+ s = model_results.loc[i, 's']
125
+ r = model_results.loc[i, 'r']
126
+ t = model_results.loc[i, 't']
127
+
128
+ LM = get_domain(domain_params, s, r, None, t, 'log_moneyness')
129
+ M = get_domain(domain_params, s, r, None, t, 'moneyness')
130
+ R = get_domain(domain_params, s, r, None, t, 'returns')
131
+ K = get_domain(domain_params, s, r, None, t, 'log_moneyness')
132
+
133
+ # Filter historical data for this maturity's lookback period
134
+ start_date = dt.datetime.now() - dt.timedelta(days=int(t * 365.25))
135
+ maturity_hist = df_hist[df_hist.index >= start_date].copy()
136
+
137
+ if len(maturity_hist) < 10:
138
+ logger.warning(f"Not enough historical data for maturity {i}, skipping.")
139
+ continue
140
+
141
+ # Calculate the number of periods that match the time to expiry
142
+ n_periods = int(t * 365.25 * 24 * 60 / minutes_per_period)
143
+
144
+ # Compute returns and weights
145
+ maturity_hist['returns'] = np.log(maturity_hist['close'] / maturity_hist['close'].shift(1)) * np.sqrt(n_periods)
146
+ maturity_hist = maturity_hist.dropna()
147
+
148
+ returns = maturity_hist['returns'].values
149
+
150
+ if len(returns) < 10:
151
+ logger.warning(f"Not enough valid returns for maturity {i}, skipping.")
152
+ continue
153
+
154
+ mu_scaled = returns.mean()
155
+ sigma_scaled = returns.std()
156
+
157
+ # Correct Girsanov adjustment to match the risk-neutral mean
158
+ expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
159
+ adjustment = mu_scaled - expected_risk_neutral_mean
160
+ adj_returns = returns - adjustment # Shift the mean to risk-neutral
161
+
162
+ # Create HD and Normalize
163
+ f = stats.gaussian_kde(adj_returns, bw_method='silverman', weights=weights)
164
+ hd_lm = f(LM)
165
+ hd_lm = np.maximum(hd_lm, 0)
166
+ total_area = np.trapz(hd_lm, LM)
167
+ if total_area > 0:
168
+ pdf_lm = hd_lm / total_area
169
+ else:
170
+ logger.warning(f"Total area is zero for maturity {i}, skipping.")
171
+ continue
172
+
173
+ pdf_k = pdf_lm / K
174
+ pdf_m = pdf_k * s
175
+ pdf_r = pdf_lm / (1 + R)
176
+
177
+ cdf = np.concatenate(([0], np.cumsum(pdf_lm[:-1] * np.diff(LM))))
178
+
179
+ if return_domain == 'log_moneyness':
180
+ x = LM
181
+ pdf = pdf_lm
182
+ moments = get_all_moments(x, pdf)
183
+ elif return_domain == 'moneyness':
184
+ x = M
185
+ pdf = pdf_m
186
+ moments = get_all_moments(x, pdf)
187
+ elif return_domain == 'returns':
188
+ x = R
189
+ pdf = pdf_r
190
+ moments = get_all_moments(x, pdf)
191
+ elif return_domain == 'strikes':
192
+ x = K
193
+ pdf = pdf_k
194
+ moments = get_all_moments(x, pdf)
195
+
196
+ # Store results
197
+ pdf_surface[i] = pdf
198
+ cdf_surface[i] = cdf
199
+ x_surface[i] = x
200
+ all_moments[i] = moments
201
+
202
+ # Create a DataFrame with moments using the same index as model_results
203
+ moments = pd.DataFrame(all_moments).T
204
+
205
+ return hd_surface, cdf_surface, x_surface, moments
206
+
207
+
208
+ @catch_exception
209
+ def get_rv_surface(model_results: pd.DataFrame,
210
+ pdf_surface: Dict[str, np.ndarray],
211
+ x_surface: Dict[str, np.ndarray],
212
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
213
+ return_domain: str = 'log_moneyness') -> Tuple[
214
+ Dict[str, np.ndarray], Dict[str, np.ndarray], pd.DataFrame]:
215
+
216
+ # Check if required columns are present
217
+ required_columns = ['s', 't', 'r']
218
+ missing_columns = [col for col in required_columns if col not in model_results.columns]
219
+ if missing_columns:
220
+ raise VolyError(f"Required columns missing in model_results: {missing_columns}")
221
+
222
+ iv_surface = {}
223
+ new_x_surface = {}
224
+ all_params = {}
225
+
226
+ # Check if hd_surface is empty
227
+ if not hd_surface:
228
+ logger.warning("Historical density surface is empty.")
229
+ return {}, {}, pd.DataFrame()
230
+
231
+ # Process each maturity
232
+ for i in model_results.index:
233
+ if i not in hd_surface:
234
+ logger.warning(f"No historical density available for maturity {i}, skipping.")
235
+ continue
236
+
237
+ # Get parameters for this maturity
238
+ s = model_results.loc[i, 's']
239
+ r = model_results.loc[i, 'r']
240
+ t = model_results.loc[i, 't']
241
+
242
+ # Get historical density for this maturity
243
+ pdf = pdf_surface[i]
244
+ x = x_surface[i]
245
+
246
+ # Calculate x_domain grids
247
+ LM = get_domain(domain_params, s, r, None, t, 'log_moneyness')
248
+ M = get_domain(domain_params, s, r, None, t, 'moneyness')
249
+ R = get_domain(domain_params, s, r, None, t, 'returns')
250
+ K = get_domain(domain_params, s, r, None, t, 'log_moneyness')
251
+
252
+ # Recover call prices from the PDF
253
+ c_recovered = np.zeros_like(LM)
254
+ for j, lm_k in enumerate(LM):
255
+ mask = LM >= lm_k
256
+ if np.any(mask):
257
+ integrand = s * (np.exp(LM[mask]) - np.exp(lm_k)) * hd[mask]
258
+ c_recovered[j] = np.exp(-r * t) * np.trapz(integrand, LM[mask])
259
+
260
+ # Ensure call prices are at least the intrinsic value
261
+ intrinsic_values = np.maximum(s - K, 0)
262
+ c_recovered = np.maximum(c_recovered, intrinsic_values)
263
+
264
+ # Determine min_lm and max_lm based on days to expiry (DTE)
265
+ dte = t * 365.25
266
+ if dte <= 30:
267
+ min_lm, max_lm = -0.3, 0.3
268
+ elif dte <= 90:
269
+ min_lm, max_lm = -0.6, 0.6
270
+ else:
271
+ min_lm, max_lm = -0.9, 0.9
272
+
273
+ # Generate key log-moneyness points
274
+ key_lm_points = generate_lm_points(min_lm, max_lm)
275
+
276
+ # Find the indices of the key log-moneyness points
277
+ key_indices = [np.argmin(np.abs(LM - lm)) for lm in key_lm_points]
278
+ key_lm_actual = LM[key_indices] # Actual log moneyness values
279
+ key_strikes = K[key_indices] # Corresponding strikes
280
+
281
+ # Extract call prices at key log-moneyness points
282
+ key_call_prices = c_recovered[key_indices]
283
+
284
+ # Compute IV at key log-moneyness points using our own iv function
285
+ key_ivs = []
286
+ for j, idx in enumerate(key_indices):
287
+ call_price = key_call_prices[j]
288
+ strike = key_strikes[j]
289
+ if call_price <= 0:
290
+ iv_value = 0.01 # Minimum IV of 1%
291
+ else:
292
+ try:
293
+ iv_value = iv(option_price=call_price, s=s, K=strike, r=r, t=t, option_type='call')
294
+ iv_value = max(0.01, min(iv_value, 3.0)) # Clamp between 1% and 300%
295
+ except Exception as e:
296
+ logger.warning(f"IV calculation failed for strike {strike}: {str(e)}")
297
+ iv_value = 0.01 # Fallback to 1%
298
+ key_ivs.append(iv_value)
299
+ key_ivs = np.array(key_ivs)
300
+
301
+ # Create a synthetic option chain for SVI fitting
302
+ # Convert to DataFrame columns that fit_model expects
303
+ synthetic_chain = pd.DataFrame({
304
+ 'maturity_name': [i] * len(key_strikes),
305
+ 'maturity_date': pd.Timestamp.now() + pd.Timedelta(days=int(t * 365.25)),
306
+ 'index_price': s,
307
+ 'underlying_price': s,
308
+ 'strike': key_strikes,
309
+ 'log_moneyness': key_lm_actual,
310
+ 'mark_iv': key_ivs,
311
+ 't': t,
312
+ 'r': r,
313
+ 'option_type': 'call'
314
+ })
315
+
316
+ # Fit the SVI model to the recovered IVs
317
+ fit_results_rv = fit_model(option_chain=synthetic_chain, model_name='svi')
318
+
319
+ # Get the parameters for this maturity
320
+ a = fit_results_rv.loc[i, 'a']
321
+ b = fit_results_rv.loc[i, 'b']
322
+ sigma = fit_results_rv.loc[i, 'sigma']
323
+ rho = fit_results_rv.loc[i, 'rho']
324
+ m = fit_results_rv.loc[i, 'm']
325
+
326
+ # Store the parameters
327
+ params = {
328
+ 's': s,
329
+ 'r': r,
330
+ 't': t,
331
+ 'a': a,
332
+ 'b': b,
333
+ 'sigma': sigma,
334
+ 'rho': rho,
335
+ 'm': m
336
+ }
337
+
338
+ nu, psi, p, c, nu_tilde = raw_to_jw_params(params['a'], params['b'], params['sigma'], params['rho'], params['m'], params['t'])
339
+ params.update({
340
+ 'nu': nu,
341
+ 'psi': psi,
342
+ 'p': p,
343
+ 'c': c,
344
+ 'nu_tilde': nu_tilde,
345
+ })
346
+ all_params[i] = params
347
+
348
+ # Calculate implied volatility using SVI model
349
+ w = np.array([SVIModel.svi(lm, a, b, sigma, rho, m) for lm in LM])
350
+ o_recovered = np.sqrt(w / t)
351
+
352
+ # Store results
353
+ iv_surface[i] = o_recovered
354
+
355
+ if return_domain == 'log_moneyness':
356
+ x = LM
357
+ elif return_domain == 'moneyness':
358
+ x = M
359
+ elif return_domain == 'returns':
360
+ x = R
361
+ elif return_domain == 'strikes':
362
+ x = K
363
+ elif return_domain == 'delta':
364
+ x = get_domain(domain_params, s, r, o_recovered, t, 'delta')
365
+
366
+ new_x_surface[i] = x
367
+
368
+ # Create a DataFrame with parameters
369
+ fit_results = pd.DataFrame(all_params).T
370
+ x_surface = new_x_surface
371
+ return iv_surface, x_surface, fit_results
@@ -4,6 +4,7 @@ Option pricing formulas and general calculations.
4
4
 
5
5
  import numpy as np
6
6
  from scipy.stats import norm
7
+ from py_vollib.black_scholes.implied_volatility import implied_volatility
7
8
  from typing import Tuple, Dict, Union, List, Optional
8
9
  from voly.utils.logger import catch_exception
9
10
  from voly.models import SVIModel
@@ -210,20 +211,17 @@ def greeks(s: float, K: float, r: float, o: float, t: float,
210
211
  @catch_exception
211
212
  @vectorize_inputs
212
213
  def iv(option_price: float, s: float, K: float, r: float, t: float,
213
- option_type: str = 'call', precision: float = 1e-8,
214
- max_iterations: int = 100) -> float:
214
+ option_type: str = 'call') -> float:
215
215
  """
216
- Calculate implied volatility using Newton-Raphson method.
216
+ Calculate implied volatility using py_volib for vectorized computation.
217
217
 
218
218
  Parameters:
219
- - option_price: MarKet price of the option
219
+ - option_price: Market price of the option
220
220
  - s: Underlying price
221
221
  - K: Strike price
222
- - r: RisK-free rate
222
+ - r: Risk-free rate
223
223
  - t: Time to expiry in years
224
224
  - option_type: 'call' or 'put'
225
- - precision: Desired precision
226
- - max_iterations: Maximum number of iterations
227
225
 
228
226
  Returns:
229
227
  - Implied volatility
@@ -245,37 +243,17 @@ def iv(option_price: float, s: float, K: float, r: float, t: float,
245
243
  if option_price >= K:
246
244
  return np.inf # Price exceeds strike
247
245
 
248
- # Initial guess - Manaster and Koehler (1982) method
249
- o = np.sqrt(2 * np.pi / t) * option_price / s
250
-
251
- # Ensure initial guess is reasonable
252
- o = max(0.001, min(o, 5.0))
253
-
254
- for _ in range(max_iterations):
255
- # Calculate option price and vega with current volatility
256
- price = bs(s, K, r, o, t, option_type)
257
- v = vega(s, K, r, o, t, option_type)
258
-
259
- # Calculate price difference
260
- price_diff = price - option_price
261
-
262
- # Check if precision reached
263
- if abs(price_diff) < precision:
264
- return o
265
-
266
- # Avoid division by zero
267
- if abs(v) < 1e-10:
268
- # Change direction based on whether price is too high or too low
269
- o = o * 1.5 if price_diff < 0 else o * 0.5
270
- else:
271
- # Newton-Raphson update
272
- o = o - price_diff / (v * 100) # Vega is for 1% change
273
-
274
- # Ensure volatility stays in reasonable bounds
275
- o = max(0.001, min(o, 5.0))
276
-
277
- # If we reach here, we didn't converge
278
- return np.nan
246
+ flag = 'c' if option_type.lower() in ["call", "c"] else 'p'
247
+ iv_value = implied_volatility(
248
+ price=option_price,
249
+ S=s,
250
+ K=K,
251
+ t=t,
252
+ r=r,
253
+ q=0.0, # Assume zero dividend yield
254
+ flag=flag
255
+ )
256
+ return iv_value
279
257
 
280
258
 
281
259
  @catch_exception
@@ -355,54 +333,4 @@ def get_domain(domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
355
333
 
356
334
  else:
357
335
  raise ValueError(
358
- f"Invalid return_domain: {return_domain}. Must be one of ['log_moneyness', 'moneyness', 'returns', 'striKes', 'delta'].")
359
-
360
-
361
- @catch_exception
362
- def pdf_to_calls(pdf_K, s, K, r, t):
363
- # Step 0: Normalize the PDF
364
- dK = np.diff(K, prepend=K[0])
365
- total_area = np.sum(pdf_K * dK)
366
- pdf_K_normalized = pdf_K / total_area
367
-
368
- # Step 1: Recover the second derivative
369
- c2_recovered = pdf_K_normalized / np.exp(r * t)
370
-
371
- # Step 2: Use Simpson's rule for smoother first derivatives recovery
372
- c1_recovered = np.zeros_like(K)
373
- for i in range(1, len(K)):
374
- h = K[i] - K[i - 1]
375
- c1_recovered[i] = c1_recovered[i - 1] + (h / 6) * (
376
- c2_recovered[i - 1] + 4 * c2_recovered[(i - 1 + i) // 2] + c2_recovered[i])
377
-
378
- # Step 3: Second integration with Simpson's rule to recover original calls
379
- c_recovered_base = np.zeros_like(K)
380
- for i in range(1, len(K)):
381
- h = K[i] - K[i - 1]
382
- c1_mid = (c1_recovered[i - 1] + c1_recovered[i]) / 2 # Midpoint approximation
383
- c_recovered_base[i] = c_recovered_base[i - 1] + (h / 6) * (c1_recovered[i - 1] + 4 * c1_mid + c1_recovered[i])
384
-
385
- # Determine the integration constants based on boundary conditions
386
- # Find the lowest strike (deep ITM) and highest strike (deep OTM)
387
- K_min = K[0]
388
- K_max = K[-1]
389
-
390
- # Boundary conditions:
391
- # c(K_max) should be close to 0
392
- # c(K_min) should be close to s - K_min*exp(-r*t)
393
- c_min_target = max(0, s - K_min * np.exp(-r * t))
394
- c_max_target = 0 # deep OTM call is worthless
395
-
396
- # Solve for a and b in c_recovered = c_recovered_base + a*K + b
397
- A = np.array([[1, K_min], [1, K_max]])
398
- b_vec = np.array([c_min_target - c_recovered_base[0], c_max_target - c_recovered_base[-1]])
399
- integration_constants = np.linalg.solve(A, b_vec)
400
-
401
- # Apply the correction
402
- ic_b, ic_a = integration_constants[0], integration_constants[1] # ic_a is slope, ic_b is intercept
403
- c_recovered = c_recovered_base + ic_b + ic_a * K
404
-
405
- # Ensure non-negative call prices
406
- c_recovered = np.maximum(c_recovered, 0)
407
-
408
- return c_recovered, ic_a, ic_b
336
+ f"Invalid return_domain: {return_domain}. Must be one of ['log_moneyness', 'moneyness', 'returns', 'strikes', 'delta'].")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.111
3
+ Version: 0.0.113
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -1,70 +0,0 @@
1
- """
2
- This module handles calculating historical densities from
3
- time series of prices and converting them to implied volatility smiles.
4
- """
5
-
6
- import ccxt
7
- import pandas as pd
8
- import datetime as dt
9
- from voly.utils.logger import logger, catch_exception
10
- from voly.exceptions import VolyError
11
-
12
-
13
- @catch_exception
14
- def get_historical_data(currency, lookback_days, granularity, exchange_name):
15
- """
16
- Fetch historical OHLCV data for a cryptocurrency.
17
-
18
- Parameters:
19
- ----------
20
- currency : str
21
- The cryptocurrency to fetch data for (e.g., 'BTC', 'ETH').
22
- lookback_days : str
23
- The lookback period in days, formatted as '90d', '30d', etc.
24
- granularity : str
25
- The time interval for data points (e.g., '15m', '1h', '1d').
26
- exchange_name : str
27
- The exchange to fetch data from (default: 'binance').
28
-
29
- Returns:
30
- -------
31
- df_hist : pandas.DataFrame containing the historical price data with OHLCV columns.
32
- """
33
-
34
- try:
35
- # Get the exchange class from ccxt
36
- exchange_class = getattr(ccxt, exchange_name.lower())
37
- exchange = exchange_class({'enableRateLimit': True})
38
- except (AttributeError, TypeError):
39
- raise VolyError(f"Exchange '{exchange_name}' not found in ccxt. Please check the exchange name.")
40
-
41
- # Form the trading pair symbol
42
- symbol = currency + '/USDT'
43
-
44
- # Convert lookback_days to timestamp
45
- if lookback_days.endswith('d'):
46
- days_ago = int(lookback_days[:-1])
47
- date_start = (dt.datetime.now() - dt.timedelta(days=days_ago)).strftime('%Y-%m-%d %H:%M:%S')
48
- else:
49
- raise VolyError("lookback_days should be in format '90d', '30d', etc.")
50
-
51
- from_ts = exchange.parse8601(date_start)
52
- ohlcv_list = []
53
- ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
54
- ohlcv_list.append(ohlcv)
55
- while True:
56
- from_ts = ohlcv[-1][0]
57
- new_ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
58
- ohlcv.extend(new_ohlcv)
59
- if len(new_ohlcv) != 1000:
60
- break
61
-
62
- # Convert to DataFrame
63
- df_hist = pd.DataFrame(ohlcv, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
64
- df_hist['date'] = pd.to_datetime(df_hist['date'], unit='ms')
65
- df_hist.set_index('date', inplace=True)
66
- df_hist = df_hist.sort_index(ascending=True)
67
-
68
- print(f"Data fetched successfully: {len(df_hist)} rows from {df_hist.index[0]} to {df_hist.index[-1]}")
69
-
70
- return df_hist
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