Bayesian Black-Litterman Prior

PortfolioOptimisers.BayesianBlackLittermanPriorType
struct BayesianBlackLittermanPrior{__T_pe, __T_f_mp, __T_mp, __T_views, __T_sets, __T_views_conf, __T_rf, __T_tau, __T_cache} <: AbstractLowOrderPriorEstimator_F

Bayesian Black-Litterman prior estimator for asset returns.

BayesianBlackLittermanPrior is a low order prior estimator that computes the mean and covariance of asset returns using a Bayesian Black-Litterman model. It combines a factor prior estimator, matrix post-processing, user or algorithmic views, asset sets, view confidences, risk-free rate, and a blending parameter tau. This estimator supports both direct and constraint-based views, flexible confidence specification, and matrix processing, and incorporates Bayesian updating for posterior inference.

Fields

  • pe: Prior estimator.
  • f_mp: Factor matrix processing estimator.
  • mp: Matrix processing 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.
  • 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.
  • tau: Blending parameter controlling the weight given to the prior relative to the views.
  • 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

BayesianBlackLittermanPrior(;    pe::AbstractLowOrderPriorEstimator_F_AF = FactorPrior(;        pe = EmpiricalPrior(;            me = EquilibriumExpectedReturns()        )    ),    f_mp::AbstractMatrixProcessingEstimator = MatrixProcessing(),    mp::AbstractMatrixProcessingEstimator = MatrixProcessing(),    views::Lc_BLV,    sets::Option{<:UniverseSets} = nothing,    views_conf::Option{<:Num_VecNum} = nothing,    rf::Number = 0.0,    tau::Option{<:Number} = nothing,    cache::Option{<:AbstractPartialFitState} = nothing) -> BayesianBlackLittermanPrior

Keywords correspond to the struct's fields.

Composition: what this estimator forwards

The views are applied to the factors and reach the assets through the regression loadings, so this estimator produces a posterior over both blocks. It forwards the wrapped prior whole and spells out its deviations:

  • mu and sigma are the asset posterior; chol is dropped, because the posterior covariance supersedes the one it factorises.
  • The factor block fpr carries the posterior factor moments — mu_hat and the inverse of the posterior precision — processed by f_mp. Its chol is dropped for the same reason; its w and that weighting's diagnostics forward untouched, because the views do not touch the observation axis.
  • Everything else forwards: X is the wrapped prior's unchanged, so w, ens, kld, ow and Z all still describe the axis they were computed over, and rr is a regression over data the views do not modify.

Because both blocks are posterior, the returned carrier is internally consistent: mu == rr.M * fpr.mu + rr.b + rf holds, and at the default rf = 0.0 that is the plain identity. FactorBlackLittermanPrior satisfies it too, for the same reason. The other two members do not — see the warnings on BlackLittermanPrior and AugmentedBlackLittermanPrior.

Warning

The returned mu and sigma are the Black-Litterman posterior, but w is the wrapped prior's observation weighting, forwarded unchanged. Black-Litterman produces no observation-level posterior, so there is no Black-Litterman-consistent alternative to forward — and dropping w would substitute the unweighted empirical distribution, which is further from the caller's intent than the weights they computed. A caller reading pr.w, pr.ens, pr.kld or pr.ow is therefore reading a property of the prior, not of the posterior.

What this estimator refuses

The update inverts the wrapped prior's factor covariance twice, so it needs a factor axis of full rank. A factor model that states a re-based Factor Family through has_family_rebasis carries fpr on the raw axis, which a re-basis makes a linear image of a smaller one, so that covariance is singular by construction. Such a carrier is refused with an ArgumentError naming the wrapped estimator.

The refusal is not decoration over a failure that would otherwise be visible. The inversion raises nothing on such a matrix: it returns entries of order 1e18, and the update carries on to a posterior whose scale looks like the prior's, so a caller reading the result sees no sign that it is meaningless. That is why the refusal reads what the result states rather than testing its rank, and why it is a refusal rather than a warning.

HighOrderFactorPriorEstimator accepts the same carrier, because it only projects through rr.M and never inverts the factor covariance.

The views are written on the factor axis

views resolves against sets.dict[sets.tfkey] — the axis UniverseSets declares for factors — because the Bayesian update lands on the factor distribution and reaches the assets through the loadings. 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

Propagated parameters

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

  • pe: 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> BayesianBlackLittermanPrior(;                                   sets = UniverseSets(;                                                       dict = Dict("nx" => ["A", "B", "C"],                                                                   "nf" => ["F1", "F2"])),                                   views = LinearConstraintEstimator(;                                                                     val = ["F1 == 0.03",                                                                            "F2 == 0.04"]))BayesianBlackLittermanPrior          pe ┼ FactorPrior             │    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 ┼ EquilibriumExpectedReturns             │       │              │   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)             │       │              │    w ┼ nothing             │       │              │    l ┴ Int64: 1             │       │      horizon ┼ nothing             │       │   fill_limit ┴ 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)             │    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             │   rsd ┴ Bool: true        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)       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          rf ┼ Float64: 0.0         tau ┴ nothing

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

  • [76] P. N. Kolm and G. Ritter. On the Bayesian interpretation of Black-Litterman. European Journal of Operational Research 258, 564–572 (2017).
  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 5.3, Equations 5.23, 5.34 and 5.35.
source
PortfolioOptimisers.priorFunction
prior(pe::BayesianBlackLittermanPrior, X::MatNum, F::MatNum,
      pnl::Option{<:AssetPanel} = nothing; dims::Int = 1, strict::Bool = false,
      kwargs...)

Compute Bayesian Black-Litterman prior moments for asset returns.

prior estimates the mean and covariance of asset returns using the Bayesian Black-Litterman model, combining a factor prior estimator, matrix post-processing, user or algorithmic views, asset sets, view confidences, risk-free rate, and blending parameter tau. This method supports both direct and constraint-based views, flexible confidence specification, and matrix processing, and incorporates Bayesian updating for posterior inference.

When pe.tau is nothing the blending parameter is 1/T, where T is the number of observations of the oriented F. pe.rf reaches the answer once, on the posterior asset expected returns; apply_rf owns that contract, and the factor block never carries the rate. This is the one Black-Litterman member that never calls vanilla_posteriors: its update is the conditional posterior of the factor parameter, and the assets follow as a posterior predictive distribution rather than as a second master-equation run.

Mathematical definition

This is not the classic Black-Litterman update run on the assets. The views land on the factor parameter $\boldsymbol{\theta}$, and the assets are the posterior predictive distribution that the factor model implies. The model is:

\[\begin{align} \boldsymbol{r} &\sim \mathcal{N}(\mathbf{M}\boldsymbol{\theta} + \boldsymbol{b},\ \mathbf{\Sigma})\,, \\ \boldsymbol{\theta} &\sim \mathcal{N}(\boldsymbol{\Pi}_f,\ \mathbf{\Sigma}_f)\,, \\ \mathbf{P}\boldsymbol{\theta} &\sim \mathcal{N}(\boldsymbol{q},\ \mathbf{\Omega})\,. \end{align}\]

The conditional posterior of $\boldsymbol{\theta}$ given the views is Gaussian, with precision $\mathbf{H}$:

\[\begin{align} \mathbf{H} &= \mathbf{\Sigma}_f^{-1} + \mathbf{P}^\intercal \mathbf{\Omega}^{-1} \mathbf{P}\,, \\ \bar{\mathbf{\Sigma}}_f &= \mathbf{H}^{-1}\,, \\ \bar{\boldsymbol{\Pi}}_f &= \mathbf{H}^{-1}\left(\mathbf{\Sigma}_f^{-1}\boldsymbol{\Pi}_f + \mathbf{P}^\intercal \mathbf{\Omega}^{-1} \boldsymbol{q}\right)\,. \end{align}\]

Writing $\mathbf{V} = \left(\mathbf{H} + \mathbf{M}^\intercal \mathbf{\Sigma}^{-1} \mathbf{M}\right)^{-1}$, the posterior predictive asset moments are:

\[\begin{align} \hat{\mathbf{\Sigma}}_{BBL} &= \left(\mathbf{\Sigma}^{-1} - \mathbf{\Sigma}^{-1}\mathbf{M}\,\mathbf{V}\,\mathbf{M}^\intercal \mathbf{\Sigma}^{-1}\right)^{-1}\,, \\ \hat{\boldsymbol{\mu}}_{BBL} &= \hat{\mathbf{\Sigma}}_{BBL}\,\mathbf{\Sigma}^{-1}\mathbf{M}\,\mathbf{V}\,\mathbf{H}\,\bar{\boldsymbol{\Pi}}_f + \boldsymbol{b} + r_{f}\,. \end{align}\]

Where:

  • $N$, $K$, $K_v$, $T$: The number of assets, of factors, of views, and of observations.
  • $\boldsymbol{r}$: $N \times 1$ asset return vector the model is written on.
  • $\boldsymbol{\theta}$: $K \times 1$ factor parameter the views land on.
  • $\hat{\boldsymbol{\mu}}_{BBL}$: $N \times 1$ Bayesian Black-Litterman posterior asset mean, pr.mu.
  • $\hat{\mathbf{\Sigma}}_{BBL}$: $N \times N$ Bayesian Black-Litterman posterior asset covariance, pr.sigma.
  • $\boldsymbol{\Pi}_f$, $\mathbf{\Sigma}_f$: $K \times 1$ and $K \times K$ prior factor moments, from pe.pe.
  • $\mathbf{H}$: $K \times K$ posterior factor precision, the sum of the prior precision and the view precision.
  • $\bar{\boldsymbol{\Pi}}_f$, $\bar{\mathbf{\Sigma}}_f$: $K \times 1$ and $K \times K$ posterior factor moments, reported in pr.fpr.
  • $\mathbf{\Sigma}$: $N \times N$ prior asset covariance matrix, from pe.pe.
  • $\mathbf{V}$: $K \times K$ inverse of the posterior factor precision closed under the loadings.
  • $\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.
  • $\boldsymbol{q}$: $K_v \times 1$ views vector.
  • $\mathbf{\Omega}$: $K_v \times K_v$ view uncertainty matrix, $\mathrm{Diag}(\mathbf{P}(\tau\mathbf{\Sigma}_f)\mathbf{P}^\intercal)$ from calc_omega and bl_preroll.
  • $\tau$: Scaling parameter, 1/T by default.
  • $r_{f}$: Risk-free rate, added once by apply_rf.

Two consequences are caller-facing. $\mathbf{P}$ is over the factor axis, so it has $K$ columns and not $N$ — the classic asset-axis master equation cannot be evaluated with this estimator's own quantities at all. And $\hat{\boldsymbol{\mu}}_{BBL}$ is $\mathbf{M}\bar{\boldsymbol{\Pi}}_f + \boldsymbol{b}$ by construction, which is the identity the Composition section above states.

The width of $\mathbf{P}$ is enforced rather than assumed: a precomputed BlackLittermanViews whose P is five columns wide, against a three-factor prior, raises a DimensionMismatch out of bl_preroll reporting size(P, 2) => 5 against size(prior_sigma, 1) => 3. Views written in asset names raise an IsNothingError instead, because no name resolves against the factor universe.

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. Fit the wrapped prior pe.pe on (X, F), giving prior_result, and check it carries a regression with assert_prior_regression.
  4. Derive the Investable Mask with investable_mask and view the fitted prior at it with investable_prior. The mask alone, and not investable_views: the views land on the factors, so there is no Non-Investable Axis to mint and no asset universe this member otherwise reads.
  5. Read posterior_X, prior_sigma, fpr and rr off the reduced prior, and refuse a rr that states a re-based Factor Family through has_family_rebasis.
  6. Assemble the views and their uncertainty with bl_preroll, over the factor prior covariance and size(F, 1) observations, and read P, Q and omega off the result with bl_view_block. The axis is :tfkey, because these views land on the factors. A view set with no row left gives a 0 × K block, and the steps below then answer the moments this member's factor model implies — see the note under bl_view_block, and note that it is not the wrapped prior.
  7. Build the posterior factor precision $\mathbf{H}$ as sigma_hat.
  8. Solve sigma_hat against the sum of the two precision-weighted means, giving mu_hat, the posterior factor mean $\bar{\boldsymbol{\Pi}}_f$.
  9. Build the posterior asset covariance from $\mathbf{H}$, the loadings and prior_sigma, giving posterior_sigma.
  10. Process posterior_sigma in place with matrix_processing!, under pe.mp and posterior_X.
  11. Build the posterior asset mean from the same quantities, add rr.b, and add pe.rf with apply_rf. This is the one site that adds the rate.
  12. Invert sigma_hat for the posterior factor covariance $\bar{\mathbf{\Sigma}}_f$, and process it in place under pe.f_mp and F.
  13. 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.
  14. Announce the departures once with announce_bl_departures, naming them with investable_universe_names.
  15. Write both asset posteriors back onto the full asset universe with expand_moment, so a non-investable asset carries NaN in mu and on the diagonal of sigma.
  16. Forward the whole of prior_result with forward_prior, replacing mu and sigma by the expanded asset pair, dropping chol, and replacing fpr by the block of step 13.

Arguments

  • pe: Bayesian 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.
  • 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

  • dims in (1, 2).
  • If pe.views is a LinearConstraintEstimator, haskey(pe.sets.dict, pe.sets.tfkey) and length(pe.sets.dict[pe.sets.tfkey]) == size(F, 2), both via factor_universe.
  • The prior produced by pe.pe must carry a regression result, via assert_prior_regression.
  • The regression result the prior carries must state no re-based Factor Family, via has_family_rebasis. A re-basis makes fpr.sigma singular, and steps 5, 6 and 10 all invert it.

Returns

  • pr::LowOrderPrior: Result object carrying the asset returns, the posterior asset mean vector, the posterior asset covariance matrix, and a factor block fpr holding the posterior factor moments. Both blocks are therefore posterior, so pr.mu == pr.rr.M * pr.fpr.mu + pr.rr.b + pe.rf holds. chol is nothing on both blocks.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[76]
P. N. Kolm and G. Ritter. On the Bayesian interpretation of Black-Litterman. European Journal of Operational Research 258, 564–572 (2017).