voly 0.0.81__tar.gz → 0.0.83__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.81/src/voly.egg-info → voly-0.0.83}/PKG-INFO +1 -1
  2. {voly-0.0.81 → voly-0.0.83}/pyproject.toml +2 -2
  3. {voly-0.0.81 → voly-0.0.83}/src/voly/client.py +28 -0
  4. {voly-0.0.81 → voly-0.0.83}/src/voly/core/fit.py +20 -12
  5. voly-0.0.83/src/voly/core/interpolate.py +132 -0
  6. {voly-0.0.81 → voly-0.0.83/src/voly.egg-info}/PKG-INFO +1 -1
  7. voly-0.0.81/src/voly/core/interpolate.py +0 -221
  8. {voly-0.0.81 → voly-0.0.83}/LICENSE +0 -0
  9. {voly-0.0.81 → voly-0.0.83}/README.md +0 -0
  10. {voly-0.0.81 → voly-0.0.83}/setup.cfg +0 -0
  11. {voly-0.0.81 → voly-0.0.83}/setup.py +0 -0
  12. {voly-0.0.81 → voly-0.0.83}/src/voly/__init__.py +0 -0
  13. {voly-0.0.81 → voly-0.0.83}/src/voly/core/__init__.py +0 -0
  14. {voly-0.0.81 → voly-0.0.83}/src/voly/core/charts.py +0 -0
  15. {voly-0.0.81 → voly-0.0.83}/src/voly/core/data.py +0 -0
  16. {voly-0.0.81 → voly-0.0.83}/src/voly/core/rnd.py +0 -0
  17. {voly-0.0.81 → voly-0.0.83}/src/voly/exceptions.py +0 -0
  18. {voly-0.0.81 → voly-0.0.83}/src/voly/formulas.py +0 -0
  19. {voly-0.0.81 → voly-0.0.83}/src/voly/models.py +0 -0
  20. {voly-0.0.81 → voly-0.0.83}/src/voly/utils/__init__.py +0 -0
  21. {voly-0.0.81 → voly-0.0.83}/src/voly/utils/logger.py +0 -0
  22. {voly-0.0.81 → voly-0.0.83}/src/voly.egg-info/SOURCES.txt +0 -0
  23. {voly-0.0.81 → voly-0.0.83}/src/voly.egg-info/dependency_links.txt +0 -0
  24. {voly-0.0.81 → voly-0.0.83}/src/voly.egg-info/requires.txt +0 -0
  25. {voly-0.0.81 → voly-0.0.83}/src/voly.egg-info/top_level.txt +0 -0
  26. {voly-0.0.81 → voly-0.0.83}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.81
3
+ Version: 0.0.83
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.81"
7
+ version = "0.0.83"
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.81"
63
+ python_version = "0.0.83"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -420,6 +420,34 @@ class VolyClient:
420
420
 
421
421
  return plots
422
422
 
423
+ # -------------------------------------------------------------------------
424
+ # Interpolate
425
+ # -------------------------------------------------------------------------
426
+
427
+ @staticmethod
428
+ def interpolate_model(fit_results: pd.DataFrame,
429
+ list_of_days: List[str] = ['7d', '30d', '90d', '150d', '240d'],
430
+ method: str = 'cubic') -> pd.DataFrame:
431
+ """
432
+ Interpolate a fitted model to specific days to expiry.
433
+
434
+ Parameters:
435
+ - fit_results: DataFrame with fitting results from fit_model()
436
+ - list_of_days: List of specific days to include (e.g., ['7d', '30d', '90d'])
437
+ - method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
438
+
439
+ Returns:
440
+ - DataFrame with interpolated model parameters for the specified days
441
+ """
442
+ logger.info(f"Interpolating model with {method} method")
443
+
444
+ # Interpolate the model
445
+ interpolated_results = interpolate_model(
446
+ fit_results, list_of_days, method
447
+ )
448
+
449
+ return interpolated_results
450
+
423
451
  # -------------------------------------------------------------------------
424
452
  # Risk-Neutral Density (RND)
425
453
  # -------------------------------------------------------------------------
@@ -93,6 +93,8 @@ def fit_model(option_chain: pd.DataFrame,
93
93
  # ANSI color codes for terminal output
94
94
  GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
95
95
 
96
+ s = option_chain['index_price'].iloc[-1]
97
+
96
98
  for ytm in unique_ytms:
97
99
  # Get data for this maturity
98
100
  maturity_data = option_chain[option_chain['ytm'] == ytm]
@@ -128,7 +130,6 @@ def fit_model(option_chain: pd.DataFrame,
128
130
  max_error = np.max(np.abs(iv_market - iv_model))
129
131
 
130
132
  # Get or calculate additional required data
131
- s = maturity_data['index_price'].iloc[0]
132
133
  u = maturity_data['underlying_price'].iloc[0]
133
134
 
134
135
  # Aggregate open interest and volume
@@ -187,14 +188,16 @@ def fit_model(option_chain: pd.DataFrame,
187
188
 
188
189
 
189
190
  @catch_exception
190
- def get_iv_surface(fit_results: pd.DataFrame,
191
+ def get_iv_surface(model_results: pd.DataFrame,
191
192
  log_moneyness_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
192
193
  return_domain: str = 'log_moneyness') -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]:
193
194
  """
194
195
  Generate implied volatility surface using optimized SVI parameters.
195
196
 
197
+ Works with both regular fit_results and interpolated_results dataframes.
198
+
196
199
  Parameters:
197
- - fit_results: DataFrame from fit_model() with maturity names as index
200
+ - model_results: DataFrame from fit_model() or interpolate_model()
198
201
  - log_moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
199
202
  - return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'strikes', 'delta')
200
203
 
@@ -203,6 +206,11 @@ def get_iv_surface(fit_results: pd.DataFrame,
203
206
  iv_surface: Dictionary mapping maturity names to IV arrays
204
207
  x_surface: Dictionary mapping maturity names to requested x domain arrays
205
208
  """
209
+ # Check if required columns are present
210
+ required_columns = ['a', 'b', 'rho', 'm', 'sigma', 'ytm']
211
+ missing_columns = [col for col in required_columns if col not in model_results.columns]
212
+ if missing_columns:
213
+ raise VolyError(f"Required columns missing in model_results: {missing_columns}")
206
214
 
207
215
  # Generate implied volatility surface in log-moneyness domain
208
216
  min_m, max_m, num_points = log_moneyness_params
@@ -212,16 +220,16 @@ def get_iv_surface(fit_results: pd.DataFrame,
212
220
  x_surface = {}
213
221
 
214
222
  # Process each maturity
215
- for maturity in fit_results.index:
223
+ for i in model_results.index:
216
224
  # Calculate SVI total implied variance and convert to IV
217
225
  params = [
218
- fit_results.loc[maturity, 'a'],
219
- fit_results.loc[maturity, 'b'],
220
- fit_results.loc[maturity, 'sigma'],
221
- fit_results.loc[maturity, 'rho'],
222
- fit_results.loc[maturity, 'm']
226
+ fit_results.loc[i, 'a'],
227
+ fit_results.loc[i, 'b'],
228
+ fit_results.loc[i, 'sigma'],
229
+ fit_results.loc[i, 'rho'],
230
+ fit_results.loc[i, 'm']
223
231
  ]
224
- ytm = fit_results.loc[maturity, 'ytm']
232
+ ytm = fit_results.loc[i, 'ytm']
225
233
 
226
234
  # Calculate implied volatility
227
235
  w_svi = np.array([SVIModel.svi(x, *params) for x in log_moneyness_array])
@@ -232,8 +240,8 @@ def get_iv_surface(fit_results: pd.DataFrame,
232
240
  x_surface[maturity] = get_x_domain(
233
241
  log_moneyness_params=log_moneyness_params,
234
242
  return_domain=return_domain,
235
- s=fit_results.loc[maturity, 's'],
236
- r=fit_results.loc[maturity, 'r'],
243
+ s=fit_results.loc[i, 's'],
244
+ r=fit_results.loc[i, 'r'],
237
245
  iv_array=iv_array,
238
246
  ytm=ytm
239
247
  )
@@ -0,0 +1,132 @@
1
+ """
2
+ Model interpolation module for the Voly package.
3
+
4
+ This module handles interpolating volatility model parameters across different
5
+ days to expiry, allowing for consistent volatility surfaces at arbitrary tenors.
6
+ """
7
+
8
+ import pandas as pd
9
+ import numpy as np
10
+ import datetime as dt
11
+ from typing import List, Dict, Tuple, Optional, Union, Any
12
+ from scipy import interpolate
13
+ from voly.utils.logger import logger, catch_exception
14
+ from voly.exceptions import VolyError
15
+
16
+
17
+ @catch_exception
18
+ def interpolate_model(fit_results: pd.DataFrame,
19
+ list_of_days: List[str] = ['7d', '30d', '90d', '150d', '240d'],
20
+ method: str = 'cubic') -> pd.DataFrame:
21
+ """
22
+ Interpolate model parameters across different days to expiry.
23
+
24
+ Parameters:
25
+ - fit_results: DataFrame with model fitting results, indexed by maturity names
26
+ - list_of_days: list of specific days to interpolate to (e.g., ['7d', '30d', '90d'])
27
+ - method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
28
+
29
+ Returns:
30
+ - DataFrame with interpolated model parameters for the specified days
31
+ """
32
+ logger.info(f"Interpolating model parameters using {method} method")
33
+
34
+ # Check if fit_results is valid
35
+ if fit_results is None or fit_results.empty:
36
+ raise VolyError("Fit results DataFrame is empty or None")
37
+
38
+ # Extract years to maturity from fit_results
39
+ original_ytms = fit_results['ytm'].values
40
+
41
+ if len(original_ytms) < 2:
42
+ raise VolyError("Need at least two maturities in fit_results to interpolate")
43
+
44
+ # Sort original years to maturity for proper interpolation
45
+ sorted_indices = np.argsort(original_ytms)
46
+ original_ytms = original_ytms[sorted_indices]
47
+
48
+ # Parse days from strings like '7d', '30d', etc.
49
+ target_days = []
50
+ for day_str in list_of_days:
51
+ if not day_str.endswith('d'):
52
+ raise VolyError(f"Invalid day format: {day_str}. Expected format: '7d', '30d', etc.")
53
+ try:
54
+ days = int(day_str[:-1])
55
+ target_days.append(days)
56
+ except ValueError:
57
+ raise VolyError(f"Invalid day value: {day_str}. Expected format: '7d', '30d', etc.")
58
+
59
+ # Convert target days to years for interpolation (to match original implementation)
60
+ target_years = [day / 365.25 for day in target_days]
61
+
62
+ # Check if target years are within the range of original years
63
+ min_original, max_original = np.min(original_ytms), np.max(original_ytms)
64
+ for years in target_years:
65
+ if years < min_original or years > max_original:
66
+ logger.warning(
67
+ f"Target time {years:.4f} years is outside the range of original data [{min_original:.4f}, {max_original:.4f}]. "
68
+ "Extrapolation may give unreliable results.")
69
+
70
+ # Columns to interpolate
71
+ param_columns = ['u', 'a', 'b', 'rho', 'm', 'sigma', 'nu', 'psi', 'p', 'c', 'nu_tilde']
72
+
73
+ # Create empty DataFrame for interpolated results
74
+ interpolated_df = pd.DataFrame(index=[f"{day}d" for day in target_days])
75
+
76
+ # Generate YTM and maturity dates for interpolated results
77
+ interpolated_df['dtm'] = target_days
78
+ interpolated_df['ytm'] = [day / 365.25 for day in target_days]
79
+
80
+ # Calculate maturity dates
81
+ now = dt.datetime.now()
82
+ maturity_dates = []
83
+ for days in target_days:
84
+ maturity_date = now + dt.timedelta(days=days)
85
+ maturity_dates.append(maturity_date)
86
+
87
+ interpolated_df['maturity_date'] = maturity_dates
88
+
89
+ # Sort fit_results by ytm
90
+ sorted_fit_results = fit_results.iloc[sorted_indices]
91
+ recent_row = sorted_fit_results.iloc[-1]
92
+ interpolated_df['s'] = recent_row['s']
93
+ interpolated_df['r'] = recent_row['r']
94
+
95
+ # Interpolate model parameters
96
+ for param in param_columns:
97
+ if param in fit_results.columns:
98
+ try:
99
+ # Create interpolation function using years to expiry
100
+ f = interpolate.interp1d(
101
+ original_ytms,
102
+ sorted_fit_results[param].values,
103
+ kind=method,
104
+ bounds_error=False,
105
+ fill_value='extrapolate'
106
+ )
107
+
108
+ # Apply interpolation with target years
109
+ interpolated_df[param] = f(target_years)
110
+ except Exception as e:
111
+ logger.error(f"Error interpolating parameter {param}: {str(e)}")
112
+ # Fallback to nearest neighbor if sophisticated method fails
113
+ f = interpolate.interp1d(
114
+ original_ytms,
115
+ sorted_fit_results[param].values,
116
+ kind='nearest',
117
+ bounds_error=False,
118
+ fill_value=(sorted_fit_results[param].iloc[0], sorted_fit_results[param].iloc[-1])
119
+ )
120
+ interpolated_df[param] = f(target_years)
121
+
122
+ # Ensure consistent ordering of columns with expected structure
123
+ expected_columns = ['s', 'u', 'maturity_date', 'dtm', 'ytm', 'a', 'b', 'rho', 'm', 'sigma',
124
+ 'nu', 'psi', 'p', 'c', 'nu_tilde', 'r']
125
+
126
+ # Create final column order based on available columns
127
+ column_order = [col for col in expected_columns if col in interpolated_df.columns]
128
+ interpolated_df = interpolated_df[column_order]
129
+
130
+ logger.info(f"Successfully interpolated model parameters for {len(target_days)} target days")
131
+
132
+ return interpolated_df
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.81
3
+ Version: 0.0.83
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -1,221 +0,0 @@
1
- """
2
- Surface interpolation module for the Voly package.
3
-
4
- This module handles interpolation of implied volatility surfaces
5
- across both moneyness and time dimensions.
6
- """
7
-
8
- import numpy as np
9
- import pandas as pd
10
- from typing import List, Tuple, Dict, Optional, Union, Any
11
- from scipy.interpolate import interp1d, pchip_interpolate
12
- from voly.utils.logger import logger, catch_exception
13
- from voly.exceptions import VolyError
14
- from voly.models import SVIModel
15
-
16
-
17
- @catch_exception
18
- def interpolate_surface_time(
19
- moneyness_grid: np.ndarray,
20
- expiries: np.ndarray,
21
- surface_values: np.ndarray,
22
- target_expiries: np.ndarray,
23
- method: str = 'cubic'
24
- ) -> np.ndarray:
25
- """
26
- Interpolate the surface across the time dimension.
27
-
28
- Parameters:
29
- - moneyness_grid: Array of log-moneyness values
30
- - expiries: Array of expiry times (in years)
31
- - surface_values: 2D array of values to interpolate (rows=expiries, cols=moneyness)
32
- - target_expiries: Array of target expiry times to interpolate to
33
- - method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
34
-
35
- Returns:
36
- - 2D array of interpolated values (rows=target_expiries, cols=moneyness)
37
- """
38
- if len(expiries) < 2:
39
- raise VolyError("At least two expiries are required for time interpolation")
40
-
41
- # Initialize the output array
42
- interpolated_surface = np.zeros((len(target_expiries), len(moneyness_grid)))
43
-
44
- # For each moneyness point, interpolate across time
45
- for i in range(len(moneyness_grid)):
46
- if method == 'pchip':
47
- # Use PCHIP (Piecewise Cubic Hermite Interpolating Polynomial)
48
- interpolated_values = pchip_interpolate(expiries, surface_values[:, i], target_expiries)
49
- else:
50
- # Use regular interpolation (linear, cubic, etc.)
51
- interp_func = interp1d(expiries, surface_values[:, i], kind=method, bounds_error=False,
52
- fill_value='extrapolate')
53
- interpolated_values = interp_func(target_expiries)
54
-
55
- interpolated_surface[:, i] = interpolated_values
56
-
57
- return interpolated_surface
58
-
59
-
60
- @catch_exception
61
- def create_target_expiries(
62
- min_dte: float,
63
- max_dte: float,
64
- num_points: int = 10,
65
- specific_days: Optional[List[int]] = None
66
- ) -> np.ndarray:
67
- """
68
- Create a grid of target expiry days for interpolation.
69
-
70
- Parameters:
71
- - min_dte: Minimum days to expiry
72
- - max_dte: Maximum days to expiry
73
- - num_points: Number of points for regular grid
74
- - specific_days: Optional list of specific days to include (e.g., [7, 30, 90, 180])
75
-
76
- Returns:
77
- - Array of target expiry times in years
78
- """
79
- # Create regular grid in days
80
- if specific_days is not None:
81
- # Filter specific days to be within range
82
- days = np.array([d for d in specific_days if min_dte <= d <= max_dte])
83
- if len(days) == 0:
84
- logger.warning("No specific days within range, using regular grid")
85
- days = np.linspace(min_dte, max_dte, num_points)
86
- else:
87
- # Use regular grid
88
- days = np.linspace(min_dte, max_dte, num_points)
89
-
90
- # Convert to years
91
- years = days / 365.25
92
-
93
- return years
94
-
95
-
96
- @catch_exception
97
- def interpolate_svi_parameters(
98
- raw_param_matrix: pd.DataFrame,
99
- target_expiries_years: np.ndarray,
100
- method: str = 'cubic'
101
- ) -> pd.DataFrame:
102
- """
103
- Interpolate SVI parameters across time.
104
-
105
- Parameters:
106
- - raw_param_matrix: Matrix of SVI parameters with maturity names as columns
107
- - target_expiries_years: Array of target expiry times (in years)
108
- - method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
109
-
110
- Returns:
111
- - Matrix of interpolated SVI parameters
112
- """
113
- # Get expiry times in years from the parameter matrix
114
- yte_values = raw_param_matrix.attrs['yte_values']
115
- dte_values = raw_param_matrix.attrs['dte_values']
116
-
117
- # Sort maturity names by DTE
118
- maturity_names = sorted(yte_values.keys(), key=lambda x: dte_values[x])
119
-
120
- # Extract expiry times in order
121
- expiry_years = np.array([yte_values[m] for m in maturity_names])
122
-
123
- # Check if we have enough points for interpolation
124
- if len(expiry_years) < 2:
125
- raise VolyError("At least two expiries are required for interpolation")
126
-
127
- # Create new parameter matrix for interpolated values
128
- interp_param_matrix = pd.DataFrame(
129
- columns=[f"t{i:.2f}" for i in target_expiries_years],
130
- index=SVIModel.PARAM_NAMES
131
- )
132
-
133
- # For each SVI parameter, interpolate across time
134
- for param in SVIModel.PARAM_NAMES:
135
- param_values = np.array([raw_param_matrix.loc[param, m] for m in maturity_names])
136
-
137
- if method == 'pchip':
138
- interpolated_values = pchip_interpolate(expiry_years, param_values, target_expiries_years)
139
- else:
140
- interp_func = interp1d(expiry_years, param_values, kind=method, bounds_error=False,
141
- fill_value='extrapolate')
142
- interpolated_values = interp_func(target_expiries_years)
143
-
144
- # Store interpolated values
145
- for i, t in enumerate(target_expiries_years):
146
- interp_param_matrix.loc[param, f"t{t:.2f}"] = interpolated_values[i]
147
-
148
- # Create matching DTE values for convenience
149
- interp_dte_values = target_expiries_years * 365.25
150
-
151
- # Store YTE and DTE as attributes in the DataFrame for reference
152
- interp_param_matrix.attrs['yte_values'] = {f"t{t:.2f}": t for t in target_expiries_years}
153
- interp_param_matrix.attrs['dte_values'] = {f"t{t:.2f}": t * 365.25 for t in target_expiries_years}
154
-
155
- return interp_param_matrix
156
-
157
-
158
- @catch_exception
159
- def interpolate_model(
160
- fit_results: Dict[str, Any],
161
- specific_days: Optional[List[int]] = None,
162
- num_points: int = 10,
163
- method: str = 'cubic'
164
- ) -> Dict[str, Any]:
165
- """
166
- Interpolate a fitted model to specific days to expiry.
167
-
168
- Parameters:
169
- - fit_results: Dictionary with fitting results from fit_model()
170
- - specific_days: Optional list of specific days to include (e.g., [7, 30, 90, 180])
171
- - num_points: Number of points for regular grid if specific_days is None
172
- - method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
173
-
174
- Returns:
175
- - Dictionary with interpolation results
176
- """
177
- # Extract required data from fit results
178
- raw_param_matrix = fit_results['raw_param_matrix']
179
- moneyness_grid = fit_results['moneyness_grid']
180
-
181
- # Get min and max DTE from the original data
182
- dte_values = list(raw_param_matrix.attrs['dte_values'].values())
183
- min_dte = min(dte_values)
184
- max_dte = max(dte_values)
185
-
186
- # Create target expiry grid
187
- target_expiries_years = create_target_expiries(min_dte, max_dte, num_points, specific_days)
188
-
189
- # Interpolate SVI parameters
190
- interp_param_matrix = interpolate_svi_parameters(raw_param_matrix, target_expiries_years, method)
191
-
192
- # Generate implied volatility surface from interpolated parameters
193
- iv_surface = {}
194
- for maturity_name, yte in interp_param_matrix.attrs['yte_values'].items():
195
- svi_params = interp_param_matrix[maturity_name].values
196
- w_svi = [SVIModel.svi(x, *svi_params) for x in moneyness_grid]
197
- iv_surface[yte] = np.sqrt(np.array(w_svi) / yte)
198
-
199
- # Convert to Jump-Wing parameters for the interpolated maturities
200
- jw_param_matrix = pd.DataFrame(
201
- columns=interp_param_matrix.columns,
202
- index=SVIModel.JW_PARAM_NAMES
203
- )
204
-
205
- for maturity_name, yte in interp_param_matrix.attrs['yte_values'].items():
206
- a, b, sigma, rho, m = interp_param_matrix[maturity_name].values
207
- nu, psi, p, c, nu_tilde = SVIModel.svi_jw_params(a, b, sigma, rho, m, yte)
208
- jw_param_matrix[maturity_name] = [nu, psi, p, c, nu_tilde]
209
-
210
- # Copy attributes from param matrix
211
- jw_param_matrix.attrs = interp_param_matrix.attrs.copy()
212
-
213
- return {
214
- 'moneyness_grid': moneyness_grid,
215
- 'target_expiries_years': target_expiries_years,
216
- 'target_expiries_days': target_expiries_years * 365.25,
217
- 'interp_param_matrix': interp_param_matrix,
218
- 'interp_jw_param_matrix': jw_param_matrix,
219
- 'iv_surface': iv_surface,
220
- 'method': method
221
- }
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
File without changes
File without changes