squeeze-kernel 0.2.0__tar.gz → 0.3.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.2.0
3
+ Version: 0.3.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,12 @@ 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
+
127
133
  **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
134
 
129
135
  ## How it works
@@ -94,6 +94,12 @@ 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
+
97
103
  **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
104
 
99
105
  ## How it works
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "squeeze-kernel"
7
- version = "0.2.0"
7
+ version = "0.3.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.2.0"
37
+ __version__ = "0.3.0"
@@ -74,6 +74,25 @@ 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.
77
96
 
78
97
  Examples
79
98
  --------
@@ -102,6 +121,8 @@ class SqueezeKernelEstimator:
102
121
  impute_threshold: float = 0.6,
103
122
  weight_statistic: str = "marginal",
104
123
  lambda_corr_fast: float | None = None,
124
+ vol_anchor_phi: float | None = None,
125
+ vol_anchor_decay: float = 0.999,
105
126
  ):
106
127
  self.n_assets = n_assets
107
128
  self.lambda_vol = lambda_vol
@@ -118,6 +139,12 @@ class SqueezeKernelEstimator:
118
139
  if lambda_corr_fast is not None and not (0.0 < lambda_corr_fast < 1.0):
119
140
  raise ValueError("lambda_corr_fast must be in (0, 1).")
120
141
  self.lambda_corr_fast = lambda_corr_fast
142
+ if vol_anchor_phi is not None and not (0.0 < vol_anchor_phi <= 1.0):
143
+ raise ValueError("vol_anchor_phi must be in (0, 1].")
144
+ if not (0.0 < vol_anchor_decay < 1.0):
145
+ raise ValueError("vol_anchor_decay must be in (0, 1).")
146
+ self.vol_anchor_phi = vol_anchor_phi
147
+ self.vol_anchor_decay = vol_anchor_decay
121
148
 
122
149
  # Resolve shrinkage
123
150
  if isinstance(shrinkage, str):
@@ -132,6 +159,7 @@ class SqueezeKernelEstimator:
132
159
  # State
133
160
  self._var_t: np.ndarray | None = None
134
161
  self._var_init: np.ndarray | None = None
162
+ self._var_anchor: np.ndarray | None = None
135
163
  self._M_t = np.eye(n_assets, dtype=np.float64) * epsilon
136
164
  self._S_t = float(epsilon)
137
165
  self._cov: np.ndarray | None = None
@@ -172,17 +200,38 @@ class SqueezeKernelEstimator:
172
200
  if self._var_t is None:
173
201
  self._var_t = np.zeros(n, dtype=np.float64)
174
202
  self._var_init = np.zeros(n, dtype=bool)
203
+ if self.vol_anchor_phi is not None:
204
+ self._var_anchor = np.zeros(n, dtype=np.float64)
175
205
 
176
206
  first = finite & ~self._var_init
177
207
  repeat = finite & self._var_init
178
208
  if np.any(first):
179
209
  self._var_t[first] = r_t[first] ** 2 + eps
180
210
  self._var_init[first] = True
211
+ if self._var_anchor is not None:
212
+ self._var_anchor[first] = self._var_t[first]
181
213
  if np.any(repeat):
182
- self._var_t[repeat] = (
183
- self.lambda_vol * self._var_t[repeat]
184
- + (1.0 - self.lambda_vol) * r_t[repeat] ** 2
185
- )
214
+ if self.vol_anchor_phi is None:
215
+ self._var_t[repeat] = (
216
+ self.lambda_vol * self._var_t[repeat]
217
+ + (1.0 - self.lambda_vol) * r_t[repeat] ** 2
218
+ )
219
+ else:
220
+ # OU anchor: mean-revert the variance prediction toward a slow
221
+ # per-asset anchor before the measurement update, then update
222
+ # the anchor itself (order matters and matches the validated
223
+ # experiment: prediction uses the *old* anchor).
224
+ phi = self.vol_anchor_phi
225
+ lam_bar = self.vol_anchor_decay
226
+ anchor = self._var_anchor[repeat]
227
+ v_pred = anchor + phi * (self._var_t[repeat] - anchor)
228
+ self._var_t[repeat] = (
229
+ self.lambda_vol * v_pred
230
+ + (1.0 - self.lambda_vol) * r_t[repeat] ** 2
231
+ )
232
+ self._var_anchor[repeat] = (
233
+ lam_bar * anchor + (1.0 - lam_bar) * r_t[repeat] ** 2
234
+ )
186
235
 
187
236
  vol_t = np.zeros(n, dtype=np.float64)
188
237
  vol_t[self._var_init] = np.sqrt(self._var_t[self._var_init])