voly 0.0.111__py3-none-any.whl → 0.0.113__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 +62 -2
- voly/core/hd.py +301 -0
- voly/formulas.py +17 -89
- {voly-0.0.111.dist-info → voly-0.0.113.dist-info}/METADATA +1 -1
- {voly-0.0.111.dist-info → voly-0.0.113.dist-info}/RECORD +8 -8
- {voly-0.0.111.dist-info → voly-0.0.113.dist-info}/WHEEL +1 -1
- {voly-0.0.111.dist-info → voly-0.0.113.dist-info}/licenses/LICENSE +0 -0
- {voly-0.0.111.dist-info → voly-0.0.113.dist-info}/top_level.txt +0 -0
voly/client.py
CHANGED
|
@@ -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
|
|
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
|
+
}
|
voly/core/hd.py
CHANGED
|
@@ -6,8 +6,13 @@ time series of prices and converting them to implied volatility smiles.
|
|
|
6
6
|
import ccxt
|
|
7
7
|
import pandas as pd
|
|
8
8
|
import datetime as dt
|
|
9
|
+
from scipy import stats
|
|
9
10
|
from voly.utils.logger import logger, catch_exception
|
|
10
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
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
@catch_exception
|
|
@@ -68,3 +73,299 @@ def get_historical_data(currency, lookback_days, granularity, exchange_name):
|
|
|
68
73
|
print(f"Data fetched successfully: {len(df_hist)} rows from {df_hist.index[0]} to {df_hist.index[-1]}")
|
|
69
74
|
|
|
70
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
|
voly/formulas.py
CHANGED
|
@@ -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'
|
|
214
|
-
max_iterations: int = 100) -> float:
|
|
214
|
+
option_type: str = 'call') -> float:
|
|
215
215
|
"""
|
|
216
|
-
Calculate implied volatility using
|
|
216
|
+
Calculate implied volatility using py_volib for vectorized computation.
|
|
217
217
|
|
|
218
218
|
Parameters:
|
|
219
|
-
- option_price:
|
|
219
|
+
- option_price: Market price of the option
|
|
220
220
|
- s: Underlying price
|
|
221
221
|
- K: Strike price
|
|
222
|
-
- r:
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
|
|
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', '
|
|
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,19 +1,19 @@
|
|
|
1
1
|
voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
|
|
2
|
-
voly/client.py,sha256=
|
|
2
|
+
voly/client.py,sha256=S007dhNUwrjeGB7b8UDQ-PxlL-BMRzA9blzbz_lUw1s,14710
|
|
3
3
|
voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
|
|
4
|
-
voly/formulas.py,sha256=
|
|
4
|
+
voly/formulas.py,sha256=HejPfgVh6-hmWDhvwbHgAIfQMt8iPbIiaKqcBrB5Spw,10766
|
|
5
5
|
voly/models.py,sha256=BF-O7BjGf0BLMpw4rCtfwW7s8_f4iyUZdUY6q1dVxLs,3363
|
|
6
6
|
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=
|
|
10
|
+
voly/core/hd.py,sha256=st0KDyza_gkjzgjRMbLVKz_3k3vpXO3W7cyKtHUja0M,13395
|
|
11
11
|
voly/core/interpolate.py,sha256=JkK172-FXyhesW3hY4pEeuJWG3Bugq7QZXbeKoRpLuo,5305
|
|
12
12
|
voly/core/rnd.py,sha256=VKM8ojLBziB9nxOEsKO5z_9Z1BSOVNxg0OQPx-Sp80I,10094
|
|
13
13
|
voly/utils/__init__.py,sha256=E05mWatyC-PDOsCxQV1p5Xi1IgpOomxrNURyCx_gB-w,200
|
|
14
14
|
voly/utils/logger.py,sha256=4-_2bVJmq17Q0d7Rd2mPg1AeR8gxv6EPvcmBDMFWcSM,1744
|
|
15
|
-
voly-0.0.
|
|
16
|
-
voly-0.0.
|
|
17
|
-
voly-0.0.
|
|
18
|
-
voly-0.0.
|
|
19
|
-
voly-0.0.
|
|
15
|
+
voly-0.0.113.dist-info/licenses/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
|
16
|
+
voly-0.0.113.dist-info/METADATA,sha256=KXkw4jikRjhOaQ5lDp2HFNfHVdwL3pTFlmTgZ78LAnY,4115
|
|
17
|
+
voly-0.0.113.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
|
18
|
+
voly-0.0.113.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
|
19
|
+
voly-0.0.113.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|