voly 0.0.1__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/__init__.py +10 -0
- voly/client.py +540 -0
- voly/core/__init__.py +11 -0
- voly/core/charts.py +984 -0
- voly/core/data.py +312 -0
- voly/core/fit.py +331 -0
- voly/core/interpolate.py +221 -0
- voly/core/rnd.py +389 -0
- voly/exceptions.py +3 -0
- voly/formulas.py +243 -0
- voly/models.py +86 -0
- voly/utils/__init__.py +8 -0
- voly/utils/logger.py +72 -0
- voly-0.0.1.dist-info/LICENSE +21 -0
- voly-0.0.1.dist-info/METADATA +132 -0
- voly-0.0.1.dist-info/RECORD +18 -0
- voly-0.0.1.dist-info/WHEEL +5 -0
- voly-0.0.1.dist-info/top_level.txt +1 -0
voly/formulas.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Option pricing formulas and general calculations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.stats import norm
|
|
7
|
+
from typing import Tuple, Dict, Union, List, Optional
|
|
8
|
+
from voly.utils.logger import catch_exception
|
|
9
|
+
from voly.models import SVIModel
|
|
10
|
+
from math import exp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def d1(s: float, k: float, r: float, vol: float, t: float) -> float:
|
|
14
|
+
if vol <= 0 or t <= 0:
|
|
15
|
+
return np.nan
|
|
16
|
+
return (np.log(s / k) + (r + vol * vol / 2) * t) / (vol * np.sqrt(t))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def d2(s: float, k: float, r: float, vol: float, t: float) -> float:
|
|
20
|
+
if vol <= 0 or t <= 0:
|
|
21
|
+
return np.nan
|
|
22
|
+
return d1(s, k, r, vol, t) - vol * np.sqrt(t)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@catch_exception
|
|
26
|
+
def bs(s: float, k: float, r: float, vol: float, t: float, option_type: str = 'call') -> float:
|
|
27
|
+
if vol <= 0 or t <= 0:
|
|
28
|
+
# Intrinsic value at expiry
|
|
29
|
+
if option_type.lower() in ["call", "c"]:
|
|
30
|
+
return max(0, s - k)
|
|
31
|
+
else:
|
|
32
|
+
return max(0, k - s)
|
|
33
|
+
|
|
34
|
+
d1_val = d1(s, k, r, vol, t)
|
|
35
|
+
d2_val = d2(s, k, r, vol, t)
|
|
36
|
+
|
|
37
|
+
if option_type.lower() in ["call", "c"]:
|
|
38
|
+
return s * norm.cdf(d1_val) - k * np.exp(-r * t) * norm.cdf(d2_val)
|
|
39
|
+
else: # put
|
|
40
|
+
return k * np.exp(-r * t) * norm.cdf(-d2_val) - s * norm.cdf(-d1_val)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@catch_exception
|
|
44
|
+
def delta(s: float, k: float, r: float, vol: float, t: float, option_type: str = 'call') -> float:
|
|
45
|
+
if vol <= 0 or t <= 0:
|
|
46
|
+
# At expiry, delta is either 0 or 1 for call, 0 or -1 for put
|
|
47
|
+
if option_type.lower() in ["call", "c"]:
|
|
48
|
+
return 1.0 if s > k else 0.0
|
|
49
|
+
else:
|
|
50
|
+
return -1.0 if s < k else 0.0
|
|
51
|
+
|
|
52
|
+
d1_val = d1(s, k, r, vol, t)
|
|
53
|
+
|
|
54
|
+
if option_type.lower() in ["call", "c"]:
|
|
55
|
+
return norm.cdf(d1_val)
|
|
56
|
+
else: # put
|
|
57
|
+
return norm.cdf(d1_val) - 1.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@catch_exception
|
|
61
|
+
def gamma(s: float, k: float, r: float, vol: float, t: float) -> float:
|
|
62
|
+
if vol <= 0 or t <= 0:
|
|
63
|
+
return 0.0
|
|
64
|
+
|
|
65
|
+
d1_val = d1(s, k, r, vol, t)
|
|
66
|
+
return norm.pdf(d1_val) / (s * vol * np.sqrt(t))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@catch_exception
|
|
70
|
+
def vega(s: float, k: float, r: float, vol: float, t: float) -> float:
|
|
71
|
+
if vol <= 0 or t <= 0:
|
|
72
|
+
return 0.0
|
|
73
|
+
|
|
74
|
+
d1_val = d1(s, k, r, vol, t)
|
|
75
|
+
return s * norm.pdf(d1_val) * np.sqrt(t) / 100 # Divided by 100 for 1% change
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@catch_exception
|
|
79
|
+
def theta(s: float, k: float, r: float, vol: float, t: float, option_type: str = 'call') -> float:
|
|
80
|
+
if vol <= 0 or t <= 0:
|
|
81
|
+
return 0.0
|
|
82
|
+
|
|
83
|
+
d1_val = d1(s, k, r, vol, t)
|
|
84
|
+
d2_val = d2(s, k, r, vol, t)
|
|
85
|
+
|
|
86
|
+
# First part of theta (same for both call and put)
|
|
87
|
+
theta_part1 = -s * norm.pdf(d1_val) * vol / (2 * np.sqrt(t))
|
|
88
|
+
|
|
89
|
+
# Second part depends on option type
|
|
90
|
+
if option_type.lower() in ["call", "c"]:
|
|
91
|
+
theta_part2 = -r * k * np.exp(-r * t) * norm.cdf(d2_val)
|
|
92
|
+
else: # put
|
|
93
|
+
theta_part2 = r * k * np.exp(-r * t) * norm.cdf(-d2_val)
|
|
94
|
+
|
|
95
|
+
# Return theta per day (t is in years)
|
|
96
|
+
return (theta_part1 + theta_part2) / 365.0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@catch_exception
|
|
100
|
+
def rho(s: float, k: float, r: float, vol: float, t: float, option_type: str = 'call') -> float:
|
|
101
|
+
if vol <= 0 or t <= 0:
|
|
102
|
+
return 0.0
|
|
103
|
+
|
|
104
|
+
d2_val = d2(s, k, r, vol, t)
|
|
105
|
+
|
|
106
|
+
if option_type.lower() in ["call", "c"]:
|
|
107
|
+
return k * t * np.exp(-r * t) * norm.cdf(d2_val) / 100
|
|
108
|
+
else: # put
|
|
109
|
+
return -k * t * np.exp(-r * t) * norm.cdf(-d2_val) / 100
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@catch_exception
|
|
113
|
+
def vanna(s: float, k: float, r: float, vol: float, t: float) -> float:
|
|
114
|
+
if vol <= 0 or t <= 0:
|
|
115
|
+
return 0.0
|
|
116
|
+
|
|
117
|
+
d1_val = d1(s, k, r, vol, t)
|
|
118
|
+
d2_val = d2(s, k, r, vol, t)
|
|
119
|
+
|
|
120
|
+
return -norm.pdf(d1_val) * d2_val / vol
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@catch_exception
|
|
124
|
+
def volga(s: float, k: float, r: float, vol: float, t: float) -> float:
|
|
125
|
+
if vol <= 0 or t <= 0:
|
|
126
|
+
return 0.0
|
|
127
|
+
|
|
128
|
+
d1_val = d1(s, k, r, vol, t)
|
|
129
|
+
d2_val = d2(s, k, r, vol, t)
|
|
130
|
+
|
|
131
|
+
return s * norm.pdf(d1_val) * np.sqrt(t) * d1_val * d2_val / vol
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@catch_exception
|
|
135
|
+
def charm(s: float, k: float, r: float, vol: float, t: float, option_type: str = 'call') -> float:
|
|
136
|
+
if vol <= 0 or t <= 0:
|
|
137
|
+
return 0.0
|
|
138
|
+
|
|
139
|
+
d1_val = d1(s, k, r, vol, t)
|
|
140
|
+
d2_val = d2(s, k, r, vol, t)
|
|
141
|
+
|
|
142
|
+
# First term is the same for calls and puts
|
|
143
|
+
term1 = -norm.pdf(d1_val) * d1_val / (2 * t)
|
|
144
|
+
|
|
145
|
+
# Second term depends on option type
|
|
146
|
+
if option_type.lower() in ["call", "c"]:
|
|
147
|
+
term2 = -r * np.exp(-r * t) * norm.cdf(d2_val)
|
|
148
|
+
else: # put
|
|
149
|
+
term2 = r * np.exp(-r * t) * norm.cdf(-d2_val)
|
|
150
|
+
|
|
151
|
+
# Return charm per day (t is in years)
|
|
152
|
+
return (term1 + term2) / 365.0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@catch_exception
|
|
156
|
+
def greeks(s: float, k: float, r: float, vol: float, t: float,
|
|
157
|
+
option_type: str = 'call') -> Dict[str, float]:
|
|
158
|
+
return {
|
|
159
|
+
'price': bs(s, k, r, vol, t, option_type),
|
|
160
|
+
'delta': delta(s, k, r, vol, t, option_type),
|
|
161
|
+
'gamma': gamma(s, k, r, vol, t),
|
|
162
|
+
'vega': vega(s, k, r, vol, t),
|
|
163
|
+
'theta': theta(s, k, r, vol, t, option_type),
|
|
164
|
+
'rho': rho(s, k, r, vol, t, option_type),
|
|
165
|
+
'vanna': vanna(s, k, r, vol, t),
|
|
166
|
+
'volga': volga(s, k, r, vol, t),
|
|
167
|
+
'charm': charm(s, k, r, vol, t, option_type)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@catch_exception
|
|
172
|
+
def iv(option_price: float, s: float, k: float, r: float, t: float,
|
|
173
|
+
option_type: str = 'call', precision: float = 1e-8,
|
|
174
|
+
max_iterations: int = 100) -> float:
|
|
175
|
+
"""
|
|
176
|
+
Calculate implied volatility using Newton-Raphson method.
|
|
177
|
+
|
|
178
|
+
Parameters:
|
|
179
|
+
- option_price: Market price of the option
|
|
180
|
+
- s: Underlying price
|
|
181
|
+
- k: Strike price
|
|
182
|
+
- r: Risk-free rate
|
|
183
|
+
- t: Time to expiry in years
|
|
184
|
+
- option_type: 'call' or 'put'
|
|
185
|
+
- precision: Desired precision
|
|
186
|
+
- max_iterations: Maximum number of iterations
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
- Implied volatility
|
|
190
|
+
"""
|
|
191
|
+
if t <= 0:
|
|
192
|
+
return np.nan
|
|
193
|
+
|
|
194
|
+
# Check if option price is within theoretical bounds
|
|
195
|
+
if option_type.lower() in ["call", "c"]:
|
|
196
|
+
intrinsic = max(0, s - k * np.exp(-r * t))
|
|
197
|
+
if option_price < intrinsic:
|
|
198
|
+
return np.nan # Price below intrinsic value
|
|
199
|
+
if option_price >= s:
|
|
200
|
+
return np.inf # Price exceeds underlying
|
|
201
|
+
else: # put
|
|
202
|
+
intrinsic = max(0, k * np.exp(-r * t) - s)
|
|
203
|
+
if option_price < intrinsic:
|
|
204
|
+
return np.nan # Price below intrinsic value
|
|
205
|
+
if option_price >= k:
|
|
206
|
+
return np.inf # Price exceeds strike
|
|
207
|
+
|
|
208
|
+
# Initial guess - Manaster and Koehler (1982) method
|
|
209
|
+
vol = np.sqrt(2 * np.pi / t) * option_price / s
|
|
210
|
+
|
|
211
|
+
# Ensure initial guess is reasonable
|
|
212
|
+
vol = max(0.001, min(vol, 5.0))
|
|
213
|
+
|
|
214
|
+
for _ in range(max_iterations):
|
|
215
|
+
# Calculate option price and vega with current volatility
|
|
216
|
+
price = bs(s, k, r, vol, t, option_type)
|
|
217
|
+
v = vega(s, k, r, vol, t)
|
|
218
|
+
|
|
219
|
+
# Calculate price difference
|
|
220
|
+
price_diff = price - option_price
|
|
221
|
+
|
|
222
|
+
# Check if precision reached
|
|
223
|
+
if abs(price_diff) < precision:
|
|
224
|
+
return vol
|
|
225
|
+
|
|
226
|
+
# Avoid division by zero
|
|
227
|
+
if abs(v) < 1e-10:
|
|
228
|
+
# Change direction based on whether price is too high or too low
|
|
229
|
+
vol = vol * 1.5 if price_diff < 0 else vol * 0.5
|
|
230
|
+
else:
|
|
231
|
+
# Newton-Raphson update
|
|
232
|
+
vol = vol - price_diff / (v * 100) # Vega is for 1% change
|
|
233
|
+
|
|
234
|
+
# Ensure volatility stays in reasonable bounds
|
|
235
|
+
vol = max(0.001, min(vol, 5.0))
|
|
236
|
+
|
|
237
|
+
# If we reach here, we didn't converge
|
|
238
|
+
return np.nan
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@catch_exception
|
|
242
|
+
def rnd(log_moneyness: float, total_var: float) -> float:
|
|
243
|
+
return np.exp(-(log_moneyness ** 2) / (2 * total_var)) / (np.sqrt(2 * np.pi * total_var))
|
voly/models.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Volatility models for the Voly package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from typing import Tuple, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
# Configuration settings
|
|
9
|
+
DEFAULT_MONEYNESS_RANGE = (-2, 2)
|
|
10
|
+
DEFAULT_MONEYNESS_POINTS = 500
|
|
11
|
+
MIN_DTE = 2.0 # Minimum days to expiry to include
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SVIModel:
|
|
15
|
+
"""
|
|
16
|
+
Stochastic Volatility Inspired (SVI) model.
|
|
17
|
+
|
|
18
|
+
This class provides methods for calculating implied volatility using the
|
|
19
|
+
SVI parameterization, as well as its derivatives and related functions.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Default initial parameters and bounds
|
|
23
|
+
DEFAULT_INITIAL_PARAMS = [0.04, 0.1, 0.2, -0.5, 0.01]
|
|
24
|
+
DEFAULT_PARAM_BOUNDS = ([-np.inf, 0, 0, -1, -np.inf], [np.inf, np.inf, np.inf, 1, np.inf])
|
|
25
|
+
|
|
26
|
+
# Parameter names for reference
|
|
27
|
+
PARAM_NAMES = ['a', 'b', 'sigma', 'rho', 'm']
|
|
28
|
+
JW_PARAM_NAMES = ['nu', 'psi', 'p', 'c', 'nu_tilde']
|
|
29
|
+
|
|
30
|
+
# Parameter descriptions for documentation
|
|
31
|
+
PARAM_DESCRIPTIONS = {
|
|
32
|
+
'a': 'Base level of total implied variance',
|
|
33
|
+
'b': 'Volatility skewness/smile modulation (controls wing slopes)',
|
|
34
|
+
'sigma': 'Convexity control of the volatility smile (reduces ATM curvature)',
|
|
35
|
+
'rho': 'Skewness/slope of the volatility smile (-1 to 1, rotates smile)',
|
|
36
|
+
'm': 'Horizontal shift of the smile peak',
|
|
37
|
+
'nu': 'ATM variance (level of ATM volatility)',
|
|
38
|
+
'psi': 'ATM volatility skew (affects the gradient of the curve at ATM point)',
|
|
39
|
+
'p': 'Slope of put wing (left side of curve)',
|
|
40
|
+
'c': 'Slope of call wing (right side of curve)',
|
|
41
|
+
'nu_tilde': 'Minimum implied total variance',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def svi(moneyness: float, a: float, b: float, sigma: float, rho: float, m: float) -> float:
|
|
46
|
+
return a + b * (rho * (moneyness - m) + np.sqrt((moneyness - m) ** 2 + sigma ** 2))
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def svi_d(moneyness: float, a: float, b: float, sigma: float, rho: float, m: float) -> float:
|
|
50
|
+
return b * (rho + ((moneyness - m) / np.sqrt((moneyness - m) ** 2 + sigma ** 2)))
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def svi_dd(moneyness: float, a: float, b: float, sigma: float, rho: float, m: float) -> float:
|
|
54
|
+
return b * sigma ** 2 / ((moneyness - m) ** 2 + sigma ** 2) ** 1.5
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def svi_min_strike(sigma: float, rho: float, m: float) -> float:
|
|
58
|
+
return m - ((sigma * rho) / np.sqrt(1 - rho ** 2))
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def raw_to_jw_params(a: float, b: float, sigma: float, rho: float, m: float, t: float) -> Tuple[float, float, float, float, float]:
|
|
62
|
+
nu = (a + b * ((-rho) * m + np.sqrt(m ** 2 + sigma ** 2))) / t
|
|
63
|
+
psi = (1 / np.sqrt(nu * t)) * (b / 2) * (rho - (m / np.sqrt(m ** 2 + sigma ** 2)))
|
|
64
|
+
p = (1 / np.sqrt(nu * t)) * b * (1 - rho)
|
|
65
|
+
c = (1 / np.sqrt(nu * t)) * b * (1 + rho)
|
|
66
|
+
nu_tilde = (1 / t) * (a + b * sigma * np.sqrt(1 - rho ** 2))
|
|
67
|
+
return nu, psi, p, c, nu_tilde
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def jw_to_raw_params(nu: float, phi: float, p: float, c: float, nu_tilde: float, t: float) -> Tuple[float, float, float, float, float]:
|
|
71
|
+
w = nu * t
|
|
72
|
+
b = (c + p) / 2
|
|
73
|
+
rho = (c - p) / (c + p)
|
|
74
|
+
beta = rho - ((2 * w * phi) / b)
|
|
75
|
+
alpha = np.sign(beta) * (np.sqrt((1 / (beta ** 2)) - 1))
|
|
76
|
+
m = (((nu ** 2) - (nu_tilde ** 2)) * t) / (
|
|
77
|
+
b * ((-rho) + (np.sign(alpha) * np.sqrt(1 + alpha ** 2)) - (alpha * np.sqrt(1 - rho ** 2))))
|
|
78
|
+
sigma = alpha * m
|
|
79
|
+
a = ((nu_tilde ** 2) * t) - (b * sigma * np.sqrt(1 - rho ** 2))
|
|
80
|
+
return a, b, sigma, rho, m
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Models dictionary for easy access
|
|
84
|
+
MODELS = {
|
|
85
|
+
'svi': SVIModel,
|
|
86
|
+
}
|
voly/utils/__init__.py
ADDED
voly/utils/logger.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logger module for the Voly package.
|
|
3
|
+
|
|
4
|
+
This module provides a centralized logger configuration and
|
|
5
|
+
a decorator for error catching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import sys
|
|
11
|
+
from functools import wraps
|
|
12
|
+
import asyncio
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
# Remove the default handler first
|
|
16
|
+
logger.remove()
|
|
17
|
+
|
|
18
|
+
# Handler for console output - less verbose output
|
|
19
|
+
logger.add(
|
|
20
|
+
sys.stderr,
|
|
21
|
+
level="INFO",
|
|
22
|
+
backtrace=False, # Don't show traceback
|
|
23
|
+
diagnose=False # Don't show variables
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def setup_file_logging(logs_dir="logs/"):
|
|
28
|
+
"""
|
|
29
|
+
Set up file-based logging. This is optional and can be called
|
|
30
|
+
by the end user if they want file-based logging.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
logs_dir (str): Directory to store log files
|
|
34
|
+
"""
|
|
35
|
+
os.makedirs(logs_dir, exist_ok=True)
|
|
36
|
+
logger.add(
|
|
37
|
+
os.path.join(logs_dir, "voly_{time:YYYY-MM-DD}.log"),
|
|
38
|
+
level="INFO",
|
|
39
|
+
rotation="00:00",
|
|
40
|
+
retention="7 days",
|
|
41
|
+
backtrace=True, # Keep full traceback in file
|
|
42
|
+
diagnose=True # Keep variables in file
|
|
43
|
+
)
|
|
44
|
+
logger.info("File logging initialized")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Decorator for error catching (supports both sync and async functions)
|
|
48
|
+
def catch_exception(func):
|
|
49
|
+
@wraps(func)
|
|
50
|
+
def sync_wrapper(*args, **kwargs):
|
|
51
|
+
try:
|
|
52
|
+
return func(*args, **kwargs)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"{func.__name__}: {str(e)}")
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
@wraps(func)
|
|
58
|
+
async def async_wrapper(*args, **kwargs):
|
|
59
|
+
try:
|
|
60
|
+
return await func(*args, **kwargs)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"{func.__name__}: {str(e)}")
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
if asyncio.iscoroutinefunction(func):
|
|
66
|
+
return async_wrapper
|
|
67
|
+
else:
|
|
68
|
+
return sync_wrapper
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Initialize logger with basic info
|
|
72
|
+
logger.info("Voly logger initialized")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 manudc22
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: voly
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Options & volatility research package
|
|
5
|
+
Author-email: Manu de Cara <manu.de.cara@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pandas>=1.3.0
|
|
22
|
+
Requires-Dist: numpy>=1.20.0
|
|
23
|
+
Requires-Dist: scipy>=1.7.0
|
|
24
|
+
Requires-Dist: plotly>=5.3.0
|
|
25
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
26
|
+
Requires-Dist: websockets>=10.0
|
|
27
|
+
Requires-Dist: requests>=2.26.0
|
|
28
|
+
Requires-Dist: loguru>=0.5.3
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=22.1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: isort>=5.10.1; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=0.931; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8>=4.0.1; extra == "dev"
|
|
35
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: build>=0.8.0; extra == "dev"
|
|
37
|
+
|
|
38
|
+
# Voly - Options & Volatility Research Package
|
|
39
|
+
|
|
40
|
+
Voly is a Python package for options data analysis, volatility surface modeling, and risk-neutral density estimation. It provides a simple interface for handling common options research tasks, including data collection, model fitting, and visualization.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Data Collection**: Fetch options data from exchanges (currently supports Deribit)
|
|
45
|
+
- **Volatility Surface Modeling**: Fit SVI (Stochastic Volatility Inspired) model to market data
|
|
46
|
+
- **Risk-Neutral Density**: Calculate and analyze risk-neutral density distributions
|
|
47
|
+
- **Surface Interpolation**: Interpolate volatility surfaces across different expiries
|
|
48
|
+
- **Options Pricing & Greeks**: Calculate Black-Scholes prices and all major Greeks
|
|
49
|
+
- **Visualizations**: Generate interactive plots using Plotly
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
You can install Voly using pip:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install voly
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import pandas as pd
|
|
63
|
+
from voly import VolyClient
|
|
64
|
+
|
|
65
|
+
# Initialize the client
|
|
66
|
+
voly = VolyClient()
|
|
67
|
+
|
|
68
|
+
# Fetch options data (or load your own data)
|
|
69
|
+
option_chain = voly.get_option_chain(exchange='deribit', currency='BTC')
|
|
70
|
+
|
|
71
|
+
# Fit an SVI model to the data with visualization
|
|
72
|
+
fit_results = voly.fit_model(option_chain, plot=True)
|
|
73
|
+
|
|
74
|
+
# Calculate risk-neutral density
|
|
75
|
+
rnd_results = voly.rnd(fit_results, spot_price=option_chain['underlying_price'].iloc[0], plot=True)
|
|
76
|
+
|
|
77
|
+
# Calculate probability of price above a target
|
|
78
|
+
probability = voly.probability(rnd_results, target_price=32000, direction='above')
|
|
79
|
+
print(f"Probability of price above 32000: {probability:.2%}")
|
|
80
|
+
|
|
81
|
+
# Calculate option Greeks
|
|
82
|
+
greeks = voly.greeks(s=30000, k=32000, r=0.05, vol=0.6, t=0.25, option_type='call')
|
|
83
|
+
print(f"Option Greeks: {greeks}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Example: Visualizing the Volatility Surface
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import pandas as pd
|
|
90
|
+
from voly import VolyClient
|
|
91
|
+
|
|
92
|
+
# Initialize the client
|
|
93
|
+
voly = VolyClient()
|
|
94
|
+
|
|
95
|
+
# Load your own data or fetch from exchange
|
|
96
|
+
# The DataFrame should have columns for:
|
|
97
|
+
# - log_moneyness
|
|
98
|
+
# - strike
|
|
99
|
+
# - mark_iv (implied volatility)
|
|
100
|
+
# - yte (years to expiry)
|
|
101
|
+
# - dte (days to expiry)
|
|
102
|
+
# - maturity_name (identifier for the expiry)
|
|
103
|
+
data = pd.read_csv('your_options_data.csv')
|
|
104
|
+
|
|
105
|
+
# Fit the SVI model
|
|
106
|
+
fit_results = voly.fit_model(data, plot=True)
|
|
107
|
+
|
|
108
|
+
# Display the 3D volatility surface
|
|
109
|
+
surface_fig = fit_results['plots']['surface_3d']
|
|
110
|
+
surface_fig.show()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Documentation
|
|
114
|
+
|
|
115
|
+
For full documentation, visit [https://docs.voly.io](https://docs.voly.io)
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
### Setting up the development environment
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Clone the repository
|
|
123
|
+
git clone https://github.com/manudc22/voly.git
|
|
124
|
+
cd voly
|
|
125
|
+
|
|
126
|
+
# Install development dependencies & activate venv
|
|
127
|
+
./env_setup.sh --all
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
|
|
2
|
+
voly/client.py,sha256=BBoMURx5nlmwXpH__eQMjNEKy5WMQi3nJyUeVPStHVc,17589
|
|
3
|
+
voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
|
|
4
|
+
voly/formulas.py,sha256=aG_HSq_a4j7TcuKiINlHSpmNdmfZa_fzYbAk8EGt954,7427
|
|
5
|
+
voly/models.py,sha256=YJ12aamLz_-aOni4Qm0_XV9u4bjKK3vfJz0J2gc1h0o,3565
|
|
6
|
+
voly/core/__init__.py,sha256=GU6l7hpxJfitPx9jnmBtcb_QIeqOO8liZsSbLXXSbq8,384
|
|
7
|
+
voly/core/charts.py,sha256=GF55IS-aZfcc_0yoSPRPIPBPcJhFD1El18wNCo_mI_A,29918
|
|
8
|
+
voly/core/data.py,sha256=4nQuinjqG0vOY6peN2p00-1_MUck6zo7Hz0saikGneQ,11355
|
|
9
|
+
voly/core/fit.py,sha256=oDNS0Rb7o1g8ioigQ-iqJLNLy45bzARcgETiP9jBi0E,11305
|
|
10
|
+
voly/core/interpolate.py,sha256=fi_OQNdedDrdgawXCwhlO79HLGUvJ-nWjWxqFLd9ijU,8212
|
|
11
|
+
voly/core/rnd.py,sha256=Qlk1Ylo0KEz-BQ6VaGZ-0qsgFyTjoMX1DqCoGXAS6kk,12163
|
|
12
|
+
voly/utils/__init__.py,sha256=E05mWatyC-PDOsCxQV1p5Xi1IgpOomxrNURyCx_gB-w,200
|
|
13
|
+
voly/utils/logger.py,sha256=mujudi610WveZoGJJzoBtGbraFNJUzwWFTza645j08U,1821
|
|
14
|
+
voly-0.0.1.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
|
15
|
+
voly-0.0.1.dist-info/METADATA,sha256=4AkfO7MvVz_fB5GZV68zvk4hv1ETQd0Z16IB0ZQhi-A,4136
|
|
16
|
+
voly-0.0.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
17
|
+
voly-0.0.1.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
|
18
|
+
voly-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
voly
|