voly 0.0.152__py3-none-any.whl → 0.0.154__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/core/rnd.py CHANGED
@@ -9,154 +9,9 @@ from typing import Dict, List, Tuple, Optional, Union, Any, Callable
9
9
  from voly.utils.logger import logger, catch_exception
10
10
  from voly.exceptions import VolyError
11
11
  from voly.models import SVIModel
12
- from voly.formulas import bs, d1, d2, get_domain
12
+ from voly.formulas import bs, d1, d2
13
13
  from scipy import stats
14
-
15
-
16
- @catch_exception
17
- def _prepare_domains(domain_params, s, r, o, t):
18
- """
19
- Calculate domain arrays for different representations (log_moneyness, moneyness, etc.).
20
-
21
- Parameters:
22
- -----------
23
- domain_params : tuple
24
- (min_log_moneyness, max_log_moneyness, num_points)
25
- s : float
26
- Spot price
27
- r : float
28
- Risk-free rate
29
- o : ndarray
30
- Implied volatility array
31
- t : float
32
- Time to expiry in years
33
-
34
- Returns:
35
- --------
36
- dict
37
- Dictionary containing arrays for different domains
38
- """
39
- domains = {}
40
- domains['log_moneyness'] = get_domain(domain_params, s, r, o, t, 'log_moneyness')
41
- domains['moneyness'] = get_domain(domain_params, s, r, o, t, 'moneyness')
42
- domains['returns'] = get_domain(domain_params, s, r, o, t, 'returns')
43
- domains['strikes'] = get_domain(domain_params, s, r, o, t, 'strikes')
44
- domains['delta'] = get_domain(domain_params, s, r, o, t, 'delta')
45
-
46
- # Precompute differentials for integration
47
- domains['dx'] = domains['log_moneyness'][1] - domains['log_moneyness'][0]
48
-
49
- return domains
50
-
51
-
52
- @catch_exception
53
- def _normalize_density(pdf_values, dx):
54
- """
55
- Normalize a probability density function to integrate to 1.
56
-
57
- Parameters:
58
- -----------
59
- pdf_values : ndarray
60
- Array of PDF values
61
- dx : float
62
- Grid spacing
63
-
64
- Returns:
65
- --------
66
- ndarray
67
- Normalized PDF values
68
- """
69
- total_area = np.sum(pdf_values * dx)
70
- if total_area <= 0:
71
- logger.warning("PDF area is negative or zero, using absolute values")
72
- total_area = np.sum(np.abs(pdf_values) * dx)
73
-
74
- return pdf_values / total_area
75
-
76
-
77
- @catch_exception
78
- def _transform_to_domains(rnd_k, domains):
79
- """
80
- Transform density from strike domain to other domains.
81
-
82
- Parameters:
83
- -----------
84
- rnd_k : ndarray
85
- PDF in strike domain
86
- domains : dict
87
- Domain arrays
88
-
89
- Returns:
90
- --------
91
- dict
92
- Dictionary of PDFs in different domains
93
- """
94
- LM = domains['log_moneyness']
95
- M = domains['moneyness']
96
- K = domains['strikes']
97
- R = domains['returns']
98
- dx = domains['dx']
99
-
100
- # Calculate PDF in different domains
101
- rnd_lm = rnd_k * K # Convert to log-moneyness domain
102
- pdf_lm = _normalize_density(rnd_lm, dx)
103
-
104
- # Transform to other domains
105
- pdf_k = pdf_lm / K
106
- pdf_m = pdf_k * domains['strikes'][0] # s = K[0] * M[0]
107
- pdf_r = pdf_lm / (1 + R)
108
-
109
- # For delta domain, need special handling due to non-monotonicity
110
- pdf_d1 = stats.norm.pdf(d1(domains['strikes'][0], K, 0, domains['delta'][0], 1, option_type='call'))
111
- dd_dK = pdf_d1 / (domains['delta'][0] * np.sqrt(1) * K)
112
- pdf_d = pdf_k / dd_dK
113
-
114
- # Calculate CDF
115
- cdf = np.cumsum(pdf_lm * dx)
116
- cdf = np.minimum(cdf / cdf[-1], 1.0) # Ensure max value is 1
117
-
118
- return {
119
- 'log_moneyness': pdf_lm,
120
- 'moneyness': pdf_m,
121
- 'returns': pdf_r,
122
- 'strikes': pdf_k,
123
- 'delta': pdf_d,
124
- 'cdf': cdf
125
- }
126
-
127
-
128
- @catch_exception
129
- def _select_domain_results(pdfs, domains, return_domain):
130
- """
131
- Select results for the requested domain.
132
-
133
- Parameters:
134
- -----------
135
- pdfs : dict
136
- PDFs in different domains
137
- domains : dict
138
- Domain arrays
139
- return_domain : str
140
- Requested domain
141
-
142
- Returns:
143
- --------
144
- tuple
145
- (pdf, cdf, x, moments)
146
- """
147
- if return_domain == 'delta':
148
- # Special handling for delta domain due to potential non-monotonicity
149
- D = domains['delta']
150
- pdf_d = pdfs['delta']
151
- sort_idx = np.argsort(D)
152
- x = D[sort_idx]
153
- pdf = pdf_d[sort_idx]
154
- else:
155
- x = domains[return_domain]
156
- pdf = pdfs[return_domain]
157
-
158
- moments = get_all_moments(x, pdf)
159
- return pdf, pdfs['cdf'], x, moments
14
+ from voly.utils.density import prepare_domains, normalize_density, transform_to_domains, select_domain_results
160
15
 
161
16
 
162
17
  @catch_exception
@@ -177,7 +32,7 @@ def breeden(domain_params, s, r, o, t, return_domain):
177
32
  t : float
178
33
  Time to expiry in years
179
34
  return_domain : str
180
- Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
35
+ Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes')
181
36
 
182
37
  Returns:
183
38
  --------
@@ -185,8 +40,9 @@ def breeden(domain_params, s, r, o, t, return_domain):
185
40
  (pdf, cdf, x, moments)
186
41
  """
187
42
  # Prepare domain arrays
188
- domains = _prepare_domains(domain_params, s, r, o, t)
43
+ domains = prepare_domains(domain_params, s)
189
44
  K = domains['strikes']
45
+ dx = domains['dx']
190
46
 
191
47
  # Calculate option prices and derivatives
192
48
  c = bs(s, K, r, o, t, option_type='call')
@@ -196,11 +52,21 @@ def breeden(domain_params, s, r, o, t, return_domain):
196
52
  # Calculate RND in strike domain and apply discount factor
197
53
  rnd_k = np.maximum(np.exp(r * t) * c2, 0)
198
54
 
55
+ # Transform to log-moneyness domain first
56
+ LM = domains['log_moneyness']
57
+ rnd_lm = rnd_k * K # Convert to log-moneyness domain
58
+ pdf_lm = normalize_density(rnd_lm, dx)
59
+
199
60
  # Transform to other domains
200
- pdfs = _transform_to_domains(rnd_k, domains)
61
+ pdfs = transform_to_domains(pdf_lm, domains)
201
62
 
202
63
  # Return results for requested domain
203
- return _select_domain_results(pdfs, domains, return_domain)
64
+ pdf, cdf, x = select_domain_results(pdfs, domains, return_domain)
65
+
66
+ # Calculate moments
67
+ moments = get_all_moments(x, pdf)
68
+
69
+ return pdf, cdf, x, moments
204
70
 
205
71
 
206
72
  @catch_exception
@@ -221,7 +87,7 @@ def rookley(domain_params, s, r, o, t, return_domain):
221
87
  t : float
222
88
  Time to expiry in years
223
89
  return_domain : str
224
- Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
90
+ Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes')
225
91
 
226
92
  Returns:
227
93
  --------
@@ -229,9 +95,10 @@ def rookley(domain_params, s, r, o, t, return_domain):
229
95
  (pdf, cdf, x, moments)
230
96
  """
231
97
  # Prepare domain arrays
232
- domains = _prepare_domains(domain_params, s, r, o, t)
98
+ domains = prepare_domains(domain_params, s)
233
99
  M = domains['moneyness']
234
100
  K = domains['strikes']
101
+ dx = domains['dx']
235
102
 
236
103
  # Calculate volatility derivatives with respect to moneyness
237
104
  o1 = np.gradient(o, M)
@@ -287,11 +154,21 @@ def rookley(domain_params, s, r, o, t, return_domain):
287
154
  # Calculate RND in strike domain and apply discount factor
288
155
  rnd_k = np.maximum(ert * s * dd_c_K, 0)
289
156
 
157
+ # Transform to log-moneyness domain first
158
+ LM = domains['log_moneyness']
159
+ rnd_lm = rnd_k * K # Convert to log-moneyness domain
160
+ pdf_lm = normalize_density(rnd_lm, dx)
161
+
290
162
  # Transform to other domains
291
- pdfs = _transform_to_domains(rnd_k, domains)
163
+ pdfs = transform_to_domains(pdf_lm, domains)
292
164
 
293
165
  # Return results for requested domain
294
- return _select_domain_results(pdfs, domains, return_domain)
166
+ pdf, cdf, x = select_domain_results(pdfs, domains, return_domain)
167
+
168
+ # Calculate moments
169
+ moments = get_all_moments(x, pdf)
170
+
171
+ return pdf, cdf, x, moments
295
172
 
296
173
 
297
174
  @catch_exception
@@ -433,7 +310,7 @@ def get_rnd_surface(model_results: pd.DataFrame,
433
310
  domain_params : tuple
434
311
  (min_log_moneyness, max_log_moneyness, num_points)
435
312
  return_domain : str
436
- Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
313
+ Domain for results ('log_moneyness', 'moneyness', 'returns', 'strikes')
437
314
  method : str
438
315
  Method for RND estimation ('rookley' or 'breeden')
439
316
 
@@ -453,7 +330,7 @@ def get_rnd_surface(model_results: pd.DataFrame,
453
330
  raise VolyError(f"Invalid method: {method}. Must be 'rookley' or 'breeden'")
454
331
 
455
332
  # Validate return_domain
456
- valid_domains = ['log_moneyness', 'moneyness', 'returns', 'strikes', 'delta']
333
+ valid_domains = ['log_moneyness', 'moneyness', 'returns', 'strikes']
457
334
  if return_domain not in valid_domains:
458
335
  raise VolyError(f"Invalid return_domain: {return_domain}. Must be one of {valid_domains}")
459
336
 
voly/utils/density.py ADDED
@@ -0,0 +1,155 @@
1
+ """
2
+ Common utility functions for probability density calculations.
3
+
4
+ This module contains shared utility functions for working with probability
5
+ densities across different domains, used by both RND and HD calculations.
6
+ """
7
+
8
+ import numpy as np
9
+ from typing import Dict, Tuple, List, Any
10
+ from scipy import stats
11
+ from voly.utils.logger import catch_exception
12
+ from voly.formulas import d1, d2
13
+
14
+
15
+ @catch_exception
16
+ def prepare_domains(domain_params: Tuple[float, float, int],
17
+ s: float) -> Dict[str, np.ndarray]:
18
+ """
19
+ Calculate domain arrays for different representations.
20
+
21
+ Parameters:
22
+ -----------
23
+ domain_params : Tuple[float, float, int]
24
+ (min_log_moneyness, max_log_moneyness, num_points)
25
+ s : float
26
+ Spot price
27
+
28
+ Returns:
29
+ --------
30
+ Dict[str, np.ndarray]
31
+ Dictionary containing arrays for different domains
32
+ """
33
+ # Create log-moneyness grid
34
+ LM = np.linspace(domain_params[0], domain_params[1], domain_params[2])
35
+
36
+ # Calculate other domains
37
+ M = np.exp(LM) # Moneyness
38
+ R = M - 1 # Returns
39
+ K = s / M # Strike prices
40
+
41
+ # Precompute differentials for integration
42
+ dx = LM[1] - LM[0]
43
+
44
+ return {
45
+ 'log_moneyness': LM,
46
+ 'moneyness': M,
47
+ 'returns': R,
48
+ 'strikes': K,
49
+ 'dx': dx
50
+ }
51
+
52
+
53
+ @catch_exception
54
+ def normalize_density(pdf_values: np.ndarray,
55
+ dx: float) -> np.ndarray:
56
+ """
57
+ Normalize a probability density function to integrate to 1.
58
+
59
+ Parameters:
60
+ -----------
61
+ pdf_values : np.ndarray
62
+ Array of PDF values
63
+ dx : float
64
+ Grid spacing
65
+
66
+ Returns:
67
+ --------
68
+ np.ndarray
69
+ Normalized PDF values
70
+ """
71
+ total_area = np.trapz(pdf_values, dx=dx)
72
+
73
+ if total_area <= 0:
74
+ # If area is negative or zero, use absolute values
75
+ total_area = np.trapz(np.abs(pdf_values), dx=dx)
76
+
77
+ # Handle very small values to prevent division by zero
78
+ if abs(total_area) < 1e-10:
79
+ return np.zeros_like(pdf_values)
80
+
81
+ return pdf_values / total_area
82
+
83
+
84
+ @catch_exception
85
+ def transform_to_domains(pdf_lm: np.ndarray,
86
+ domains: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
87
+ """
88
+ Transform density from log-moneyness domain to other domains.
89
+
90
+ Parameters:
91
+ -----------
92
+ pdf_lm : np.ndarray
93
+ PDF in log-moneyness domain
94
+ domains : Dict[str, np.ndarray]
95
+ Domain arrays
96
+
97
+ Returns:
98
+ --------
99
+ Dict[str, np.ndarray]
100
+ Dictionary of PDFs in different domains
101
+ """
102
+ LM = domains['log_moneyness']
103
+ M = domains['moneyness']
104
+ K = domains['strikes']
105
+ R = domains['returns']
106
+ dx = domains['dx']
107
+
108
+ # Transform to other domains
109
+ pdf_m = pdf_lm / M
110
+ pdf_k = pdf_lm / K
111
+ pdf_r = pdf_lm / (1 + R)
112
+
113
+ # Calculate CDF
114
+ cdf = np.cumsum(pdf_lm * dx)
115
+ cdf = np.minimum(cdf / cdf[-1], 1.0) # Ensure max value is 1
116
+
117
+ return {
118
+ 'log_moneyness': pdf_lm,
119
+ 'moneyness': pdf_m,
120
+ 'returns': pdf_r,
121
+ 'strikes': pdf_k,
122
+ 'cdf': cdf
123
+ }
124
+
125
+
126
+ @catch_exception
127
+ def select_domain_results(pdfs: Dict[str, np.ndarray],
128
+ domains: Dict[str, np.ndarray],
129
+ return_domain: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
130
+ """
131
+ Select results for the requested domain.
132
+
133
+ Parameters:
134
+ -----------
135
+ pdfs : Dict[str, np.ndarray]
136
+ PDFs in different domains
137
+ domains : Dict[str, np.ndarray]
138
+ Domain arrays
139
+ return_domain : str
140
+ Requested domain
141
+
142
+ Returns:
143
+ --------
144
+ Tuple[np.ndarray, np.ndarray, np.ndarray]
145
+ (pdf, cdf, x) in the requested domain
146
+ """
147
+ if return_domain not in domains or return_domain not in pdfs:
148
+ valid_domains = set(domains.keys()).intersection(set(pdfs.keys()))
149
+ raise ValueError(f"Invalid return_domain: {return_domain}. Must be one of {valid_domains}")
150
+
151
+ x = domains[return_domain]
152
+ pdf = pdfs[return_domain]
153
+ cdf = pdfs['cdf']
154
+
155
+ return pdf, cdf, x
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voly
3
- Version: 0.0.152
3
+ Version: 0.0.154
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
2
- voly/client.py,sha256=Jlep-VUW0jFW6dX6AE13CUsbG78u1UeNaWNLW7g2l08,13414
2
+ voly/client.py,sha256=JCofJv9BsLHh5E-NQ-1Y2IB0PbZBhE7OqT_bcDL-ktM,14355
3
3
  voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
4
4
  voly/formulas.py,sha256=G_soRiPwQlHy6milOAj6TdmBWr-fNZpMvm0joXAMZ90,10767
5
5
  voly/models.py,sha256=o-pHujGfr5Gn8ItckMzLI4Q8yaX9FQaV8UjCxv2zgTY,3364
@@ -7,13 +7,14 @@ voly/core/__init__.py,sha256=bu6fS2I1Pj9fPPnl-zY3L7NqrZSY5Zy6NY2uMUvdhKs,183
7
7
  voly/core/charts.py,sha256=E21OZB5lTY4YL2flgaFJ6s5g3_ExtAQT2zryZZxLPyM,12735
8
8
  voly/core/data.py,sha256=pDeuYhP0GX4RbtlqByvsE3rfHcIkix0BU5MLW8sKIeI,8935
9
9
  voly/core/fit.py,sha256=Tb9eeG7e_2dQTcqt6aqEwFrZdy6jR9rSNqe6tzOdVhQ,9245
10
- voly/core/hd.py,sha256=inw1AfGKgOfFsuiYELxcCrnRs8fM0JIKlO4L2MQBBC8,30431
10
+ voly/core/hd.py,sha256=_JLdeNlnwZPafs4oRO_ydGrG6aqgcZKo_HQ503Ifo0s,8525
11
11
  voly/core/interpolate.py,sha256=JkK172-FXyhesW3hY4pEeuJWG3Bugq7QZXbeKoRpLuo,5305
12
- voly/core/rnd.py,sha256=AvGsITB52-Ti9AOA2lj2N4UAyJUtD-Bs37HRwLbzRsA,16043
12
+ voly/core/rnd.py,sha256=3rhIA2rN2jcw2JvXDFsZgA62RZVgepshNculwo2X9zc,12912
13
13
  voly/utils/__init__.py,sha256=E05mWatyC-PDOsCxQV1p5Xi1IgpOomxrNURyCx_gB-w,200
14
+ voly/utils/density.py,sha256=fUJf1AsbyqwxBlhAP53RvcI4KxVEmRRtXXW1WSMFTFE,4017
14
15
  voly/utils/logger.py,sha256=4-_2bVJmq17Q0d7Rd2mPg1AeR8gxv6EPvcmBDMFWcSM,1744
15
- voly-0.0.152.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
16
- voly-0.0.152.dist-info/METADATA,sha256=ojr_D--Jhv-Ldsfql3-PGcx0TAFaxMADg37TaYsKyNI,4115
17
- voly-0.0.152.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
18
- voly-0.0.152.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
19
- voly-0.0.152.dist-info/RECORD,,
16
+ voly-0.0.154.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
17
+ voly-0.0.154.dist-info/METADATA,sha256=BqU2_2IpUkTunLU5iY2t1yiKGi3-yy0NHriboOKX88k,4115
18
+ voly-0.0.154.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
19
+ voly-0.0.154.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
20
+ voly-0.0.154.dist-info/RECORD,,
File without changes