From fe819692820eb735b44b5141c730f13436b36eff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 08:34:26 +0000 Subject: [PATCH 1/4] Initial plan From c643f28d336d8e1debb078f62f12240e8d7ed7c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 08:42:38 +0000 Subject: [PATCH 2/4] Fix all 4 identified package issues - Issue 1: Added missing max_iter parameter to README API Reference - Issue 2: Fixed all 6 mypy strict mode type annotation errors - Issue 3: Handle ill-conditioned Hessian gracefully by checking for negative variances before sqrt - Issue 4: Handle half-life display when persistence is near 1.0 with special case for near unit root Co-authored-by: studiofarzulla <62593503+studiofarzulla@users.noreply.github.com> --- README.md | 3 ++- src/gjr_garch_x/__init__.py | 49 +++++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6e07ad0..3bea037 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Enforced automatically during estimation. ## API Reference -### `estimate_gjr_garch_x(returns, exog_vars, method='SLSQP', verbose=False)` +### `estimate_gjr_garch_x(returns, exog_vars, method='SLSQP', max_iter=1000, verbose=False)` Main estimation function. @@ -108,6 +108,7 @@ Main estimation function. - `returns`: `pd.Series` of log returns (recommend × 100 for numerical stability) - `exog_vars`: `pd.DataFrame` of exogenous variables, aligned with returns index - `method`: Optimization method (`'SLSQP'`, `'L-BFGS-B'`, `'trust-constr'`) +- `max_iter`: Maximum number of optimizer iterations (default: 1000) - `verbose`: Print estimation progress **Returns:** `GJRGARCHXResults` object diff --git a/src/gjr_garch_x/__init__.py b/src/gjr_garch_x/__init__.py index 0b40c5a..16499df 100644 --- a/src/gjr_garch_x/__init__.py +++ b/src/gjr_garch_x/__init__.py @@ -36,7 +36,7 @@ import warnings from dataclasses import dataclass -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, cast import numpy as np import pandas as pd @@ -114,7 +114,7 @@ class GJRGARCHXResults: iterations: int n_obs: int = 0 - def __post_init__(self): + def __post_init__(self) -> None: """Set n_obs from volatility length if not provided.""" if self.n_obs == 0 and len(self.volatility) > 0: object.__setattr__(self, "n_obs", len(self.volatility)) @@ -166,9 +166,11 @@ def summary(self) -> str: lines.append("") lines.append(f"Persistence (α + β + |γ|/2): {persistence:.4f}") - if 0 < persistence < 1: + if 0 < persistence < 0.9999: half_life = -np.log(0.5) / np.log(persistence) lines.append(f"Half-life of shocks: {half_life:.1f} periods") + elif persistence >= 0.9999: + lines.append("Half-life of shocks: ∞ (near unit root)") # Unconditional variance (if stationary) omega = self.params.get("omega", 0) @@ -281,14 +283,14 @@ def __init__( def _unpack_params(self, params: np.ndarray) -> Dict[str, float]: """Unpack parameter vector into named dictionary.""" param_dict = { - "omega": params[0], - "alpha": params[1], - "gamma": params[2], - "beta": params[3], - "nu": params[4], + "omega": float(params[0]), + "alpha": float(params[1]), + "gamma": float(params[2]), + "beta": float(params[3]), + "nu": float(params[4]), } for i, name in enumerate(self.exog_names): - param_dict[name] = params[5 + i] + param_dict[name] = float(params[5 + i]) return param_dict def _variance_recursion( @@ -312,13 +314,13 @@ def _variance_recursion( variance = np.zeros(self.n_obs) mean_return = self.returns.mean() - residuals = (self.returns - mean_return).values + residuals = np.asarray((self.returns - mean_return).values) # Initialize with unconditional variance estimate variance[0] = np.var(self.returns) for t in range(1, self.n_obs): - eps_sq_prev = residuals[t - 1] ** 2 + eps_sq_prev: float = float(residuals[t - 1] ** 2) leverage_term = gamma * eps_sq_prev * (residuals[t - 1] < 0) variance[t] = ( @@ -368,7 +370,7 @@ def _log_likelihood(self, params: np.ndarray) -> float: except (ValueError, OverflowError, RuntimeWarning): return 1e8 - def _parameter_constraints(self) -> List[Dict]: + def _parameter_constraints(self) -> List[Dict[str, Any]]: """Define optimization constraints including stationarity.""" return [ {"type": "ineq", "fun": lambda x: x[0] - 1e-8}, # omega > 0 @@ -394,8 +396,10 @@ def _get_starting_values(self) -> np.ndarray: 5.0, # nu ]) if self.has_exog: - start_vals = np.append(start_vals, np.zeros(self.n_exog)) - return start_vals + exog_zeros = np.zeros(self.n_exog) + result = np.concatenate([start_vals, exog_zeros]) + return cast(np.ndarray, result) + return cast(np.ndarray, start_vals) def estimate( self, @@ -425,12 +429,12 @@ def estimate( start_vals = self._get_starting_values() - bounds = [ + bounds: List[Tuple[Optional[float], Optional[float]]] = [ (1e-8, None), # omega > 0 (1e-8, 0.3), # alpha (-0.5, 0.5), # gamma (leverage) (1e-8, 0.95), # beta - (2.1, 50), # nu + (2.1, 50.0), # nu ] # Exogenous coefficients are unbounded for _ in range(self.n_exog): @@ -533,7 +537,16 @@ def _compute_standard_errors( try: hessian = self._numerical_hessian(params) cov_matrix = np.linalg.inv(hessian) - std_errs = np.sqrt(np.diag(cov_matrix)) + diag = np.diag(cov_matrix) + + # Check for negative variances before sqrt (ill-conditioned Hessian) + if np.any(diag < 0): + return ( + {name: np.nan for name in self.param_names}, + {name: np.nan for name in self.param_names}, + ) + + std_errs = np.sqrt(diag) dof = self.n_obs - self.n_params if dof <= 0: @@ -559,7 +572,7 @@ def _compute_standard_errors( def _numerical_hessian(self, params: np.ndarray, h: float = 1e-5) -> np.ndarray: """Compute numerical Hessian via central differences.""" n = len(params) - hessian = np.zeros((n, n)) + hessian: np.ndarray = np.zeros((n, n)) for i in range(n): for j in range(n): From 1ca86f23f9d7ebdf20d1707fed96f6c19477f3b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 08:46:22 +0000 Subject: [PATCH 3/4] Address code review feedback - Extract magic number 0.9999 to named constant NEAR_UNIT_ROOT_THRESHOLD - Remove unnecessary float() conversion in variance recursion loop - Eliminate duplicate dictionary comprehensions by using a single nan_dict variable Co-authored-by: studiofarzulla <62593503+studiofarzulla@users.noreply.github.com> --- src/gjr_garch_x/__init__.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/gjr_garch_x/__init__.py b/src/gjr_garch_x/__init__.py index 16499df..3b09e3d 100644 --- a/src/gjr_garch_x/__init__.py +++ b/src/gjr_garch_x/__init__.py @@ -48,6 +48,9 @@ __author__ = "Murad Farzulla" __email__ = "murad@farzulla.org" +# Constants +NEAR_UNIT_ROOT_THRESHOLD = 0.9999 # Threshold for persistence near 1.0 + __all__ = [ "estimate_gjr_garch_x", "GJRGARCHXResults", @@ -166,10 +169,10 @@ def summary(self) -> str: lines.append("") lines.append(f"Persistence (α + β + |γ|/2): {persistence:.4f}") - if 0 < persistence < 0.9999: + if 0 < persistence < NEAR_UNIT_ROOT_THRESHOLD: half_life = -np.log(0.5) / np.log(persistence) lines.append(f"Half-life of shocks: {half_life:.1f} periods") - elif persistence >= 0.9999: + elif persistence >= NEAR_UNIT_ROOT_THRESHOLD: lines.append("Half-life of shocks: ∞ (near unit root)") # Unconditional variance (if stationary) @@ -320,7 +323,7 @@ def _variance_recursion( variance[0] = np.var(self.returns) for t in range(1, self.n_obs): - eps_sq_prev: float = float(residuals[t - 1] ** 2) + eps_sq_prev = residuals[t - 1] ** 2 leverage_term = gamma * eps_sq_prev * (residuals[t - 1] < 0) variance[t] = ( @@ -541,19 +544,15 @@ def _compute_standard_errors( # Check for negative variances before sqrt (ill-conditioned Hessian) if np.any(diag < 0): - return ( - {name: np.nan for name in self.param_names}, - {name: np.nan for name in self.param_names}, - ) + nan_dict = {name: np.nan for name in self.param_names} + return (nan_dict, nan_dict) std_errs = np.sqrt(diag) dof = self.n_obs - self.n_params if dof <= 0: - return ( - {name: np.nan for name in self.param_names}, - {name: np.nan for name in self.param_names}, - ) + nan_dict = {name: np.nan for name in self.param_names} + return (nan_dict, nan_dict) t_stats = params / std_errs pvals = 2 * (1 - student_t.cdf(np.abs(t_stats), dof)) @@ -564,10 +563,8 @@ def _compute_standard_errors( ) except (np.linalg.LinAlgError, ValueError): - return ( - {name: np.nan for name in self.param_names}, - {name: np.nan for name in self.param_names}, - ) + nan_dict = {name: np.nan for name in self.param_names} + return (nan_dict, nan_dict) def _numerical_hessian(self, params: np.ndarray, h: float = 1e-5) -> np.ndarray: """Compute numerical Hessian via central differences.""" From 50b20d1920ea92942cfcd51fc3f532c38814c896 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 08:49:17 +0000 Subject: [PATCH 4/4] Add detailed documentation for NEAR_UNIT_ROOT_THRESHOLD constant Explain mathematical rationale for the threshold value and its purpose in preventing numerically unstable half-life calculations Co-authored-by: studiofarzulla <62593503+studiofarzulla@users.noreply.github.com> --- src/gjr_garch_x/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gjr_garch_x/__init__.py b/src/gjr_garch_x/__init__.py index 3b09e3d..3e1dc55 100644 --- a/src/gjr_garch_x/__init__.py +++ b/src/gjr_garch_x/__init__.py @@ -49,7 +49,11 @@ __email__ = "murad@farzulla.org" # Constants -NEAR_UNIT_ROOT_THRESHOLD = 0.9999 # Threshold for persistence near 1.0 +# Threshold for detecting near-unit-root persistence in GARCH models. +# When persistence is >= 0.9999, the half-life calculation becomes numerically +# unstable and produces misleading negative values. This threshold prevents +# displaying nonsensical half-life values for processes very close to non-stationary. +NEAR_UNIT_ROOT_THRESHOLD = 0.9999 __all__ = [ "estimate_gjr_garch_x",