squeeze-kernel 0.3.0__tar.gz → 0.4.0__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: 0.3.0
3
+ Version: 0.4.0
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
@@ -130,6 +130,12 @@ est = SqueezeKernelEstimator(n_assets=100, kappa=1.0, weight_statistic="mahalano
130
130
  est = SqueezeKernelEstimator(n_assets=100, vol_anchor_phi=0.995)
131
131
  ```
132
132
 
133
+ **Cluster shrinkage target** (`shrinkage_target="cluster"`): generalizes the equicorrelation shrinkage target to respect the correlation matrix's own block/cluster structure — with **no clustering algorithm**. The target morphs with the shrinkage intensity, T = (1−α)·T_equi + α·[(1−γ)I + γ·(C∘C)], where C∘C is the Hadamard square of the current correlation (positive semi-definite by the Schur product theorem; entries are pairwise shared-variance fractions) and γ is level-matched automatically. Zero added parameters, still O(n²), and as α→0 it reduces exactly to the default estimator. Held-out one-step NLL on the S&P 500 benchmark: ±0.1 at n=100, **−4.2 at n=200, −25.0 at n=300** — recommended whenever the universe size approaches the effective sample size.
134
+
135
+ ```python
136
+ est = SqueezeKernelEstimator(n_assets=300, shrinkage_target="cluster")
137
+ ```
138
+
133
139
  **Alternative kernels**: pass `kernel_fn=kernel_exponential` (with `kernel_kwargs={"gamma": ...}`) or `kernel_chi2_cdf`, or any callable `(d2, *, n_observed, **kw) -> float` mapping to `[0, 1)`. The PSD guarantee holds for any such kernel.
134
140
 
135
141
  ## How it works
@@ -100,6 +100,12 @@ est = SqueezeKernelEstimator(n_assets=100, kappa=1.0, weight_statistic="mahalano
100
100
  est = SqueezeKernelEstimator(n_assets=100, vol_anchor_phi=0.995)
101
101
  ```
102
102
 
103
+ **Cluster shrinkage target** (`shrinkage_target="cluster"`): generalizes the equicorrelation shrinkage target to respect the correlation matrix's own block/cluster structure — with **no clustering algorithm**. The target morphs with the shrinkage intensity, T = (1−α)·T_equi + α·[(1−γ)I + γ·(C∘C)], where C∘C is the Hadamard square of the current correlation (positive semi-definite by the Schur product theorem; entries are pairwise shared-variance fractions) and γ is level-matched automatically. Zero added parameters, still O(n²), and as α→0 it reduces exactly to the default estimator. Held-out one-step NLL on the S&P 500 benchmark: ±0.1 at n=100, **−4.2 at n=200, −25.0 at n=300** — recommended whenever the universe size approaches the effective sample size.
104
+
105
+ ```python
106
+ est = SqueezeKernelEstimator(n_assets=300, shrinkage_target="cluster")
107
+ ```
108
+
103
109
  **Alternative kernels**: pass `kernel_fn=kernel_exponential` (with `kernel_kwargs={"gamma": ...}`) or `kernel_chi2_cdf`, or any callable `(d2, *, n_observed, **kw) -> float` mapping to `[0, 1)`. The PSD guarantee holds for any such kernel.
104
110
 
105
111
  ## How it works
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "squeeze-kernel"
7
- version = "0.3.0"
7
+ version = "0.4.0"
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"
@@ -34,4 +34,4 @@ __all__ = [
34
34
  "kernel_chi2_cdf",
35
35
  ]
36
36
 
37
- __version__ = "0.3.0"
37
+ __version__ = "0.4.0"
@@ -93,6 +93,20 @@ class SqueezeKernelEstimator:
93
93
  Decay of the slow per-asset variance anchor (default 0.999,
94
94
  effective memory ≈ 1000 trading days). Only used when
95
95
  ``vol_anchor_phi`` is set.
96
+ shrinkage_target : str
97
+ Geometry of the adaptive shrinkage target. ``'equicorrelation'``
98
+ (default) is the published single-factor target. ``'cluster'``
99
+ uses the concentration-morphing cluster target
100
+ T = (1−α)·T_equi + α·[(1−γ)I + γ·(C∘C)], where C∘C is the Hadamard
101
+ square of the current correlation (PSD by the Schur product
102
+ theorem) and γ = min(1, ρ̄/mean-offdiag(C∘C)) level-matches the
103
+ target to the equicorrelation mass. Respects the correlation
104
+ matrix's own block/cluster structure without any clustering
105
+ algorithm; adds no parameters and stays O(n²). As α → 0 it
106
+ reduces exactly to the published estimator, so behaviour at low
107
+ concentration is unchanged. Held-out one-step NLL on the S&P-500
108
+ benchmark: +0.14 (negligible) at n=100, −4.2 at n=200, −25.0 at
109
+ n=300. Recommended when n approaches the effective sample size.
96
110
 
97
111
  Examples
98
112
  --------
@@ -123,6 +137,7 @@ class SqueezeKernelEstimator:
123
137
  lambda_corr_fast: float | None = None,
124
138
  vol_anchor_phi: float | None = None,
125
139
  vol_anchor_decay: float = 0.999,
140
+ shrinkage_target: str = "equicorrelation",
126
141
  ):
127
142
  self.n_assets = n_assets
128
143
  self.lambda_vol = lambda_vol
@@ -145,6 +160,9 @@ class SqueezeKernelEstimator:
145
160
  raise ValueError("vol_anchor_decay must be in (0, 1).")
146
161
  self.vol_anchor_phi = vol_anchor_phi
147
162
  self.vol_anchor_decay = vol_anchor_decay
163
+ if shrinkage_target not in ("equicorrelation", "cluster"):
164
+ raise ValueError("shrinkage_target must be 'equicorrelation' or 'cluster'.")
165
+ self.shrinkage_target = shrinkage_target
148
166
 
149
167
  # Resolve shrinkage
150
168
  if isinstance(shrinkage, str):
@@ -384,9 +402,25 @@ class SqueezeKernelEstimator:
384
402
  if alpha > 0.0 and n > 1:
385
403
  # Off-diagonal mean: O(n^2) sum, no mask allocation.
386
404
  rho_bar = (corr.sum() - corr.trace()) / self._n_off
387
- corr *= (1.0 - alpha)
388
- corr += alpha * rho_bar
389
- np.fill_diagonal(corr, 1.0)
405
+ if self.shrinkage_target == "equicorrelation" or rho_bar <= 0.0:
406
+ corr *= (1.0 - alpha)
407
+ corr += alpha * rho_bar
408
+ np.fill_diagonal(corr, 1.0)
409
+ else:
410
+ # Cluster (concentration-morphing) target:
411
+ # T = (1-alpha) T_equi + alpha [(1-gamma) I + gamma (C o C)]
412
+ # C o C is the Hadamard square of the raw correlation (PSD by
413
+ # the Schur product theorem, unit diagonal for free); gamma is
414
+ # level-matched so the target carries the same average
415
+ # correlation mass as the equicorrelation target. As
416
+ # alpha -> 0 this reduces exactly to the published estimator.
417
+ had = corr * corr # Hadamard square, O(n^2)
418
+ mean_off = (had.sum() - np.trace(had)) / self._n_off
419
+ gamma = min(1.0, rho_bar / max(mean_off, eps))
420
+ corr *= (1.0 - alpha)
421
+ corr += (alpha * (1.0 - alpha)) * rho_bar
422
+ corr += (alpha * alpha * gamma) * had
423
+ np.fill_diagonal(corr, 1.0)
390
424
 
391
425
  # Build covariance from corr and vol_t. We need an output array
392
426
  # that the caller can keep, so allocate one cov here (cannot reuse