voly 0.0.193__tar.gz → 0.0.195__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.
- {voly-0.0.193/src/voly.egg-info → voly-0.0.195}/PKG-INFO +1 -1
- {voly-0.0.193 → voly-0.0.195}/pyproject.toml +2 -2
- {voly-0.0.193 → voly-0.0.195}/src/voly/core/fit.py +65 -79
- {voly-0.0.193 → voly-0.0.195}/src/voly/models.py +8 -6
- {voly-0.0.193 → voly-0.0.195/src/voly.egg-info}/PKG-INFO +1 -1
- {voly-0.0.193 → voly-0.0.195}/LICENSE +0 -0
- {voly-0.0.193 → voly-0.0.195}/README.md +0 -0
- {voly-0.0.193 → voly-0.0.195}/setup.cfg +0 -0
- {voly-0.0.193 → voly-0.0.195}/setup.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/__init__.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/client.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/core/__init__.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/core/charts.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/core/data.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/core/hd.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/core/rnd.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/exceptions.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/formulas.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/utils/density.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly/utils/logger.py +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly.egg-info/requires.txt +0 -0
- {voly-0.0.193 → voly-0.0.195}/src/voly.egg-info/top_level.txt +0 -0
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | |
| 4 4 |  | 
| 5 5 | 
             
            [project]
         | 
| 6 6 | 
             
            name = "voly"
         | 
| 7 | 
            -
            version = "0.0. | 
| 7 | 
            +
            version = "0.0.195"
         | 
| 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. | 
| 63 | 
            +
            python_version = "0.0.195"
         | 
| 64 64 | 
             
            warn_return_any = true
         | 
| 65 65 | 
             
            warn_unused_configs = true
         | 
| 66 66 | 
             
            disallow_untyped_defs = true
         | 
| @@ -29,7 +29,7 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 29 29 | 
             
                - option_chain: DataFrame with market data
         | 
| 30 30 |  | 
| 31 31 | 
             
                Returns:
         | 
| 32 | 
            -
                - DataFrame with all fit results and performance metrics as columns,  | 
| 32 | 
            +
                - DataFrame with all fit results and performance metrics as columns, maturity_names as index
         | 
| 33 33 | 
             
                """
         | 
| 34 34 | 
             
                # Start overall timer
         | 
| 35 35 | 
             
                start_total = time.time()
         | 
| @@ -66,15 +66,16 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 66 66 | 
             
                s = option_chain['index_price'].iloc[0]
         | 
| 67 67 | 
             
                unique_ts = sorted(option_chain['t'].unique())
         | 
| 68 68 | 
             
                maturity_names = [option_chain[option_chain['t'] == t]['maturity_name'].iloc[0] for t in unique_ts]
         | 
| 69 | 
            -
                 | 
| 69 | 
            +
                maturity_dates = [option_chain[option_chain['t'] == t]['maturity_date'].iloc[0] for t in unique_ts]
         | 
| 70 | 
            +
                maturity_data_groups = option_chain.groupby('maturity_date')
         | 
| 70 71 | 
             
                params_dict = {}
         | 
| 71 72 | 
             
                results_data = {col: [] for col in column_dtypes.keys()}
         | 
| 72 73 | 
             
                num_points = 2000  # Number of points for k_grid
         | 
| 73 74 |  | 
| 74 | 
            -
                def process_maturity(maturity,  | 
| 75 | 
            +
                def process_maturity(maturity, maturity_data):
         | 
| 75 76 | 
             
                    """Process single maturity for SVI calibration."""
         | 
| 76 | 
            -
                     | 
| 77 | 
            -
                    duplicated_iv =  | 
| 77 | 
            +
                    maturity_data = maturity_data[maturity_data['option_type'] == 'C']
         | 
| 78 | 
            +
                    duplicated_iv = maturity_data[maturity_data.duplicated('mark_iv', keep=False)]
         | 
| 78 79 |  | 
| 79 80 | 
             
                    # For each duplicated IV, keep the row closest to log_moneyness=0
         | 
| 80 81 | 
             
                    def keep_closest_to_zero(subgroup):
         | 
| @@ -88,17 +89,17 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 88 89 | 
             
                    )
         | 
| 89 90 |  | 
| 90 91 | 
             
                    # Get rows with unique mark_iv (no duplicates)
         | 
| 91 | 
            -
                    unique_iv =  | 
| 92 | 
            +
                    unique_iv = maturity_data.drop_duplicates('mark_iv', keep=False)
         | 
| 92 93 |  | 
| 93 94 | 
             
                    # Combine cleaned duplicates and unique rows
         | 
| 94 95 | 
             
                    maturity_data = pd.concat([unique_iv, cleaned_duplicated_iv])
         | 
| 95 96 | 
             
                    maturity_date = maturity_data['maturity_date'].iloc[0]
         | 
| 97 | 
            +
                    maturity_name = maturity_data['maturity_name'].iloc[0]
         | 
| 96 98 |  | 
| 97 | 
            -
             | 
| 98 | 
            -
                     | 
| 99 | 
            -
                     | 
| 100 | 
            -
                     | 
| 101 | 
            -
                    vega = group['vega'].values if 'vega' in group.columns else np.ones_like(iv)
         | 
| 99 | 
            +
                    t = maturity_data['t'].iloc[0]
         | 
| 100 | 
            +
                    K = maturity_data['strikes'].values
         | 
| 101 | 
            +
                    iv = maturity_data['mark_iv'].values
         | 
| 102 | 
            +
                    vega = maturity_data['vega'].values if 'vega' in maturity_data.columns else np.ones_like(iv)
         | 
| 102 103 | 
             
                    k = np.log(K / s)
         | 
| 103 104 | 
             
                    w = (iv ** 2) * t
         | 
| 104 105 | 
             
                    mask = ~np.isnan(w) & ~np.isnan(vega) & ~np.isnan(k) & (iv >= 0)
         | 
| @@ -107,48 +108,35 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 107 108 | 
             
                    params = [np.nan] * 5
         | 
| 108 109 | 
             
                    loss = np.inf
         | 
| 109 110 | 
             
                    nu = psi = p = c = nu_tilde = np.nan
         | 
| 110 | 
            -
                    rmse = mae = r2 = max_error = np.nan
         | 
| 111 111 | 
             
                    butterfly_arbitrage_free = True
         | 
| 112 112 | 
             
                    r = maturity_data['interest_rate'].iloc[0] if 'interest_rate' in maturity_data.columns else 0
         | 
| 113 113 | 
             
                    log_min_strike = usd_min_strike = np.nan
         | 
| 114 114 |  | 
| 115 115 | 
             
                    if len(k) > 5:
         | 
| 116 116 | 
             
                        params, loss = SVIModel.fit(tiv=w, vega=vega, k=k, tau=t)
         | 
| 117 | 
            -
                         | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 130 | 
            -
                             | 
| 131 | 
            -
                             | 
| 132 | 
            -
                             | 
| 133 | 
            -
             | 
| 134 | 
            -
                             | 
| 135 | 
            -
             | 
| 136 | 
            -
             | 
| 137 | 
            -
             | 
| 138 | 
            -
             | 
| 139 | 
            -
             | 
| 140 | 
            -
                            w_k = lambda k: SVIModel.svi(k, a_scaled, b_scaled, m, rho, sigma)
         | 
| 141 | 
            -
                            w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m) ** 2 + sigma ** 2))
         | 
| 142 | 
            -
                            w_double_prime = lambda k: b_scaled * sigma ** 2 / ((k - m) ** 2 + sigma ** 2) ** (3 / 2)
         | 
| 143 | 
            -
             | 
| 144 | 
            -
                            for k_val in k_range:
         | 
| 145 | 
            -
                                wk = w_k(k_val)
         | 
| 146 | 
            -
                                wp = w_prime(k_val)
         | 
| 147 | 
            -
                                wpp = w_double_prime(k_val)
         | 
| 148 | 
            -
                                g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
         | 
| 149 | 
            -
                                if g < 0:
         | 
| 150 | 
            -
                                    butterfly_arbitrage_free = False
         | 
| 151 | 
            -
                                    break
         | 
| 117 | 
            +
                        a, b, m, rho, sigma = params
         | 
| 118 | 
            +
                        a_scaled, b_scaled = a * t, b * t
         | 
| 119 | 
            +
                        params_dict[maturity_date] = (t, params)
         | 
| 120 | 
            +
                        nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t)
         | 
| 121 | 
            +
                        log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
         | 
| 122 | 
            +
                        usd_min_strike = np.exp(log_min_strike) * s
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                        # Butterfly arbitrage check
         | 
| 125 | 
            +
                        k_range = np.linspace(min(k), max(k), num_points)
         | 
| 126 | 
            +
                        w_k = lambda k: SVIModel.svi(k, a_scaled, b_scaled, m, rho, sigma)
         | 
| 127 | 
            +
                        w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m) ** 2 + sigma ** 2))
         | 
| 128 | 
            +
                        w_double_prime = lambda k: b_scaled * sigma ** 2 / ((k - m) ** 2 + sigma ** 2) ** (3 / 2)
         | 
| 129 | 
            +
                        for k_val in k_range:
         | 
| 130 | 
            +
                            wk = w_k(k_val)
         | 
| 131 | 
            +
                            wp = w_prime(k_val)
         | 
| 132 | 
            +
                            wpp = w_double_prime(k_val)
         | 
| 133 | 
            +
                            g = (1 - (k_val * wp) / (2 * wk)) ** 2 - (wp ** 2) / 4 * (1 / wk + 1 / 4) + wpp / 2
         | 
| 134 | 
            +
                            if g < 0:
         | 
| 135 | 
            +
                                butterfly_arbitrage_free = False
         | 
| 136 | 
            +
                                break
         | 
| 137 | 
            +
                    else:
         | 
| 138 | 
            +
                        params = [np.nan] * 5
         | 
| 139 | 
            +
                        loss = np.inf
         | 
| 152 140 |  | 
| 153 141 | 
             
                    # Log result
         | 
| 154 142 | 
             
                    GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
         | 
| @@ -175,24 +163,21 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 175 163 | 
             
                    results_data['fit_success'].append(bool(not np.isnan(params[0])))
         | 
| 176 164 | 
             
                    results_data['butterfly_arbitrage_free'].append(butterfly_arbitrage_free)
         | 
| 177 165 | 
             
                    results_data['calendar_arbitrage_free'].append(True)  # Updated after check
         | 
| 178 | 
            -
                    results_data['rmse'].append( | 
| 179 | 
            -
                    results_data['mae'].append( | 
| 180 | 
            -
                    results_data['r2'].append( | 
| 181 | 
            -
                    results_data['max_error'].append( | 
| 166 | 
            +
                    results_data['rmse'].append(np.nan)
         | 
| 167 | 
            +
                    results_data['mae'].append(np.nan)
         | 
| 168 | 
            +
                    results_data['r2'].append(np.nan)
         | 
| 169 | 
            +
                    results_data['max_error'].append(np.nan)
         | 
| 182 170 | 
             
                    results_data['loss'].append(float(loss))
         | 
| 183 171 | 
             
                    results_data['n_points'].append(int(len(k)))
         | 
| 184 172 |  | 
| 185 | 
            -
                    return  | 
| 173 | 
            +
                    return maturity_name
         | 
| 186 174 |  | 
| 187 | 
            -
                # Parallel processing of maturities | 
| 188 | 
            -
                 | 
| 175 | 
            +
                # Parallel processing of maturities
         | 
| 176 | 
            +
                logger.info("\nStarting parallel processing of maturities...")
         | 
| 189 177 | 
             
                with ThreadPoolExecutor() as executor:
         | 
| 190 | 
            -
                    futures = [executor.submit(process_maturity, maturity,  | 
| 191 | 
            -
                               for maturity,  | 
| 192 | 
            -
                    for future in futures | 
| 193 | 
            -
                        future.result()
         | 
| 194 | 
            -
                end_parallel = time.time()
         | 
| 195 | 
            -
                logger.info(f"Processing completed in {end_parallel - start_parallel:.4f} seconds")
         | 
| 178 | 
            +
                    futures = [executor.submit(process_maturity, maturity, maturity_data)
         | 
| 179 | 
            +
                               for maturity, maturity_data in maturity_data_groups]
         | 
| 180 | 
            +
                    maturity_names = [future.result() for future in futures]
         | 
| 196 181 |  | 
| 197 182 | 
             
                # Create results DataFrame
         | 
| 198 183 | 
             
                results_df = pd.DataFrame(results_data, index=maturity_names)
         | 
| @@ -208,7 +193,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 208 193 | 
             
                # Sort by time to maturity
         | 
| 209 194 | 
             
                results_df = results_df.sort_values(by='t')
         | 
| 210 195 |  | 
| 211 | 
            -
                # Calendar arbitrage check (pre-correction) | 
| 196 | 
            +
                # Calendar arbitrage check (pre-correction)
         | 
| 197 | 
            +
                logger.info("\nChecking calendar arbitrage (pre-correction)...")
         | 
| 212 198 | 
             
                k_grid = np.linspace(-2, 2, num_points)
         | 
| 213 199 | 
             
                sorted_maturities = sorted(params_dict.keys(), key=lambda x: params_dict[x][0])
         | 
| 214 200 | 
             
                calendar_arbitrage_free = True
         | 
| @@ -222,8 +208,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 222 208 | 
             
                    if np.isnan(a1) or np.isnan(a2):
         | 
| 223 209 | 
             
                        continue
         | 
| 224 210 |  | 
| 225 | 
            -
                     | 
| 226 | 
            -
                    K =  | 
| 211 | 
            +
                    maturity_data = maturity_data_groups.get_group(mat2)
         | 
| 212 | 
            +
                    K = maturity_data['strikes'].values
         | 
| 227 213 | 
             
                    k_market = np.log(K / s)
         | 
| 228 214 | 
             
                    mask = ~np.isnan(k_market)
         | 
| 229 215 | 
             
                    k_check = np.unique(np.concatenate([k_market[mask], np.linspace(min(k_market[mask]), max(k_market[mask]), num_points)]))
         | 
| @@ -241,7 +227,7 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 241 227 | 
             
                for mat in results_df['maturity_date']:
         | 
| 242 228 | 
             
                    results_df.loc[results_df['maturity_date'] == mat, 'calendar_arbitrage_free'] = calendar_arbitrage_free
         | 
| 243 229 |  | 
| 244 | 
            -
                # Calendar arbitrage correction | 
| 230 | 
            +
                # Calendar arbitrage correction
         | 
| 245 231 | 
             
                logger.info("Performing calendar arbitrage correction...")
         | 
| 246 232 | 
             
                for i in range(1, len(sorted_maturities)):
         | 
| 247 233 | 
             
                    mat2 = sorted_maturities[i]
         | 
| @@ -252,10 +238,10 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 252 238 | 
             
                    if np.any(np.isnan(params2)) or np.any(np.isnan(params1)):
         | 
| 253 239 | 
             
                        continue
         | 
| 254 240 |  | 
| 255 | 
            -
                     | 
| 256 | 
            -
                    K =  | 
| 257 | 
            -
                    iv =  | 
| 258 | 
            -
                    vega =  | 
| 241 | 
            +
                    maturity_data = maturity_data_groups.get_group(mat2)
         | 
| 242 | 
            +
                    K = maturity_data['strikes'].values
         | 
| 243 | 
            +
                    iv = maturity_data['mark_iv'].values
         | 
| 244 | 
            +
                    vega = maturity_data['vega'].values if 'vega' in maturity_data.columns else np.ones_like(iv)
         | 
| 259 245 | 
             
                    k = np.log(K / s)
         | 
| 260 246 | 
             
                    w = (iv ** 2) * t2
         | 
| 261 247 | 
             
                    mask = ~np.isnan(w) & ~np.isnan(vega) & ~np.isnan(k) & (iv >= 0)
         | 
| @@ -266,13 +252,14 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 266 252 | 
             
                        prev_params=params1, prev_t=t1, k_grid=k_grid
         | 
| 267 253 | 
             
                    )
         | 
| 268 254 |  | 
| 269 | 
            -
                    params_dict[mat2] = (t2, new_params)
         | 
| 270 | 
            -
             | 
| 271 255 | 
             
                    a, b, m, rho, sigma = new_params
         | 
| 272 256 | 
             
                    a_scaled, b_scaled = a * t2, b * t2
         | 
| 257 | 
            +
                    params_dict[mat2] = (t2, new_params)
         | 
| 273 258 | 
             
                    nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a_scaled, b_scaled, m, rho, sigma, t2)
         | 
| 259 | 
            +
                    log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
         | 
| 260 | 
            +
                    usd_min_strike = np.exp(log_min_strike) * s
         | 
| 274 261 |  | 
| 275 | 
            -
                    #  | 
| 262 | 
            +
                    # Compute fit statistics
         | 
| 276 263 | 
             
                    w_model = np.array([SVIModel.svi(x, a_scaled, b_scaled, m, rho, sigma) for x in k])
         | 
| 277 264 | 
             
                    iv_model = np.sqrt(w_model / t2)
         | 
| 278 265 | 
             
                    iv_market = iv
         | 
| @@ -281,17 +268,12 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 281 268 | 
             
                    r2 = r2_score(iv_market, iv_model)
         | 
| 282 269 | 
             
                    max_error = np.max(np.abs(iv_market - iv_model))
         | 
| 283 270 |  | 
| 284 | 
            -
                    # Recompute min strike
         | 
| 285 | 
            -
                    log_min_strike = SVIModel.svi_min_strike(sigma, rho, m)
         | 
| 286 | 
            -
                    usd_min_strike = np.exp(log_min_strike) * s
         | 
| 287 | 
            -
             | 
| 288 271 | 
             
                    # Update butterfly arbitrage check
         | 
| 289 272 | 
             
                    butterfly_arbitrage_free = True
         | 
| 290 273 | 
             
                    k_range = np.linspace(min(k), max(k), num_points)
         | 
| 291 274 | 
             
                    w_k = lambda k: SVIModel.svi(k, a_scaled, b_scaled, m, rho, sigma)
         | 
| 292 275 | 
             
                    w_prime = lambda k: b_scaled * (rho + (k - m) / np.sqrt((k - m) ** 2 + sigma ** 2))
         | 
| 293 276 | 
             
                    w_double_prime = lambda k: b_scaled * sigma ** 2 / ((k - m) ** 2 + sigma ** 2) ** (3 / 2)
         | 
| 294 | 
            -
             | 
| 295 277 | 
             
                    for k_val in k_range:
         | 
| 296 278 | 
             
                        wk = w_k(k_val)
         | 
| 297 279 | 
             
                        wp = w_prime(k_val)
         | 
| @@ -302,7 +284,7 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 302 284 | 
             
                            break
         | 
| 303 285 |  | 
| 304 286 | 
             
                    # Update results_df using maturity_name
         | 
| 305 | 
            -
                    mat_name =  | 
| 287 | 
            +
                    mat_name = maturity_data['maturity_name'].iloc[0]
         | 
| 306 288 | 
             
                    results_df.loc[mat_name, 'a'] = float(a_scaled)
         | 
| 307 289 | 
             
                    results_df.loc[mat_name, 'b'] = float(b_scaled)
         | 
| 308 290 | 
             
                    results_df.loc[mat_name, 'm'] = float(m)
         | 
| @@ -321,6 +303,10 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 321 303 | 
             
                    results_df.loc[mat_name, 'usd_min_strike'] = float(usd_min_strike)
         | 
| 322 304 | 
             
                    results_df.loc[mat_name, 'butterfly_arbitrage_free'] = butterfly_arbitrage_free
         | 
| 323 305 | 
             
                    results_df.loc[mat_name, 'fit_success'] = bool(not np.isnan(a))
         | 
| 306 | 
            +
                else:
         | 
| 307 | 
            +
                    logger.warning(f"Skipping parameter update for maturity {mat2} due to invalid parameters")
         | 
| 308 | 
            +
                    mat_name = maturity_data['maturity_name'].iloc[0]
         | 
| 309 | 
            +
                    results_df.loc[mat_name, 'fit_success'] = False
         | 
| 324 310 |  | 
| 325 311 | 
             
                # Calendar arbitrage check (post-correction)
         | 
| 326 312 | 
             
                calendar_arbitrage_free = True
         | 
| @@ -334,8 +320,8 @@ def fit_model(option_chain: pd.DataFrame) -> pd.DataFrame: | |
| 334 320 | 
             
                    if np.isnan(a1) or np.isnan(a2):
         | 
| 335 321 | 
             
                        continue
         | 
| 336 322 |  | 
| 337 | 
            -
                     | 
| 338 | 
            -
                    K =  | 
| 323 | 
            +
                    maturity_data = maturity_data_groups.get_group(mat2)
         | 
| 324 | 
            +
                    K = maturity_data['strikes'].values
         | 
| 339 325 | 
             
                    k_market = np.log(K / s)
         | 
| 340 326 | 
             
                    mask = ~np.isnan(k_market)
         | 
| 341 327 | 
             
                    k_check = np.unique(np.concatenate(
         | 
| @@ -90,8 +90,6 @@ class SVIModel: | |
| 90 90 | 
             
                    if len(k) <= 5:
         | 
| 91 91 | 
             
                        return [np.nan] * 5, np.inf
         | 
| 92 92 |  | 
| 93 | 
            -
                    from scipy.optimize import minimize
         | 
| 94 | 
            -
             | 
| 95 93 | 
             
                    vega = vega / vega.max() if vega.max() > 0 else np.ones_like(vega)
         | 
| 96 94 | 
             
                    m_init = np.mean(k)
         | 
| 97 95 | 
             
                    sigma_init = max(0.1, np.std(k) * 0.1)
         | 
| @@ -102,7 +100,7 @@ class SVIModel: | |
| 102 100 | 
             
                        return loss
         | 
| 103 101 |  | 
| 104 102 | 
             
                    result = minimize(score, [sigma_init, m_init], bounds=[(0.001, None), (None, None)],
         | 
| 105 | 
            -
             | 
| 103 | 
            +
                                     tol=1e-16, method="SLSQP", options={'maxfun': 5000})
         | 
| 106 104 |  | 
| 107 105 | 
             
                    sigma, m = result.x
         | 
| 108 106 | 
             
                    c, d, a_calib, loss = cls.calibration(tiv, vega, k, m, sigma)
         | 
| @@ -116,7 +114,9 @@ class SVIModel: | |
| 116 114 | 
             
                        a_svi = a_calib / tau
         | 
| 117 115 | 
             
                        rho_svi = b_svi = 0
         | 
| 118 116 |  | 
| 119 | 
            -
                     | 
| 117 | 
            +
                    # Validate parameters
         | 
| 118 | 
            +
                    params = [a_svi, b_svi, m, rho_svi, sigma]
         | 
| 119 | 
            +
                    return params, loss
         | 
| 120 120 |  | 
| 121 121 | 
             
                @classmethod
         | 
| 122 122 | 
             
                def correct_calendar_arbitrage(cls, params, t, tiv, vega, k, prev_params, prev_t, k_grid):
         | 
| @@ -162,11 +162,13 @@ class SVIModel: | |
| 162 162 |  | 
| 163 163 | 
             
                    if result.success:
         | 
| 164 164 | 
             
                        new_params = result.x
         | 
| 165 | 
            -
                         | 
| 165 | 
            +
                        a, b, m, rho, sigma = new_params
         | 
| 166 | 
            +
                        a_scaled, b_scaled = a * t, b * t
         | 
| 167 | 
            +
                        w_current = cls.svi(k_constraint, a_scaled, b_scaled, m, rho, sigma)
         | 
| 166 168 | 
             
                        w_prev = cls.svi(k_constraint, a_prev * prev_t, b_prev * prev_t, m_prev, rho_prev, sigma_prev)
         | 
| 167 169 | 
             
                        violation = np.min(w_current - w_prev)
         | 
| 168 170 | 
             
                        logger.info(f"Calendar arbitrage correction {'successful' if violation >= -1e-6 else 'failed'} for t={t:.4f}, "
         | 
| 169 | 
            -
             | 
| 171 | 
            +
                                    f"min margin={violation:.6f}")
         | 
| 170 172 | 
             
                        return new_params
         | 
| 171 173 | 
             
                    logger.warning(f"Calendar arbitrage correction failed for t={t:.4f}")
         | 
| 172 174 | 
             
                    return params
         | 
| 
            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
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         |