Skip to main content

factormodels

Factor construction, GLS premia, capital market assumptions and factor covariance estimation — multi-asset factor models in Python from tradable instruments and observed levels to point-in-time loadings, covariance and provider-implied factor premia.

Start here: minimal model definition and rolling covariance.

Its standalone output is a completed FactorModelSnapshot with labelled loadings, factor, residual and asset covariance, and provenance at a stated cutoff. Inspect a completed model, including units, dates, residual policy and table export, with the canonical offline example. Statistical estimation belongs to factorlasso, and return preparation and risk export to qis. OptimalPortfolios consumes the fitted covariance and is not required to complete a fit.

Install: python -m pip install factormodels · Import: factormodels · Status: Released

PyPI Python Docs Downloads CI License

Papers: Sepp, A., Ossa, I. and Kastenholz, M. (2026), Robust Optimization of Strategic and Tactical Asset Allocation for Multi-Asset Portfolios, The Journal of Portfolio Management 52(4) · Sepp, A., Hansen, E. and Kastenholz, M. (2027), Capital Market Assumptions and Strategic Asset Allocation Using Multi-Asset Tradable Factors · Estimator: Sepp, A. and Kastenholz, M. (2026), factorlasso. See Statement of need for what each paper contributes and Citation for the BibTeX entries.

Documentation: completed model · reference workflows · API reference


Why factormodels

Multi-asset allocation needs a factor model that serves two purposes at once. The covariance has to price the risk of funds, indices and private-market series that report at different frequencies, and the expected returns have to be stated in the same coordinates so that an optimiser can trade one against the other. Commercial risk systems (MSCI Barra, Axioma, Bloomberg's multi-asset-class models) give loadings on vendor factors that the investor can neither hold nor forecast. Academic factor returns are long-short portfolios without transaction costs or volatility scaling, so their loadings change meaning with the volatility regime of the sample. The MATF model defines each premium factor as a basket the investor can hold at a target volatility. The loading is then an exposure ratio, the premium is a return the investor can earn, and observed levels such as the VIX can be added for risk only.

The model is the linear factor decomposition

$$ r_i(t) = \alpha_i + \beta_i^{\top} f(t) + \varepsilon_i(t), \qquad \Sigma = B F B^{\top} + R, \qquad \mu^{excess} = B \lambda + w \odot \alpha, $$

where $f(t)$ holds the factor returns in their declared coordinates (log returns of a NAV, or differences of a level), $B$ is the asset-by-factor loading matrix estimated at the information cutoff, $F$ is the annual factor covariance, $R$ is the selected residual covariance, $\lambda$ are the factor premia and $w$ the admission weights on the residual alpha. Because the same estimated loading matrix enters the expected return and the covariance, alpha and beta are consistent by construction. This separates the approach from building-block expected returns and from Black-Litterman posteriors, where the return view and the risk model are estimated apart. factormodels is the implementation that produces that loading matrix, the covariance around it, and the premia implied by a set of forecasts. It is written in pandas and numpy on top of factorlasso and qis; the regression solver is CVXPY through factorlasso (default CLARABEL), so every loading is the solution of an explicit, auditable problem.

Key differentiators

1. Factors as declared histories. A Factor carries a return type: a basket or a tracked index uses LOG returns of its NAV, an observed level uses DIFFERENCE changes in its native units and is copied through construction unchanged. The declared types travel with the fit; a supplied factor covariance in the wrong coordinates is rejected. Callers retain the native units of every factor alongside their inputs. See factor construction.

2. Baskets held at a volatility target. A basket factor is a long side, an optional short side, and the volatility the combined basket is held at. The leverage $l_f(t) = \sigma^{*}_f / \hat\sigma_f(t)$ makes the regression loading read as an exposure ratio rather than an artefact of the volatility regime the factor was estimated in; a FactorFamily declares the target once, so loadings are comparable across regions. Construction runs through qis.backtest_model_portfolio, which holds units between rebalancing dates, and compute_beta_loading_signs turns the family rules into the sign-constraint matrix factorlasso consumes.

3. One annual covariance from assets at different cadences. Assets are supplied as a dictionary of return panels keyed by cadence. Each panel is regressed on the factor returns sampled at its own grid, with its own regression span, and the factor covariance is estimated once on the cadence you choose; $B F B^{\top} + R$ is assembled in annual units. The FactorCovarEstimator configuration is a frozen dataclass and every fit copies the LassoModel without fitted state. See factor covariance.

4. Point-in-time fits with provenance. fit_current(cutoff=...) truncates every history before estimation and fit_rolling(time_period=...) produces one snapshot per rebalancing date, each causal at its own date. A supplied DatedFactorCovariance carries an observation date and an availability date and is rejected when either lies after the cutoff. The test suite proves the contract by altering every observation after the cutoff and asserting an identical fit.

5. Residual covariance policies retained in the result. The residual block $R$ is selected at fit time and travels with the snapshot: orthogonal (diagonal), empirical (EWMA residual correlation on a common grid, scaled by current residual volatilities), exposure_cluster or residual_cluster (within-cluster mean correlation, zero between clusters). residual_corr_weight and residual_var_weight scale the off-diagonal and the whole block; snapshot.to_risk_model() exports the selected covariance as a qis.RiskModel.

6. GLS factor premia from provider forecasts. infer_factor_premia projects provider forecasts, net of cash and of the admitted alpha, onto the fitted loadings by generalised least squares with the residual covariance as the weight (Aitken 1935), returning premia, conditional Student-t intervals, the rank of the whitened design, the factor-mimicking portfolios and the asset-level reconciliation; compute_gls_uncertainty, compute_gls_loading_stability and compute_gls_provider_contrast propagate declared uncertainty. See GLS factor premia.

7. CMA audit and identification. compute_cma_diagnostics reconciles each asset's forecast into loaded premia, admitted alpha and residual, accounts for the implied squared Sharpe ratio under the fitted covariance, and reports an explicit status for every check; compute_cma_identification tests pairwise premium contrasts against declared equivalence margins; compute_conditional_gls_intervals gives pointwise intervals under the diagonal working model. reports.cma_diagnostics renders the audit with qis plotting primitives. See CMA diagnostics.

When to use it — and when not

Use it when:

  • Your universe mixes asset classes, vehicles and reporting cadences, and you need one annual covariance with stated units and a stated cutoff.
  • You define the factors yourself, as baskets, tracked indices or observed levels, and you want the loadings to be hedge ratios rather than regression artefacts.
  • You need expected returns and risk from one loading matrix, with factor premia inferred from provider forecasts and audited for the Sharpe ratio they imply.
  • You run the estimation on a schedule and need every snapshot to be reproducible from recorded inputs and provenance.

Reach for something else when:

  • You want the sparse multi-output estimator on its own. Use factorlasso.
  • You want a characteristic-based model for thousands of single stocks. The cross-sectional regression of a Barra-type model is a different estimator.
  • You need portfolio optimisation. Pass the fitted covariance to optimalportfolios.
  • You need non-linear or regime-switching factor dynamics.

Installation

python -m pip install factormodels

Requires Python ≥ 3.11, factorlasso ≥ 1.0.4.post1 (MIT), qis ≥ 5.38.1, and numpy / pandas / scipy / matplotlib. For a source checkout, use python -m pip install .. Dependency licences are listed in THIRD_PARTY_NOTICES.md and the dependency licensing notes.

Table of contents

  1. Why factormodels
  2. Installation
  3. Statement of need
  4. Overview
  5. Five-minute quickstart
  6. Examples
  7. Testing
  8. Changelog
  9. Feedback & contributing
  10. Citation
  11. Ecosystem
  12. License
  13. Disclaimer

Statement of need

A factor model for multi-asset allocation must be estimable on short, mixed-frequency histories of funds, indices and private-market series, must be causal at every date it is used on, and must state its expected returns in the coordinates of its covariance. factormodels implements the factor construction, estimation and premia inference of the papers below on top of the factorlasso estimator; the two kinds of factor it defines are multi-asset tradable factors (MATF) built from net asset values of tradable futures and quantitative investment strategies, for risk attribution and portfolio hedging using liquid instruments, and price- and level-based factors such as the VIX, government yields and economic data, for risk attribution and scenario analysis using macro data.

Paper Implemented analytics Where
Sepp, A., Ossa, I. and Kastenholz, M. (2026), Robust Optimization of Strategic and Tactical Asset Allocation for Multi-Asset Portfolios, The Journal of Portfolio Management, 52(4), 86–120 Tradable-factor construction as volatility-targeted baskets, sign-constrained loadings and the factor covariance that feed the ROSAA risk model factor construction, factor covariance
Sepp, A., Hansen, E. and Kastenholz, M. (2027), Capital Market Assumptions and Strategic Asset Allocation Using Multi-Asset Tradable Factors, SSRN 6785958 Factor premia inferred from provider forecasts by GLS on the fitted loadings, the arithmetic/log bridge annual_arithmetic_from_log, and the CMA audit of the Sharpe ratio the forecasts imply under the fitted covariance GLS factor premia, CMA diagnostics
Sepp, A. and Kastenholz, M. (2026), factorlasso: Sparse Multi-Output Regression with Cluster-Grouped Sign Constraints in Python, submitted to the Journal of Statistical Software The estimator behind every loading: sign constraints, priors and cluster penalties factorlasso

Overview

factormodels adds the financial model around the factorlasso estimator: factor definitions and NAV construction, the mixed-cadence FactorCovarEstimator, the completed FactorModelSnapshot, GLS premia and CMA diagnostics, and the export to a qis.RiskModel. The API reference lists every public object and the downstream integration page shows the handoff to optimalportfolios.

Methods

Method Formula Implementation Article · example
Volatility-targeted basket factors $l_f(t) = \sigma^{*}_f / \hat\sigma_f(t)$, NAV through qis.backtest_model_portfolio Factor, FactorFamily, VolTargetPolicy, assemble_factor_model, build_factor_panel, compute_beta_loading_signs construction · construct_factors.py
Point-in-time loadings and covariance $\Sigma = B F B^{\top} + R$, annual units, per-cadence regression spans FactorCovarEstimator.fit_current, .fit_rolling, DatedFactorCovariance, FactorModelSnapshot factor covariance · methods/factor_covariance_hcgl.py
Residual covariance policies $R$ orthogonal, empirical, exposure-cluster or residual-cluster; residual_corr_weight, residual_var_weight FactorCovarEstimator(residual_type=...), snapshot.residual_covariance, snapshot.to_risk_model() completed model · reference_workflows.py
GLS factor premia $\mu^{excess} = B \lambda + w \odot \alpha$ solved by GLS with $R$ as weight; bridge $A,(\bar r_{\log} + \tfrac{1}{2}\operatorname{Var}(r_{\log}))$ infer_factor_premia → GlsPremiaResult, annual_arithmetic_from_log, compute_gls_uncertainty, compute_gls_loading_stability, compute_gls_provider_contrast GLS factor premia · gls_cma_workflow.py, methods/gls_factor_premia.py
CMA audit and identification forecast = loaded premia + admitted alpha + residual; implied squared Sharpe under $\Sigma$ compute_cma_diagnostics → CmaDiagnosticResult, compute_cma_identification, compute_conditional_gls_intervals, reports.cma_diagnostics CMA diagnostics · reference_workflows.py

Analytics at a glance

Area What the package computes
Factor construction Long/short baskets held at a family volatility target, tracked indices, observed levels with LOG or DIFFERENCE return types; sign-constraint matrices from family rules
Loadings and covariance Per-cadence sign-constrained LASSO loadings, annual factor covariance on a chosen grid, asset covariance $B F B^{\top} + R$, rolling causal snapshots with provenance (cutoff, observation dates, spans)
Residual risk Orthogonal, empirical, exposure-cluster and residual-cluster residual covariances; diagonal reference decomposition on request
Expected returns GLS factor premia with Student-t intervals, factor-mimicking portfolios, asset-level reconciliation, forecast-box and cash-bound sensitivity ranges, propagated forecast and loading uncertainty
Audit CMA reconciliation and Sharpe accounting with explicit per-check status; premium precision and pairwise contrasts against equivalence margins
Data contract Supplied factor histories and log asset-return DataFrames by cadence; export of the selected covariance as qis.RiskModel

Five-minute quickstart

Minimal model: definition and rolling covariance

Start with minimal_model.py: define two factors, construct their NAVs, estimate three assets at four quarterly cutoffs, and retrieve an asset covariance by date. The self-contained code below uses public APIs available in factormodels 0.1.0. Inputs are synthetic monthly USD log returns and total-return NAVs; each factor holds one source without volatility targeting or cash/FX adjustment.

import numpy as np
import pandas as pd
import qis
from factorlasso import LassoModel, LassoModelType

from factormodels import (
    Factor, FactorCovarEstimator, FactorFamily, FactorModelSpec,
    RiskFactors, VolTargetPolicy, build_factor_panel,
)


class MinimalFactors(RiskFactors):
    """Ordered factor identifiers for this two-factor model."""

    EQUITY = "Equity"
    RATES = "Rates"

# 1. Define each factor as a single-source basket, without volatility targeting.
spec = FactorModelSpec(
    name="Minimal two-factor model", factors=MinimalFactors, file_name="minimal_model",
    baskets={
        member: Factor(
            name=member.value, family=FactorFamily(member.value, vol_target=None),
            long_side_tickers=(f"{member.value} source",), long_side_weights=(1.,),
            vol_target_policy=VolTargetPolicy.NONE,
        )
        for member in MinimalFactors
    },
)

# Synthetic monthly inputs; replace these two panels with your own histories.
dates = pd.date_range("2015-12-31", "2024-12-31", freq="ME")
rng = np.random.default_rng(42)
factor_log_returns = rng.normal([.005, .002], [.04, .02], (len(dates) - 1, 2))
source_prices = pd.DataFrame(
    100. * np.exp(np.vstack([np.zeros(2), factor_log_returns.cumsum(axis=0)])),
    index=dates, columns=["Equity source", "Rates source"],
)
# These generating loadings are only for simulation; the estimator fits its own.
loadings = np.array([[.9, .1], [.1, .8], [.5, .4]])
asset_returns = pd.DataFrame(
    factor_log_returns @ loadings.T + rng.normal(0., .01, (len(dates) - 1, 3)),
    index=dates[1:], columns=["Equity asset", "Bond asset", "Balanced asset"],
)
factor_prices = build_factor_panel(
    spec, source_prices, returns_freq="ME", rebalancing_freq="ME",
)

# 2. Estimate loadings and covariance at each quarter-end, using monthly inputs.
estimator = FactorCovarEstimator(
    lasso_model=LassoModel(
        model_type=LassoModelType.LASSO, span=36, warmup_period=6,
        reg_lambda=1e-5, solver="CLARABEL",
    ),
    factor_returns_freq="ME", factor_covar_span=36,
    rebalancing_freq="QE", residual_type="orthogonal",
)
rolling = estimator.fit_rolling(
    risk_factor_prices=factor_prices, asset_returns_dict={"ME": asset_returns},
    time_period=qis.TimePeriod("2024-03-31", "2024-12-31"),
    assets=list(asset_returns.columns),
)

# 3. Retrieve the full asset covariance by an exact fitted date, or the latest date.
covariances = rolling.covariances  # dict[pd.Timestamp, pd.DataFrame]
as_of = pd.Timestamp("2024-06-30")
covariance = covariances[as_of]
latest_date = max(covariances)
latest_covariance = covariances[latest_date]
print(spec.factor_names())
print([str(date.date()) for date in covariances])
print(f"Selected {as_of.date()}: {covariance.shape}")
print(f"Latest {latest_date.date()}: {latest_covariance.shape}")
['Equity', 'Rates']
['2024-03-31', '2024-06-30', '2024-09-30', '2024-12-31']
Selected 2024-06-30: (3, 3)
Latest 2024-12-31: (3, 3)

Each retrieved DataFrame is the full annualised asset log-return covariance in the supplied asset order, including factor risk and the selected diagonal residual block. The 36-observation EWMA spans weight expanding histories through each cutoff; they are not fixed 36-month windows. Retrieval uses an exact fitted date.

From a source checkout, run the full example to print the covariance matrices:

python -m examples.minimal_model

Fit supplied DataFrames directly

All inputs are synthetic. The local synthetic_prices() function below supplies two DataFrames with 121 monthly USD NAV observations for two factors and four assets, with a fixed seed. Prepare returns and configure the estimator explicitly. The repository workflow in examples/reference_workflows.py uses the same input generator.

import qis
from factorlasso import LassoModel, LassoModelType
from factormodels import FactorCovarEstimator


def synthetic_prices() -> tuple[pd.DataFrame, pd.DataFrame]:
    """Return independent synthetic factor NAVs and asset total-return prices.

    The fixed seed produces 121 monthly USD observations from January 2014 through
    January 2024 for two factors and four assets. These are illustrative prices,
    not market data. Callers prepare log returns and configure their estimator.
    No acquisition, fitting, rendering or persistence occurs.

    Returns:
        tuple[pd.DataFrame, pd.DataFrame]: Factor NAVs, then asset prices.
    """
    rng = np.random.default_rng(6102026)
    dates = pd.date_range("2014-01-31", periods=121, freq="ME")
    x = rng.normal([.004, .001], [.035, .018], (120, 2))
    b = np.array([[.9, .1], [.1, .9], [.6, .4], [.4, -.2]])
    y = x @ b.T + rng.normal(0., .013, (120, 4))
    levels = pd.DataFrame(100 * np.exp(np.vstack([np.zeros(2), x.cumsum(axis=0)])),
                          index=dates, columns=["Equity", "Rates"])
    prices = pd.DataFrame(100 * np.exp(np.vstack([np.zeros(4), y.cumsum(axis=0)])),
                          index=dates, columns=["Asset A", "Asset B", "Asset C", "Asset D"])
    return levels, prices

factor_navs, asset_prices = synthetic_prices()
asset_returns = qis.to_returns(asset_prices, freq="ME", is_log_returns=True, ffill_nans=False)
current_estimator = FactorCovarEstimator(
    lasso_model=LassoModel(
        model_type=LassoModelType.LASSO, span=36, warmup_period=6,
        reg_lambda=1e-5, demean=True, solver="CLARABEL", loss_normalization="sample",
    ),
    factor_returns_freq="ME", factor_covar_span=36, residual_type="orthogonal",
)
model = current_estimator.fit_current(
    risk_factor_prices=factor_navs, asset_returns_dict={"ME": asset_returns},
    assets=list(asset_returns.columns), cutoff=factor_navs.index[-1],
)

print(model.cutoff.date())
print(model.loadings.round(3))
print(model.factor_covariance.round(6))
print(model.asset_covariance.round(6))
2024-01-31
         Equity  Rates
Asset A   0.926  0.169
Asset B   0.082  0.829
Asset C   0.526  0.390
Asset D   0.343 -0.000
          Equity     Rates
Equity  0.014735  0.000077
Rates   0.000077  0.004587
          Asset A   Asset B   Asset C   Asset D
Asset A  0.014411  0.001822  0.007512  0.004682
Asset B  0.001822  0.005197  0.002156  0.000435
Asset C  0.007512  0.002156  0.006363  0.002665
Asset D  0.004682  0.000435  0.002665  0.003494

Returns are decimal log returns, covariances are annualised with 12 periods a year, and the cutoff is the last observation. The FactorModelSnapshot also carries residual_covariance, provenance and the FactorLasso fit in model.data. model.to_risk_model() exports the selected covariance as a qis.RiskModel without refitting. The loadings are LASSO estimates over a 36-month span, so a small planted loading can shrink to zero.

Examples

Highlighted examples

From the fitted model to factor premia. Expected returns are annual arithmetic excess returns and the risk model is built from annualised log-return moments. The paper bridges the two with the second-order rule $A,(\bar r_{\log} + \tfrac{1}{2}\operatorname{Var}(r_{\log}))$, exported as annual_arithmetic_from_log. The snippet continues from the quickstart model, assumes a 4% premium on each factor, credits the historical residual alpha, tilts the asset forecasts, and recovers the premia by GLS.

import numpy as np
import pandas as pd
from factormodels import (
    CmaDiagnosticInputs, annual_arithmetic_from_log, compute_cma_diagnostics,
    infer_factor_premia,
)

B, F = model.loadings, model.factor_covariance
R, S = model.residual_covariance, model.asset_covariance

premia_assumed = pd.Series(0.04, index=B.columns)              # annual arithmetic
alpha_log = model.data.estimate_alpha(alpha_span=36)            # annualised log residual mean
asset_log_mean = B @ (premia_assumed - 0.5 * pd.Series(np.diag(F), index=B.columns)) + alpha_log
asset_mean = annual_arithmetic_from_log(asset_log_mean, pd.Series(np.diag(S), index=B.index))
alpha = asset_mean - B @ premia_assumed
forecast = asset_mean + pd.Series(np.linspace(-0.0025, 0.0025, len(B)), index=B.index)

gls = infer_factor_premia(B, R, forecast, cash_rate=0.0, alpha=alpha, alpha_weight=1.0)
audit = compute_cma_diagnostics(CmaDiagnosticInputs(
    betas=B, residual_variances=pd.Series(np.diag(R), index=B.index),
    factor_covariance=F, production_covariance=S,
    excess_cma=forecast, factor_premia=gls.premia, admitted_alpha=alpha,
    channels=pd.DataFrame({"admitted_alpha": alpha, "forecast_residual": gls.assets.residual}),
    provenance={"risk_basis": "annualised_log_return_covariance"}))

print(gls.factors[["estimate", "lower", "upper"]].round(4))
print(audit.summary["accounting_status"])
        estimate   lower   upper
Equity    0.0393  0.0288  0.0497
Rates     0.0395  0.0261  0.0530
pass

gls is a GlsPremiaResult with premia, conditional Student-t intervals, the asset-level reconciliation and the factor-mimicking portfolios. audit is a CmaDiagnosticResult with the Sharpe accounting of the forecasts under the fitted covariance. The canonical example records the same assumptions in provenance and exports the tables and a report.

From construction to a dated covariance. The four blocks below continue in the same session and are executed in order by tests/test_readme.py, which also asserts the strict-cutoff contract on their output. Factors are declared histories with a return type; a basket is held at its family's volatility target; assets at different cadences are regressed on their own grid and assembled into one annual covariance; and a supplied factor covariance carries observation and availability dates that the fit checks against its cutoff.

from qis import ReturnTypes
from factormodels import Factor, FactorFamily, VolTargetPolicy

equity = Factor(name="Equity", family=FactorFamily("Equity", vol_target=0.10, long_only_sign=1.),
                long_side_tickers=("ES", "NQ"), long_side_weights=(0.5, 0.5))

vix = Factor(name="VIX", family=FactorFamily("Volatility", vol_target=None),
             long_side_tickers=("VIX",), long_side_weights=(1.,),
             vol_target_policy=VolTargetPolicy.NONE, is_observed_index=True,
             return_type=ReturnTypes.DIFFERENCE)
from factormodels import RiskFactors, assemble_factor_model, build_factor_panel

# Deterministic teaching prices; these are not market observations.
dates = pd.bdate_range("2010-01-01", "2022-09-30")
rng = np.random.default_rng(42)
prices = pd.DataFrame(
    100.0 * np.exp(np.cumsum(rng.normal(0.0001, [0.010, 0.012, 0.005], (len(dates), 3)), axis=0)),
    index=dates, columns=["ES", "NQ", "TY"],
)

class Factors(RiskFactors):
    EQUITY = "Equity"
    RATES = "Rates"

rates = Factor(name="Rates", family=FactorFamily("Rates", vol_target=0.05),
               long_side_tickers=("TY",), long_side_weights=(1.,))
spec = assemble_factor_model(name="Teaching model", factors=Factors, file_name="teaching",
                             catalogue={"Equity": equity, "Rates": rates})
panel = build_factor_panel(spec, prices, rebalancing_freq="QE", returns_freq="W-WED")

Construction runs through qis.backtest_model_portfolio, which holds units between rebalancing dates; the result is a NAV, not a weighted average of returns. Volatility targets are annualised decimals and the EWMA span counts weekly periods.

from factorlasso import LassoModel, LassoModelType
from factormodels import FactorCovarEstimator

import qis

factor_prices = panel
# Separate synthetic assets at monthly and quarterly reporting cadences.
monthly_returns = qis.to_returns(
    prices[["ES", "NQ"]].rename(columns={"ES": "Asset A", "NQ": "Asset B"}),
    freq="ME", is_log_returns=True, drop_first=True,
)
quarterly_returns = qis.to_returns(
    prices[["TY"]].rename(columns={"TY": "Asset C"}),
    freq="QE", is_log_returns=True, drop_first=True,
)
returns_by_cadence = {"ME": monthly_returns, "QE": quarterly_returns}

estimator = FactorCovarEstimator(
    lasso_model=LassoModel(model_type=LassoModelType.LASSO, reg_lambda=1e-5,
                           span_freq_dict={"ME": 36, "QE": 12}, warmup_period=6,
                           solver="CLARABEL"),
    factor_returns_freq="W-WED",   # factor covariance grid
    factor_covar_span=52,          # EWMA span in weeks
    residual_type="orthogonal",
)
# factor_prices: daily factor NAVs such as `panel` above; the asset panels hold log returns
snapshot = estimator.fit_current(factor_prices, {"ME": monthly_returns, "QE": quarterly_returns},
                                 cutoff="2021-12-31")
import qis
from factormodels import DatedFactorCovariance

rolling = estimator.fit_rolling(factor_prices, returns_by_cadence,
                                time_period=qis.TimePeriod("2018-12-31", "2021-12-31"),
                                rebalancing_freq="QE")
rolling.snapshots[pd.Timestamp("2021-12-31")].provenance   # cutoff, observation dates, spans

# Estimate the supplied matrix only from information available on its observation date.
matrix = estimator.fit_current(
    factor_prices, returns_by_cadence, cutoff="2021-11-30",
).factor_covariance

supplied = DatedFactorCovariance(matrix, observation_date="2021-11-30", available_date="2021-12-15")
snapshot = estimator.fit_current(factor_prices, returns_by_cadence, cutoff="2021-12-31",
                                 factor_covariance=supplied)

fit_current(cutoff=...) truncates every history before estimation and fit_rolling produces one snapshot per rebalancing date, each causal at its own date; a supplied covariance whose observation or availability date lies after the cutoff is rejected.

Runnable examples

Runnable examples in examples/, all offline:

  • minimal_model.py: start here for factor definition, rolling estimation and covariance retrieval by date, using synthetic monthly inputs.
  • reference_workflows.py: synthetic DataFrames, caller-supplied price CSVs and public-ETF proxy construction, with the fitted model connected to GLS premia and the CMA audit, and table export on request.
  • construct_factors.py: factor definitions, NAV construction and loading-sign constraints from synthetic daily prices.
  • gls_cma_workflow.py: a self-contained arithmetic calibration of the GLS inference with an independent QR reference.
  • methods/factor_covariance_hcgl.py: mixed-frequency estimation with explicit EWMA reference sums and strict-cutoff comparisons.
  • methods/gls_factor_premia.py: offline synthetic prices → LASSO loadings and alpha → provider-implied GLS premia, teaching the two stages on monthly excess-return NAVs.
  • downstream_handoff.py: fitted model to an optimalportfolios solve to qis tracking error. Requires optimalportfolios.

Method documentation: completed model, factor construction, factor covariance, GLS factor premia, CMA diagnostics, estimation from CSVs, downstream integration and the API reference.

Testing

uv sync --locked --group test --group lint
uv run --no-sync pytest
uv run --no-sync ruff check src tests tools

The suite covers construction identities against independent sums, point-in-time contracts with deliberate counterexamples, residual-policy assembly, direct DataFrame and CSV workflows, GLS and audit identities against independent QR and Cholesky references, and the documented examples. The optimalportfolios import ban is enforced by ruff in CI.

Changelog

Release history is maintained in CHANGELOG.md.

Feedback & contributing

  • Bug: open an issue with the version, Python/platform, a minimal reproducer, and expected versus actual output.
  • Feature: open an issue and describe the modelling goal, the current workaround, and the smallest useful API. In particular: which construction rule, residual policy or diagnostic is missing?
  • Question or methodology: open an issue and identify the method, paper section, or convention involved.
  • Contribution: follow CONTRIBUTING.md.

Citation

A machine-readable citation is available in CITATION.cff.

If you use factormodels in academic work, please cite the software, the papers that define the model and its applications, and the estimator it builds on:

@software{factormodels,
  author  = {Sepp, Artur},
  title   = {factormodels: Multi-Asset Factor Models in {Python}},
  year    = {2026},
  version = {0.2.0},
  url     = {https://github.com/ArturSepp/FactorModels},
}

@article{SeppOssaKastenholz2026,
  author  = {Sepp, Artur and Ossa, Ivan and Kastenholz, Mika},
  title   = {Robust Optimization of Strategic and Tactical Asset Allocation
             for Multi-Asset Portfolios},
  journal = {The Journal of Portfolio Management},
  year    = {2026},
  volume  = {52},
  number  = {4},
  pages   = {86--120},
}

@article{SeppHansenKastenholz2027MATF,
  author  = {Sepp, Artur and Hansen, Emilie and Kastenholz, Mika},
  title   = {Capital Market Assumptions and Strategic Asset Allocation Using
             Multi-Asset Tradable Factors},
  journal = {The Journal of Portfolio Management},
  year    = {2027},
  note    = {Forthcoming. SSRN 6785958, https://ssrn.com/abstract=6785958}
}

@article{SeppKastenholz2026factorlasso,
  author  = {Sepp, Artur and Kastenholz, Mika},
  title   = {{factorlasso}: Sparse Multi-Output Regression with
             Cluster-Grouped Sign Constraints in {Python}},
  journal = {Journal of Statistical Software},
  year    = {2026},
  note    = {Submitted.}
}

Ecosystem

This package is part of an open-source Python stack for quantitative finance. The ArturSepp profile is the canonical catalogue:

Package Purpose
qis Performance analytics, backtesting, and factsheet reporting
optimalportfolios Portfolio construction and rolling backtesting
factorlasso Sparse factor-model estimation
factormodels (this package) Factor definitions, histories, fitted loadings and covariance
bbg-fetch Bloomberg data in pandas DataFrames
option-chain-analytics Point-in-time option-chain data and queries
vanilla-option-pricers Numba-vectorised BSM and Bachelier pricing
stochvolmodels Stochastic-volatility pricing and calibration
trendfollowing Closed-form trend-following analytics
privateassets Multi-factor PME for private assets
goal-based-allocation Goal-based allocation under regime-switching jump-diffusions

factormodels depends on factorlasso and qis. It does not import optimalportfolios, and the import ban is enforced by ruff in CI.

License

MIT, see LICENSE. Dependencies and bundled libraries retain their own licences, see THIRD_PARTY_NOTICES.md and the dependency licensing notes. factorlasso is MIT from 1.0.4.post1, which is the declared floor.

Disclaimer

factormodels is distributed FREE & WITHOUT ANY WARRANTY under the MIT License. See LICENSE in the release for details. Use the routes in Feedback & contributing for bugs, feature requests and methodology questions.

Metadata

Release files for factormodels 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for factormodels 0.2.0
File Size Uploaded Provenance
factormodels-0.2.0.tar.gz 535.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for factormodels 0.2.0
File Interpreter ABI Platform
factormodels-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 797.2 kB

Release files / factormodels-0.2.0.tar.gz

Download URL factormodels-0.2.0.tar.gz
Size 535.1 kB
Tags Source
SHA-256 checksum
How to use checksums
cd8e7eca6ddd7aad79a0f05cd51273d113016681d8490c27714a22c9cda4a02d
BLAKE2b-256 checksum
How to use checksums
28b716dc2741f168406a09ce6338381578cdf60563d8e24e350a20b7f480bb95
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / factormodels-0.2.0-py3-none-any.whl

Download URL factormodels-0.2.0-py3-none-any.whl
Size 262.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a5864015d1583894989276d0dc7f7e98a37186d5e15bb1bc126b7d9bffea41e3
BLAKE2b-256 checksum
How to use checksums
892e603c7fd0e067926e95f5eb63819f91ebd70748ac68b69279260bf6dd8e07
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for factormodels 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for factormodels-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page