squeeze-kernel 0.4.0__tar.gz → 0.5.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.
- {squeeze_kernel-0.4.0 → squeeze_kernel-0.5.0}/PKG-INFO +12 -3
- {squeeze_kernel-0.4.0 → squeeze_kernel-0.5.0}/README.md +11 -2
- {squeeze_kernel-0.4.0 → squeeze_kernel-0.5.0}/pyproject.toml +1 -1
- {squeeze_kernel-0.4.0 → squeeze_kernel-0.5.0}/src/squeeze_kernel/__init__.py +1 -1
- {squeeze_kernel-0.4.0 → squeeze_kernel-0.5.0}/src/squeeze_kernel/estimator.py +101 -20
- {squeeze_kernel-0.4.0 → squeeze_kernel-0.5.0}/src/squeeze_kernel/batch.py +0 -0
- {squeeze_kernel-0.4.0 → squeeze_kernel-0.5.0}/src/squeeze_kernel/kernels.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: squeeze-kernel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
A **streaming covariance estimator for panels of daily financial returns**. One `O(n²)` update per day, positive semi-definite **by construction** at every step, missing values handled **natively**, and defaults that require no tuning. Only dependency: NumPy.
|
|
39
39
|
|
|
40
|
-
Reference: *"The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage"* (Kende, 2026).
|
|
40
|
+
Reference: *"The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage"* (Kende, 2026) — [SSRN abstract 6455918](https://ssrn.com/abstract=6455918).
|
|
41
41
|
|
|
42
42
|
## Why
|
|
43
43
|
|
|
@@ -116,6 +116,14 @@ kappa = SqueezeKernelEstimator.calibrate_kappa(burn_in_returns, target_weight=0.
|
|
|
116
116
|
|
|
117
117
|
## Advanced options
|
|
118
118
|
|
|
119
|
+
**Scale-free correlation memory** (`corr_half_lives=(43, 173, 693)`, `corr_theta=0.25`): replaces the single correlation timescale with a positive combination of EWMAs on a geometric half-life ladder — each scale normalized and adaptively shrunk against its own effective sample size, then the covariances blended with weights ∝ half-life^`corr_theta`. By Bernstein's theorem this approximates the power-law memory of financial correlations (the streaming analogue of HAR); positive weights keep it PSD by construction, and `None` (default) reproduces the published single-scale estimator exactly. This is the paper's **headline configuration**: on the S&P 500 benchmark it leads every tested method at every universe size (held-out one-step NLL −3.9 at n=100, up to −18 at n=300 before the cluster target), and the 90% model confidence set collapses to it alone. Cost is O(K·n²) per update. Composes with `shrinkage_target="cluster"`; mutually exclusive with `lambda_corr_fast`.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
est = SqueezeKernelEstimator(n_assets=100, corr_half_lives=(43, 173, 693), corr_theta=0.25)
|
|
123
|
+
# maximal variant at high dimension:
|
|
124
|
+
est = SqueezeKernelEstimator(n_assets=300, corr_half_lives=(43, 173, 693), shrinkage_target="cluster")
|
|
125
|
+
```
|
|
126
|
+
|
|
119
127
|
**Score-exact weighting** (`weight_statistic="mahalanobis"`, use with `kappa=1.0`): drives the kernel with the Mahalanobis surprise `z'C⁻¹z/N` against the estimator's own correlation instead of the marginal dispersion. Improves accuracy in the moderate-concentration regime — use only when `n / T_eff ≲ 0.5` (e.g. n ≤ 100 at the default `lambda_corr`); at higher concentration the estimated inverse degrades it and the default is strictly better.
|
|
120
128
|
|
|
121
129
|
```python
|
|
@@ -165,7 +173,8 @@ Releases: publishing a GitHub release from a `v*` tag triggers the [publish work
|
|
|
165
173
|
@article{kende2026squeeze,
|
|
166
174
|
title = {The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage},
|
|
167
175
|
author = {Kende, Robert},
|
|
168
|
-
year = {2026}
|
|
176
|
+
year = {2026},
|
|
177
|
+
note = {Available at SSRN: \url{https://ssrn.com/abstract=6455918}}
|
|
169
178
|
}
|
|
170
179
|
```
|
|
171
180
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
A **streaming covariance estimator for panels of daily financial returns**. One `O(n²)` update per day, positive semi-definite **by construction** at every step, missing values handled **natively**, and defaults that require no tuning. Only dependency: NumPy.
|
|
9
9
|
|
|
10
|
-
Reference: *"The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage"* (Kende, 2026).
|
|
10
|
+
Reference: *"The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage"* (Kende, 2026) — [SSRN abstract 6455918](https://ssrn.com/abstract=6455918).
|
|
11
11
|
|
|
12
12
|
## Why
|
|
13
13
|
|
|
@@ -86,6 +86,14 @@ kappa = SqueezeKernelEstimator.calibrate_kappa(burn_in_returns, target_weight=0.
|
|
|
86
86
|
|
|
87
87
|
## Advanced options
|
|
88
88
|
|
|
89
|
+
**Scale-free correlation memory** (`corr_half_lives=(43, 173, 693)`, `corr_theta=0.25`): replaces the single correlation timescale with a positive combination of EWMAs on a geometric half-life ladder — each scale normalized and adaptively shrunk against its own effective sample size, then the covariances blended with weights ∝ half-life^`corr_theta`. By Bernstein's theorem this approximates the power-law memory of financial correlations (the streaming analogue of HAR); positive weights keep it PSD by construction, and `None` (default) reproduces the published single-scale estimator exactly. This is the paper's **headline configuration**: on the S&P 500 benchmark it leads every tested method at every universe size (held-out one-step NLL −3.9 at n=100, up to −18 at n=300 before the cluster target), and the 90% model confidence set collapses to it alone. Cost is O(K·n²) per update. Composes with `shrinkage_target="cluster"`; mutually exclusive with `lambda_corr_fast`.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
est = SqueezeKernelEstimator(n_assets=100, corr_half_lives=(43, 173, 693), corr_theta=0.25)
|
|
93
|
+
# maximal variant at high dimension:
|
|
94
|
+
est = SqueezeKernelEstimator(n_assets=300, corr_half_lives=(43, 173, 693), shrinkage_target="cluster")
|
|
95
|
+
```
|
|
96
|
+
|
|
89
97
|
**Score-exact weighting** (`weight_statistic="mahalanobis"`, use with `kappa=1.0`): drives the kernel with the Mahalanobis surprise `z'C⁻¹z/N` against the estimator's own correlation instead of the marginal dispersion. Improves accuracy in the moderate-concentration regime — use only when `n / T_eff ≲ 0.5` (e.g. n ≤ 100 at the default `lambda_corr`); at higher concentration the estimated inverse degrades it and the default is strictly better.
|
|
90
98
|
|
|
91
99
|
```python
|
|
@@ -135,7 +143,8 @@ Releases: publishing a GitHub release from a `v*` tag triggers the [publish work
|
|
|
135
143
|
@article{kende2026squeeze,
|
|
136
144
|
title = {The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage},
|
|
137
145
|
author = {Kende, Robert},
|
|
138
|
-
year = {2026}
|
|
146
|
+
year = {2026},
|
|
147
|
+
note = {Available at SSRN: \url{https://ssrn.com/abstract=6455918}}
|
|
139
148
|
}
|
|
140
149
|
```
|
|
141
150
|
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "squeeze-kernel"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.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"
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
5
7
|
import numpy as np
|
|
6
8
|
|
|
7
9
|
from squeeze_kernel.kernels import (
|
|
8
10
|
KernelFn, kernel_fisher, calibrate_kappa, extract_d2_series,
|
|
9
11
|
)
|
|
10
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
11
16
|
|
|
12
17
|
class SqueezeKernelEstimator:
|
|
13
18
|
"""Streaming robust covariance estimator with pluggable kernel weighting.
|
|
@@ -107,6 +112,28 @@ class SqueezeKernelEstimator:
|
|
|
107
112
|
concentration is unchanged. Held-out one-step NLL on the S&P-500
|
|
108
113
|
benchmark: +0.14 (negligible) at n=100, −4.2 at n=200, −25.0 at
|
|
109
114
|
n=300. Recommended when n approaches the effective sample size.
|
|
115
|
+
corr_half_lives : sequence of float, optional
|
|
116
|
+
Scale-free correlation memory. When set, the single correlation
|
|
117
|
+
timescale is replaced by a positive combination of EWMAs on the
|
|
118
|
+
given geometric half-life ladder (in trading days, e.g.
|
|
119
|
+
``(43, 173, 693)``): each scale is normalised and adaptively
|
|
120
|
+
shrunk against its own effective sample size, and the resulting
|
|
121
|
+
covariances are blended with weights ∝ half-life\\ :sup:`corr_theta`.
|
|
122
|
+
By Bernstein's theorem this approximates the power-law memory of
|
|
123
|
+
financial correlations (the streaming analogue of HAR). PSD by
|
|
124
|
+
construction (positive combination of PSD matrices). ``None``
|
|
125
|
+
(default) is the published single-scale estimator, bit-for-bit;
|
|
126
|
+
a one-element ladder reduces to a single-scale estimator at that
|
|
127
|
+
half-life. Mutually exclusive with ``lambda_corr_fast``; composes
|
|
128
|
+
with ``shrinkage_target='cluster'``. Cost is O(K·n²) per update.
|
|
129
|
+
Held-out one-step NLL on the S&P-500 benchmark improves at every
|
|
130
|
+
dimension (−3.9 at n=100, up to −18 at n=300 before the cluster
|
|
131
|
+
target); the $90\\%$ model confidence set collapses to this
|
|
132
|
+
configuration alone. Recommended default: the base-centred ladder
|
|
133
|
+
``(43, 173, 693)`` with ``corr_theta=0.25``.
|
|
134
|
+
corr_theta : float
|
|
135
|
+
Long-memory exponent controlling the ladder weights (default
|
|
136
|
+
0.25). Only used when ``corr_half_lives`` is set.
|
|
110
137
|
|
|
111
138
|
Examples
|
|
112
139
|
--------
|
|
@@ -138,6 +165,8 @@ class SqueezeKernelEstimator:
|
|
|
138
165
|
vol_anchor_phi: float | None = None,
|
|
139
166
|
vol_anchor_decay: float = 0.999,
|
|
140
167
|
shrinkage_target: str = "equicorrelation",
|
|
168
|
+
corr_half_lives: "Sequence[float] | None" = None,
|
|
169
|
+
corr_theta: float = 0.25,
|
|
141
170
|
):
|
|
142
171
|
self.n_assets = n_assets
|
|
143
172
|
self.lambda_vol = lambda_vol
|
|
@@ -164,6 +193,34 @@ class SqueezeKernelEstimator:
|
|
|
164
193
|
raise ValueError("shrinkage_target must be 'equicorrelation' or 'cluster'.")
|
|
165
194
|
self.shrinkage_target = shrinkage_target
|
|
166
195
|
|
|
196
|
+
# Scale-free correlation memory (opt-in): replace the single correlation
|
|
197
|
+
# timescale by a positive combination of EWMAs on a geometric half-life
|
|
198
|
+
# ladder, blended per-scale (Mode A). None => single-scale, published
|
|
199
|
+
# behaviour bit-for-bit. See ``corr_half_lives`` in the class docstring.
|
|
200
|
+
self.corr_half_lives = None
|
|
201
|
+
self.corr_theta = corr_theta
|
|
202
|
+
self._corr_lam: np.ndarray | None = None
|
|
203
|
+
self._corr_w: np.ndarray | None = None
|
|
204
|
+
self._M_list: list[np.ndarray] | None = None
|
|
205
|
+
self._S_list: list[float] | None = None
|
|
206
|
+
if corr_half_lives is not None:
|
|
207
|
+
hl = np.asarray(corr_half_lives, dtype=np.float64)
|
|
208
|
+
if hl.ndim != 1 or hl.size < 1 or np.any(hl <= 0.0):
|
|
209
|
+
raise ValueError("corr_half_lives must be a non-empty sequence of positive half-lives.")
|
|
210
|
+
if corr_theta < 0.0:
|
|
211
|
+
raise ValueError("corr_theta must be >= 0.")
|
|
212
|
+
if lambda_corr_fast is not None:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"corr_half_lives and lambda_corr_fast are mutually exclusive "
|
|
215
|
+
"correlation-memory mechanisms; set at most one."
|
|
216
|
+
)
|
|
217
|
+
self.corr_half_lives = hl
|
|
218
|
+
self._corr_lam = 2.0 ** (-1.0 / hl)
|
|
219
|
+
w = hl ** corr_theta
|
|
220
|
+
self._corr_w = w / w.sum()
|
|
221
|
+
self._M_list = [np.eye(n_assets, dtype=np.float64) * epsilon for _ in hl]
|
|
222
|
+
self._S_list = [float(epsilon) for _ in hl]
|
|
223
|
+
|
|
167
224
|
# Resolve shrinkage
|
|
168
225
|
if isinstance(shrinkage, str):
|
|
169
226
|
self._shrinkage_alpha = -1.0 if shrinkage == "auto" else 0.0
|
|
@@ -277,22 +334,45 @@ class SqueezeKernelEstimator:
|
|
|
277
334
|
if self.impute_missing and 0 < n_obs < n:
|
|
278
335
|
self._impute(z_t, finite)
|
|
279
336
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
337
|
+
if self._corr_lam is None:
|
|
338
|
+
# ── Single-scale correlation EWMA (published path, unchanged) ──
|
|
339
|
+
lam_c = self.lambda_corr
|
|
340
|
+
if self.lambda_corr_fast is not None:
|
|
341
|
+
# Score-driven memory: stress days (w_t → 1) shorten the memory
|
|
342
|
+
# toward lambda_corr_fast; calm days keep the slow decay.
|
|
343
|
+
lam_c = self.lambda_corr + (self.lambda_corr_fast - self.lambda_corr) * w_t
|
|
344
|
+
self._S_t = lam_c * self._S_t + w_t
|
|
345
|
+
self._M_t *= lam_c
|
|
346
|
+
if w_t > 0.0 and n_obs > 0:
|
|
347
|
+
# np.multiply.outer with out= avoids the temporary that
|
|
348
|
+
# np.outer otherwise allocates each step.
|
|
349
|
+
np.multiply.outer(z_t, z_t, out=self._scratch_outer)
|
|
350
|
+
self._M_t += w_t * self._scratch_outer
|
|
351
|
+
self._cov, self._corr = self._extract(vol_t)
|
|
352
|
+
else:
|
|
353
|
+
# ── Scale-free ladder (Mode A) ──
|
|
354
|
+
# Update K correlation accumulators on the geometric half-life
|
|
355
|
+
# ladder; normalise and adaptively shrink each against its OWN
|
|
356
|
+
# effective sample size, then blend the per-scale covariances.
|
|
357
|
+
add = w_t > 0.0 and n_obs > 0
|
|
358
|
+
if add:
|
|
359
|
+
np.multiply.outer(z_t, z_t, out=self._scratch_outer)
|
|
360
|
+
cov = np.zeros((n, n), dtype=np.float64)
|
|
361
|
+
s_eff = 0.0
|
|
362
|
+
for k in range(self._corr_lam.size):
|
|
363
|
+
self._S_list[k] = self._corr_lam[k] * self._S_list[k] + w_t
|
|
364
|
+
self._M_list[k] *= self._corr_lam[k]
|
|
365
|
+
if add:
|
|
366
|
+
self._M_list[k] += w_t * self._scratch_outer
|
|
367
|
+
cov_k, _ = self._extract(vol_t, self._M_list[k], self._S_list[k])
|
|
368
|
+
cov += self._corr_w[k] * cov_k
|
|
369
|
+
s_eff += self._corr_w[k] * self._S_list[k]
|
|
370
|
+
cov = 0.5 * (cov + cov.T)
|
|
371
|
+
self._cov = cov
|
|
372
|
+
self._S_t = s_eff # blended effective size (for the property)
|
|
373
|
+
d = np.sqrt(np.maximum(np.diagonal(cov), eps))
|
|
374
|
+
self._corr = cov / np.outer(d, d)
|
|
375
|
+
np.fill_diagonal(self._corr, 1.0)
|
|
296
376
|
self._last_weight = w_t
|
|
297
377
|
return w_t
|
|
298
378
|
|
|
@@ -373,22 +453,23 @@ class SqueezeKernelEstimator:
|
|
|
373
453
|
if den > 0.0:
|
|
374
454
|
z_t[i] = num / den
|
|
375
455
|
|
|
376
|
-
def _extract(self, vol_t: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
456
|
+
def _extract(self, vol_t: np.ndarray, M_t=None, S_in=None) -> tuple[np.ndarray, np.ndarray]:
|
|
377
457
|
eps = self.epsilon
|
|
378
|
-
|
|
458
|
+
M_t = self._M_t if M_t is None else M_t
|
|
459
|
+
S_t = max(self._S_t if S_in is None else S_in, eps)
|
|
379
460
|
n = self.n_assets
|
|
380
461
|
|
|
381
462
|
# Normalised standardised covariance matrix sigma_z = M_t / S_t.
|
|
382
463
|
# Compute correlations directly into the cached scratch buffer to
|
|
383
464
|
# avoid two intermediate allocations (sigma_z and corr).
|
|
384
|
-
diag_z = np.diagonal(
|
|
465
|
+
diag_z = np.diagonal(M_t).copy()
|
|
385
466
|
diag_z /= S_t # in-place
|
|
386
467
|
inv_diag = 1.0 / np.sqrt(np.maximum(diag_z, eps))
|
|
387
468
|
# corr_ij = (M_ij / S_t) * inv_diag_i * inv_diag_j; this writes
|
|
388
469
|
# the rescaled outer-product into _scratch_corr in one pass.
|
|
389
470
|
np.multiply.outer(inv_diag, inv_diag, out=self._scratch_corr)
|
|
390
471
|
corr = self._scratch_corr
|
|
391
|
-
corr *=
|
|
472
|
+
corr *= M_t # in-place; corr = sigma_z * outer(inv_diag,inv_diag)
|
|
392
473
|
corr *= (1.0 / S_t) # absorb the M_t / S_t scale
|
|
393
474
|
np.fill_diagonal(corr, np.where(diag_z > eps, 1.0, 0.0))
|
|
394
475
|
|
|
File without changes
|
|
File without changes
|