squeeze-kernel 3.1.0__tar.gz → 3.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: squeeze-kernel
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: Streaming, PSD-by-construction covariance estimator with Fisher-kernel weighting and adaptive shrinkage
5
5
  Keywords: covariance,correlation,ewma,kernel,risk,streaming
6
6
  Author: Robert Kende
@@ -35,6 +35,7 @@ Description-Content-Type: text/markdown
35
35
  [![PyPI](https://img.shields.io/pypi/v/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
36
36
  [![Python](https://img.shields.io/pypi/pyversions/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
37
37
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
38
+ [![DOI](https://zenodo.org/badge/1188987927.svg)](https://doi.org/10.5281/zenodo.19170801)
38
39
 
39
40
  A **streaming covariance estimator for panels of financial returns** whose entire public surface is **one number** — the decay `lam` of the anchor correlation timescale. Every other quantity is derived from it, fixed by a structural argument, or computed online from the estimator's own state. An `O(Kn²)` state update per day, positive semi-definite **by construction**, missing values handled **natively**, no tuning, no refits. Only dependency: NumPy.
40
41
 
@@ -4,6 +4,7 @@
4
4
  [![PyPI](https://img.shields.io/pypi/v/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
5
5
  [![Python](https://img.shields.io/pypi/pyversions/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
+ [![DOI](https://zenodo.org/badge/1188987927.svg)](https://doi.org/10.5281/zenodo.19170801)
7
8
 
8
9
  A **streaming covariance estimator for panels of financial returns** whose entire public surface is **one number** — the decay `lam` of the anchor correlation timescale. Every other quantity is derived from it, fixed by a structural argument, or computed online from the estimator's own state. An `O(Kn²)` state update per day, positive semi-definite **by construction**, missing values handled **natively**, no tuning, no refits. Only dependency: NumPy.
9
10
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "squeeze-kernel"
7
- version = "3.1.0"
7
+ version = "3.1.1"
8
8
  description = "Streaming, PSD-by-construction covariance estimator with Fisher-kernel weighting and adaptive shrinkage"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "squeeze-kernel"
7
- version = "3.1.0"
7
+ version = "3.1.1"
8
8
  description = "Streaming, PSD-by-construction covariance estimator with Fisher-kernel weighting and adaptive shrinkage"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -39,4 +39,4 @@ __all__ = [
39
39
  "kernel_fisher",
40
40
  ]
41
41
 
42
- __version__ = "3.1.0"
42
+ __version__ = "3.1.1"
@@ -153,6 +153,18 @@ class _BlendGradient:
153
153
  """
154
154
 
155
155
  _JITTER = 1e-12
156
+ _FLOOR = 1e-8 # spectral floor of the scored blend
157
+ _CERT = 10.0 # certify lambda_min >= _CERT * _FLOOR before skipping the floor
158
+
159
+ @staticmethod
160
+ def _solve(sig: np.ndarray, r: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
161
+ """(sig^-1 r, sig^-1) from one Cholesky factorisation."""
162
+ if _cho_factor is not None:
163
+ cf = _cho_factor(sig, lower=True, check_finite=False)
164
+ return (_cho_solve(cf, r, check_finite=False),
165
+ _cho_solve(cf, np.eye(sig.shape[0]), check_finite=False))
166
+ sinv = np.linalg.inv(sig)
167
+ return sinv @ r, sinv
156
168
 
157
169
  def __init__(self, half_lives: np.ndarray, prior: np.ndarray,
158
170
  split: bool = False, n_experts: int = 3) -> None:
@@ -224,23 +236,31 @@ class _BlendGradient:
224
236
  for k in range(K):
225
237
  sig += self.prev_w[k] * self.prev_sig[k][np.ix_(idx, idx)]
226
238
  sig = 0.5 * (sig + sig.T)
227
- # Floor the spectrum before factorising, exactly as the research
228
- # engine's scoring path does: a near-singular early blend would
229
- # otherwise hand the running scale one enormous gradient and
230
- # silence the mixer for years.
231
- min_eig = float(np.linalg.eigvalsh(sig).min())
232
- if min_eig < 1e-8:
233
- sig = sig + np.eye(idx.size) * (1e-8 - min_eig)
239
+ # The spectrum is floored at _FLOOR before factorising, exactly as
240
+ # the research engine's scoring path does: a near-singular early
241
+ # blend would otherwise hand the running scale one enormous
242
+ # gradient and silence the mixer for years. The floor acts only
243
+ # when lambda_min < _FLOOR, so factorise first and certify: the
244
+ # inverse gives lambda_min >= 1/||sig^-1||_F, and a certified day
245
+ # is bit-identical to the floored path (no floor, same factor).
246
+ # Uncertified or failed days take the eigenvalue floor as before.
247
+ sol: tuple[np.ndarray, np.ndarray] | None = None
234
248
  try:
235
- if _cho_factor is not None:
236
- cf = _cho_factor(sig, lower=True, check_finite=False)
237
- u = _cho_solve(cf, r_t[idx], check_finite=False)
238
- sinv = _cho_solve(cf, np.eye(idx.size), check_finite=False)
239
- else:
240
- sinv = np.linalg.inv(sig)
241
- u = sinv @ r_t[idx]
249
+ sol = self._solve(sig, r_t[idx])
250
+ fro = float(np.linalg.norm(sol[1]))
251
+ if not (np.isfinite(fro) and fro * self._CERT * self._FLOOR <= 1.0):
252
+ sol = None
242
253
  except (np.linalg.LinAlgError, ValueError):
243
- return None
254
+ sol = None
255
+ if sol is None:
256
+ min_eig = float(np.linalg.eigvalsh(sig).min())
257
+ if min_eig < self._FLOOR:
258
+ sig = sig + np.eye(idx.size) * (self._FLOOR - min_eig)
259
+ try:
260
+ sol = self._solve(sig, r_t[idx])
261
+ except (np.linalg.LinAlgError, ValueError):
262
+ return None
263
+ u, sinv = sol
244
264
  g = np.empty(K)
245
265
  self._gc = None
246
266
  if self.split and self.prev_comps is not None: