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.
Files changed (63) hide show
  1. conformal_oracle-0.2.1/LICENSE +21 -0
  2. conformal_oracle-0.2.1/PKG-INFO +56 -0
  3. conformal_oracle-0.2.1/README.md +105 -0
  4. conformal_oracle-0.2.1/pyproject.toml +93 -0
  5. conformal_oracle-0.2.1/setup.cfg +4 -0
  6. conformal_oracle-0.2.1/src/conformal_oracle/__init__.py +53 -0
  7. conformal_oracle-0.2.1/src/conformal_oracle/_conventions.py +23 -0
  8. conformal_oracle-0.2.1/src/conformal_oracle/_protocols.py +27 -0
  9. conformal_oracle-0.2.1/src/conformal_oracle/_types.py +205 -0
  10. conformal_oracle-0.2.1/src/conformal_oracle/audit/__init__.py +55 -0
  11. conformal_oracle-0.2.1/src/conformal_oracle/audit/benchmark.py +142 -0
  12. conformal_oracle-0.2.1/src/conformal_oracle/audit/regime.py +64 -0
  13. conformal_oracle-0.2.1/src/conformal_oracle/audit/single_rolling.py +227 -0
  14. conformal_oracle-0.2.1/src/conformal_oracle/audit/single_static.py +225 -0
  15. conformal_oracle-0.2.1/src/conformal_oracle/conformal/__init__.py +15 -0
  16. conformal_oracle-0.2.1/src/conformal_oracle/conformal/bootstrap.py +51 -0
  17. conformal_oracle-0.2.1/src/conformal_oracle/conformal/rolling.py +86 -0
  18. conformal_oracle-0.2.1/src/conformal_oracle/conformal/static.py +35 -0
  19. conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/__init__.py +22 -0
  20. conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/acerbi_szekely.py +38 -0
  21. conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/basel.py +34 -0
  22. conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/christoffersen.py +71 -0
  23. conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/diebold_mariano.py +57 -0
  24. conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/kupiec.py +34 -0
  25. conformal_oracle-0.2.1/src/conformal_oracle/diagnostics/scoring.py +39 -0
  26. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/__init__.py +46 -0
  27. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/garch_normal.py +12 -0
  28. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/gjr_garch.py +80 -0
  29. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/hist_sim.py +27 -0
  30. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/__init__.py +53 -0
  31. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/_base.py +70 -0
  32. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/_cache.py +97 -0
  33. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/chronos.py +117 -0
  34. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/lag_llama.py +157 -0
  35. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/moirai.py +193 -0
  36. conformal_oracle-0.2.1/src/conformal_oracle/forecasters/tsfm/timesfm.py +119 -0
  37. conformal_oracle-0.2.1/src/conformal_oracle/panel/__init__.py +9 -0
  38. conformal_oracle-0.2.1/src/conformal_oracle/panel/audit.py +79 -0
  39. conformal_oracle-0.2.1/src/conformal_oracle/panel/cross_sectional.py +66 -0
  40. conformal_oracle-0.2.1/src/conformal_oracle/panel/diagnostic_regression.py +156 -0
  41. conformal_oracle-0.2.1/src/conformal_oracle/panel/diebold_mariano.py +91 -0
  42. conformal_oracle-0.2.1/src/conformal_oracle/panel/kupiec_panel.py +46 -0
  43. conformal_oracle-0.2.1/src/conformal_oracle/panel/latex.py +212 -0
  44. conformal_oracle-0.2.1/src/conformal_oracle/panel/result.py +383 -0
  45. conformal_oracle-0.2.1/src/conformal_oracle/panel/wildcluster_bootstrap.py +149 -0
  46. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/__init__.py +30 -0
  47. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/aci.py +74 -0
  48. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/base.py +74 -0
  49. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/evt_pot.py +108 -0
  50. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/fhs.py +61 -0
  51. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/gamlss_sst.py +49 -0
  52. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/gbm_qr.py +121 -0
  53. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/historical_quantile.py +32 -0
  54. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/quantile_regression.py +101 -0
  55. conformal_oracle-0.2.1/src/conformal_oracle/recalibration/scale_correction.py +36 -0
  56. conformal_oracle-0.2.1/src/conformal_oracle/reporting/__init__.py +13 -0
  57. conformal_oracle-0.2.1/src/conformal_oracle/reporting/latex.py +119 -0
  58. conformal_oracle-0.2.1/src/conformal_oracle/reporting/plotting.py +53 -0
  59. conformal_oracle-0.2.1/src/conformal_oracle.egg-info/PKG-INFO +56 -0
  60. conformal_oracle-0.2.1/src/conformal_oracle.egg-info/SOURCES.txt +61 -0
  61. conformal_oracle-0.2.1/src/conformal_oracle.egg-info/dependency_links.txt +1 -0
  62. conformal_oracle-0.2.1/src/conformal_oracle.egg-info/requires.txt +45 -0
  63. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]