voly 0.0.86__py3-none-any.whl → 0.0.87__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/client.py +18 -201
- voly/core/charts.py +10 -517
- voly/core/data.py +1 -4
- voly/core/fit.py +34 -42
- voly/core/interpolate.py +5 -6
- voly/core/rnd.py +255 -334
- voly/formulas.py +27 -29
- voly/models.py +0 -2
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/METADATA +1 -1
- voly-0.0.87.dist-info/RECORD +18 -0
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/WHEEL +1 -1
- voly-0.0.86.dist-info/RECORD +0 -18
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/LICENSE +0 -0
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/top_level.txt +0 -0
voly/core/rnd.py
CHANGED
|
@@ -9,359 +9,280 @@ from typing import Dict, List, Tuple, Optional, Union, Any
|
|
|
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 get_all_domains
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
# Breeden-Litzenberger Method
|
|
14
16
|
@catch_exception
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
def breeden(domain_params, s, o, r, t, return_domain):
|
|
18
|
+
LM = get_domain(domain_params, s, o, r, t, 'log_moneyness')
|
|
19
|
+
M = get_domain(domain_params, s, o, r, t, 'moneyness')
|
|
20
|
+
R = get_domain(domain_params, s, o, r, t, 'returns')
|
|
21
|
+
K = get_domain(domain_params, s, o, r, t, 'strikes')
|
|
22
|
+
D = get_domain(domain_params, s, o, r, t, 'delta')
|
|
23
|
+
|
|
24
|
+
c = voly.bs(s, K, r, o, t, option_type='call')
|
|
25
|
+
c1 = np.gradient(c, K)
|
|
26
|
+
c2 = np.gradient(c1, K)
|
|
27
|
+
|
|
28
|
+
rnd_k = np.maximum(np.exp(r * t) * c2, 0)
|
|
29
|
+
rnd_lm = rnd_k * K
|
|
30
|
+
|
|
31
|
+
dx = LM[1] - LM[0]
|
|
32
|
+
total_area = np.sum(rnd_lm * dx)
|
|
33
|
+
pdf_lm = rnd_lm / total_area
|
|
34
|
+
pdf_k = pdf_lm / K
|
|
35
|
+
pdf_m = pdf_k * s
|
|
36
|
+
pdf_r = pdf_lm / (1 + R)
|
|
37
|
+
|
|
38
|
+
n_d1 = stats.norm.pdf(voly.d1(s, K, r, o, t, option_type='call'))
|
|
39
|
+
dd_dK = n_d1 / (o * np.sqrt(t) * K)
|
|
40
|
+
pdf_d = pdf_k / dd_dK
|
|
41
|
+
|
|
42
|
+
cdf = np.cumsum(pdf_lm) * dx
|
|
43
|
+
cdf = cdf / cdf[-1]
|
|
44
|
+
|
|
45
|
+
if return_domain == 'log_moneyness':
|
|
46
|
+
x = LM
|
|
47
|
+
pdf = pdf_lm
|
|
48
|
+
moments = get_all_moments(x, pdf)
|
|
49
|
+
return pdf, cdf, x, moments
|
|
50
|
+
elif return_domain == 'moneyness':
|
|
51
|
+
x = M
|
|
52
|
+
pdf = pdf_m
|
|
53
|
+
moments = get_all_moments(x, pdf)
|
|
54
|
+
return pdf, cdf, x, moments
|
|
55
|
+
elif return_domain == 'returns':
|
|
56
|
+
x = R
|
|
57
|
+
pdf = pdf_r
|
|
58
|
+
moments = get_all_moments(x, pdf)
|
|
59
|
+
return pdf, cdf, x, moments
|
|
60
|
+
elif return_domain == 'strikes':
|
|
61
|
+
x = K
|
|
62
|
+
pdf = pdf_k
|
|
63
|
+
moments = get_all_moments(x, pdf)
|
|
64
|
+
return pdf, cdf, x, moments
|
|
65
|
+
elif return_domain == 'delta':
|
|
66
|
+
sort_idx = np.argsort(D)
|
|
67
|
+
x = D[sort_idx]
|
|
68
|
+
pdf = pdf_d[sort_idx]
|
|
69
|
+
moments = get_all_moments(x, pdf)
|
|
70
|
+
return pdf, cdf, x, moments
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Rookley's Method
|
|
19
74
|
@catch_exception
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# and probability of going below negative thresholds
|
|
110
|
-
if threshold >= 0:
|
|
111
|
-
# P(X > threshold) = 1 - P(X ≤ threshold)
|
|
112
|
-
result[f"p_above_{threshold}"] = 1.0 - prob
|
|
113
|
-
else:
|
|
114
|
-
# P(X < threshold) = P(X ≤ threshold)
|
|
115
|
-
result[f"p_below_{threshold}"] = prob
|
|
116
|
-
|
|
117
|
-
return result
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@catch_exception
|
|
121
|
-
def calculate_moments(
|
|
122
|
-
moneyness_grid: np.ndarray,
|
|
123
|
-
rnd_values: np.ndarray) -> Dict[str, float]:
|
|
124
|
-
"""
|
|
125
|
-
Calculate statistical moments (mean, variance, skewness, kurtosis) of the RND.
|
|
126
|
-
|
|
127
|
-
Parameters:
|
|
128
|
-
- moneyness_grid: Grid of log-moneyness values
|
|
129
|
-
- rnd_values: Array of RND values
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
- Dictionary with statistical moments
|
|
133
|
-
"""
|
|
134
|
-
# Calculate total probability (for normalization)
|
|
135
|
-
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
136
|
-
total_prob = np.sum(rnd_values) * dx
|
|
137
|
-
|
|
138
|
-
# Normalize the RND if needed
|
|
139
|
-
normalized_rnd = rnd_values / total_prob if total_prob > 0 else rnd_values
|
|
140
|
-
|
|
141
|
-
# Calculate moments in percentage terms
|
|
142
|
-
# First, convert log-moneyness to percentage returns
|
|
143
|
-
returns_pct = (np.exp(moneyness_grid) - 1) * 100 # Convert to percentage returns
|
|
144
|
-
|
|
145
|
-
# Calculate mean (expected return in %)
|
|
146
|
-
mean_pct = np.sum(returns_pct * normalized_rnd) * dx
|
|
147
|
-
|
|
148
|
-
# Calculate variance (in % squared)
|
|
149
|
-
centered_returns = returns_pct - mean_pct
|
|
150
|
-
variance_pct = np.sum(centered_returns ** 2 * normalized_rnd) * dx
|
|
151
|
-
std_dev_pct = np.sqrt(variance_pct)
|
|
152
|
-
|
|
153
|
-
# Skewness and kurtosis are unitless
|
|
154
|
-
skewness = np.sum(centered_returns ** 3 * normalized_rnd) * dx / (std_dev_pct ** 3) if std_dev_pct > 0 else 0
|
|
155
|
-
kurtosis = np.sum(centered_returns ** 4 * normalized_rnd) * dx / (variance_pct ** 2) if variance_pct > 0 else 0
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
"mean_pct": mean_pct, # Mean return in percentage
|
|
159
|
-
"variance_pct": variance_pct, # Variance in percentage squared
|
|
160
|
-
"std_dev_pct": std_dev_pct, # Standard deviation in percentage
|
|
161
|
-
"skewness": skewness, # Unitless
|
|
162
|
-
"kurtosis": kurtosis, # Unitless
|
|
163
|
-
"excess_kurtosis": kurtosis - 3 # Unitless
|
|
164
|
-
}
|
|
75
|
+
def rookley(domain_params, s, o, r, t, return_domain):
|
|
76
|
+
LM = get_domain(domain_params, s, o, r, t, 'log_moneyness')
|
|
77
|
+
M = get_domain(domain_params, s, o, r, t, 'moneyness')
|
|
78
|
+
R = get_domain(domain_params, s, o, r, t, 'returns')
|
|
79
|
+
K = get_domain(domain_params, s, o, r, t, 'strikes')
|
|
80
|
+
D = get_domain(domain_params, s, o, r, t, 'delta')
|
|
81
|
+
|
|
82
|
+
o1 = np.gradient(o, M)
|
|
83
|
+
o2 = np.gradient(o1, M)
|
|
84
|
+
|
|
85
|
+
st = np.sqrt(t)
|
|
86
|
+
rt = r * t
|
|
87
|
+
ert = np.exp(rt)
|
|
88
|
+
|
|
89
|
+
d1 = (np.log(M) + (r + 1 / 2 * o ** 2) * t) / (o * st)
|
|
90
|
+
d2 = d1 - o * st
|
|
91
|
+
|
|
92
|
+
del_d1_M = 1 / (M * o * st)
|
|
93
|
+
del_d2_M = del_d1_M
|
|
94
|
+
del_d1_o = -(np.log(M) + rt) / (o ** 2 * st) + st / 2
|
|
95
|
+
del_d2_o = -(np.log(M) + rt) / (o ** 2 * st) - st / 2
|
|
96
|
+
|
|
97
|
+
d_d1_M = del_d1_M + del_d1_o * o1
|
|
98
|
+
d_d2_M = del_d2_M + del_d2_o * o1
|
|
99
|
+
|
|
100
|
+
dd_d1_M = (
|
|
101
|
+
-(1 / (M * o * st)) * (1 / M + o1 / o)
|
|
102
|
+
+ o2 * (st / 2 - (np.log(M) + rt) / (o ** 2 * st))
|
|
103
|
+
+ o1 * (2 * o1 * (np.log(M) + rt) / (o ** 3 * st) - 1 / (M * o ** 2 * st))
|
|
104
|
+
)
|
|
105
|
+
dd_d2_M = (
|
|
106
|
+
-(1 / (M * o * st)) * (1 / M + o1 / o)
|
|
107
|
+
- o2 * (st / 2 + (np.log(M) + rt) / (o ** 2 * st))
|
|
108
|
+
+ o1 * (2 * o1 * (np.log(M) + rt) / (o ** 3 * st) - 1 / (M * o ** 2 * st))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
d_c_M = stats.norm.pdf(d1) * d_d1_M - 1 / ert * stats.norm.pdf(d2) / M * d_d2_M + 1 / ert * stats.norm.cdf(d2) / (
|
|
112
|
+
M ** 2)
|
|
113
|
+
dd_c_M = (
|
|
114
|
+
stats.norm.pdf(d1) * (dd_d1_M - d1 * (d_d1_M) ** 2)
|
|
115
|
+
- stats.norm.pdf(d2) / (ert * M) * (dd_d2_M - 2 / M * d_d2_M - d2 * (d_d2_M) ** 2)
|
|
116
|
+
- 2 * stats.norm.cdf(d2) / (ert * M ** 3)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
dd_c_K = dd_c_M * (M / K) ** 2 + 2 * d_c_M * (M / K ** 2)
|
|
120
|
+
|
|
121
|
+
rnd_k = np.maximum(ert * s * dd_c_K, 0)
|
|
122
|
+
rnd_lm = rnd_k * K
|
|
123
|
+
|
|
124
|
+
dx = LM[1] - LM[0]
|
|
125
|
+
total_area = np.sum(rnd_lm * dx)
|
|
126
|
+
pdf_lm = rnd_lm / total_area
|
|
127
|
+
pdf_k = pdf_lm / K
|
|
128
|
+
pdf_m = pdf_k * s
|
|
129
|
+
pdf_r = pdf_lm / (1 + R)
|
|
130
|
+
|
|
131
|
+
n_d1 = stats.norm.pdf(voly.d1(s, K, r, o, t, option_type='call'))
|
|
132
|
+
dd_dK = n_d1 / (o * np.sqrt(t) * K)
|
|
133
|
+
pdf_d = pdf_k / dd_dK
|
|
134
|
+
|
|
135
|
+
cdf = np.cumsum(pdf_lm) * dx
|
|
136
|
+
cdf = cdf / cdf[-1]
|
|
137
|
+
|
|
138
|
+
if return_domain == 'log_moneyness':
|
|
139
|
+
x = LM
|
|
140
|
+
pdf = pdf_lm
|
|
141
|
+
moments = get_all_moments(x, pdf)
|
|
142
|
+
return pdf, cdf, x, moments
|
|
143
|
+
elif return_domain == 'moneyness':
|
|
144
|
+
x = M
|
|
145
|
+
pdf = pdf_m
|
|
146
|
+
moments = get_all_moments(x, pdf)
|
|
147
|
+
return pdf, cdf, x, moments
|
|
148
|
+
elif return_domain == 'returns':
|
|
149
|
+
x = R
|
|
150
|
+
pdf = pdf_r
|
|
151
|
+
moments = get_all_moments(x, pdf)
|
|
152
|
+
return pdf, cdf, moments
|
|
153
|
+
elif return_domain == 'strikes':
|
|
154
|
+
x = K
|
|
155
|
+
pdf = pdf_k
|
|
156
|
+
moments = get_all_moments(x, pdf)
|
|
157
|
+
return pdf, cdf, x, moments
|
|
158
|
+
elif return_domain == 'delta':
|
|
159
|
+
sort_idx = np.argsort(D)
|
|
160
|
+
x = D[sort_idx]
|
|
161
|
+
pdf = pdf_d[sort_idx]
|
|
162
|
+
moments = get_all_moments(x, pdf)
|
|
163
|
+
return pdf, cdf, x, moments
|
|
165
164
|
|
|
166
165
|
|
|
167
166
|
@catch_exception
|
|
168
|
-
def
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
167
|
+
def get_all_moments(x, pdf):
|
|
168
|
+
mean = np.trapz(x * pdf, x) # E[X]
|
|
169
|
+
median = x[np.searchsorted(np.cumsum(pdf * np.diff(x, prepend=x[0])), 0.5)] # Median (50th percentile)
|
|
170
|
+
mode = x[np.argmax(pdf)] # Mode (peak of PDF)
|
|
171
|
+
variance = np.trapz((x - mean) ** 2 * pdf, x) # Var[X] = E[(X - μ)^2]
|
|
172
|
+
std_dev = np.sqrt(variance) # Standard deviation
|
|
173
|
+
skewness = np.trapz((x - mean) ** 3 * pdf, x) / std_dev ** 3 # Skewness
|
|
174
|
+
kurtosis = np.trapz((x - mean) ** 4 * pdf, x) / std_dev ** 4 # Kurtosis
|
|
175
|
+
excess_kurtosis = kurtosis - 3 # Excess kurtosis (relative to normal dist.)
|
|
176
|
+
q25 = x[np.searchsorted(np.cumsum(pdf * np.diff(x, prepend=x[0])), 0.25)] # 25th percentile
|
|
177
|
+
q75 = x[np.searchsorted(np.cumsum(pdf * np.diff(x, prepend=x[0])), 0.75)] # 75th percentile
|
|
178
|
+
iqr = q75 - q25 # Inter-quartile range
|
|
179
|
+
entropy = -np.trapz(pdf * np.log(pdf + 1e-10), x) # Differential entropy (avoid log(0))
|
|
180
|
+
|
|
181
|
+
# Full Z-score areas
|
|
182
|
+
dx = np.diff(x, prepend=x[0])
|
|
183
|
+
z = (x - mean) / std_dev
|
|
184
|
+
o1p = np.sum(pdf[(z > 0) & (z < 1)] * dx[(z > 0) & (z < 1)])
|
|
185
|
+
o2p = np.sum(pdf[(z >= 1) & (z < 2)] * dx[(z >= 1) & (z < 2)])
|
|
186
|
+
o3p = np.sum(pdf[(z >= 2) & (z < 3)] * dx[(z >= 2) & (z < 3)])
|
|
187
|
+
o4p = np.sum(pdf[z >= 3] * dx[z >= 3])
|
|
188
|
+
o1n = np.sum(pdf[(z < 0) & (z > -1)] * dx[(z < 0) & (z > -1)])
|
|
189
|
+
o2n = np.sum(pdf[(z <= -1) & (z > -2)] * dx[(z <= -1) & (z > -2)])
|
|
190
|
+
o3n = np.sum(pdf[(z <= -2) & (z > -3)] * dx[(z <= -2) & (z > -3)])
|
|
191
|
+
o4n = np.sum(pdf[z <= -3] * dx[z <= -3])
|
|
192
|
+
|
|
193
|
+
moments = {
|
|
194
|
+
'mean': mean,
|
|
195
|
+
'median': median,
|
|
196
|
+
'mode': mode,
|
|
197
|
+
'variance': variance,
|
|
198
|
+
'std_dev': std_dev,
|
|
199
|
+
'skewness': skewness,
|
|
200
|
+
'kurtosis': kurtosis,
|
|
201
|
+
'excess_kurtosis': excess_kurtosis,
|
|
202
|
+
'q25': q25,
|
|
203
|
+
'q75': q75,
|
|
204
|
+
'iqr': iqr,
|
|
205
|
+
'entropy': entropy,
|
|
206
|
+
'o1p': o1p,
|
|
207
|
+
'o2p': o2p,
|
|
208
|
+
'o3p': o3p,
|
|
209
|
+
'o4p': o4p,
|
|
210
|
+
'o1n': o1n,
|
|
211
|
+
'o2n': o2n,
|
|
212
|
+
'o3n': o3n,
|
|
213
|
+
'o4n': o4n
|
|
196
214
|
}
|
|
197
|
-
|
|
198
|
-
# Calculate moments for each expiry
|
|
199
|
-
for maturity_name, rnd in rnd_surface.items():
|
|
200
|
-
moments = calculate_moments(moneyness_grid, rnd)
|
|
201
|
-
|
|
202
|
-
data["maturity_name"].append(maturity_name)
|
|
203
|
-
data["dte"].append(dte_values[maturity_name])
|
|
204
|
-
data["yte"].append(yte_values[maturity_name])
|
|
205
|
-
data["mean_pct"].append(moments["mean_pct"])
|
|
206
|
-
data["std_dev_pct"].append(moments["std_dev_pct"])
|
|
207
|
-
data["skewness"].append(moments["skewness"])
|
|
208
|
-
data["excess_kurtosis"].append(moments["excess_kurtosis"])
|
|
209
|
-
|
|
210
|
-
# Create DataFrame and sort by DTE
|
|
211
|
-
stats_df = pd.DataFrame(data)
|
|
212
|
-
stats_df = stats_df.sort_values(by="dte")
|
|
213
|
-
|
|
214
|
-
return stats_df
|
|
215
|
+
return moments
|
|
215
216
|
|
|
216
217
|
|
|
217
218
|
@catch_exception
|
|
218
|
-
def
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
def get_rnd_surface(model_results: pd.DataFrame,
|
|
220
|
+
domain_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
|
|
221
|
+
return_domain: str = 'log_moneyness',
|
|
222
|
+
method: str = 'rookley') -> Tuple[
|
|
223
|
+
Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray], pd.DataFrame]:
|
|
223
224
|
"""
|
|
224
|
-
|
|
225
|
+
Generate RND surface from vol smile parameters.
|
|
225
226
|
|
|
226
|
-
|
|
227
|
-
- moneyness_grid: Grid of log-moneyness values
|
|
228
|
-
- rnd_values: Array of RND values
|
|
229
|
-
- spot_price: Spot price of the underlying
|
|
230
|
-
|
|
231
|
-
Returns:
|
|
232
|
-
- Tuple of (prices, pdf_values) for plotting
|
|
233
|
-
"""
|
|
234
|
-
# Calculate step size for normalization
|
|
235
|
-
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
236
|
-
|
|
237
|
-
# Normalize the RND
|
|
238
|
-
total_density = np.sum(rnd_values) * dx
|
|
239
|
-
pdf_values = rnd_values / total_density if total_density > 0 else rnd_values
|
|
240
|
-
|
|
241
|
-
# Convert log-moneyness to actual prices
|
|
242
|
-
prices = spot_price * np.exp(moneyness_grid)
|
|
243
|
-
|
|
244
|
-
return prices, pdf_values
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
@catch_exception
|
|
248
|
-
def calculate_cdf(
|
|
249
|
-
moneyness_grid: np.ndarray,
|
|
250
|
-
rnd_values: np.ndarray,
|
|
251
|
-
spot_price: float = 1.0
|
|
252
|
-
) -> Tuple[np.ndarray, np.ndarray]:
|
|
253
|
-
"""
|
|
254
|
-
Calculate cumulative distribution function (CDF) from RND values.
|
|
227
|
+
Works with both regular fit_results and interpolated_results dataframes.
|
|
255
228
|
|
|
256
229
|
Parameters:
|
|
257
|
-
-
|
|
258
|
-
-
|
|
259
|
-
-
|
|
230
|
+
- model_results: DataFrame from fit_model() or interpolate_model(). Maturity names or DTM as Index
|
|
231
|
+
- domain_params: Tuple of (min, max, num_points) for the x-domain grid
|
|
232
|
+
- return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'returns', 'strikes', 'delta')
|
|
233
|
+
- method: 'rookley' or 'breeden'
|
|
260
234
|
|
|
261
235
|
Returns:
|
|
262
|
-
- Tuple
|
|
236
|
+
- Tuple containing:
|
|
237
|
+
- pdf_surface: Dictionary mapping maturity/dtm names to PDF arrays of their requested domain
|
|
238
|
+
- cdf_surface: Dictionary mapping maturity/dtm names to CDF arrays
|
|
239
|
+
- x_surface: Dictionary mapping maturity/dtm names to requested x domain arrays
|
|
240
|
+
- moments_df: DataFrame with moments of the distributions using model_results index
|
|
263
241
|
"""
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
- Probability (0 to 1)
|
|
300
|
-
"""
|
|
301
|
-
# Convert target price to log-moneyness
|
|
302
|
-
target_moneyness = np.log(target_price / spot_price)
|
|
303
|
-
|
|
304
|
-
# Calculate CDF
|
|
305
|
-
_, cdf_values = calculate_cdf(moneyness_grid, rnd_values, spot_price)
|
|
306
|
-
|
|
307
|
-
# Find the nearest index to target moneyness
|
|
308
|
-
target_idx = np.abs(moneyness_grid - target_moneyness).argmin()
|
|
309
|
-
|
|
310
|
-
# Get probability
|
|
311
|
-
if target_idx < len(cdf_values):
|
|
312
|
-
cdf_at_target = cdf_values[target_idx]
|
|
313
|
-
else:
|
|
314
|
-
cdf_at_target = 1.0
|
|
242
|
+
# Check if required columns are present
|
|
243
|
+
required_columns = ['s', 'a', 'b', 'sigma', 'm', 'rho', 't', 'r']
|
|
244
|
+
missing_columns = [col for col in required_columns if col not in model_results.columns]
|
|
245
|
+
if missing_columns:
|
|
246
|
+
raise VolyError(f"Required columns missing in model_results: {missing_columns}")
|
|
247
|
+
|
|
248
|
+
pdf_surface = {}
|
|
249
|
+
cdf_surface = {}
|
|
250
|
+
x_surface = {}
|
|
251
|
+
all_moments = {}
|
|
252
|
+
|
|
253
|
+
# Process each maturity/dtm
|
|
254
|
+
for i in model_results.index:
|
|
255
|
+
# Calculate SVI total implied variance and convert to IV
|
|
256
|
+
params = [
|
|
257
|
+
model_results.loc[i, 'a'],
|
|
258
|
+
model_results.loc[i, 'b'],
|
|
259
|
+
model_results.loc[i, 'sigma'],
|
|
260
|
+
model_results.loc[i, 'rho'],
|
|
261
|
+
model_results.loc[i, 'm']
|
|
262
|
+
]
|
|
263
|
+
s = model_results.loc[i, 's']
|
|
264
|
+
r = model_results.loc[i, 'r']
|
|
265
|
+
t = model_results.loc[i, 't']
|
|
266
|
+
|
|
267
|
+
# Calculate implied volatility
|
|
268
|
+
LM = np.linspace(domain_params[0], domain_params[1], domain_params[2])
|
|
269
|
+
w = np.array([SVIModel.svi(x, *params) for x in LM])
|
|
270
|
+
o = np.sqrt(w / t)
|
|
271
|
+
|
|
272
|
+
if method == 'rookley':
|
|
273
|
+
pdf, cdf, x, moments = rookley(domain_params, s, o, r, t, return_domain)
|
|
274
|
+
else:
|
|
275
|
+
pdf, cdf, x, moments = breeden(domain_params, s, o, r, t, return_domain)
|
|
315
276
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return cdf_at_target
|
|
277
|
+
pdf_surface[i] = pdf
|
|
278
|
+
cdf_surface[i] = cdf
|
|
279
|
+
x_surface[i] = x
|
|
280
|
+
all_moments[i] = moments
|
|
321
281
|
|
|
282
|
+
# Create a DataFrame with moments using the same index as model_results
|
|
283
|
+
moments = pd.DataFrame(all_moments).T
|
|
322
284
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
fit_results: Dict[str, Any],
|
|
326
|
-
maturity: Optional[str] = None) -> Dict[str, Any]:
|
|
327
|
-
"""
|
|
328
|
-
Calculate risk-neutral density from fit results.
|
|
285
|
+
# Ensure the index matches the model_results index
|
|
286
|
+
moments.index = model_results.index
|
|
329
287
|
|
|
330
|
-
|
|
331
|
-
- fit_results: Dictionary with fitting results from fit_model()
|
|
332
|
-
- maturity: Optional maturity name to calculate RND for a specific expiry
|
|
333
|
-
|
|
334
|
-
Returns:
|
|
335
|
-
- Dictionary with RND results
|
|
336
|
-
"""
|
|
337
|
-
# Extract required data from fit results
|
|
338
|
-
raw_param_matrix = fit_results['raw_param_matrix']
|
|
339
|
-
moneyness_grid = fit_results['moneyness_grid']
|
|
340
|
-
|
|
341
|
-
# Calculate RND for all expiries or just the specified one
|
|
342
|
-
if maturity is not None:
|
|
343
|
-
# Validate maturity
|
|
344
|
-
if maturity not in raw_param_matrix.columns:
|
|
345
|
-
raise VolyError(f"Maturity '{maturity}' not found in fit results")
|
|
346
|
-
|
|
347
|
-
# Just calculate for the specified maturity
|
|
348
|
-
yte = raw_param_matrix.attrs['yte_values'][maturity]
|
|
349
|
-
params = raw_param_matrix[maturity].values
|
|
350
|
-
rnd_values = calculate_risk_neutral_density(moneyness_grid, params, yte)
|
|
351
|
-
rnd_surface = {maturity: rnd_values}
|
|
352
|
-
else:
|
|
353
|
-
# Calculate for all maturities
|
|
354
|
-
rnd_surface = calculate_rnd_for_all_expiries(moneyness_grid, raw_param_matrix)
|
|
355
|
-
|
|
356
|
-
# Calculate statistics
|
|
357
|
-
rnd_statistics = analyze_rnd_statistics(moneyness_grid, rnd_surface, raw_param_matrix)
|
|
358
|
-
|
|
359
|
-
# Calculate probabilities
|
|
360
|
-
rnd_probabilities = analyze_rnd_probabilities(moneyness_grid, rnd_surface, raw_param_matrix)
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
'moneyness_grid': moneyness_grid,
|
|
364
|
-
'rnd_surface': rnd_surface,
|
|
365
|
-
'rnd_statistics': rnd_statistics,
|
|
366
|
-
'rnd_probabilities': rnd_probabilities
|
|
367
|
-
}
|
|
288
|
+
return pdf_surface, cdf_surface, x_surface, moments
|