conformal-oracle 0.2.1__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.
- conformal_oracle-0.2.1/LICENSE +21 -0
- conformal_oracle-0.2.1/PKG-INFO +56 -0
- conformal_oracle-0.2.1/README.md +105 -0
- conformal_oracle-0.2.1/pyproject.toml +93 -0
- conformal_oracle-0.2.1/setup.cfg +4 -0
- conformal_oracle-0.2.1/src/conformal_oracle/__init__.py +53 -0
- conformal_oracle-0.2.1/src/conformal_oracle/_conventions.py +23 -0
- conformal_oracle-0.2.1/src/conformal_oracle/_protocols.py +27 -0
- conformal_oracle-0.2.1/src/conformal_oracle/_types.py +205 -0
- conformal_oracle-0.2.1/src/conformal_oracle/audit/__init__.py +55 -0
- conformal_oracle-0.2.1/src/conformal_oracle/audit/benchmark.py +142 -0
- conformal_oracle-0.2.1/src/conformal_oracle/audit/regime.py +64 -0
- conformal_oracle-0.2.1/src/conformal_oracle/audit/single_rolling.py +227 -0
- conformal_oracle-0.2.1/src/conformal_oracle/audit/single_static.py +225 -0
- conformal_oracle-0.2.1/src/conformal_oracle/conformal/__init__.py +15 -0
- conformal_oracle-0.2.1/src/conformal_oracle/conformal/bootstrap.py +51 -0
- conformal_oracle-0.2.1/src/conformal_oracle/conformal/rolling.py +86 -0
- conformal_oracle-0.2.1/src/conformal_oracle/conformal/static.py +35 -0
- conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/__init__.py +22 -0
- conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/acerbi_szekely.py +38 -0
- conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/basel.py +34 -0
- conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/christoffersen.py +71 -0
- conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/diebold_mariano.py +57 -0
- conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/kupiec.py +34 -0
- conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/scoring.py +39 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/__init__.py +46 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/garch_normal.py +12 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/gjr_garch.py +80 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/hist_sim.py +27 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/__init__.py +53 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/_base.py +70 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/_cache.py +97 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/chronos.py +117 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/lag_llama.py +157 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/moirai.py +193 -0
- conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/timesfm.py +119 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/__init__.py +9 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/audit.py +79 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/cross_sectional.py +66 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/diagnostic_regression.py +156 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/diebold_mariano.py +91 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/kupiec_panel.py +46 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/latex.py +212 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/result.py +383 -0
- conformal_oracle-0.2.1/src/conformal_oracle/panel/wildcluster_bootstrap.py +149 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/__init__.py +30 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/aci.py +74 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/base.py +74 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/evt_pot.py +108 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/fhs.py +61 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/gamlss_sst.py +49 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/gbm_qr.py +121 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/historical_quantile.py +32 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/quantile_regression.py +101 -0
- conformal_oracle-0.2.1/src/conformal_oracle/recalibration/scale_correction.py +36 -0
- conformal_oracle-0.2.1/src/conformal_oracle/reporting/__init__.py +13 -0
- conformal_oracle-0.2.1/src/conformal_oracle/reporting/latex.py +119 -0
- conformal_oracle-0.2.1/src/conformal_oracle/reporting/plotting.py +53 -0
- conformal_oracle-0.2.1/src/conformal_oracle.egg-info/PKG-INFO +56 -0
- conformal_oracle-0.2.1/src/conformal_oracle.egg-info/SOURCES.txt +61 -0
- conformal_oracle-0.2.1/src/conformal_oracle.egg-info/dependency_links.txt +1 -0
- conformal_oracle-0.2.1/src/conformal_oracle.egg-info/requires.txt +45 -0
- conformal_oracle-0.2.1/src/conformal_oracle.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Traian Pele
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: conformal-oracle
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Conformal recalibration audit for tail quantile forecasters
|
|
5
|
+
Author-email: Daniel Traian Pele <danpele@ase.ro>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: conformal prediction,VaR,risk management,backtesting
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
Requires-Dist: pandas>=2.0
|
|
19
|
+
Requires-Dist: scipy>=1.10
|
|
20
|
+
Requires-Dist: statsmodels>=0.14
|
|
21
|
+
Requires-Dist: arch>=6.0
|
|
22
|
+
Requires-Dist: matplotlib>=3.7
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.5; extra == "dev"
|
|
28
|
+
Requires-Dist: scikit-learn>=1.0; extra == "dev"
|
|
29
|
+
Provides-Extra: chronos
|
|
30
|
+
Requires-Dist: chronos-forecasting>=1.5; extra == "chronos"
|
|
31
|
+
Requires-Dist: torch>=2.0; extra == "chronos"
|
|
32
|
+
Requires-Dist: transformers>=4.30; extra == "chronos"
|
|
33
|
+
Provides-Extra: timesfm
|
|
34
|
+
Requires-Dist: timesfm>=1.2; extra == "timesfm"
|
|
35
|
+
Requires-Dist: torch>=2.0; extra == "timesfm"
|
|
36
|
+
Provides-Extra: moirai
|
|
37
|
+
Requires-Dist: uni2ts>=0.1; extra == "moirai"
|
|
38
|
+
Requires-Dist: torch>=2.0; extra == "moirai"
|
|
39
|
+
Requires-Dist: einops>=0.6; extra == "moirai"
|
|
40
|
+
Requires-Dist: gluonts>=0.14; extra == "moirai"
|
|
41
|
+
Requires-Dist: huggingface-hub>=0.20; extra == "moirai"
|
|
42
|
+
Provides-Extra: lag-llama
|
|
43
|
+
Requires-Dist: torch>=2.0; extra == "lag-llama"
|
|
44
|
+
Requires-Dist: gluonts>=0.14; extra == "lag-llama"
|
|
45
|
+
Requires-Dist: huggingface-hub>=0.20; extra == "lag-llama"
|
|
46
|
+
Requires-Dist: einops>=0.6; extra == "lag-llama"
|
|
47
|
+
Provides-Extra: tsfm-all
|
|
48
|
+
Requires-Dist: chronos-forecasting>=1.5; extra == "tsfm-all"
|
|
49
|
+
Requires-Dist: timesfm>=1.2; extra == "tsfm-all"
|
|
50
|
+
Requires-Dist: uni2ts>=0.1; extra == "tsfm-all"
|
|
51
|
+
Requires-Dist: torch>=2.0; extra == "tsfm-all"
|
|
52
|
+
Requires-Dist: transformers>=4.30; extra == "tsfm-all"
|
|
53
|
+
Requires-Dist: gluonts>=0.14; extra == "tsfm-all"
|
|
54
|
+
Requires-Dist: huggingface-hub>=0.20; extra == "tsfm-all"
|
|
55
|
+
Requires-Dist: einops>=0.6; extra == "tsfm-all"
|
|
56
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# conformal-oracle
|
|
2
|
+
|
|
3
|
+
Conformal recalibration audit for tail quantile forecasters.
|
|
4
|
+
|
|
5
|
+
Given any black-box probabilistic forecaster and a return series,
|
|
6
|
+
`conformal-oracle` computes a one-parameter conformal correction
|
|
7
|
+
(static or rolling), classifies the forecaster as signal-preserving
|
|
8
|
+
or replacement, and reports a full backtest panel.
|
|
9
|
+
|
|
10
|
+
Implements the methodology from:
|
|
11
|
+
|
|
12
|
+
> Pele, D.T., Bolovăneanu, V., Ginavar, A.T., Lessmann, S., Härdle, W.K.
|
|
13
|
+
> "Recalibrating Tail Event Forecasts under Temporal Dependence" (2026).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install conformal-oracle
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For TSFM wrappers:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install conformal-oracle[chronos] # Chronos
|
|
25
|
+
pip install conformal-oracle[lag_llama] # Lag-Llama
|
|
26
|
+
pip install conformal-oracle[tsfm_all] # all four TSFMs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For development:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/QuantLet/Conformal_Oracle.git
|
|
33
|
+
cd Conformal_Oracle/python
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart — static audit
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import pandas as pd
|
|
41
|
+
from conformal_oracle import audit_static
|
|
42
|
+
from conformal_oracle.forecasters import GJRGARCHForecaster
|
|
43
|
+
|
|
44
|
+
returns = pd.read_csv("returns.csv", index_col=0, parse_dates=True).squeeze()
|
|
45
|
+
result = audit_static(returns, GJRGARCHForecaster(), alpha=0.01)
|
|
46
|
+
print(result.summary())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart — rolling audit
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from conformal_oracle import audit_rolling
|
|
53
|
+
from conformal_oracle.forecasters import GJRGARCHForecaster
|
|
54
|
+
|
|
55
|
+
result = audit_rolling(returns, GJRGARCHForecaster(), alpha=0.01, window=250)
|
|
56
|
+
print(result.summary())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quickstart — benchmark comparison
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from conformal_oracle import audit_with_benchmarks
|
|
63
|
+
|
|
64
|
+
comp = audit_with_benchmarks(returns, my_forecaster, benchmarks=["gjr_garch", "hist_sim"])
|
|
65
|
+
print(comp.comparison_table())
|
|
66
|
+
print(comp.diebold_mariano(baseline="gjr_garch"))
|
|
67
|
+
print(comp.comparison_table_latex())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Custom forecaster
|
|
71
|
+
|
|
72
|
+
Any object implementing `fit(returns)` and `forecast(returns, t)` works:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from conformal_oracle._types import SampleDistribution
|
|
76
|
+
|
|
77
|
+
class MyForecaster:
|
|
78
|
+
def fit(self, returns): pass
|
|
79
|
+
def forecast(self, returns, t):
|
|
80
|
+
hist = returns.iloc[max(0, t-250):t]
|
|
81
|
+
return SampleDistribution(samples=hist.values)
|
|
82
|
+
|
|
83
|
+
result = audit_static(returns, MyForecaster(), alpha=0.01)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
See `examples/04_custom_forecaster.py` for a full example.
|
|
87
|
+
|
|
88
|
+
## Worked examples
|
|
89
|
+
|
|
90
|
+
- [Reproduce Table 1 (S&P 500)](examples/notebooks/reproduce_table_1_sp500.ipynb) —
|
|
91
|
+
GJR-GARCH and Lag-Llama audits with conformal correction on real data.
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
- [API Reference](docs/api.md)
|
|
96
|
+
- [Methodology](docs/methodology.md)
|
|
97
|
+
- [Conventions](docs/conventions.md) (return units, VaR sign, alpha)
|
|
98
|
+
|
|
99
|
+
## Requirements
|
|
100
|
+
|
|
101
|
+
Python 3.10+, numpy, pandas, scipy, statsmodels, arch, matplotlib.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "conformal-oracle"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Conformal recalibration audit for tail quantile forecasters"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Daniel Traian Pele", email = "danpele@ase.ro" },
|
|
13
|
+
]
|
|
14
|
+
keywords = ["conformal prediction", "VaR", "risk management", "backtesting"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.24",
|
|
26
|
+
"pandas>=2.0",
|
|
27
|
+
"scipy>=1.10",
|
|
28
|
+
"statsmodels>=0.14",
|
|
29
|
+
"arch>=6.0",
|
|
30
|
+
"matplotlib>=3.7",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"pytest-cov>=4.0",
|
|
37
|
+
"ruff>=0.1",
|
|
38
|
+
"mypy>=1.5",
|
|
39
|
+
"scikit-learn>=1.0",
|
|
40
|
+
]
|
|
41
|
+
chronos = [
|
|
42
|
+
"chronos-forecasting>=1.5",
|
|
43
|
+
"torch>=2.0",
|
|
44
|
+
"transformers>=4.30",
|
|
45
|
+
]
|
|
46
|
+
timesfm = [
|
|
47
|
+
"timesfm>=1.2",
|
|
48
|
+
"torch>=2.0",
|
|
49
|
+
]
|
|
50
|
+
moirai = [
|
|
51
|
+
"uni2ts>=0.1",
|
|
52
|
+
"torch>=2.0",
|
|
53
|
+
"einops>=0.6",
|
|
54
|
+
"gluonts>=0.14",
|
|
55
|
+
"huggingface-hub>=0.20",
|
|
56
|
+
]
|
|
57
|
+
lag_llama = [
|
|
58
|
+
"torch>=2.0",
|
|
59
|
+
"gluonts>=0.14",
|
|
60
|
+
"huggingface-hub>=0.20",
|
|
61
|
+
"einops>=0.6",
|
|
62
|
+
]
|
|
63
|
+
tsfm_all = [
|
|
64
|
+
"chronos-forecasting>=1.5",
|
|
65
|
+
"timesfm>=1.2",
|
|
66
|
+
"uni2ts>=0.1",
|
|
67
|
+
"torch>=2.0",
|
|
68
|
+
"transformers>=4.30",
|
|
69
|
+
"gluonts>=0.14",
|
|
70
|
+
"huggingface-hub>=0.20",
|
|
71
|
+
"einops>=0.6",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.setuptools.packages.find]
|
|
75
|
+
where = ["src"]
|
|
76
|
+
|
|
77
|
+
[tool.pytest.ini_options]
|
|
78
|
+
testpaths = ["tests"]
|
|
79
|
+
addopts = "--tb=short -q"
|
|
80
|
+
pythonpath = ["."]
|
|
81
|
+
|
|
82
|
+
[tool.ruff]
|
|
83
|
+
target-version = "py310"
|
|
84
|
+
line-length = 88
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint]
|
|
87
|
+
select = ["E", "F", "I", "W"]
|
|
88
|
+
|
|
89
|
+
[tool.mypy]
|
|
90
|
+
# v0.1: non-strict. Strict mode has 16 kwargs-forwarding errors — fix in v0.2.
|
|
91
|
+
python_version = "3.10"
|
|
92
|
+
warn_return_any = true
|
|
93
|
+
warn_unused_configs = true
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Conformal recalibration audit for tail quantile forecasters."""
|
|
2
|
+
|
|
3
|
+
from conformal_oracle._protocols import Forecaster
|
|
4
|
+
from conformal_oracle._types import (
|
|
5
|
+
ParametricDistribution,
|
|
6
|
+
PredictiveDistribution,
|
|
7
|
+
QuantileGridDistribution,
|
|
8
|
+
SampleDistribution,
|
|
9
|
+
)
|
|
10
|
+
from conformal_oracle.audit import (
|
|
11
|
+
audit,
|
|
12
|
+
audit_rolling,
|
|
13
|
+
audit_static,
|
|
14
|
+
audit_with_benchmarks,
|
|
15
|
+
)
|
|
16
|
+
from conformal_oracle.panel import audit_panel
|
|
17
|
+
from conformal_oracle.recalibration import (
|
|
18
|
+
AdaptiveConformalInference,
|
|
19
|
+
ConformalShift,
|
|
20
|
+
ExtremeValueTheoryPOT,
|
|
21
|
+
FilteredHistoricalSimulation,
|
|
22
|
+
GBMQuantileRegression,
|
|
23
|
+
HistoricalQuantileRecalibration,
|
|
24
|
+
IsotonicQuantileRegression,
|
|
25
|
+
LinearQuantileRegression,
|
|
26
|
+
RecalibrationMethod,
|
|
27
|
+
ScaleCorrectionRecalibration,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "0.2.1"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SampleDistribution",
|
|
34
|
+
"QuantileGridDistribution",
|
|
35
|
+
"ParametricDistribution",
|
|
36
|
+
"PredictiveDistribution",
|
|
37
|
+
"Forecaster",
|
|
38
|
+
"audit",
|
|
39
|
+
"audit_static",
|
|
40
|
+
"audit_rolling",
|
|
41
|
+
"audit_with_benchmarks",
|
|
42
|
+
"audit_panel",
|
|
43
|
+
"RecalibrationMethod",
|
|
44
|
+
"ConformalShift",
|
|
45
|
+
"HistoricalQuantileRecalibration",
|
|
46
|
+
"ScaleCorrectionRecalibration",
|
|
47
|
+
"LinearQuantileRegression",
|
|
48
|
+
"IsotonicQuantileRegression",
|
|
49
|
+
"AdaptiveConformalInference",
|
|
50
|
+
"GBMQuantileRegression",
|
|
51
|
+
"ExtremeValueTheoryPOT",
|
|
52
|
+
"FilteredHistoricalSimulation",
|
|
53
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Documented constants and conventions.
|
|
2
|
+
|
|
3
|
+
Returns: pandas Series of log returns, decimal form (not percent),
|
|
4
|
+
negative = loss.
|
|
5
|
+
|
|
6
|
+
VaR sign: VaR is reported as a positive number representing the loss
|
|
7
|
+
threshold. A violation occurs when r_t < -VaR_t.
|
|
8
|
+
|
|
9
|
+
Quantile level alpha: small (e.g. 0.01) means the lower-tail probability.
|
|
10
|
+
The forecast is the alpha-quantile of the predictive
|
|
11
|
+
distribution.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
DEFAULT_ALPHA: float = 0.01
|
|
15
|
+
DEFAULT_CALIBRATION_SPLIT: float = 0.70
|
|
16
|
+
DEFAULT_ROLLING_WINDOW: int = 250
|
|
17
|
+
DEFAULT_WARMUP: int = 250
|
|
18
|
+
DEFAULT_SAMPLE_SIZE: int = 1000
|
|
19
|
+
DEFAULT_BLOCK_LENGTH: int = 20
|
|
20
|
+
DEFAULT_N_BOOT: int = 999
|
|
21
|
+
DEFAULT_SEED: int = 2026
|
|
22
|
+
DEFAULT_PERSISTENCE: int = 20
|
|
23
|
+
REPLACEMENT_THRESHOLD: float = 1.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Forecaster protocol that user implementations must satisfy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from conformal_oracle._types import PredictiveDistribution
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Forecaster(Protocol):
|
|
13
|
+
"""Protocol for one-step-ahead probabilistic forecasters.
|
|
14
|
+
|
|
15
|
+
User's forecaster must implement fit() and forecast().
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def fit(self, returns: pd.Series) -> None:
|
|
19
|
+
"""Fit on calibration data. May be a no-op for zero-shot models."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def forecast(self, returns: pd.Series, t: int) -> PredictiveDistribution:
|
|
23
|
+
"""One-step-ahead predictive distribution at time t.
|
|
24
|
+
|
|
25
|
+
The forecaster sees only returns.iloc[:t] (history up to t-1).
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Predictive distribution types used throughout the package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Union
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import stats
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SampleDistribution:
|
|
14
|
+
"""Predictive distribution represented as Monte Carlo samples."""
|
|
15
|
+
|
|
16
|
+
samples: np.ndarray
|
|
17
|
+
|
|
18
|
+
def quantile(self, alpha: float) -> float:
|
|
19
|
+
return float(np.quantile(self.samples, alpha))
|
|
20
|
+
|
|
21
|
+
def expected_shortfall(self, alpha: float) -> float:
|
|
22
|
+
q = self.quantile(alpha)
|
|
23
|
+
tail = self.samples[self.samples <= q]
|
|
24
|
+
if len(tail) == 0:
|
|
25
|
+
return q
|
|
26
|
+
return float(np.mean(tail))
|
|
27
|
+
|
|
28
|
+
def cdf(self, x: float) -> float:
|
|
29
|
+
return float(np.mean(self.samples <= x))
|
|
30
|
+
|
|
31
|
+
def __len__(self) -> int:
|
|
32
|
+
return len(self.samples)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class QuantileGridDistribution:
|
|
37
|
+
"""Predictive distribution as a finite quantile grid."""
|
|
38
|
+
|
|
39
|
+
levels: np.ndarray
|
|
40
|
+
quantiles: np.ndarray
|
|
41
|
+
|
|
42
|
+
def quantile(
|
|
43
|
+
self, alpha: float, completion: str = "student_t"
|
|
44
|
+
) -> float:
|
|
45
|
+
idx_exact = np.where(np.isclose(self.levels, alpha))[0]
|
|
46
|
+
if len(idx_exact) > 0:
|
|
47
|
+
return float(self.quantiles[idx_exact[0]])
|
|
48
|
+
|
|
49
|
+
if self.levels.min() <= alpha <= self.levels.max():
|
|
50
|
+
return float(np.interp(alpha, self.levels, self.quantiles))
|
|
51
|
+
|
|
52
|
+
return self._tail_completion_quantile(alpha, completion)
|
|
53
|
+
|
|
54
|
+
def expected_shortfall(
|
|
55
|
+
self, alpha: float, completion: str = "student_t"
|
|
56
|
+
) -> float:
|
|
57
|
+
if completion == "linear":
|
|
58
|
+
return self._es_linear(alpha)
|
|
59
|
+
params = self._fit_parametric(completion)
|
|
60
|
+
if completion == "student_t":
|
|
61
|
+
df, loc, scale = params
|
|
62
|
+
rv = stats.t(df=df, loc=loc, scale=scale)
|
|
63
|
+
else:
|
|
64
|
+
loc, scale = params
|
|
65
|
+
rv = stats.norm(loc=loc, scale=scale)
|
|
66
|
+
q = rv.ppf(alpha)
|
|
67
|
+
tail_samples = rv.rvs(size=10000, random_state=42)
|
|
68
|
+
tail_samples = tail_samples[tail_samples <= q]
|
|
69
|
+
if len(tail_samples) == 0:
|
|
70
|
+
return float(q)
|
|
71
|
+
return float(np.mean(tail_samples))
|
|
72
|
+
|
|
73
|
+
def cdf(self, x: float, completion: str = "student_t") -> float:
|
|
74
|
+
"""CDF evaluated at x. Within the grid range, interpolates
|
|
75
|
+
between grid points. Outside the grid, uses the same parametric
|
|
76
|
+
tail completion as quantile() (Student-t by default). Users who
|
|
77
|
+
want strict in-grid-only behaviour can check
|
|
78
|
+
``min(levels) <= cdf_value <= max(levels)`` themselves."""
|
|
79
|
+
below = self.quantiles[self.quantiles <= x]
|
|
80
|
+
if len(below) == 0 and x < self.quantiles.min():
|
|
81
|
+
params = self._fit_parametric(completion)
|
|
82
|
+
if completion == "student_t":
|
|
83
|
+
df, loc, scale = params
|
|
84
|
+
return float(stats.t.cdf(x, df=df, loc=loc, scale=scale))
|
|
85
|
+
else:
|
|
86
|
+
loc, scale = params
|
|
87
|
+
return float(stats.norm.cdf(x, loc=loc, scale=scale))
|
|
88
|
+
if x >= self.quantiles.max():
|
|
89
|
+
return 1.0
|
|
90
|
+
return float(np.interp(x, self.quantiles, self.levels))
|
|
91
|
+
|
|
92
|
+
def _tail_completion_quantile(self, alpha: float, method: str) -> float:
|
|
93
|
+
if method == "linear":
|
|
94
|
+
if alpha < self.levels.min():
|
|
95
|
+
slope = (self.quantiles[1] - self.quantiles[0]) / (
|
|
96
|
+
self.levels[1] - self.levels[0]
|
|
97
|
+
)
|
|
98
|
+
return float(
|
|
99
|
+
self.quantiles[0] + slope * (alpha - self.levels[0])
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
slope = (self.quantiles[-1] - self.quantiles[-2]) / (
|
|
103
|
+
self.levels[-1] - self.levels[-2]
|
|
104
|
+
)
|
|
105
|
+
return float(
|
|
106
|
+
self.quantiles[-1] + slope * (alpha - self.levels[-1])
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
params = self._fit_parametric(method)
|
|
110
|
+
if method == "student_t":
|
|
111
|
+
df, loc, scale = params
|
|
112
|
+
return float(stats.t.ppf(alpha, df=df, loc=loc, scale=scale))
|
|
113
|
+
else:
|
|
114
|
+
loc, scale = params
|
|
115
|
+
return float(stats.norm.ppf(alpha, loc=loc, scale=scale))
|
|
116
|
+
|
|
117
|
+
def _fit_parametric(self, method: str) -> tuple[float, ...]:
|
|
118
|
+
if method == "student_t":
|
|
119
|
+
return self._fit_student_t_quantile()
|
|
120
|
+
else:
|
|
121
|
+
loc = float(np.mean(self.quantiles))
|
|
122
|
+
scale = float(np.std(self.quantiles))
|
|
123
|
+
return (loc, scale)
|
|
124
|
+
|
|
125
|
+
def _fit_student_t_quantile(self) -> tuple[float, float, float]:
|
|
126
|
+
from scipy.optimize import minimize as _minimize
|
|
127
|
+
|
|
128
|
+
levels = self.levels
|
|
129
|
+
qvals = self.quantiles
|
|
130
|
+
|
|
131
|
+
def residuals(params: np.ndarray) -> float:
|
|
132
|
+
nu, mu, sigma = params
|
|
133
|
+
if nu <= 2 or sigma <= 0:
|
|
134
|
+
return 1e10
|
|
135
|
+
predicted = mu + sigma * stats.t.ppf(levels, nu)
|
|
136
|
+
return float(np.sum((predicted - qvals) ** 2))
|
|
137
|
+
|
|
138
|
+
x0 = [8.0, float(np.median(qvals)), max(float(np.std(qvals)), 1e-8)]
|
|
139
|
+
res = _minimize(residuals, x0, method="Nelder-Mead",
|
|
140
|
+
options={"maxiter": 2000, "xatol": 1e-8})
|
|
141
|
+
nu, mu, sigma = res.x
|
|
142
|
+
nu = max(nu, 2.01)
|
|
143
|
+
sigma = max(sigma, 1e-8)
|
|
144
|
+
return (nu, mu, sigma)
|
|
145
|
+
|
|
146
|
+
def _es_linear(self, alpha: float) -> float:
|
|
147
|
+
q = self.quantile(alpha, completion="linear")
|
|
148
|
+
below = self.quantiles[self.levels <= alpha]
|
|
149
|
+
if len(below) == 0:
|
|
150
|
+
return float(q)
|
|
151
|
+
return float(np.mean(np.append(below, q)))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class ParametricDistribution:
|
|
156
|
+
"""Predictive distribution from a closed-form parametric family."""
|
|
157
|
+
|
|
158
|
+
location: float
|
|
159
|
+
scale: float
|
|
160
|
+
family: Literal["normal", "student_t", "skewed_t"]
|
|
161
|
+
df: float | None = None
|
|
162
|
+
skew: float | None = None
|
|
163
|
+
|
|
164
|
+
def quantile(self, alpha: float) -> float:
|
|
165
|
+
rv = self._distribution()
|
|
166
|
+
return float(rv.ppf(alpha))
|
|
167
|
+
|
|
168
|
+
def expected_shortfall(self, alpha: float) -> float:
|
|
169
|
+
rv = self._distribution()
|
|
170
|
+
q = rv.ppf(alpha)
|
|
171
|
+
samples = rv.rvs(size=50000, random_state=42)
|
|
172
|
+
tail = samples[samples <= q]
|
|
173
|
+
if len(tail) == 0:
|
|
174
|
+
return float(q)
|
|
175
|
+
return float(np.mean(tail))
|
|
176
|
+
|
|
177
|
+
def cdf(self, x: float) -> float:
|
|
178
|
+
rv = self._distribution()
|
|
179
|
+
return float(rv.cdf(x))
|
|
180
|
+
|
|
181
|
+
def _distribution(self) -> stats.rv_continuous:
|
|
182
|
+
if self.family == "normal":
|
|
183
|
+
return stats.norm(loc=self.location, scale=self.scale)
|
|
184
|
+
elif self.family == "student_t":
|
|
185
|
+
if self.df is None:
|
|
186
|
+
raise ValueError("df required for student_t family")
|
|
187
|
+
return stats.t(df=self.df, loc=self.location, scale=self.scale)
|
|
188
|
+
elif self.family == "skewed_t":
|
|
189
|
+
if self.df is None or self.skew is None:
|
|
190
|
+
raise ValueError("df and skew required for skewed_t family")
|
|
191
|
+
# Hansen's skewed-t: use scipy's nct as approximation
|
|
192
|
+
return stats.nct(
|
|
193
|
+
df=self.df,
|
|
194
|
+
nc=self.skew,
|
|
195
|
+
loc=self.location,
|
|
196
|
+
scale=self.scale,
|
|
197
|
+
)
|
|
198
|
+
raise ValueError(f"Unknown family: {self.family}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
PredictiveDistribution = Union[
|
|
202
|
+
SampleDistribution,
|
|
203
|
+
QuantileGridDistribution,
|
|
204
|
+
ParametricDistribution,
|
|
205
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Audit API: static, rolling, benchmarks, and convenience dispatcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Union
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from conformal_oracle._protocols import Forecaster
|
|
10
|
+
from conformal_oracle.audit.benchmark import (
|
|
11
|
+
BenchmarkComparison,
|
|
12
|
+
audit_with_benchmarks,
|
|
13
|
+
)
|
|
14
|
+
from conformal_oracle.audit.single_rolling import (
|
|
15
|
+
RollingAuditResult,
|
|
16
|
+
audit_rolling,
|
|
17
|
+
)
|
|
18
|
+
from conformal_oracle.audit.single_static import (
|
|
19
|
+
StaticAuditResult,
|
|
20
|
+
audit_static,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def audit(
|
|
25
|
+
returns: pd.Series,
|
|
26
|
+
forecaster: Forecaster,
|
|
27
|
+
alpha: float = 0.01,
|
|
28
|
+
mode: Literal["static", "rolling"] = "static",
|
|
29
|
+
recalibration: object | None = None,
|
|
30
|
+
**kwargs: object,
|
|
31
|
+
) -> Union[StaticAuditResult, RollingAuditResult]:
|
|
32
|
+
"""Convenience dispatcher for static or rolling audit."""
|
|
33
|
+
if mode == "static":
|
|
34
|
+
return audit_static(
|
|
35
|
+
returns, forecaster, alpha=alpha,
|
|
36
|
+
recalibration=recalibration, **kwargs,
|
|
37
|
+
)
|
|
38
|
+
elif mode == "rolling":
|
|
39
|
+
return audit_rolling(
|
|
40
|
+
returns, forecaster, alpha=alpha,
|
|
41
|
+
recalibration=recalibration, **kwargs,
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Unknown mode: {mode!r}. Use 'static' or 'rolling'.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"audit",
|
|
49
|
+
"audit_static",
|
|
50
|
+
"audit_rolling",
|
|
51
|
+
"audit_with_benchmarks",
|
|
52
|
+
"StaticAuditResult",
|
|
53
|
+
"RollingAuditResult",
|
|
54
|
+
"BenchmarkComparison",
|
|
55
|
+
]
|