squeeze-kernel 0.7.1__tar.gz → 3.1.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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: squeeze-kernel
3
+ Version: 3.1.0
4
+ Summary: Streaming, PSD-by-construction covariance estimator with Fisher-kernel weighting and adaptive shrinkage
5
+ Keywords: covariance,correlation,ewma,kernel,risk,streaming
6
+ Author: Robert Kende
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Operating System :: OS Independent
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
20
+ Requires-Dist: ruff>=0.7 ; extra == 'dev'
21
+ Requires-Dist: scipy>=1.11 ; extra == 'dev'
22
+ Requires-Dist: mypy>=1.10 ; extra == 'dev'
23
+ Requires-Dist: scipy>=1.11 ; extra == 'full'
24
+ Requires-Python: >=3.10
25
+ Project-URL: Homepage, https://github.com/r0k3/squeeze-kernel
26
+ Project-URL: Repository, https://github.com/r0k3/squeeze-kernel
27
+ Project-URL: Issues, https://github.com/r0k3/squeeze-kernel/issues
28
+ Provides-Extra: dev
29
+ Provides-Extra: full
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Squeeze Kernel Covariance Estimator
33
+
34
+ [![CI](https://github.com/r0k3/squeeze-kernel/actions/workflows/ci.yml/badge.svg)](https://github.com/r0k3/squeeze-kernel/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
38
+
39
+ 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
+ ```python
42
+ from squeeze_kernel import SqueezeKernel
43
+
44
+ sk = SqueezeKernel(lam=0.996) # the entire public surface
45
+ for r_t in returns: # NaN marks missing assets
46
+ sk.update(r_t)
47
+ cov = sk.covariance()
48
+ ```
49
+
50
+ Reference: *"The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage"* (Kende, 2026) — [SSRN abstract 6455918](https://ssrn.com/abstract=6455918); the 2.0 estimator is described in the paper's current revision.
51
+
52
+ ## Why
53
+
54
+ Markets do not keep calendar time. Following Mandelbrot, the estimator treats a panel as a collection of partially coupled markets, **each advancing on its own activity-driven clock** — and reads those clocks from the panel's own correlation structure, so a hot cluster (say precious metals and FX) advances its correlation state while an idle one (agriculture) does not, without anyone identifying a cluster. On those clocks it runs a single recursion that:
55
+
56
+ - **is PSD at every step, structurally** — the correlation state evolves by a diagonal-congruence flow (a congruence plus a rank-one term); no eigenvalue clipping, no nearest-PSD repair, and no factorisation anywhere in the state update. (The adaptive timescale weights are the one exception: they read each timescale's predictive likelihood, which costs one Cholesky per timescale per day. Turn them off and the estimator is pure `O(Kn²)`.)
57
+ - **learns in market time and forgets in calendar time** — observations enter with a saturating, self-studentising weight (no day counts more than one unit of trading time); memory decays at fixed per-day rates on a geometric ladder of three timescales `(lam⁴, lam, lam^¼)`. Pairs accrue covariance at the geometric mean of their two clock increments, which is the most positive semi-definiteness allows and exactly the Cauchy–Schwarz bound on how far two assets' clocks can overlap;
58
+ - **regularises itself** — each timescale's shrinkage intensity is computed from two online statistics, the concentration `n/ν` (dimension per unit trading time) and the de-noised fraction of correlation dispersion the target explains; the target is the Hadamard square of the running correlation, which is exactly the correlation matrix of the *squared* returns (cluster-respecting, PSD by the Schur product theorem, and sign-blind by construction — the signs are carried by the unshrunk term);
59
+ - **adapts its memory to regime breaks, in both stages** — the timescale mix moves by an exponentiated-gradient step on the *blend's* own log score (not on any single timescale's, which would select rather than blend), at a temperature calibrated so that uninformative evidence leaves the mix within a factor e of its prior; the marginal variance is tracked on its own three-rung ladder, two octaves below the correlation ladder, and pooled panel-wide by the same rule on saturated evidence, so one spike day cannot hand a stale rung weeks of weight. Nothing in either mixture is fitted: the ladders are derived from `lam`, the evidence memory is the fastest rung's, and the temperature is the null's;
60
+ - **ingests missing values natively** — listings, delistings, halts enter as `NaN`;
61
+ - **is fast** — one Cholesky and one triangular solve per day; a thirty-year daily pass at n=300 runs in about two minutes single-threaded, well under daily rolling-window refits.
62
+
63
+ **Evidence.** On thirty years of S&P 500 constituents against an eleven-method field (EWMA, DCC, Ledoit–Wolf, OAS, nonlinear shrinkage, RMT filtering, Gerber, IEWMA, CM-IEWMA, and the published v1 estimator) it leads at every universe size from 50 to 300 and is the **sole member of the 90% model confidence set at every size**. Carried **zero-shot** to a diversified panel of 121 futures across eight asset classes it beats the same field *calibrated on that panel's own history* — matched-backbone IEWMA by 6.9 NLL/day (p = 4·10⁻⁴), calibrated DCC by 17.9 — out-of-time.
64
+
65
+ ## See the difference
66
+
67
+ A passive strategy any allocator would recognize: long-only minimum-variance over 300 liquid US stocks, scaled to a 15% volatility target, rebalanced monthly, 5 bps costs. Two runs on identical data; the only difference is the covariance matrix. The Squeeze Kernel arm runs `SqueezeKernel(lam=0.996)` — **nothing tuned on this panel**.
68
+
69
+ <picture>
70
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/r0k3/squeeze-kernel/main/examples/figures/vol_targeted_portfolio_dark.png">
71
+ <img alt="Vol-targeted long-only minimum-variance portfolio on 300 US equities: Squeeze Kernel vs Ledoit-Wolf equity curves, drawdown, realized volatility, and risk/return profile" src="https://raw.githubusercontent.com/r0k3/squeeze-kernel/main/examples/figures/vol_targeted_portfolio.png">
72
+ </picture>
73
+
74
+ | Method | CAGR | Vol | Sharpe | MaxDD | Calmar | Vol-target RMSE |
75
+ |---|---|---|---|---|---|---|
76
+ | **Squeeze Kernel (default)** | **12.4%** | **13.5%** | **0.91** | **-34.6%** | **0.36** | **7.10%** |
77
+ | Ledoit-Wolf (252d) | 11.7% | 14.6% | 0.80 | -38.7% | 0.30 | 7.51% |
78
+
79
+ Reproduce from the repo alone (the 300-stock panel ships as a parquet; survivorship and provenance are documented in the script):
80
+
81
+ ```bash
82
+ pip install squeeze-kernel pandas pyarrow scikit-learn matplotlib
83
+ python examples/vol_targeted_portfolio.py # ~2 minutes
84
+ ```
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install squeeze-kernel # NumPy only
90
+ pip install "squeeze-kernel[full]" # + SciPy (faster detector factorisations)
91
+ ```
92
+
93
+ ## Quickstart
94
+
95
+ ```python
96
+ import numpy as np
97
+ from squeeze_kernel import SqueezeKernel, estimate_squeeze_cov
98
+
99
+ returns = np.random.default_rng(42).normal(0.0, 0.01, size=(500, 30))
100
+
101
+ sk = SqueezeKernel() # lam=0.996 (anchor half-life ~173 days)
102
+ for r_t in returns:
103
+ w = sk.update(r_t) # returns the day's kernel weight
104
+ cov, corr = sk.covariance(), sk.correlation()
105
+ sk.state() # kernel scale, per-timescale effective sizes, mixture tilt
106
+
107
+ # batch mode: full panel in, covariance path out
108
+ cov_path, corr_path, weights = estimate_squeeze_cov(returns, with_weights=True)
109
+ ```
110
+
111
+ Missing values: pass `NaN` (or `mask=` on `update`). Newly listed, delisted or halted assets need no imputation and no complete-case subsetting.
112
+
113
+ ## What derives from `lam`
114
+
115
+ | quantity | value |
116
+ |---|---|
117
+ | timescale ladder | decays `(lam⁴, lam, lam^¼)` — half-lives `(h/4, h, 4h)`, `h = -1/log2(lam)` |
118
+ | kernel scale | state: `κ_t = ⅓ · EWMA(activity)` at the anchor rate |
119
+ | shrinkage intensity | per timescale, `α = min(1,c) · g̃²/(g̃² + (1−g̃)²·max(0, 1/c − 1))` from the online concentration `c = n/ν` and target-fit `g̃` |
120
+ | timescale weights | prior ∝ √h, moved by exponentiated gradient on the blend log score at the null temperature, fast-rung memory |
121
+ | volatility memory | ladder `(h/16, h/4, h)`, pooled panel-wide by the same rule on tanh-saturated evidence, uniform prior |
122
+ | structural constants | K=3, b=4, θ=½, κ-scale ⅓, Schur power 2 — each bracketed by ablation in the paper |
123
+ | fitted constants | none (the 2.x volatility clock `λ_v = 0.98` is replaced by the ladder above) |
124
+
125
+ `from squeeze_kernel import CONSTANTS` exposes the structural constants for research. The published v1 estimator (all its knobs) remains available as `SqueezeKernelEstimator` / `SqueezeKernel.v1(...)`; every 2.0 mechanism is also an estimator-level switch for ablation. See [MIGRATION.md](MIGRATION.md).
126
+
127
+ ## How it works
128
+
129
+ One daily update: variance on a three-rung ladder per asset, pooled by panel-wide self-adapting weights → standardised surprise → per-asset clock increments from the Schur-square-weighted neighbourhood mean of squared surprises → diagonal-congruence update of each timescale's correlation state on those clocks → per-timescale shrinkage as a learned pool of the raw correlation, its equicorrelation level and its Hadamard square, with the self-tuning intensity rule as the prior and the blend gradient as the correction → blend across timescales, moved by the gradient of the blend's own log score → covariance. The paper gives the derivations, guarantees (PSD, conditioning floor, exact reductions to the published special cases), and the full evaluation.
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ uv sync --extra full --extra dev
135
+ uv run python -m pytest # test suite
136
+ uv run python -m ruff check . # lint
137
+ uv run mypy # strict type check (src/squeeze_kernel)
138
+ uv build # build sdist + wheel
139
+ ```
140
+
141
+ ## Citation
142
+
143
+ ```bibtex
144
+ @article{kende2026squeeze,
145
+ title = {The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage},
146
+ author = {Kende, Robert},
147
+ year = {2026},
148
+ note = {Available at SSRN: \url{https://ssrn.com/abstract=6455918}}
149
+ }
150
+ ```
151
+
152
+ See also [`CITATION.cff`](CITATION.cff).
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,125 @@
1
+ # Squeeze Kernel Covariance Estimator
2
+
3
+ [![CI](https://github.com/r0k3/squeeze-kernel/actions/workflows/ci.yml/badge.svg)](https://github.com/r0k3/squeeze-kernel/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/squeeze-kernel.svg)](https://pypi.org/project/squeeze-kernel/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
+
8
+ 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
+ ```python
11
+ from squeeze_kernel import SqueezeKernel
12
+
13
+ sk = SqueezeKernel(lam=0.996) # the entire public surface
14
+ for r_t in returns: # NaN marks missing assets
15
+ sk.update(r_t)
16
+ cov = sk.covariance()
17
+ ```
18
+
19
+ Reference: *"The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage"* (Kende, 2026) — [SSRN abstract 6455918](https://ssrn.com/abstract=6455918); the 2.0 estimator is described in the paper's current revision.
20
+
21
+ ## Why
22
+
23
+ Markets do not keep calendar time. Following Mandelbrot, the estimator treats a panel as a collection of partially coupled markets, **each advancing on its own activity-driven clock** — and reads those clocks from the panel's own correlation structure, so a hot cluster (say precious metals and FX) advances its correlation state while an idle one (agriculture) does not, without anyone identifying a cluster. On those clocks it runs a single recursion that:
24
+
25
+ - **is PSD at every step, structurally** — the correlation state evolves by a diagonal-congruence flow (a congruence plus a rank-one term); no eigenvalue clipping, no nearest-PSD repair, and no factorisation anywhere in the state update. (The adaptive timescale weights are the one exception: they read each timescale's predictive likelihood, which costs one Cholesky per timescale per day. Turn them off and the estimator is pure `O(Kn²)`.)
26
+ - **learns in market time and forgets in calendar time** — observations enter with a saturating, self-studentising weight (no day counts more than one unit of trading time); memory decays at fixed per-day rates on a geometric ladder of three timescales `(lam⁴, lam, lam^¼)`. Pairs accrue covariance at the geometric mean of their two clock increments, which is the most positive semi-definiteness allows and exactly the Cauchy–Schwarz bound on how far two assets' clocks can overlap;
27
+ - **regularises itself** — each timescale's shrinkage intensity is computed from two online statistics, the concentration `n/ν` (dimension per unit trading time) and the de-noised fraction of correlation dispersion the target explains; the target is the Hadamard square of the running correlation, which is exactly the correlation matrix of the *squared* returns (cluster-respecting, PSD by the Schur product theorem, and sign-blind by construction — the signs are carried by the unshrunk term);
28
+ - **adapts its memory to regime breaks, in both stages** — the timescale mix moves by an exponentiated-gradient step on the *blend's* own log score (not on any single timescale's, which would select rather than blend), at a temperature calibrated so that uninformative evidence leaves the mix within a factor e of its prior; the marginal variance is tracked on its own three-rung ladder, two octaves below the correlation ladder, and pooled panel-wide by the same rule on saturated evidence, so one spike day cannot hand a stale rung weeks of weight. Nothing in either mixture is fitted: the ladders are derived from `lam`, the evidence memory is the fastest rung's, and the temperature is the null's;
29
+ - **ingests missing values natively** — listings, delistings, halts enter as `NaN`;
30
+ - **is fast** — one Cholesky and one triangular solve per day; a thirty-year daily pass at n=300 runs in about two minutes single-threaded, well under daily rolling-window refits.
31
+
32
+ **Evidence.** On thirty years of S&P 500 constituents against an eleven-method field (EWMA, DCC, Ledoit–Wolf, OAS, nonlinear shrinkage, RMT filtering, Gerber, IEWMA, CM-IEWMA, and the published v1 estimator) it leads at every universe size from 50 to 300 and is the **sole member of the 90% model confidence set at every size**. Carried **zero-shot** to a diversified panel of 121 futures across eight asset classes it beats the same field *calibrated on that panel's own history* — matched-backbone IEWMA by 6.9 NLL/day (p = 4·10⁻⁴), calibrated DCC by 17.9 — out-of-time.
33
+
34
+ ## See the difference
35
+
36
+ A passive strategy any allocator would recognize: long-only minimum-variance over 300 liquid US stocks, scaled to a 15% volatility target, rebalanced monthly, 5 bps costs. Two runs on identical data; the only difference is the covariance matrix. The Squeeze Kernel arm runs `SqueezeKernel(lam=0.996)` — **nothing tuned on this panel**.
37
+
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/r0k3/squeeze-kernel/main/examples/figures/vol_targeted_portfolio_dark.png">
40
+ <img alt="Vol-targeted long-only minimum-variance portfolio on 300 US equities: Squeeze Kernel vs Ledoit-Wolf equity curves, drawdown, realized volatility, and risk/return profile" src="https://raw.githubusercontent.com/r0k3/squeeze-kernel/main/examples/figures/vol_targeted_portfolio.png">
41
+ </picture>
42
+
43
+ | Method | CAGR | Vol | Sharpe | MaxDD | Calmar | Vol-target RMSE |
44
+ |---|---|---|---|---|---|---|
45
+ | **Squeeze Kernel (default)** | **12.4%** | **13.5%** | **0.91** | **-34.6%** | **0.36** | **7.10%** |
46
+ | Ledoit-Wolf (252d) | 11.7% | 14.6% | 0.80 | -38.7% | 0.30 | 7.51% |
47
+
48
+ Reproduce from the repo alone (the 300-stock panel ships as a parquet; survivorship and provenance are documented in the script):
49
+
50
+ ```bash
51
+ pip install squeeze-kernel pandas pyarrow scikit-learn matplotlib
52
+ python examples/vol_targeted_portfolio.py # ~2 minutes
53
+ ```
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install squeeze-kernel # NumPy only
59
+ pip install "squeeze-kernel[full]" # + SciPy (faster detector factorisations)
60
+ ```
61
+
62
+ ## Quickstart
63
+
64
+ ```python
65
+ import numpy as np
66
+ from squeeze_kernel import SqueezeKernel, estimate_squeeze_cov
67
+
68
+ returns = np.random.default_rng(42).normal(0.0, 0.01, size=(500, 30))
69
+
70
+ sk = SqueezeKernel() # lam=0.996 (anchor half-life ~173 days)
71
+ for r_t in returns:
72
+ w = sk.update(r_t) # returns the day's kernel weight
73
+ cov, corr = sk.covariance(), sk.correlation()
74
+ sk.state() # kernel scale, per-timescale effective sizes, mixture tilt
75
+
76
+ # batch mode: full panel in, covariance path out
77
+ cov_path, corr_path, weights = estimate_squeeze_cov(returns, with_weights=True)
78
+ ```
79
+
80
+ Missing values: pass `NaN` (or `mask=` on `update`). Newly listed, delisted or halted assets need no imputation and no complete-case subsetting.
81
+
82
+ ## What derives from `lam`
83
+
84
+ | quantity | value |
85
+ |---|---|
86
+ | timescale ladder | decays `(lam⁴, lam, lam^¼)` — half-lives `(h/4, h, 4h)`, `h = -1/log2(lam)` |
87
+ | kernel scale | state: `κ_t = ⅓ · EWMA(activity)` at the anchor rate |
88
+ | shrinkage intensity | per timescale, `α = min(1,c) · g̃²/(g̃² + (1−g̃)²·max(0, 1/c − 1))` from the online concentration `c = n/ν` and target-fit `g̃` |
89
+ | timescale weights | prior ∝ √h, moved by exponentiated gradient on the blend log score at the null temperature, fast-rung memory |
90
+ | volatility memory | ladder `(h/16, h/4, h)`, pooled panel-wide by the same rule on tanh-saturated evidence, uniform prior |
91
+ | structural constants | K=3, b=4, θ=½, κ-scale ⅓, Schur power 2 — each bracketed by ablation in the paper |
92
+ | fitted constants | none (the 2.x volatility clock `λ_v = 0.98` is replaced by the ladder above) |
93
+
94
+ `from squeeze_kernel import CONSTANTS` exposes the structural constants for research. The published v1 estimator (all its knobs) remains available as `SqueezeKernelEstimator` / `SqueezeKernel.v1(...)`; every 2.0 mechanism is also an estimator-level switch for ablation. See [MIGRATION.md](MIGRATION.md).
95
+
96
+ ## How it works
97
+
98
+ One daily update: variance on a three-rung ladder per asset, pooled by panel-wide self-adapting weights → standardised surprise → per-asset clock increments from the Schur-square-weighted neighbourhood mean of squared surprises → diagonal-congruence update of each timescale's correlation state on those clocks → per-timescale shrinkage as a learned pool of the raw correlation, its equicorrelation level and its Hadamard square, with the self-tuning intensity rule as the prior and the blend gradient as the correction → blend across timescales, moved by the gradient of the blend's own log score → covariance. The paper gives the derivations, guarantees (PSD, conditioning floor, exact reductions to the published special cases), and the full evaluation.
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ uv sync --extra full --extra dev
104
+ uv run python -m pytest # test suite
105
+ uv run python -m ruff check . # lint
106
+ uv run mypy # strict type check (src/squeeze_kernel)
107
+ uv build # build sdist + wheel
108
+ ```
109
+
110
+ ## Citation
111
+
112
+ ```bibtex
113
+ @article{kende2026squeeze,
114
+ title = {The Squeeze Kernel Covariance Estimator: Dual-Timescale Tracking with Adaptive Shrinkage},
115
+ author = {Kende, Robert},
116
+ year = {2026},
117
+ note = {Available at SSRN: \url{https://ssrn.com/abstract=6455918}}
118
+ }
119
+ ```
120
+
121
+ See also [`CITATION.cff`](CITATION.cff).
122
+
123
+ ## License
124
+
125
+ MIT
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "squeeze-kernel"
7
- version = "0.7.1"
7
+ version = "3.1.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"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "squeeze-kernel"
7
- version = "0.7.1"
7
+ version = "3.1.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"
@@ -0,0 +1,42 @@
1
+ """
2
+ Squeeze Kernel Covariance Estimator
3
+ ====================================
4
+
5
+ Streaming, PSD-by-construction covariance estimator. The 2.0 API is one
6
+ number; everything else derives from that decay, is a frozen
7
+ structural constant, or is self-tuning state — including per-asset
8
+ market clocks read from the correlation structure itself.
9
+
10
+ Quick start::
11
+
12
+ import numpy as np
13
+ from squeeze_kernel import SqueezeKernel
14
+
15
+ returns = np.random.default_rng(42).normal(0.0, 0.01, size=(250, 30))
16
+
17
+ sk = SqueezeKernel(lam=0.996) # the entire public surface
18
+ for r_t in returns:
19
+ sk.update(r_t) # NaN marks missing assets
20
+
21
+ cov = sk.covariance()
22
+ corr = sk.correlation()
23
+
24
+ The published v1 estimator (all legacy knobs) remains available as
25
+ ``SqueezeKernelEstimator`` or ``SqueezeKernel.v1(...)``; see MIGRATION.md.
26
+ """
27
+
28
+ from squeeze_kernel.core import CONSTANTS, SqueezeKernel, StructuralConstants
29
+ from squeeze_kernel.estimator import SqueezeKernelEstimator
30
+ from squeeze_kernel.kernels import kernel_fisher
31
+ from squeeze_kernel.batch import estimate_squeeze_cov
32
+
33
+ __all__ = [
34
+ "SqueezeKernel",
35
+ "StructuralConstants",
36
+ "CONSTANTS",
37
+ "SqueezeKernelEstimator",
38
+ "estimate_squeeze_cov",
39
+ "kernel_fisher",
40
+ ]
41
+
42
+ __version__ = "3.1.0"
@@ -0,0 +1,52 @@
1
+ """Batch estimation over an entire returns panel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from numpy.typing import ArrayLike
7
+
8
+ from squeeze_kernel.core import SqueezeKernel
9
+
10
+
11
+ def estimate_squeeze_cov(
12
+ returns: ArrayLike,
13
+ *,
14
+ lam: float = 0.996,
15
+ with_corr: bool = True,
16
+ with_weights: bool = False,
17
+ ) -> tuple[np.ndarray, np.ndarray | None, np.ndarray | None]:
18
+ """Run :class:`SqueezeKernel` over a panel and collect the estimate path.
19
+
20
+ Parameters
21
+ ----------
22
+ returns : array-like, shape (T, n)
23
+ Daily returns; NaN marks missing observations.
24
+ lam : float
25
+ The estimator's single parameter (default 0.996).
26
+ with_corr : bool
27
+ Also return the correlation path.
28
+ with_weights : bool
29
+ Also return the per-day kernel weight.
30
+
31
+ Returns
32
+ -------
33
+ cov : ndarray, shape (T, n, n)
34
+ corr : ndarray or None, shape (T, n, n)
35
+ weights : ndarray or None, shape (T,)
36
+ """
37
+ values = np.asarray(returns, dtype=np.float64)
38
+ if values.ndim != 2:
39
+ raise ValueError(f"Expected 2D returns, got shape {values.shape}.")
40
+ t_total, n_assets = values.shape
41
+ sk = SqueezeKernel(lam=lam)
42
+ cov = np.empty((t_total, n_assets, n_assets), dtype=np.float64)
43
+ corr = np.empty_like(cov) if with_corr else None
44
+ weights = np.empty(t_total, dtype=np.float64) if with_weights else None
45
+ for t in range(t_total):
46
+ w_t = sk.update(values[t])
47
+ cov[t] = sk.covariance()
48
+ if corr is not None:
49
+ corr[t] = sk.correlation()
50
+ if weights is not None:
51
+ weights[t] = w_t
52
+ return cov, corr, weights
@@ -0,0 +1,187 @@
1
+ """squeeze-kernel 2.0 public API: the one-number estimator.
2
+
3
+ ``SqueezeKernel`` exposes the v2 configuration of the estimator — every
4
+ constant either derives from ``half_life`` or is a frozen structural
5
+ constant (``CONSTANTS``) — with a public surface of one number and two
6
+ booleans. The v1 estimator remains available unchanged as
7
+ ``SqueezeKernelEstimator`` (or via :meth:`SqueezeKernel.v1`).
8
+
9
+ Configuration (research record: squeeze_cov V2_STATUS.md, branch v2):
10
+
11
+ - correlation ladder at ``half_life * (43/173, 1, 693/173)`` — the
12
+ canonical rungs at the default half-life; rung weights ``pi ~ sqrt(h)``
13
+ (theta = 1/2, structural),
14
+ - volatility on a ladder at ``half_life * (1/16, 1/4, 1)``, pooled
15
+ panel-wide by Bayes at the null-calibrated temperature on saturated
16
+ evidence with the fastest rung's memory and a uniform prior; this
17
+ replaced the 2.x fitted constant ``lambda_vol = 0.98`` (still accepted
18
+ by ``SqueezeKernelEstimator`` and ignored when ``vol_ladder=True``),
19
+ - timescale weights moved by exponentiated gradient on the blend's log
20
+ score at the null temperature (fast-rung memory) from the prior
21
+ ``pi ~ sqrt(h)``; replaced the 2.x CUSUM detector,
22
+ - (3.1) each timescale's shrunk correlation is a learned convex
23
+ combination of three PSD unit-diagonal experts -- its raw
24
+ correlation, the equicorrelation matrix at its mean level, and its
25
+ Schur square -- whose prior is the intensity rule's morph
26
+ ``(1-a, a(1-a), a^2)`` and whose weights the same blend gradient
27
+ corrects online (studentised within the timescale, same temperature
28
+ and memory). At the prior the estimator is the 3.0 one,
29
+ - kernel scale as state, not parameter: ``kappa_t = (1/3) EWMA_h(d^2)``
30
+ (chi-squared-null constant),
31
+ - self-tuning shrinkage intensity per rung from the online concentration
32
+ ``c = n / nu`` and the de-noised equicorrelation-explained fraction
33
+ ``g``: ``alpha = min(1, c) g^2 / (g^2 + (1-g)^2 max(0, 1/c - 1))``,
34
+ - shrinkage target: gamma = 1 Schur-square cluster target (PSD by the
35
+ Schur product theorem) or equicorrelation,
36
+ - sequential surprise-gated rung weights (Page CUSUM on the studentised
37
+ inter-rung predictive-score drift), switchable via ``detector``.
38
+ """
39
+ from __future__ import annotations
40
+
41
+ from dataclasses import dataclass
42
+
43
+ import numpy as np
44
+ from numpy.typing import ArrayLike
45
+
46
+ from .estimator import SqueezeKernelEstimator, _BlendGradient
47
+
48
+ __all__ = ["SqueezeKernel", "StructuralConstants", "CONSTANTS"]
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class StructuralConstants:
53
+ """Frozen structural constants of the v2 estimator.
54
+
55
+ Importable for research; not constructor arguments.
56
+ """
57
+
58
+ b: float = 4.0 # ladder spacing: rungs (h/b, h, h*b)
59
+ theta: float = 0.5 # rung weights ~ h^theta
60
+ lambda_vol: float = 0.98 # 2.x constant; unused by the 3.0 front-end (vol ladder)
61
+ kappa_c: float = 1.0 / 3.0 # kernel scale: kappa = kappa_c * EWMA(activity)
62
+ schur_p: int = 2 # Hadamard power of the cluster target
63
+ epsilon: float = 1e-8
64
+
65
+
66
+ CONSTANTS = StructuralConstants()
67
+
68
+
69
+ class SqueezeKernel:
70
+ """Streaming covariance estimator whose public surface is one number.
71
+
72
+ Parameters
73
+ ----------
74
+ lam : float
75
+ The single tunable: the exponential decay of the anchor
76
+ correlation timescale per trading day, in the classical EWMA
77
+ convention (default 0.996, a half-life of about 173 days). The
78
+ timescale ladder ``(lam**b, lam, lam**(1/b))`` with ``b = 4``, the
79
+ kernel-scale clock, and the rung weights all derive from it.
80
+
81
+ Everything else is structural or self-tuning state: per-asset market
82
+ clocks read from the correlation neighborhood (row-normalized Schur-
83
+ square weighting of squared surprises), a per-timescale shrinkage
84
+ intensity from the online concentration and target-fit, the Schur-
85
+ square cluster target, and the surprise-gated timescale weights.
86
+ The number of assets is inferred from the first ``update`` call.
87
+ The published v1 estimator and every ablation switch remain available
88
+ on ``SqueezeKernelEstimator``.
89
+ """
90
+
91
+ def __init__(self, lam: float = 0.996) -> None:
92
+ if not (0.0 < lam < 1.0):
93
+ raise ValueError("lam must be in (0, 1).")
94
+ self.lam = float(lam)
95
+ self._est: SqueezeKernelEstimator | None = None
96
+ self._t = 0
97
+
98
+ @property
99
+ def half_life(self) -> float:
100
+ """Anchor half-life in trading days implied by ``lam``."""
101
+ return float(-1.0 / np.log2(self.lam))
102
+
103
+ # ── lifecycle ────────────────────────────────────────────────────────
104
+
105
+ def _build(self, n_assets: int) -> SqueezeKernelEstimator:
106
+ c = CONSTANTS
107
+ h = self.half_life # ladder = (lam**b, lam, lam**(1/b))
108
+ ladder = (h / c.b, h, h * c.b)
109
+ return SqueezeKernelEstimator(
110
+ n_assets,
111
+ lambda_vol=c.lambda_vol,
112
+ shrinkage="auto",
113
+ shrinkage_target="cluster",
114
+ corr_half_lives=ladder,
115
+ corr_theta=c.theta,
116
+ kappa_mode="adaptive",
117
+ alpha_rule="selftuning",
118
+ level_match=False,
119
+ detector=True,
120
+ clock="asset",
121
+ vol_ladder=True,
122
+ weights="eg_blend",
123
+ split_learn=True,
124
+ epsilon=c.epsilon,
125
+ )
126
+
127
+ # ── public API ───────────────────────────────────────────────────────
128
+
129
+ def update(self, r_t: ArrayLike, mask: ArrayLike | None = None) -> float:
130
+ """Process one return vector; returns the kernel weight w_t.
131
+
132
+ ``r_t`` may contain NaN for missing assets; ``mask`` (optional
133
+ boolean, True = observed) is an alternative way to mark them.
134
+ """
135
+ r = np.asarray(r_t, dtype=np.float64)
136
+ if r.ndim != 1:
137
+ raise ValueError("r_t must be one-dimensional.")
138
+ if mask is not None:
139
+ m = np.asarray(mask, dtype=bool)
140
+ if m.shape != r.shape:
141
+ raise ValueError("mask must match r_t's shape.")
142
+ r = np.where(m, r, np.nan)
143
+ if self._est is None:
144
+ self._est = self._build(r.size)
145
+ self._t += 1
146
+ return self._est.update(r)
147
+
148
+ def covariance(self) -> np.ndarray:
149
+ """Current covariance estimate (n x n)."""
150
+ if self._est is None:
151
+ raise RuntimeError("Call update() at least once first.")
152
+ return self._est.get_cov()
153
+
154
+ def correlation(self) -> np.ndarray:
155
+ """Current correlation estimate (n x n)."""
156
+ if self._est is None:
157
+ raise RuntimeError("Call update() at least once first.")
158
+ return self._est.get_corr()
159
+
160
+ def state(self) -> dict[str, object]:
161
+ """Diagnostics: update count, last weight, kernel scale, per-rung
162
+ effective sizes, detector tilt."""
163
+ if self._est is None:
164
+ return {"n_assets": None, "t": 0}
165
+ est = self._est
166
+ det = est._detector
167
+ S = list(est._S_list or [])
168
+ J = list(est._J_list or [])
169
+ nu = [s * s / max(j, est.epsilon) for s, j in zip(S, J)]
170
+ return {
171
+ "n_assets": est.n_assets,
172
+ "t": self._t,
173
+ "last_weight": est.weight,
174
+ "kappa_t": getattr(est, "_last_kappa",
175
+ CONSTANTS.kappa_c * est._kap_state),
176
+ "rung_S": S,
177
+ "rung_nu": nu,
178
+ "detector_tilt": 0.0 if det is None else det.tilt,
179
+ "split": det.v.copy() if isinstance(det, _BlendGradient) and det.split else None,
180
+ }
181
+
182
+ # ── v1 escape hatch ──────────────────────────────────────────────────
183
+
184
+ @classmethod
185
+ def v1(cls, n_assets: int, **kwargs: object) -> SqueezeKernelEstimator:
186
+ """The published v1 estimator, unchanged (all legacy knobs)."""
187
+ return SqueezeKernelEstimator(n_assets, **kwargs) # type: ignore[arg-type]