跳转至

Risk validation

fincore separates risk estimation (EVT, GARCH) from risk validation (out-of-sample backtesting). The enhanced layer in fincore.risk.models and fincore.risk.backtesting records what was forecast, under which convention and horizon, and whether realized outcomes pass, fail, or are statistically inconclusive.

Forecasting VaR and ES

forecast_var and forecast_es return an immutable RiskEstimate:

import numpy as np
import pandas as pd

from fincore.risk.models import forecast_es, forecast_var

rng = np.random.default_rng(7)
returns = pd.Series(rng.normal(0.0, 0.02, 500))

var = forecast_var(returns, method="historical", confidence_level=0.99)
es = forecast_es(returns, method="historical", confidence_level=0.99)

print(var.estimate)   # negative under the losses_negative convention
print(var.sign_convention)

method can be historical (empirical quantile), evt (extreme-value theory) or garch (conditional volatility). The underlying legacy EVT/GARCH kernels live in fincore.risk.evt and fincore.risk.garch; their enhanced adapters preserve the losses_negative sign convention.

EVT tail-index convention

hill_estimator estimates the extreme-value tail index from positive tail magnitudes. With a positive threshold u, it returns the threshold Hill estimate mean(log(x / u)) over observations x > u, together with those selected magnitudes. Lower return tails are reflected into positive loss magnitudes first. It is a legacy estimator rather than an out-of-sample validated risk model, so use its threshold and tail choice as explicit model assumptions.

EVT threshold and Expected Shortfall semantics

For a GPD peaks-over-threshold (POT) estimate, alpha is an unconditional return-tail probability. An explicit threshold is accepted only when the fitted exceedance fraction covers it: alpha <= n_exceed / n_total. Otherwise the body of the return distribution is not modelled by the conditional GPD and the function raises ValueError rather than silently extrapolating below the threshold. When evt_var or evt_cvar receives no threshold, it keeps the usual 90th percentile of the selected tail if that covers alpha; otherwise it selects the highest empirical threshold that still does. gpd_fit on its own continues to use the 90th tail percentile because it fits parameters rather than answering a particular VaR/ES query.

GEV estimates are for the selected block-extreme distribution, so their alpha is a block-tail probability, not automatically a daily probability. GEV Expected Shortfall is the conditional tail mean beyond GEV VaR and is defined only for xi < 1; it is not an arbitrary constant increment from VaR. These legacy estimators are still not out-of-sample validated models.

Backtesting VaR

# -- minimal-backtest
from fincore.risk.backtesting import backtest_var

forecast = pd.Series([-0.02, -0.02, -0.02], index=pd.date_range("2024-01-01", periods=3, tz="UTC"))
realized = pd.Series([-0.01, -0.03, -0.02], index=forecast.index)

result = backtest_var(forecast, realized, confidence_level=0.99)

assert result.observations == 3
assert result.exceptions == 1
# -- minimal-backtest

backtest_var reports the exception count plus two standard statistics:

  • Unconditional coverage (Kupiec): does the exception rate match the chosen confidence level?
  • Independence (Christoffersen): are exceptions clustered?

Both are likelihood-ratio tests with an explicit null hypothesis. When the sample is too small to be meaningful (fewer than 3 observations, or fewer than 5 expected exceptions), the result is inconclusive rather than a silent pass.

Auditable walk-forward VaR (experimental)

For the enhanced walk-forward boundary, use RiskModelSpec and walk_forward_var. Each forecast uses only data strictly before its timestamp. walk_forward_var returns a WalkForwardVaRResult; pass that result to build_risk_validation_report to write every forecast, realised return, exception, refit parameters, timestamp index name/timezone, and both input/backtest digests to a deterministic JSON artifact. When a VaR backtest is available, the artifact also contains a traffic-light zone together with the observations and confidence level used to derive that reference field. Timezone metadata is emitted only as a portable IANA name or fixed UTC-offset token; a timezone that cannot be represented and replayed that way is rejected when the report is built. Timestamp index names must likewise be native JSON scalars so the backtest digest can be replayed exactly.

import numpy as np
import pandas as pd

from fincore.risk.diagnostics import walk_forward_var
from fincore.risk.report import build_risk_validation_report
from fincore.risk.specs import RiskModelSpec

returns = pd.Series(
    np.linspace(-0.02, 0.02, 60),
    index=pd.date_range("2024-01-02", periods=60, freq="B", tz="UTC"),
)
spec = RiskModelSpec(confidence_level=0.95, distribution="normal", window=40, refit_cadence=5)
walk_forward = walk_forward_var(returns, spec)
audit_report = build_risk_validation_report(walk_forward)
audit_report.write_json("risk-validation.json")

This surface is experimental. It currently validates one-step lower-tail VaR with Normal or finite-sample calibrated historical forecasts; it does not turn legacy EVT/GARCH estimates into an out-of-sample validated model. Its Basel traffic-light and backtest fields are reference aids, not regulatory approval or a compliance certification.

Backtesting ES (experimental)

Expected Shortfall backtesting is an open problem. The first fincore implementation uses a bootstrap calibration score (mean realised shortfall in the exception tail versus the forecast ES) and reports status experimental; it is not a compliance statement.

from fincore.risk.backtesting import backtest_es

result = backtest_es(forecast, realized, confidence_level=0.975)
print(result.status)   # "experimental"

Sign convention

All enhanced risk results use the losses_negative convention: a VaR/ES estimate is a negative number, and an exception occurs when the realized return falls strictly below the forecast threshold.