quantlab.risk.bias.risk_model_bias_statistics#

quantlab.risk.bias.risk_model_bias_statistics(model, start, end, *, horizon=1, window=None, min_observations=None, random_portfolios=100, portfolio_size=500, seed=0)[source]#

Return the bias statistics of a factor risk model’s forecasts from its stores.

Every estimate row is a forecast made at its bar t of the next bar. Over a horizon of h bars, a forecast is taken every h estimate bars from start (the outcomes do not overlap), its outcome is the sum of the next h bars’ returns in the regression store, and its forecast volatility sqrt(h) times the one-bar one: the one-bar forecasts are of the variance per bar of a multi-bar return, which is what Newey-West adjusts, so a horizon longer than one bar tests that adjustment. A forecast without h regression bars after it, or an outcome missing one of its bars, has none. The forecasts, outcomes, exposures, market caps and estimation universe are read aligned by FactorRiskModel.forecast_window.

The eigenfactors of a bar are those of the covariance block of the factors covered_factors keeps, numbered from 1, the lowest eigenvalue; with fewer factors kept, the last ones are missing that bar. An eigenfactor has no outcome on a bar missing the return of a kept factor, nor when its eigenvalue is not positive (a pairwise estimate need not be positive semi-definite).

A random active portfolio holds, at each bar, the portfolio_size eligible stocks it ranks first, capitalization weighted, less every eligible stock capitalization weighted, held over the horizon. Each portfolio ranks the symbols of the estimate store at random once, from seed (our choice: a stock is kept while it stays eligible, and models sharing a regression store get the same portfolios). A stock is eligible at t when the model’s forecast at t covers it (FactorRiskModel.forecast: every exposure, a specific risk, no exposure to a factor without a covariance, the rule the portfolio’s covariance estimator uses), and it is in the estimation universe, with a market cap and a return over every bar of the horizon (our choice: the stores hold no return for a stock that stops trading, so it leaves the portfolios before). Its forecast variance is that forecast’s, h (x' F x + sum w^2 s^2) with w its active weights and x its factor exposures; its realized return is sum w (X f + u) summed over the horizon’s bars, the stocks’ excess returns (each bar’s X the exposures of the bar before it), a factor without a return on a bar contributing nothing.

Parameters:
modelFactorRiskModel

A model whose regression and estimate stores cover the range (the regression store to at least the bar after end for the last forecast to have an outcome).

start, endstr, datetime.date or pd.Timestamp

The forecast bars, both inclusive, inside the estimate store’s range.

horizonint, default 1

Bars per outcome. USE4 tests monthly (21 daily bars).

windowint, optional

Outcomes per rolling window (bias_statistics); by default about a year, round(252 / horizon) (USE4: 12 months).

min_observationsint, optional

Fewest outcomes in a rolling window (bias_statistics).

random_portfoliosint, default 100

Random active portfolios (USE4 Empirical Notes: 100).

portfolio_sizeint, default 500

Stocks per random portfolio (USE4 Empirical Notes: 500). With as many stocks as the eligible universe, a portfolio is the benchmark and has no active risk.

seedint, default 0

Seed of the random ranking.

Returns:
dict of str to xr.Dataset

"factor" on (timestamp, factor), "eigenfactor" on (timestamp, eigenfactor), "specific" on (timestamp, symbol) and "random" on (timestamp, portfolio), each as returned by bias_statistics, timestamp the forecast bar.

Raises:
ValueError

If horizon is below 1, or a store has no recorded range or does not cover the range (RiskStore.read).

Examples

With model the Use4RiskModel of examples/sharadar_us_equity/risk_model.py, its stores built over the Sharadar history:

>>> stats = risk_model_bias_statistics(model, "2007-07-13", "2026-10-02")
>>> sorted(stats)
['eigenfactor', 'factor', 'random', 'specific']
>>> round(float(stats["random"]["bias"].mean()), 3)
1.003
>>> round(float(stats["factor"]["bias"].sel(factor="style_beta")), 3)
1.119