Factor Black-Litterman Prior

PortfolioOptimisers.FactorBlackLittermanPriorType
struct FactorBlackLittermanPrior{__T_pe, __T_f_mp, __T_mp, __T_re, __T_ve, __T_views, __T_sets, __T_views_conf, __T_w, __T_rf, __T_l, __T_tau, __T_rsd, __T_cache} <: AbstractLowOrderPriorEstimator_F

Factor Black-Litterman prior estimator for asset returns.

FactorBlackLittermanPrior is a low order prior estimator that computes the mean and covariance of asset returns using a factor-based Black-Litterman model. It combines an asset prior estimator, matrix post-processing for factors and assets, regression and variance estimators, user or algorithmic views, asset sets, view confidences, weights, risk-free rate, leverage, blending parameter tau, and a residual variance flag. This estimator supports both direct and constraint-based views, flexible confidence specification, and matrix processing, and incorporates factor regression and residual adjustment for posterior inference.

Fields

  • pe: Prior estimator.
  • f_mp: Factor matrix processing estimator.
  • mp: Matrix processing estimator.
  • re: Regression estimator.
  • ve: Variance estimator.
  • views: Views estimator or result.
  • sets: Universe sets. The time-series factor axis, sets.dict[sets.tfkey], is what this estimator reads: it is the universe the views are written in, and it must name the columns of F in order. The asset axis is required by UniverseSets and is what a view slices — the factor entries come back from port_opt_view untouched.
  • views_conf: Views confidence estimator or result.
  • w: Optional equilibrium weights vector assets × 1. If nothing, equal weights are used.
  • rf: Risk-free rate. The Black-Litterman update blends the prior mean against the view returns, so it runs on the total-return scale those are written on. A mean taken from a wrapped prior estimator is on that scale already; an equilibrium mean is a bare risk premium, and the rate converts it before the update. A member with no equilibrium branch has nothing to convert and adds the rate to the posterior asset expected returns instead. It is added exactly once either way, and the wrapped prior estimators are left alone, so a risk-free rate one of them applied internally stays where it is.
  • l: Risk aversion parameter.
  • tau: Blending parameter controlling the weight given to the prior relative to the views.
  • rsd: Whether to include residual variance in the posterior covariance.
  • cache: Optional partial-fit state. It is nothing until partial_fit! writes one, and the estimator's read-out verb reads it when the caller gives no data matrix. Each propagation channel does one thing with it: factory carries it unchanged, because a factory call resolves configuration rather than the sample; port_opt_view slices it to the selected assets by index copy, so the viewed estimator answers over those assets alone; and obs_weights_view drops it, because no slice of a state exists on the observation axis. A family whose state has no exact asset slice drops it on both axes and names the reason.

Constructors

FactorBlackLittermanPrior(;    pe::AbstractLowOrderPriorEstimator_A_AF = EmpiricalPrior(),    f_mp::AbstractMatrixProcessingEstimator = MatrixProcessing(),    mp::AbstractMatrixProcessingEstimator = MatrixProcessing(),    re::AbstractTimeSeriesRegressionEstimator = StepwiseRegression(),    ve::AbstractVarianceEstimator = SimpleVariance(),    views::Lc_BLV,    sets::Option{<:UniverseSets} = nothing,    views_conf::Option{<:Num_VecNum} = nothing,    w::Option{<:VecNum} = nothing,    rf::Number = 0.0,    l::Option{<:Number} = nothing,    tau::Option{<:Number} = nothing,    rsd::Bool = true,    cache::Option{<:AbstractPartialFitState} = nothing) -> FactorBlackLittermanPrior

Keywords correspond to the struct's fields.

Composition: what this estimator forwards

This estimator lifts a factor-axis prior onto the asset axis, reconstructing X as F * transpose(M) .+ transpose(b), so it builds its carrier directly rather than forwarding one along its own axis; each field still follows the pattern of a posterior field replacing its prior counterpart while a structural field forwards whole. It is the member of the Black-Litterman family whose factor block is modified rather than passed through — the views land on the factor distribution, and the assets are its projection.

  • The factor block fpr is the posterior factor distribution, processed by f_mp, with chol dropped because the posterior covariance supersedes the one it factorises. Its w and that weighting's diagnostics forward untouched.
  • mu and sigma are that block projected through the loadings, so the returned carrier is internally consistent: mu == rr.M * fpr.mu + rr.b holds by construction, whatever rf is, because the rate is inside fpr.mu where it is present at all. sigma optionally gains a residual correction when rsd is true.
  • w is the factor prior's, and is over the right axis: this estimator wraps only a factor prior, and posterior_X has exactly F's rows, so it is the only weighting in existence.

Its siblings differ: BayesianBlackLittermanPrior also satisfies the identity exactly, while BlackLittermanPrior and AugmentedBlackLittermanPrior do not — see their warnings.

The views are written on the factor axis

views resolves against sets.dict[sets.tfkey] — the axis UniverseSets declares for factors — because that is the distribution they update. The asset axis is still required (every UniverseSets carries one) and is what port_opt_view slices; the factor entries come back untouched, which is why this field is @vprop rather than exempted by hand.

sets.dict[sets.tfkey] must name the columns of F in order; factor_universe checks it, and reports the factor axis rather than the asset one when it is missing or the wrong length.

Validation

w is not validated here. Its length is a property of the returns matrix, which the constructor never sees, so a wrong length surfaces at prior as a DimensionMismatch out of equilibrium_mu and only when l is set. The constructor also accepts an empty w, where the sibling AugmentedBlackLittermanPrior refuses one.

Propagated parameters

When factory is called on this type, the following @fprop-tagged fields are automatically propagated:

  • pe: Recursively updated via factory.
  • re: Recursively updated via factory.
  • ve: Recursively updated via factory.

View parameters

When port_opt_view is called on this type, the following @vprop-tagged fields are automatically subset to the selected indices:

Examples

julia> FactorBlackLittermanPrior(;                                 sets = UniverseSets(;                                                     dict = Dict("nx" => ["A", "B", "C"],                                                                 "nf" => ["F1", "F2"])),                                 views = LinearConstraintEstimator(;                                                                   val = ["F1 == 0.03",                                                                          "F2 == 0.04"]))FactorBlackLittermanPrior          pe ┼ EmpiricalPrior             │           ce ┼ PortfolioOptimisersCovariance             │              │   ce ┼ Covariance             │              │      │    me ┼ SimpleExpectedReturns             │              │      │       │   w ┴ nothing             │              │      │    ce ┼ GeneralCovariance             │              │      │       │   ce ┼ StatsBase.SimpleCovariance: StatsBase.SimpleCovariance(true)             │              │      │       │    w ┴ nothing             │              │      │   alg ┼ FullMoment()             │              │      │     w ┴ nothing             │              │   mp ┼ MatrixProcessing             │              │      │     pdm ┼ Posdef             │              │      │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton             │              │      │         │   kwargs ┴ @NamedTuple{}: NamedTuple()             │              │      │      dn ┼ nothing             │              │      │      dt ┼ nothing             │              │      │     alg ┼ nothing             │              │      │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)             │           me ┼ SimpleExpectedReturns             │              │   w ┴ nothing             │      horizon ┼ nothing             │   fill_limit ┴ nothing        f_mp ┼ MatrixProcessing             │     pdm ┼ Posdef             │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton             │         │   kwargs ┴ @NamedTuple{}: NamedTuple()             │      dn ┼ nothing             │      dt ┼ nothing             │     alg ┼ nothing             │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)          mp ┼ MatrixProcessing             │     pdm ┼ Posdef             │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton             │         │   kwargs ┴ @NamedTuple{}: NamedTuple()             │      dn ┼ nothing             │      dt ┼ nothing             │     alg ┼ nothing             │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)          re ┼ StepwiseRegression             │   crit ┼ PValue             │        │   t ┴ Float64: 0.05             │    alg ┼ ForwardSelection()             │    tgt ┼ LinearModel             │        │   kwargs ┴ @NamedTuple{}: NamedTuple()          ve ┼ SimpleVariance             │          me ┼ SimpleExpectedReturns             │             │   w ┴ nothing             │           w ┼ nothing             │   corrected ┴ Bool: true       views ┼ LinearConstraintEstimator             │   val ┼ Vector{String}: ["F1 == 0.03", "F2 == 0.04"]             │   key ┴ nothing        sets ┼ UniverseSets             │     xkey ┼ String: "nx"             │    uxkey ┼ String: "ux"             │    tfkey ┼ String: "nf"             │   utfkey ┼ String: "uf"             │    cfkey ┼ String: "ncf"             │   ucfkey ┼ String: "ucf"             │    nikey ┼ String: "ni"             │     dict ┴ Dict{String, Vector{String}}: Dict("nf" => ["F1", "F2"], "nx" => ["A", "B", "C"])  views_conf ┼ nothing           w ┼ nothing          rf ┼ Float64: 0.0           l ┼ nothing         tau ┼ nothing         rsd ┴ Bool: true

The incremental fit

This prior has no exact incremental recursion, so it takes the online step by refitting from a sample buffer: Online seeds cache, partial_fit! appends each observation to it verbatim, and the one-argument prior runs this estimator's own batch verb over the rows the buffer kept. The answer is therefore exactly a batch fit over those rows, and a max_history on the wrapper windows the whole fit.

cache travels the three propagation channels as every partial-fit state does: factory carries it unchanged, port_opt_view slices it to the selected assets, and obs_weights_view drops it, because no slice of a state exists on the observation axis. It is not rendered, because a running buffer is not the configuration a reader looks the type up for.

Related

References

  • [29] F. Black and R. Litterman. Global portfolio optimization. Financial Analysts Journal 48, 28–43 (1992).
  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 5.1, Equations 5.13 to 5.15, over the factor axis, and Section 4.1, Equations 4.2 and 4.3, for the lift onto the assets.
source
PortfolioOptimisers.priorFunction
prior(pe::FactorBlackLittermanPrior, X::MatNum, F::MatNum,
      pnl::Option{<:AssetPanel} = nothing; dims::Int = 1,
      strict::Bool = false, kwargs...)

Compute factor Black-Litterman prior moments for asset returns.

prior estimates the mean and covariance of asset returns using the factor-based Black-Litterman model, combining an asset prior estimator, matrix post-processing for factors and assets, regression and variance estimators, user or algorithmic views, asset sets, view confidences, weights, risk-free rate, leverage, blending parameter tau, and a residual variance flag. This method supports both direct and constraint-based views, flexible confidence specification, and matrix processing, and incorporates factor regression and residual adjustment for posterior inference.

When pe.tau is nothing the blending parameter is 1/T, where T is the number of observations of the oriented X. The mean handed to the update is a total return over the factors, which is the scale the view returns in Q are written on, and the factor block is reported on that scale. pe.rf reaches the update on the pe.l branch alone, because that is the only branch whose prior mean is a risk premium rather than a total return. The closed form below states the shift it leaves there. Where pe.l is nothing nothing reads the field, so it does not reach the answer.

Mathematical definition

Black-Litterman views are applied directly to the factor space, updating factor moments $(\boldsymbol{\Pi}_f, \mathbf{\Sigma}_f)$ via the standard BL equations, then asset posteriors are reconstructed through the loadings matrix:

\[\begin{align} \hat{\boldsymbol{\mu}} &= \mathbf{M} \hat{\boldsymbol{\mu}}_{f,BL} + \boldsymbol{b}\,. \end{align}\]

\[\begin{align} \hat{\mathbf{\Sigma}} &= \mathbf{M} \hat{\mathbf{\Sigma}}_{f,BL} \mathbf{M}^\intercal + \mathbf{\Sigma}_\varepsilon\,. \end{align}\]

Where:

  • $N$, $K$, $K_v$, $T$: The number of assets, of factors, of views, and of observations.
  • $\hat{\boldsymbol{\mu}}$: $N \times 1$ posterior asset mean vector, pr.mu.
  • $\hat{\mathbf{\Sigma}}$: $N \times N$ posterior asset covariance matrix, pr.sigma.
  • $\boldsymbol{\Pi}_f$, $\mathbf{\Sigma}_f$: $K \times 1$ and $K \times K$ prior factor moments, from pe.pe fit on F.
  • $\hat{\boldsymbol{\mu}}_{f,BL}$: $K \times 1$ Black-Litterman posterior factor mean, pr.fpr.mu.
  • $\hat{\mathbf{\Sigma}}_{f,BL}$: $K \times K$ Black-Litterman posterior factor covariance, pr.fpr.sigma.
  • $\mathbf{M}$: $N \times K$ factor loadings matrix, pr.rr.M.
  • $\boldsymbol{b}$: $N \times 1$ regression intercept vector, pr.rr.b.
  • $\mathbf{P}$: $K_v \times K$ views matrix, over the factor axis.
  • $\mathbf{\Omega}$: $K_v \times K_v$ view uncertainty matrix, from calc_omega and bl_preroll.
  • $\mathbf{\Sigma}_\varepsilon$: $N \times N$ diagonal residual variance matrix, zero when rsd = false.
  • $\tau$: Scaling parameter, 1/T by default.
  • $r_{f}$: Risk-free rate, added once by apply_rf to the equilibrium factor mean. It is therefore inside $\hat{\boldsymbol{\mu}}_{f,BL}$ where pe.l is set, and absent where pe.l is nothing.

The factor moments are the ordinary Black-Litterman posterior of vanilla_posteriors, computed over the factor axis: $\mathbf{P}$ has $K$ columns, and $\mathbf{\Sigma}_f$ is the factor prior's covariance. That is literal, not an analogy: running vanilla_posteriors by hand on the factor prior and the bl_preroll triple reproduces pr.fpr.mu and pr.fpr.sigma, and the two lifted forms above reproduce pr.mu and pr.sigma on both rsd branches. The chol this lift returns still factorises the covariance it is returned with, so the residual block reaches the factor and the covariance together.

The rate reaches the answer through the factors, and only on one branch. Where pe.l is set the equilibrium mean is a risk premium, so apply_rf converts it to a total factor return before the update. The blend and the lift then carry that whole factor mean, the rate with it. Writing $\mathbf{G} = \tau\mathbf{\Sigma}_f\mathbf{P}^\intercal(\mathbf{P}\tau\mathbf{\Sigma}_f\mathbf{P}^\intercal + \mathbf{\Omega})^{-1}$ for the update gain and $\mathbf{1}$ for the vector of ones, the answer moves against the same estimator at $r_f = 0$ by:

\[\begin{align} \Delta\hat{\boldsymbol{\mu}} &= r_{f}\mathbf{M}\left(\mathbf{I} - \mathbf{G}\mathbf{P}\right)\mathbf{1}\,. \end{align}\]

The shift is linear in $r_f$ and depends on the views through $\mathbf{G}$. It is [0.489, 0.831, 0.868, 0.183, 0.973] per unit of $r_f$ on the sample above, matching the closed form to 1e-16 and agreeing between rf = 0.03 and rf = 0.06. Where pe.l is nothing the prior mean is the factor prior's own, a total return already, so nothing reads pe.rf and the same two fits differ by 0.0 in every entry.

Algorithm

  1. Orient X and F with dims_oriented, to observations × assets and observations × factors.
  2. When pe.views resolves names, check the declared factor axis against the width of F with factor_universe. A precomputed BlackLittermanViews resolves no name, so step 6 checks its width instead.
  3. Reduce X to the assets it can be fitted over with coverage_reduction, under pnl, giving the mask and Xi. This member wraps a factor prior, so there is no asset-side prior result to read an Investable Mask off and the gap is read out of the returns themselves.
  4. Fit the wrapped prior pe.pe on F alone, giving f_prior, and read prior_mu and prior_sigma off it. The wrapped estimator is bounded over the asset axis, but the matrix it is handed here is the factor one.
  5. Regress Xi on F with factor_reconstruction under pe.re, giving the regression result rr and the reconstructed returns posterior_X.
  6. Assemble the views and their uncertainty with bl_preroll, over prior_sigma and size(Xi, 1) observations, giving blp. The axis is :tfkey, so no view row is ever dropped for a departed asset and no ledger is kept.
  7. Put the prior mean on the total-return scale the views are written on, giving prior_total_mu. When pe.l is set this is the equilibrium mean of equilibrium_mu, a risk premium, plus pe.rf by apply_rf, over pe.w sliced to the reduced axis by investable_weights_view; otherwise it is prior_mu, which is on that scale already.
  8. Run the master equations with bl_posteriors, giving the posterior factor pair. When no view row survived it hands back prior_total_mu and the factor prior covariance instead, and step 10 lifts those exactly as it lifts a posterior pair.
  9. Process the posterior factor covariance in place with matrix_processing!, under pe.f_mp and F.
  10. Lift the posterior factor pair onto the reduced assets with factor_lift, giving mu, sigma, chol and esigma. This is the lift FactorPrior applies; only the factor moments handed to it differ. It adds the residual block when pe.rsd is true, and processes sigma under pe.mp.
  11. Write esigma onto the esigma field of rr. Under pe.rsd = true the field holds the residual variances the lift measured, and under pe.rsd = false it holds nothing, because the lift added no residual block.
  12. Forward the factor block with forward_prior, replacing mu and sigma by the posterior factor pair and dropping chol. It is not expanded: the reduction never touched the factor axis.
  13. Announce the departures once with announce_bl_departures, naming them with investable_universe_names.
  14. Write every asset-axis block back onto the full universe: the moment pair with expand_moment, the reconstruction with expand_columns and the regression with expand_regression. chol is dropped instead of expanded, because a NaN frame has no factorisation.
  15. Build the carrier directly, taking w and its diagnostics from f_prior and carrying no Z.

Arguments

  • pe: Factor Black-Litterman prior estimator.
  • X: Asset returns matrix (observations × assets).
  • F: Factor matrix (observations × factors).
  • pnl: Optional AssetPanel, the panel the carrier held. A wrapping prior forwards it unchanged, so that it can compose an estimator that is fitted on a panel. An estimator that reads no panel ignores it. The prior this estimator nests is fitted on the factors, whose axis no panel describes, so the panel stops here.
  • dims: Dimension along which to perform the computation.
  • strict: If true, enforce strict validation of views and sets. Default is false.
  • kwargs...: Additional keyword arguments passed to underlying estimators and matrix processing.

Validation

pe.w has no named check. When pe.l is set, a pe.w whose length is not size(X, 2) raises a bare DimensionMismatch from the multiplication inside equilibrium_mu. When pe.l is nothing, pe.w is never read.

Returns

  • pr::LowOrderPrior: Result object carrying the reconstructed asset returns, the posterior asset mean vector, the posterior asset covariance matrix, its Cholesky factor, the factor prior's observation weighting and diagnostics, the regression result, and a factor block fpr holding the posterior factor moments, on the total-return scale the update ran on. fpr.chol is nothing, and no Z is carried.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[29]
F. Black and R. Litterman. Global portfolio optimization. Financial Analysts Journal 48, 28–43 (1992).