voly 0.0.138__tar.gz → 0.0.139__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.138/src/voly.egg-info → voly-0.0.139}/PKG-INFO +1 -1
  2. {voly-0.0.138 → voly-0.0.139}/pyproject.toml +2 -2
  3. {voly-0.0.138 → voly-0.0.139}/src/voly/client.py +18 -39
  4. voly-0.0.139/src/voly/core/hd.py +455 -0
  5. {voly-0.0.138 → voly-0.0.139}/src/voly/core/rnd.py +6 -1
  6. {voly-0.0.138 → voly-0.0.139/src/voly.egg-info}/PKG-INFO +1 -1
  7. voly-0.0.138/src/voly/core/hd.py +0 -566
  8. {voly-0.0.138 → voly-0.0.139}/LICENSE +0 -0
  9. {voly-0.0.138 → voly-0.0.139}/README.md +0 -0
  10. {voly-0.0.138 → voly-0.0.139}/setup.cfg +0 -0
  11. {voly-0.0.138 → voly-0.0.139}/setup.py +0 -0
  12. {voly-0.0.138 → voly-0.0.139}/src/voly/__init__.py +0 -0
  13. {voly-0.0.138 → voly-0.0.139}/src/voly/core/__init__.py +0 -0
  14. {voly-0.0.138 → voly-0.0.139}/src/voly/core/charts.py +0 -0
  15. {voly-0.0.138 → voly-0.0.139}/src/voly/core/data.py +0 -0
  16. {voly-0.0.138 → voly-0.0.139}/src/voly/core/fit.py +0 -0
  17. {voly-0.0.138 → voly-0.0.139}/src/voly/core/interpolate.py +0 -0
  18. {voly-0.0.138 → voly-0.0.139}/src/voly/exceptions.py +0 -0
  19. {voly-0.0.138 → voly-0.0.139}/src/voly/formulas.py +0 -0
  20. {voly-0.0.138 → voly-0.0.139}/src/voly/models.py +0 -0
  21. {voly-0.0.138 → voly-0.0.139}/src/voly/utils/__init__.py +0 -0
  22. {voly-0.0.138 → voly-0.0.139}/src/voly/utils/logger.py +0 -0
  23. {voly-0.0.138 → voly-0.0.139}/src/voly.egg-info/SOURCES.txt +0 -0
  24. {voly-0.0.138 → voly-0.0.139}/src/voly.egg-info/dependency_links.txt +0 -0
  25. {voly-0.0.138 → voly-0.0.139}/src/voly.egg-info/requires.txt +0 -0
  26. {voly-0.0.138 → voly-0.0.139}/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.138
3
+ Version: 0.0.139
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.138"
7
+ version = "0.0.139"
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.138"
63
+ python_version = "0.0.139"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -342,59 +342,38 @@ class VolyClient:
342
342
  def get_hd_surface(model_results: pd.DataFrame,
343
343
  df_hist: pd.DataFrame,
344
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
- return get_hd_surface(
350
- model_results=model_results,
351
- df_hist=df_hist,
352
- domain_params=domain_params,
353
- return_domain=return_domain
354
- )
355
-
356
- @staticmethod
357
- def get_garch_hd_surface(model_results: pd.DataFrame,
358
- df_hist: pd.DataFrame,
359
- domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
360
- return_domain: str = 'log_moneyness',
361
- n_fits: int = 400,
362
- simulations: int = 5000,
363
- window_length: int = 365,
364
- variate_parameters: bool = True,
365
- bandwidth: float = 0.15) -> Dict[str, Any]:
345
+ return_domain: str = 'log_moneyness',
346
+ method: str = 'garch',
347
+ **kwargs) -> Dict[str, Any]:
366
348
  """
367
- Generate historical density using GARCH(1,1) model and Monte Carlo simulation.
368
-
369
- This method implements the approach from SPD Trading, using:
370
- 1. GARCH(1,1) model fit with sliding windows
371
- 2. Monte Carlo simulation with innovation resampling
372
- 3. Kernel density estimation of terminal prices
349
+ Generate historical density surface from historical price data.
373
350
 
374
351
  Parameters:
375
352
  model_results: DataFrame with model parameters and maturities
376
353
  df_hist: DataFrame with historical price data
377
354
  domain_params: Tuple of (min, max, num_points) for x-domain
378
355
  return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes')
379
- n_fits: Number of sliding windows for GARCH parameter estimation
380
- simulations: Number of Monte Carlo simulations
381
- window_length: Length of each sliding window for GARCH estimation
382
- variate_parameters: Whether to vary GARCH parameters between simulations
383
- bandwidth: Bandwidth for KDE of final density
356
+ method: Method to use for HD estimation ('hist_returns' or 'garch')
357
+ **kwargs: Additional parameters for specific methods:
358
+ For 'garch' method:
359
+ n_fits: Number of sliding windows (default: 400)
360
+ simulations: Number of Monte Carlo simulations (default: 5000)
361
+ window_length: Length of sliding windows (default: 365)
362
+ variate_parameters: Whether to vary GARCH parameters (default: True)
363
+ bandwidth: KDE bandwidth (default: 'silverman')
364
+ For 'hist_returns' method:
365
+ bandwidth: KDE bandwidth (default: 'silverman')
384
366
 
385
367
  Returns:
386
368
  Dictionary containing pdf_surface, cdf_surface, x_surface, and moments
387
369
  """
388
- logger.info("Calculating GARCH historical density surface")
370
+ logger.info(f"Calculating historical density surface using {method} method")
389
371
 
390
- return get_garch_hd_surface(
372
+ return get_hd_surface(
391
373
  model_results=model_results,
392
374
  df_hist=df_hist,
393
375
  domain_params=domain_params,
394
376
  return_domain=return_domain,
395
- n_fits=n_fits,
396
- simulations=simulations,
397
- window_length=window_length,
398
- variate_parameters=variate_parameters,
399
- bandwidth=bandwidth
377
+ method=method,
378
+ **kwargs
400
379
  )
@@ -0,0 +1,455 @@
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 numpy as np
9
+ import datetime as dt
10
+ from scipy import stats
11
+ from typing import Dict, List, Tuple, Optional, Union, Any
12
+ from voly.utils.logger import logger, catch_exception
13
+ from voly.exceptions import VolyError
14
+ from voly.core.rnd import get_all_moments
15
+ from voly.formulas import iv, get_domain
16
+ from voly.models import SVIModel
17
+ from voly.core.fit import fit_model
18
+ from arch import arch_model
19
+
20
+
21
+ @catch_exception
22
+ def get_historical_data(currency, lookback_days, granularity, exchange_name):
23
+ """
24
+ Fetch historical OHLCV data for a cryptocurrency.
25
+
26
+ Parameters:
27
+ ----------
28
+ currency : str
29
+ The cryptocurrency to fetch data for (e.g., 'BTC', 'ETH').
30
+ lookback_days : str
31
+ The lookback period in days, formatted as '90d', '30d', etc.
32
+ granularity : str
33
+ The time interval for data points (e.g., '15m', '1h', '1d').
34
+ exchange_name : str
35
+ The exchange to fetch data from (default: 'binance').
36
+
37
+ Returns:
38
+ -------
39
+ df_hist : pandas.DataFrame containing the historical price data with OHLCV columns.
40
+ """
41
+
42
+ try:
43
+ # Get the exchange class from ccxt
44
+ exchange_class = getattr(ccxt, exchange_name.lower())
45
+ exchange = exchange_class({'enableRateLimit': True})
46
+ except (AttributeError, TypeError):
47
+ raise VolyError(f"Exchange '{exchange_name}' not found in ccxt. Please check the exchange name.")
48
+
49
+ # Form the trading pair symbol
50
+ symbol = currency + '/USDT'
51
+
52
+ # Convert lookback_days to timestamp
53
+ if lookback_days.endswith('d'):
54
+ days_ago = int(lookback_days[:-1])
55
+ date_start = (dt.datetime.now() - dt.timedelta(days=days_ago)).strftime('%Y-%m-%d %H:%M:%S')
56
+ else:
57
+ raise VolyError("lookback_days should be in format '90d', '30d', etc.")
58
+
59
+ from_ts = exchange.parse8601(date_start)
60
+ ohlcv_list = []
61
+ ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
62
+ ohlcv_list.append(ohlcv)
63
+ while True:
64
+ from_ts = ohlcv[-1][0]
65
+ new_ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
66
+ ohlcv.extend(new_ohlcv)
67
+ if len(new_ohlcv) != 1000:
68
+ break
69
+
70
+ # Convert to DataFrame
71
+ df_hist = pd.DataFrame(ohlcv, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
72
+ df_hist['date'] = pd.to_datetime(df_hist['date'], unit='ms')
73
+ df_hist.set_index('date', inplace=True)
74
+ df_hist = df_hist.sort_index(ascending=True)
75
+
76
+ print(f"Data fetched successfully: {len(df_hist)} rows from {df_hist.index[0]} to {df_hist.index[-1]}")
77
+
78
+ return df_hist
79
+
80
+
81
+ @catch_exception
82
+ def fit_garch_model(log_returns, n_fits=400, window_length=365):
83
+ """
84
+ Fit a GARCH(1,1) model to log returns.
85
+
86
+ Args:
87
+ log_returns: Array of log returns
88
+ n_fits: Number of sliding windows
89
+ window_length: Length of each window
90
+
91
+ Returns:
92
+ Dict with GARCH parameters and processes
93
+ """
94
+
95
+ if len(log_returns) < window_length + n_fits:
96
+ raise VolyError(f"Not enough data points. Need at least {window_length + n_fits}, got {len(log_returns)}")
97
+
98
+ # Adjust window sizes if necessary
99
+ n_fits = min(n_fits, len(log_returns) // 3)
100
+ window_length = min(window_length, len(log_returns) // 3)
101
+
102
+ start = window_length + n_fits
103
+ end = n_fits
104
+
105
+ parameters = np.zeros((n_fits, 4)) # [mu, omega, alpha, beta]
106
+ z_process = []
107
+
108
+ logger.info(f"Fitting GARCH model with {n_fits} windows...")
109
+
110
+ for i in range(n_fits):
111
+ window = log_returns[end - i - 1:start - i - 1]
112
+ data = window - np.mean(window)
113
+
114
+ model = arch_model(data, vol='GARCH', p=1, q=1)
115
+ try:
116
+ GARCH_fit = model.fit(disp='off')
117
+
118
+ mu, omega, alpha, beta = [
119
+ GARCH_fit.params["mu"],
120
+ GARCH_fit.params["omega"],
121
+ GARCH_fit.params["alpha[1]"],
122
+ GARCH_fit.params["beta[1]"],
123
+ ]
124
+ parameters[i, :] = [mu, omega, alpha, beta]
125
+
126
+ # Calculate sigma2 and innovations for last observation
127
+ if i == 0:
128
+ sigma2_tm1 = omega / (1 - alpha - beta)
129
+ else:
130
+ e_tm1 = data.tolist()[-2]
131
+ sigma2_tm1 = omega + alpha * e_tm1 ** 2 + beta * sigma2_tm1
132
+
133
+ e_t = data.tolist()[-1]
134
+ sigma2_t = omega + alpha * data.tolist()[-2] ** 2 + beta * sigma2_tm1
135
+ z_t = e_t / np.sqrt(sigma2_t)
136
+ z_process.append(z_t)
137
+
138
+ except Exception as e:
139
+ logger.warning(f"GARCH fit failed for window {i}: {str(e)}")
140
+
141
+ # Clean up any failed fits
142
+ if len(z_process) < n_fits / 2:
143
+ raise VolyError("Too many GARCH fits failed. Check your data.")
144
+
145
+ avg_params = np.mean(parameters, axis=0)
146
+ std_params = np.std(parameters, axis=0)
147
+
148
+ return {
149
+ 'parameters': parameters,
150
+ 'avg_params': avg_params,
151
+ 'std_params': std_params,
152
+ 'z_process': np.array(z_process)
153
+ }
154
+
155
+
156
+ @catch_exception
157
+ def simulate_garch_paths(garch_model, horizon, simulations=5000, variate_parameters=True):
158
+ """
159
+ Simulate future paths using a fitted GARCH model.
160
+
161
+ Args:
162
+ garch_model: Dict with GARCH model parameters
163
+ horizon: Number of steps to simulate
164
+ simulations: Number of paths to simulate
165
+ variate_parameters: Whether to vary parameters between simulations
166
+
167
+ Returns:
168
+ Array of simulated log returns
169
+ """
170
+ parameters = garch_model['parameters']
171
+ z_process = garch_model['z_process']
172
+
173
+ # Use mean parameters as starting point
174
+ pars = garch_model['avg_params'].copy() # [mu, omega, alpha, beta]
175
+ bounds = garch_model['std_params'].copy()
176
+
177
+ mu, omega, alpha, beta = pars
178
+ logger.info(f"GARCH parameters: mu={mu:.6f}, omega={omega:.6f}, alpha={alpha:.6f}, beta={beta:.6f}")
179
+
180
+ # Create KDE for innovations
181
+ kde = stats.gaussian_kde(z_process)
182
+ z_range = np.linspace(min(z_process), max(z_process), 1000)
183
+ z_prob = kde(z_range)
184
+ z_prob = z_prob / np.sum(z_prob)
185
+
186
+ # Simulate paths
187
+ simulated_returns = np.zeros(simulations)
188
+
189
+ for i in range(simulations):
190
+ if (i + 1) % (simulations // 10) == 0:
191
+ logger.info(f"Simulation progress: {i + 1}/{simulations}")
192
+
193
+ # Optionally vary parameters
194
+ if variate_parameters and (i + 1) % (simulations // 20) == 0:
195
+ new_pars = []
196
+ for j, (par, bound) in enumerate(zip(pars, bounds)):
197
+ var = bound ** 2 / len(parameters)
198
+ new_par = np.random.normal(par, var)
199
+ if j >= 1 and new_par <= 0: # Ensure omega, alpha, beta are positive
200
+ new_par = 0.01
201
+ new_pars.append(new_par)
202
+ mu, omega, alpha, beta = new_pars
203
+
204
+ # Initial values
205
+ sigma2 = omega / (1 - alpha - beta)
206
+ returns_sum = 0
207
+
208
+ # Simulate path
209
+ for _ in range(horizon):
210
+ # Sample from innovation distribution
211
+ z = np.random.choice(z_range, p=z_prob)
212
+
213
+ # Calculate return and update volatility
214
+ e = z * np.sqrt(sigma2)
215
+ returns_sum += e + mu
216
+ sigma2 = omega + alpha * e ** 2 + beta * sigma2
217
+
218
+ simulated_returns[i] = returns_sum
219
+
220
+ return simulated_returns, mu * horizon
221
+
222
+
223
+ def get_hd_surface(model_results: pd.DataFrame,
224
+ df_hist: pd.DataFrame,
225
+ domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
226
+ return_domain: str = 'log_moneyness',
227
+ method: str = 'garch',
228
+ **kwargs) -> Dict[str, Any]:
229
+ """
230
+ Generate historical density surface from historical price data.
231
+
232
+ Parameters:
233
+ model_results: DataFrame with model parameters and maturities
234
+ df_hist: DataFrame with historical price data
235
+ domain_params: Tuple of (min, max, num_points) for x-domain
236
+ return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes')
237
+ method: Method to use for HD estimation ('hist_returns' or 'garch')
238
+ **kwargs: Additional parameters for specific methods:
239
+ For 'garch' method:
240
+ n_fits: Number of sliding windows (default: 400)
241
+ simulations: Number of Monte Carlo simulations (default: 5000)
242
+ window_length: Length of sliding windows (default: 365)
243
+ variate_parameters: Whether to vary GARCH parameters (default: True)
244
+ bandwidth: KDE bandwidth (default: 'silverman')
245
+ For 'hist_returns' method:
246
+ bandwidth: KDE bandwidth (default: 'silverman')
247
+
248
+ Returns:
249
+ Dictionary containing pdf_surface, cdf_surface, x_surface, and moments
250
+ """
251
+
252
+ # Check if required columns are present
253
+ required_columns = ['s', 't', 'r']
254
+ missing_columns = [col for col in required_columns if col not in model_results.columns]
255
+ if missing_columns:
256
+ raise VolyError(f"Required columns missing in model_results: {missing_columns}")
257
+
258
+ # Determine granularity from df_hist
259
+ if len(df_hist) > 1:
260
+ # Calculate minutes between consecutive timestamps
261
+ minutes_diff = (df_hist.index[1] - df_hist.index[0]).total_seconds() / 60
262
+ minutes_per_period = int(minutes_diff)
263
+ else:
264
+ raise VolyError("Cannot determine granularity from df_hist.")
265
+
266
+ # Get method-specific parameters
267
+ if method == 'garch':
268
+ n_fits = kwargs.get('n_fits', 400)
269
+ simulations = kwargs.get('simulations', 5000)
270
+ window_length = kwargs.get('window_length', 365)
271
+ variate_parameters = kwargs.get('variate_parameters', True)
272
+ bandwidth = kwargs.get('bandwidth', 'silverman')
273
+ logger.info(f"Using GARCH method with {n_fits} fits, {simulations} simulations")
274
+ elif method == 'hist_returns':
275
+ bandwidth = kwargs.get('bandwidth', 'silverman')
276
+ logger.info(f"Using returns-based KDE method with bandwidth {bandwidth}")
277
+ else:
278
+ raise VolyError(f"Unknown method: {method}. Use 'hist_returns' or 'garch'.")
279
+
280
+ # Calculate log returns from price history
281
+ log_returns = np.log(df_hist['close'] / df_hist['close'].shift(1)) * 100
282
+ log_returns = log_returns.dropna().values
283
+
284
+ # Fit GARCH model once if using garch method
285
+ garch_model = None
286
+ if method == 'garch':
287
+ garch_model = fit_garch_model(log_returns, n_fits, window_length)
288
+
289
+ pdf_surface = {}
290
+ cdf_surface = {}
291
+ x_surface = {}
292
+ all_moments = {}
293
+
294
+ # Process each maturity
295
+ for i in model_results.index:
296
+ # Get parameters for this maturity
297
+ s = model_results.loc[i, 's'] # Current spot price
298
+ r = model_results.loc[i, 'r'] # Risk-free rate
299
+ t = model_results.loc[i, 't'] # Time to maturity in years
300
+
301
+ # Get domain grids
302
+ LM = np.linspace(domain_params[0], domain_params[1], domain_params[2])
303
+ M = np.exp(LM) # Moneyness
304
+ R = M - 1 # Returns
305
+ K = s / M # Strike prices
306
+
307
+ # For time scaling calculations
308
+ tau_days_float = t * 365.25 # Exact number of days
309
+ n_periods = max(1, int(t * 365.25 * 24 * 60 / minutes_per_period))
310
+
311
+ logger.info(f"Processing HD for maturity {i} (t={t:.4f} years, {tau_days_float:.2f} days)")
312
+
313
+ if method == 'hist_returns':
314
+ # Standard returns-based method (your existing implementation)
315
+ # Filter historical data for this maturity's lookback period
316
+ start_date = pd.Timestamp.now() - pd.Timedelta(days=int(t * 365.25))
317
+ maturity_hist = df_hist[df_hist.index >= start_date].copy()
318
+
319
+ if len(maturity_hist) < 10:
320
+ logger.warning(f"Not enough historical data for maturity {i}, skipping.")
321
+ continue
322
+
323
+ # Calculate scaled returns
324
+ maturity_hist['log_returns'] = np.log(maturity_hist['close'] / maturity_hist['close'].shift(1)) * np.sqrt(
325
+ n_periods)
326
+ maturity_hist = maturity_hist.dropna()
327
+
328
+ returns = maturity_hist['log_returns'].values
329
+ if len(returns) < 2:
330
+ logger.warning(f"Not enough valid returns for maturity {i}, skipping.")
331
+ continue
332
+
333
+ # Girsanov adjustment to shift to risk-neutral measure
334
+ mu_scaled = returns.mean()
335
+ sigma_scaled = returns.std()
336
+ expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
337
+ adjustment = mu_scaled - expected_risk_neutral_mean
338
+ adj_returns = returns - adjustment
339
+
340
+ # Create HD and normalize
341
+ f = stats.gaussian_kde(adj_returns, bw_method=bandwidth)
342
+ pdf_values = f(LM)
343
+
344
+ elif method == 'garch':
345
+ # GARCH-based method
346
+ if garch_model is None:
347
+ logger.warning(f"GARCH model fitting failed, skipping maturity {i}")
348
+ continue
349
+
350
+ # Simulate paths with the GARCH model
351
+ horizon = max(1, int(tau_days_float))
352
+ simulated_returns, simulated_mu = simulate_garch_paths(
353
+ garch_model,
354
+ horizon,
355
+ simulations,
356
+ variate_parameters
357
+ )
358
+
359
+ # Scale the simulated returns to match target time horizon
360
+ scaling_factor = np.sqrt(n_periods / tau_days_float)
361
+ scaled_returns = simulated_returns * scaling_factor
362
+
363
+ # Risk-neutral adjustment
364
+ mu_scaled = scaled_returns.mean()
365
+ sigma_scaled = scaled_returns.std()
366
+ expected_risk_neutral_mean = (r - 0.5 * (sigma_scaled / 100) ** 2) * 100 * np.sqrt(t)
367
+ adjustment = mu_scaled - expected_risk_neutral_mean
368
+ risk_neutral_returns = scaled_returns - adjustment
369
+
370
+ # Convert to terminal prices
371
+ simulated_prices = s * np.exp(risk_neutral_returns / 100)
372
+
373
+ # Convert to moneyness domain
374
+ simulated_moneyness = s / simulated_prices
375
+
376
+ # Perform KDE to get PDF
377
+ kde = stats.gaussian_kde(simulated_moneyness, bw_method=bandwidth)
378
+ pdf_values = kde(M)
379
+
380
+ # Include GARCH params in moments
381
+ avg_params = garch_model['avg_params']
382
+ model_params = {
383
+ 'mu': avg_params[0],
384
+ 'omega': avg_params[1],
385
+ 'alpha': avg_params[2],
386
+ 'beta': avg_params[3],
387
+ 'persistence': avg_params[2] + avg_params[3]
388
+ }
389
+ else:
390
+ continue # Skip this maturity if method is invalid
391
+
392
+ # Ensure density integrates to 1
393
+ dx = LM[1] - LM[0]
394
+ total_area = np.sum(pdf_values * dx)
395
+ if total_area <= 0:
396
+ logger.warning(f"Invalid density (area <= 0) for maturity {i}, skipping.")
397
+ continue
398
+
399
+ pdf_values = pdf_values / total_area
400
+
401
+ # Common processing for both methods
402
+
403
+ # Transform densities to various domains
404
+ if method == 'hist_returns':
405
+ pdf_lm = pdf_values
406
+ pdf_m = pdf_lm / M
407
+ pdf_k = pdf_lm / K
408
+ pdf_r = pdf_lm / (1 + R)
409
+ else: # 'garch'
410
+ pdf_m = pdf_values
411
+ pdf_lm = pdf_m * M
412
+ pdf_k = pdf_lm / K
413
+ pdf_r = pdf_lm / (1 + R)
414
+
415
+ # Calculate CDF
416
+ cdf = np.cumsum(pdf_lm * dx)
417
+ cdf = np.minimum(cdf / cdf[-1], 1.0)
418
+
419
+ # Select appropriate domain and calculate moments
420
+ if return_domain == 'log_moneyness':
421
+ x = LM
422
+ pdf = pdf_lm
423
+ moments = get_all_moments(x, pdf, model_params if method == 'garch' else None)
424
+ elif return_domain == 'moneyness':
425
+ x = M
426
+ pdf = pdf_m
427
+ moments = get_all_moments(x, pdf, model_params if method == 'garch' else None)
428
+ elif return_domain == 'returns':
429
+ x = R
430
+ pdf = pdf_r
431
+ moments = get_all_moments(x, pdf, model_params if method == 'garch' else None)
432
+ elif return_domain == 'strikes':
433
+ x = K
434
+ pdf = pdf_k
435
+ moments = get_all_moments(x, pdf, model_params if method == 'garch' else None)
436
+ else:
437
+ raise VolyError(f"Unsupported return_domain: {return_domain}")
438
+
439
+ # Store results
440
+ pdf_surface[i] = pdf
441
+ cdf_surface[i] = cdf
442
+ x_surface[i] = x
443
+ all_moments[i] = moments
444
+
445
+ # Create DataFrame with moments
446
+ moments = pd.DataFrame(all_moments).T
447
+
448
+ logger.info(f"Historical density calculation complete using {method} method")
449
+
450
+ return {
451
+ 'pdf_surface': pdf_surface,
452
+ 'cdf_surface': cdf_surface,
453
+ 'x_surface': x_surface,
454
+ 'moments': moments
455
+ }
@@ -165,7 +165,7 @@ def rookley(domain_params, s, r, o, t, return_domain):
165
165
 
166
166
 
167
167
  @catch_exception
168
- def get_all_moments(x, pdf):
168
+ def get_all_moments(x, pdf, model_params=None):
169
169
  mean = np.trapz(x * pdf, x) # E[X]
170
170
  median = x[np.searchsorted(np.cumsum(pdf * np.diff(x, prepend=x[0])), 0.5)] # Median (50th percentile)
171
171
  mode = x[np.argmax(pdf)] # Mode (peak of PDF)
@@ -213,6 +213,11 @@ def get_all_moments(x, pdf):
213
213
  'o3n': o3n,
214
214
  'o4n': o4n
215
215
  }
216
+
217
+ # Add model parameters if provided
218
+ if model_params is not None:
219
+ moments.update(model_params)
220
+
216
221
  return moments
217
222
 
218
223
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.138
3
+ Version: 0.0.139
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -1,566 +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 numpy as np
9
- import datetime as dt
10
- from scipy import stats
11
- from typing import Dict, List, Tuple, Optional, Union, Any
12
- from voly.utils.logger import logger, catch_exception
13
- from voly.exceptions import VolyError
14
- from voly.core.rnd import get_all_moments
15
- from voly.formulas import iv, get_domain
16
- from voly.models import SVIModel
17
- from voly.core.fit import fit_model
18
- from arch import arch_model
19
-
20
-
21
- @catch_exception
22
- def get_historical_data(currency, lookback_days, granularity, exchange_name):
23
- """
24
- Fetch historical OHLCV data for a cryptocurrency.
25
-
26
- Parameters:
27
- ----------
28
- currency : str
29
- The cryptocurrency to fetch data for (e.g., 'BTC', 'ETH').
30
- lookback_days : str
31
- The lookback period in days, formatted as '90d', '30d', etc.
32
- granularity : str
33
- The time interval for data points (e.g., '15m', '1h', '1d').
34
- exchange_name : str
35
- The exchange to fetch data from (default: 'binance').
36
-
37
- Returns:
38
- -------
39
- df_hist : pandas.DataFrame containing the historical price data with OHLCV columns.
40
- """
41
-
42
- try:
43
- # Get the exchange class from ccxt
44
- exchange_class = getattr(ccxt, exchange_name.lower())
45
- exchange = exchange_class({'enableRateLimit': True})
46
- except (AttributeError, TypeError):
47
- raise VolyError(f"Exchange '{exchange_name}' not found in ccxt. Please check the exchange name.")
48
-
49
- # Form the trading pair symbol
50
- symbol = currency + '/USDT'
51
-
52
- # Convert lookback_days to timestamp
53
- if lookback_days.endswith('d'):
54
- days_ago = int(lookback_days[:-1])
55
- date_start = (dt.datetime.now() - dt.timedelta(days=days_ago)).strftime('%Y-%m-%d %H:%M:%S')
56
- else:
57
- raise VolyError("lookback_days should be in format '90d', '30d', etc.")
58
-
59
- from_ts = exchange.parse8601(date_start)
60
- ohlcv_list = []
61
- ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
62
- ohlcv_list.append(ohlcv)
63
- while True:
64
- from_ts = ohlcv[-1][0]
65
- new_ohlcv = exchange.fetch_ohlcv(symbol, granularity, since=from_ts, limit=1000)
66
- ohlcv.extend(new_ohlcv)
67
- if len(new_ohlcv) != 1000:
68
- break
69
-
70
- # Convert to DataFrame
71
- df_hist = pd.DataFrame(ohlcv, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
72
- df_hist['date'] = pd.to_datetime(df_hist['date'], unit='ms')
73
- df_hist.set_index('date', inplace=True)
74
- df_hist = df_hist.sort_index(ascending=True)
75
-
76
- print(f"Data fetched successfully: {len(df_hist)} rows from {df_hist.index[0]} to {df_hist.index[-1]}")
77
-
78
- return df_hist
79
-
80
-
81
- @catch_exception
82
- def get_hd_surface(model_results: pd.DataFrame,
83
- df_hist: pd.DataFrame,
84
- domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
85
- return_domain: str = 'log_moneyness') -> Tuple[
86
- Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray], pd.DataFrame]:
87
-
88
- # Check if required columns are present
89
- required_columns = ['s', 't', 'r']
90
- missing_columns = [col for col in required_columns if col not in model_results.columns]
91
- if missing_columns:
92
- raise VolyError(f"Required columns missing in model_results: {missing_columns}")
93
-
94
- # Determine granularity from df_hist
95
- if len(df_hist) > 1:
96
- # Calculate minutes between consecutive timestamps
97
- minutes_diff = (df_hist.index[1] - df_hist.index[0]).total_seconds() / 60
98
- minutes_per_period = int(minutes_diff)
99
- else:
100
- VolyError("Cannot determine granularity from df_hist.")
101
- return
102
-
103
- pdf_surface = {}
104
- cdf_surface = {}
105
- x_surface = {}
106
- all_moments = {}
107
-
108
- # Process each maturity
109
- for i in model_results.index:
110
- # Get parameters for this maturity
111
- s = model_results.loc[i, 's']
112
- r = model_results.loc[i, 'r']
113
- t = model_results.loc[i, 't']
114
-
115
- LM = get_domain(domain_params, s, r, None, t, 'log_moneyness')
116
- M = get_domain(domain_params, s, r, None, t, 'moneyness')
117
- R = get_domain(domain_params, s, r, None, t, 'returns')
118
- K = get_domain(domain_params, s, r, None, t, 'log_moneyness')
119
-
120
- # Filter historical data for this maturity's lookback period
121
- start_date = dt.datetime.now() - dt.timedelta(days=int(t * 365.25))
122
- maturity_hist = df_hist[df_hist.index >= start_date].copy()
123
-
124
- if len(maturity_hist) < 2:
125
- logger.warning(f"Not enough historical data for maturity {i}, skipping.")
126
- continue
127
-
128
- # Calculate the number of periods that match the time to expiry
129
- n_periods = int(t * 365.25 * 24 * 60 / minutes_per_period)
130
-
131
- # Compute returns and weights
132
- maturity_hist['returns'] = np.log(maturity_hist['close'] / maturity_hist['close'].shift(1)) * np.sqrt(n_periods)
133
- maturity_hist = maturity_hist.dropna()
134
-
135
- returns = maturity_hist['returns'].values
136
-
137
- if len(returns) < 10:
138
- logger.warning(f"Not enough valid returns for maturity {i}, skipping.")
139
- continue
140
-
141
- mu_scaled = returns.mean()
142
- sigma_scaled = returns.std()
143
-
144
- # Correct Girsanov adjustment to match the risk-neutral mean
145
- expected_risk_neutral_mean = (r - 0.5 * sigma_scaled ** 2) * np.sqrt(t)
146
- adjustment = mu_scaled - expected_risk_neutral_mean
147
- adj_returns = returns - adjustment # Shift the mean to risk-neutral
148
-
149
- # Create HD and Normalize
150
- f = stats.gaussian_kde(adj_returns, bw_method='silverman')
151
- hd_lm = f(LM)
152
- hd_lm = np.maximum(hd_lm, 0)
153
- total_area = np.trapz(hd_lm, LM)
154
- if total_area > 0:
155
- pdf_lm = hd_lm / total_area
156
- else:
157
- logger.warning(f"Total area is zero for maturity {i}, skipping.")
158
- continue
159
-
160
- pdf_k = pdf_lm / K
161
- pdf_m = pdf_k * s
162
- pdf_r = pdf_lm / (1 + R)
163
-
164
- cdf = np.concatenate(([0], np.cumsum(pdf_lm[:-1] * np.diff(LM))))
165
-
166
- if return_domain == 'log_moneyness':
167
- x = LM
168
- pdf = pdf_lm
169
- moments = get_all_moments(x, pdf)
170
- elif return_domain == 'moneyness':
171
- x = M
172
- pdf = pdf_m
173
- moments = get_all_moments(x, pdf)
174
- elif return_domain == 'returns':
175
- x = R
176
- pdf = pdf_r
177
- moments = get_all_moments(x, pdf)
178
- elif return_domain == 'strikes':
179
- x = K
180
- pdf = pdf_k
181
- moments = get_all_moments(x, pdf)
182
-
183
- # Store results
184
- pdf_surface[i] = pdf
185
- cdf_surface[i] = cdf
186
- x_surface[i] = x
187
- all_moments[i] = moments
188
-
189
- # Create a DataFrame with moments using the same index as model_results
190
- moments = pd.DataFrame(all_moments).T
191
-
192
- return {
193
- 'pdf_surface': pdf_surface,
194
- 'cdf_surface': cdf_surface,
195
- 'x_surface': x_surface,
196
- 'moments': moments
197
- }
198
-
199
-
200
- class GARCHModel:
201
- """
202
- GARCH(1,1) model for volatility modeling and simulation.
203
-
204
- Fits a GARCH(1,1) model to historical returns and simulates future paths
205
- for historical density estimation.
206
- """
207
-
208
- def __init__(self,
209
- data: np.ndarray,
210
- data_name: str,
211
- n_fits: int = 400,
212
- window_length: int = 365,
213
- z_h: float = 0.1):
214
- """
215
- Initialize the GARCH model.
216
-
217
- Args:
218
- data: Array of log returns
219
- data_name: Identifier for the dataset
220
- n_fits: Number of sliding windows to use for parameter estimation
221
- window_length: Length of each sliding window
222
- z_h: Bandwidth factor for kernel density estimation of innovations
223
- """
224
- self.data = data
225
- self.data_name = data_name
226
- self.n_fits = n_fits
227
- self.window_length = window_length
228
- self.z_h = z_h
229
-
230
- # Parameters to be created during fitting and simulation
231
- self.parameters = None
232
- self.e_process = None
233
- self.z_process = None
234
- self.sigma2_process = None
235
- self.z_dens = None
236
- self.simulated_log_returns = None
237
- self.simulated_tau_mu = None
238
-
239
- def fit(self):
240
- """
241
- Fit GARCH(1,1) model to historical data using sliding windows.
242
-
243
- For each window, estimates parameters (ω, α, β) and extracts innovations.
244
- """
245
-
246
- if len(self.data) < self.window_length + self.n_fits:
247
- raise VolyError(
248
- f"Not enough data points. Need at least {self.window_length + self.n_fits}, got {len(self.data)}")
249
-
250
- start = self.window_length + self.n_fits
251
- end = self.n_fits
252
-
253
- parameters = np.zeros((self.n_fits, 4))
254
- z_process = []
255
- e_process = []
256
- sigma2_process = []
257
-
258
- logger.info(f"Fitting GARCH model with {self.n_fits} windows...")
259
-
260
- for i in range(self.n_fits):
261
- window = self.data[end - i - 1:start - i - 1]
262
- data = window - np.mean(window)
263
-
264
- model = arch_model(data, vol='GARCH', p=1, q=1)
265
- GARCH_fit = model.fit(disp='off')
266
-
267
- mu, omega, alpha, beta = [
268
- GARCH_fit.params["mu"],
269
- GARCH_fit.params["omega"],
270
- GARCH_fit.params["alpha[1]"],
271
- GARCH_fit.params["beta[1]"],
272
- ]
273
- parameters[i, :] = [mu, omega, alpha, beta]
274
-
275
- if i == 0:
276
- sigma2_tm1 = omega / (1 - alpha - beta)
277
- else:
278
- sigma2_tm1 = sigma2_process[-1]
279
-
280
- e_t = data.tolist()[-1] # last observed log-return
281
- e_tm1 = data.tolist()[-2] # previous observed log-return
282
- sigma2_t = omega + alpha * e_tm1 ** 2 + beta * sigma2_tm1
283
- z_t = e_t / np.sqrt(sigma2_t)
284
-
285
- e_process.append(e_t)
286
- z_process.append(z_t)
287
- sigma2_process.append(sigma2_t)
288
-
289
- self.parameters = parameters
290
- self.e_process = e_process
291
- self.z_process = z_process
292
- self.sigma2_process = sigma2_process
293
-
294
- # Kernel density estimation for innovations
295
- z_dens_x = np.linspace(min(self.z_process), max(self.z_process), 500)
296
- h_dyn = self.z_h * (np.max(z_process) - np.min(z_process))
297
-
298
- # Use scipy's gaussian_kde for innovation distribution
299
- kde = stats.gaussian_kde(np.array(z_process), bw_method=h_dyn)
300
- z_dens_y = kde(z_dens_x)
301
-
302
- self.z_dens = {"x": z_dens_x, "y": z_dens_y}
303
-
304
- logger.info("GARCH model fitting complete")
305
-
306
- def _GARCH_simulate(self, pars, horizon):
307
- """
308
- Simulate a single GARCH path to specified horizon.
309
-
310
- Args:
311
- pars: Tuple of (mu, omega, alpha, beta)
312
- horizon: Number of steps to simulate
313
-
314
- Returns:
315
- Tuple of (sigma2_process, e_process) of simulated values
316
- """
317
- mu, omega, alpha, beta = pars
318
- burnin = horizon * 2
319
- sigma2 = [omega / (1 - alpha - beta)]
320
- e = [self.data.tolist()[-1] - mu] # last observed log-return mean adjusted
321
-
322
- # Convert density to probability weights
323
- weights = self.z_dens["y"] / np.sum(self.z_dens["y"])
324
-
325
- for _ in range(horizon + burnin):
326
- sigma2_tp1 = omega + alpha * e[-1] ** 2 + beta * sigma2[-1]
327
- # Sample from the estimated innovation distribution
328
- z_tp1 = np.random.choice(self.z_dens["x"], 1, p=weights)[0]
329
- e_tp1 = z_tp1 * np.sqrt(sigma2_tp1)
330
- sigma2.append(sigma2_tp1)
331
- e.append(e_tp1)
332
-
333
- return sigma2[-horizon:], e[-horizon:]
334
-
335
- def _variate_pars(self, pars, bounds):
336
- """
337
- Add variation to GARCH parameters for simulation uncertainty.
338
-
339
- Args:
340
- pars: Array of mean parameters [mu, omega, alpha, beta]
341
- bounds: Standard deviation bounds for parameters
342
-
343
- Returns:
344
- Array of slightly varied parameters
345
- """
346
- new_pars = []
347
- for i, (par, bound) in enumerate(zip(pars, bounds)):
348
- var = bound ** 2 / self.n_fits
349
- new_par = np.random.normal(par, var, 1)[0]
350
- if (new_par <= 0) and (i >= 1):
351
- new_par = 0.01
352
- new_pars.append(new_par)
353
- return new_pars
354
-
355
- def simulate_paths(self, horizon, simulations=5000, variate_parameters=True):
356
- """
357
- Simulate multiple GARCH paths using Monte Carlo.
358
-
359
- Args:
360
- horizon: Number of steps to simulate (days)
361
- simulations: Number of Monte Carlo simulations
362
- variate_parameters: Whether to add variation to GARCH parameters
363
-
364
- Returns:
365
- Tuple of (simulated_log_returns, simulated_tau_mu)
366
- """
367
- if self.parameters is None:
368
- self.fit()
369
-
370
- pars = np.mean(self.parameters, axis=0).tolist() # [mu, omega, alpha, beta]
371
- bounds = np.std(self.parameters, axis=0).tolist()
372
-
373
- logger.info(f"Simulating {simulations} GARCH paths for {horizon} steps...")
374
- logger.info(f"GARCH parameters: mu={pars[0]:.6f}, omega={pars[1]:.6f}, alpha={pars[2]:.6f}, beta={pars[3]:.6f}")
375
-
376
- np.random.seed(42) # For reproducibility
377
-
378
- new_pars = pars.copy() # start with unchanged parameters
379
- simulated_log_returns = np.zeros(simulations)
380
- simulated_tau_mu = np.zeros(simulations)
381
-
382
- for i in range(simulations):
383
- if ((i + 1) % (simulations // 10) == 0):
384
- logger.info(f"Simulation progress: {i + 1}/{simulations}")
385
-
386
- if ((i + 1) % (simulations // 20) == 0) and variate_parameters:
387
- new_pars = self._variate_pars(pars, bounds)
388
-
389
- sigma2, e = self._GARCH_simulate(new_pars, horizon)
390
- simulated_log_returns[i] = np.sum(e) # Sum log returns over horizon
391
- simulated_tau_mu[i] = horizon * pars[0] # Total drift
392
-
393
- self.simulated_log_returns = simulated_log_returns
394
- self.simulated_tau_mu = simulated_tau_mu
395
-
396
- return simulated_log_returns, simulated_tau_mu
397
-
398
-
399
- @catch_exception
400
- def get_garch_hd_surface(model_results: pd.DataFrame,
401
- df_hist: pd.DataFrame,
402
- domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
403
- return_domain: str = 'log_moneyness',
404
- n_fits: int = 400,
405
- simulations: int = 5000,
406
- window_length: int = 365,
407
- variate_parameters: bool = True,
408
- bandwidth: float = 0.15) -> Dict[str, Any]:
409
- """
410
- Generate historical density surface using GARCH(1,1) model and Monte Carlo simulation.
411
-
412
- Parameters:
413
- model_results: DataFrame with model parameters and maturities
414
- df_hist: DataFrame with historical price data (must have 'close' column)
415
- domain_params: Tuple of (min, max, num_points) for x-domain
416
- return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes')
417
- n_fits: Number of sliding windows for GARCH parameter estimation
418
- simulations: Number of Monte Carlo simulations
419
- window_length: Length of each sliding window for GARCH estimation
420
- variate_parameters: Whether to vary GARCH parameters between simulations
421
- bandwidth: Bandwidth for kernel density estimation of final density
422
-
423
- Returns:
424
- Dictionary containing pdf_surface, cdf_surface, x_surface, and moments
425
- """
426
- # Check if required columns are present
427
- required_columns = ['s', 't', 'r']
428
- missing_columns = [col for col in required_columns if col not in model_results.columns]
429
- if missing_columns:
430
- raise VolyError(f"Required columns missing in model_results: {missing_columns}")
431
-
432
- # Determine granularity from df_hist
433
- if len(df_hist) > 1:
434
- # Calculate minutes between consecutive timestamps
435
- minutes_diff = (df_hist.index[1] - df_hist.index[0]).total_seconds() / 60
436
- minutes_per_period = int(minutes_diff)
437
- else:
438
- raise VolyError("Cannot determine granularity from df_hist.")
439
-
440
- # Calculate log returns based on the determined granularity
441
- log_returns = np.log(df_hist['close'] / df_hist['close'].shift(1)) * 100
442
- log_returns = log_returns.dropna().values
443
-
444
- pdf_surface = {}
445
- cdf_surface = {}
446
- x_surface = {}
447
- all_moments = {}
448
-
449
- # Process each maturity
450
- for i in model_results.index:
451
- # Get parameters for this maturity
452
- s = model_results.loc[i, 's'] # Current spot price
453
- r = model_results.loc[i, 'r'] # Risk-free rate
454
- t = model_results.loc[i, 't'] # Time to maturity in years
455
-
456
- # Fix for very short-term maturities - use floating-point days
457
- tau_days_float = t * 365.25 # Exact number of days (as float)
458
- tau_day = max(1, int(tau_days_float)) # Ensure minimum of 1 day for simulation
459
-
460
- logger.info(f"Processing GARCH HD for maturity {i} (t={t:.4f} years, {tau_days_float:.2f} days)")
461
-
462
- # Calculate the number of periods that match the time to expiry
463
- n_periods = max(1, int(t * 365.25 * 24 * 60 / minutes_per_period))
464
-
465
- # Initialize GARCH model
466
- garch_model = GARCHModel(
467
- data=log_returns,
468
- data_name=str(i),
469
- n_fits=min(n_fits, len(log_returns) // 3),
470
- window_length=min(window_length, len(log_returns) // 3),
471
- z_h=0.1
472
- )
473
-
474
- # Simulate paths
475
- simulated_log_returns, simulated_tau_mu = garch_model.simulate_paths(
476
- horizon=tau_day,
477
- simulations=simulations,
478
- variate_parameters=variate_parameters
479
- )
480
-
481
- # Scale the simulated returns to match target time horizon
482
- # Use floating-point days to avoid division by zero
483
- scaling_factor = np.sqrt(n_periods / tau_days_float)
484
- scaled_log_returns = simulated_log_returns * scaling_factor
485
-
486
- # Risk-neutral adjustment (Girsanov transformation)
487
- # Calculate empirical mean and volatility of the scaled returns
488
- mu_scaled = scaled_log_returns.mean()
489
- sigma_scaled = scaled_log_returns.std()
490
-
491
- # Expected risk-neutral drift
492
- expected_risk_neutral_mean = (r - 0.5 * (sigma_scaled / 100) ** 2) * 100 * np.sqrt(t)
493
-
494
- # Calculate adjustment to shift physical to risk-neutral measure
495
- adjustment = mu_scaled - expected_risk_neutral_mean
496
-
497
- # Adjust the returns to the risk-neutral measure
498
- risk_neutral_log_returns = scaled_log_returns - adjustment
499
-
500
- # Convert to terminal prices using the risk-neutral returns
501
- simulated_prices = s * np.exp(risk_neutral_log_returns / 100)
502
-
503
- # Convert to moneyness domain
504
- simulated_moneyness = s / simulated_prices
505
-
506
- # Get x domain grid based on requested return_domain
507
- LM = np.linspace(domain_params[0], domain_params[1], domain_params[2])
508
- M = np.exp(LM) # Moneyness
509
- R = M - 1 # Returns
510
- K = s / M # Strike prices
511
-
512
- # Perform kernel density estimation in moneyness domain
513
- kde = stats.gaussian_kde(simulated_moneyness, bw_method=bandwidth)
514
- pdf_m = kde(M)
515
-
516
- # Ensure density integrates to 1
517
- dx = LM[1] - LM[0]
518
- total_area = np.sum(pdf_m * dx)
519
- pdf_m = pdf_m / total_area
520
-
521
- # Transform to other domains as needed
522
- pdf_lm = pdf_m * M # Transform to log-moneyness domain
523
- pdf_k = pdf_lm / K # Transform to strike domain
524
- pdf_r = pdf_lm / (1 + R) # Transform to returns domain
525
-
526
- # Calculate CDF
527
- cdf = np.cumsum(pdf_lm) * dx
528
- cdf = np.minimum(cdf / cdf[-1], 1.0) # Normalize and cap at 1.0
529
-
530
- # Select appropriate domain for return
531
- if return_domain == 'log_moneyness':
532
- x = LM
533
- pdf = pdf_lm
534
- moments = get_all_moments(x, pdf)
535
- elif return_domain == 'moneyness':
536
- x = M
537
- pdf = pdf_m
538
- moments = get_all_moments(x, pdf)
539
- elif return_domain == 'returns':
540
- x = R
541
- pdf = pdf_r
542
- moments = get_all_moments(x, pdf)
543
- elif return_domain == 'strikes':
544
- x = K
545
- pdf = pdf_k
546
- moments = get_all_moments(x, pdf)
547
- else:
548
- raise VolyError(f"Unsupported return_domain: {return_domain}")
549
-
550
- # Store results
551
- pdf_surface[i] = pdf
552
- cdf_surface[i] = cdf
553
- x_surface[i] = x
554
- all_moments[i] = moments
555
-
556
- # Create DataFrame with moments
557
- moments = pd.DataFrame(all_moments).T
558
-
559
- logger.info("GARCH historical density calculation complete")
560
-
561
- return {
562
- 'pdf_surface': pdf_surface,
563
- 'cdf_surface': cdf_surface,
564
- 'x_surface': x_surface,
565
- 'moments': moments
566
- }
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