squeeze-kernel 0.2.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.
- {squeeze_kernel-0.2.0 → squeeze_kernel-0.4.0}/PKG-INFO +13 -1
- {squeeze_kernel-0.2.0 → squeeze_kernel-0.4.0}/README.md +12 -0
- {squeeze_kernel-0.2.0 → squeeze_kernel-0.4.0}/pyproject.toml +1 -1
- {squeeze_kernel-0.2.0 → squeeze_kernel-0.4.0}/src/squeeze_kernel/__init__.py +1 -1
- {squeeze_kernel-0.2.0 → squeeze_kernel-0.4.0}/src/squeeze_kernel/estimator.py +90 -7
- {squeeze_kernel-0.2.0 → squeeze_kernel-0.4.0}/src/squeeze_kernel/batch.py +0 -0
- {squeeze_kernel-0.2.0 → squeeze_kernel-0.4.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.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
|
|
@@ -124,6 +124,18 @@ est = SqueezeKernelEstimator(n_assets=100, kappa=1.0, weight_statistic="mahalano
|
|
|
124
124
|
|
|
125
125
|
**Score-driven memory** (`lambda_corr_fast=0.99`): lets stress days also *shorten* the correlation memory (decay slides from `lambda_corr` toward `lambda_corr_fast` as the kernel weight rises). Do **not** combine with the Mahalanobis option — they act on the same channel and the combination degrades accuracy.
|
|
126
126
|
|
|
127
|
+
**OU volatility anchor** (`vol_anchor_phi=0.995`): mean-reverts each asset's variance prediction toward a slow per-asset anchor (a ~1000-day EWMA of squared returns) before the daily update — a two-timescale, component-style volatility structure. One global parameter with a clean interpretation (deviation half-life ≈ ln 2/(1−φ) days; φ=0.995 ≈ 139 d). On the S&P 500 n=100 benchmark this improved held-out one-step NLL by 3.3 points (4.3 at φ=0.99) and five-step NLL by 3.9 (5.0), with no degradation at n=300. `None` (default) or φ=1 reproduces the published estimator exactly.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
est = SqueezeKernelEstimator(n_assets=100, vol_anchor_phi=0.995)
|
|
131
|
+
```
|
|
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
|
+
|
|
127
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.
|
|
128
140
|
|
|
129
141
|
## How it works
|
|
@@ -94,6 +94,18 @@ est = SqueezeKernelEstimator(n_assets=100, kappa=1.0, weight_statistic="mahalano
|
|
|
94
94
|
|
|
95
95
|
**Score-driven memory** (`lambda_corr_fast=0.99`): lets stress days also *shorten* the correlation memory (decay slides from `lambda_corr` toward `lambda_corr_fast` as the kernel weight rises). Do **not** combine with the Mahalanobis option — they act on the same channel and the combination degrades accuracy.
|
|
96
96
|
|
|
97
|
+
**OU volatility anchor** (`vol_anchor_phi=0.995`): mean-reverts each asset's variance prediction toward a slow per-asset anchor (a ~1000-day EWMA of squared returns) before the daily update — a two-timescale, component-style volatility structure. One global parameter with a clean interpretation (deviation half-life ≈ ln 2/(1−φ) days; φ=0.995 ≈ 139 d). On the S&P 500 n=100 benchmark this improved held-out one-step NLL by 3.3 points (4.3 at φ=0.99) and five-step NLL by 3.9 (5.0), with no degradation at n=300. `None` (default) or φ=1 reproduces the published estimator exactly.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
est = SqueezeKernelEstimator(n_assets=100, vol_anchor_phi=0.995)
|
|
101
|
+
```
|
|
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
|
+
|
|
97
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.
|
|
98
110
|
|
|
99
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.
|
|
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"
|
|
@@ -74,6 +74,39 @@ class SqueezeKernelEstimator:
|
|
|
74
74
|
Do not combine with ``weight_statistic='mahalanobis'`` — the two
|
|
75
75
|
mechanisms act on the same reactivity channel and their combination
|
|
76
76
|
degraded out-of-sample accuracy in testing.
|
|
77
|
+
vol_anchor_phi : float or None
|
|
78
|
+
If set, enables the OU volatility anchor: each asset's variance
|
|
79
|
+
prediction mean-reverts toward a slow per-asset anchor before the
|
|
80
|
+
measurement update, v_pred = v̄ + φ·(v − v̄), with the anchor v̄ a
|
|
81
|
+
slow EWMA of squared returns (see ``vol_anchor_decay``). φ is the
|
|
82
|
+
per-step retention of deviations from the anchor (deviation
|
|
83
|
+
half-life ≈ ln 2 / (1 − φ) days); φ = 1 or ``None`` (default)
|
|
84
|
+
reproduces the published estimator exactly. Recommended φ = 0.995
|
|
85
|
+
(conservative; the range [0.99, 0.995] is robust). On the S&P-500
|
|
86
|
+
n=100 benchmark this improved held-out one-step NLL by 3.3 points
|
|
87
|
+
(φ=0.995; 4.3 at φ=0.99) and five-step NLL by 3.9 (5.0), with no
|
|
88
|
+
degradation at n=300. Mechanism: a two-timescale (component-style)
|
|
89
|
+
volatility structure — it changes persistence, not shock response.
|
|
90
|
+
Validated with the default marginal kernel; interaction with the
|
|
91
|
+
correlation-side extensions above is untested.
|
|
92
|
+
vol_anchor_decay : float
|
|
93
|
+
Decay of the slow per-asset variance anchor (default 0.999,
|
|
94
|
+
effective memory ≈ 1000 trading days). Only used when
|
|
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.
|
|
77
110
|
|
|
78
111
|
Examples
|
|
79
112
|
--------
|
|
@@ -102,6 +135,9 @@ class SqueezeKernelEstimator:
|
|
|
102
135
|
impute_threshold: float = 0.6,
|
|
103
136
|
weight_statistic: str = "marginal",
|
|
104
137
|
lambda_corr_fast: float | None = None,
|
|
138
|
+
vol_anchor_phi: float | None = None,
|
|
139
|
+
vol_anchor_decay: float = 0.999,
|
|
140
|
+
shrinkage_target: str = "equicorrelation",
|
|
105
141
|
):
|
|
106
142
|
self.n_assets = n_assets
|
|
107
143
|
self.lambda_vol = lambda_vol
|
|
@@ -118,6 +154,15 @@ class SqueezeKernelEstimator:
|
|
|
118
154
|
if lambda_corr_fast is not None and not (0.0 < lambda_corr_fast < 1.0):
|
|
119
155
|
raise ValueError("lambda_corr_fast must be in (0, 1).")
|
|
120
156
|
self.lambda_corr_fast = lambda_corr_fast
|
|
157
|
+
if vol_anchor_phi is not None and not (0.0 < vol_anchor_phi <= 1.0):
|
|
158
|
+
raise ValueError("vol_anchor_phi must be in (0, 1].")
|
|
159
|
+
if not (0.0 < vol_anchor_decay < 1.0):
|
|
160
|
+
raise ValueError("vol_anchor_decay must be in (0, 1).")
|
|
161
|
+
self.vol_anchor_phi = vol_anchor_phi
|
|
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
|
|
121
166
|
|
|
122
167
|
# Resolve shrinkage
|
|
123
168
|
if isinstance(shrinkage, str):
|
|
@@ -132,6 +177,7 @@ class SqueezeKernelEstimator:
|
|
|
132
177
|
# State
|
|
133
178
|
self._var_t: np.ndarray | None = None
|
|
134
179
|
self._var_init: np.ndarray | None = None
|
|
180
|
+
self._var_anchor: np.ndarray | None = None
|
|
135
181
|
self._M_t = np.eye(n_assets, dtype=np.float64) * epsilon
|
|
136
182
|
self._S_t = float(epsilon)
|
|
137
183
|
self._cov: np.ndarray | None = None
|
|
@@ -172,17 +218,38 @@ class SqueezeKernelEstimator:
|
|
|
172
218
|
if self._var_t is None:
|
|
173
219
|
self._var_t = np.zeros(n, dtype=np.float64)
|
|
174
220
|
self._var_init = np.zeros(n, dtype=bool)
|
|
221
|
+
if self.vol_anchor_phi is not None:
|
|
222
|
+
self._var_anchor = np.zeros(n, dtype=np.float64)
|
|
175
223
|
|
|
176
224
|
first = finite & ~self._var_init
|
|
177
225
|
repeat = finite & self._var_init
|
|
178
226
|
if np.any(first):
|
|
179
227
|
self._var_t[first] = r_t[first] ** 2 + eps
|
|
180
228
|
self._var_init[first] = True
|
|
229
|
+
if self._var_anchor is not None:
|
|
230
|
+
self._var_anchor[first] = self._var_t[first]
|
|
181
231
|
if np.any(repeat):
|
|
182
|
-
self.
|
|
183
|
-
self.
|
|
184
|
-
|
|
185
|
-
|
|
232
|
+
if self.vol_anchor_phi is None:
|
|
233
|
+
self._var_t[repeat] = (
|
|
234
|
+
self.lambda_vol * self._var_t[repeat]
|
|
235
|
+
+ (1.0 - self.lambda_vol) * r_t[repeat] ** 2
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
# OU anchor: mean-revert the variance prediction toward a slow
|
|
239
|
+
# per-asset anchor before the measurement update, then update
|
|
240
|
+
# the anchor itself (order matters and matches the validated
|
|
241
|
+
# experiment: prediction uses the *old* anchor).
|
|
242
|
+
phi = self.vol_anchor_phi
|
|
243
|
+
lam_bar = self.vol_anchor_decay
|
|
244
|
+
anchor = self._var_anchor[repeat]
|
|
245
|
+
v_pred = anchor + phi * (self._var_t[repeat] - anchor)
|
|
246
|
+
self._var_t[repeat] = (
|
|
247
|
+
self.lambda_vol * v_pred
|
|
248
|
+
+ (1.0 - self.lambda_vol) * r_t[repeat] ** 2
|
|
249
|
+
)
|
|
250
|
+
self._var_anchor[repeat] = (
|
|
251
|
+
lam_bar * anchor + (1.0 - lam_bar) * r_t[repeat] ** 2
|
|
252
|
+
)
|
|
186
253
|
|
|
187
254
|
vol_t = np.zeros(n, dtype=np.float64)
|
|
188
255
|
vol_t[self._var_init] = np.sqrt(self._var_t[self._var_init])
|
|
@@ -335,9 +402,25 @@ class SqueezeKernelEstimator:
|
|
|
335
402
|
if alpha > 0.0 and n > 1:
|
|
336
403
|
# Off-diagonal mean: O(n^2) sum, no mask allocation.
|
|
337
404
|
rho_bar = (corr.sum() - corr.trace()) / self._n_off
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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)
|
|
341
424
|
|
|
342
425
|
# Build covariance from corr and vol_t. We need an output array
|
|
343
426
|
# that the caller can keep, so allocate one cov here (cannot reuse
|
|
File without changes
|
|
File without changes
|