Bayesian Black-Litterman Prior
PortfolioOptimisers.BayesianBlackLittermanPrior — Type
struct BayesianBlackLittermanPrior{__T_pe, __T_f_mp, __T_mp, __T_views, __T_sets, __T_views_conf, __T_rf, __T_tau, __T_cache} <: AbstractLowOrderPriorEstimator_FBayesian 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 ofFin order. The asset axis is required byUniverseSetsand is what a view slices — the factor entries come back fromport_opt_viewuntouched.
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 isnothinguntilpartial_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:factorycarries it unchanged, because a factory call resolves configuration rather than the sample;port_opt_viewslices it to the selected assets by index copy, so the viewed estimator answers over those assets alone; andobs_weights_viewdrops 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) -> BayesianBlackLittermanPriorKeywords 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:
muandsigmaare the asset posterior;cholis dropped, because the posterior covariance supersedes the one it factorises.- The factor block
fprcarries the posterior factor moments —mu_hatand the inverse of the posterior precision — processed byf_mp. Itscholis dropped for the same reason; itswand that weighting's diagnostics forward untouched, because the views do not touch the observation axis. - Everything else forwards:
Xis the wrapped prior's unchanged, sow,ens,kld,owandZall still describe the axis they were computed over, andrris 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.
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
- If
viewsis aLinearConstraintEstimator,!isnothing(sets). - If
views_confis notnothing,views_confis validated withassert_bl_views_conf. - If
tauis notnothing,tau > 0.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
pe: Recursively updated viafactory.
View parameters
When port_opt_view is called on this type, the following @vprop-tagged fields are automatically subset to the selected indices:
pe: Recursively viewed viaport_opt_view.sets: Sliced to the selected indices viaport_opt_view.
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 ┴ nothingThe 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
AbstractLowOrderPriorEstimator_FFactorPriorBlackLittermanViewsUniverseSetsLowOrderPriorpriorfactoryport_opt_view
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.
PortfolioOptimisers.prior — Function
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_omegaandbl_preroll. - $\tau$: Scaling parameter,
1/Tby 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
- Orient
XandFwithdims_oriented, toobservations × assetsandobservations × factors. - When
pe.viewsresolves names, check the declared factor axis against the width ofFwithfactor_universe. A precomputedBlackLittermanViewsresolves no name, so step 6 checks its width instead. - Fit the wrapped prior
pe.peon(X, F), givingprior_result, and check it carries a regression withassert_prior_regression. - Derive the Investable Mask with
investable_maskand view the fitted prior at it withinvestable_prior. The mask alone, and notinvestable_views: the views land on the factors, so there is no Non-Investable Axis to mint and no asset universe this member otherwise reads. - Read
posterior_X,prior_sigma,fprandrroff the reduced prior, and refuse arrthat states a re-based Factor Family throughhas_family_rebasis. - Assemble the views and their uncertainty with
bl_preroll, over the factor prior covariance andsize(F, 1)observations, and readP,Qandomegaoff the result withbl_view_block. The axis is:tfkey, because these views land on the factors. A view set with no row left gives a0 × Kblock, and the steps below then answer the moments this member's factor model implies — see the note underbl_view_block, and note that it is not the wrapped prior. - Build the posterior factor precision $\mathbf{H}$ as
sigma_hat. - Solve
sigma_hatagainst the sum of the two precision-weighted means, givingmu_hat, the posterior factor mean $\bar{\boldsymbol{\Pi}}_f$. - Build the posterior asset covariance from $\mathbf{H}$, the loadings and
prior_sigma, givingposterior_sigma. - Process
posterior_sigmain place withmatrix_processing!, underpe.mpandposterior_X. - Build the posterior asset mean from the same quantities, add
rr.b, and addpe.rfwithapply_rf. This is the one site that adds the rate. - Invert
sigma_hatfor the posterior factor covariance $\bar{\mathbf{\Sigma}}_f$, and process it in place underpe.f_mpandF. - Forward the factor block with
forward_prior, replacingmuandsigmaby the posterior factor pair and droppingchol. It is not expanded: the reduction never touched the factor axis. - Announce the departures once with
announce_bl_departures, naming them withinvestable_universe_names. - Write both asset posteriors back onto the full asset universe with
expand_moment, so a non-investable asset carriesNaNinmuand on the diagonal ofsigma. - Forward the whole of
prior_resultwithforward_prior, replacingmuandsigmaby the expanded asset pair, droppingchol, and replacingfprby the block of step 13.
Arguments
pe: Bayesian Black-Litterman prior estimator.X: Asset returns matrix (observations × assets).F: Factor matrix (observations × factors).pnl: OptionalAssetPanel, 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: Iftrue, enforce strict validation of views and sets. Default isfalse.kwargs...: Additional keyword arguments passed to underlying estimators and matrix processing.
Validation
dims in (1, 2).- If
pe.viewsis aLinearConstraintEstimator,haskey(pe.sets.dict, pe.sets.tfkey)andlength(pe.sets.dict[pe.sets.tfkey]) == size(F, 2), both viafactor_universe. - The prior produced by
pe.pemust carry a regression result, viaassert_prior_regression. - The regression result the prior carries must state no re-based Factor Family, via
has_family_rebasis. A re-basis makesfpr.sigmasingular, 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 blockfprholding the posterior factor moments. Both blocks are therefore posterior, sopr.mu == pr.rr.M * pr.fpr.mu + pr.rr.b + pe.rfholds.cholisnothingon both blocks.
Related
BayesianBlackLittermanPriorLowOrderPriorpriorbl_preroll: AssemblesP,Qandomegaatpe.sets.tfkey, and resolvespe.tauto1/Twhen the estimator carries none.calc_omegaapply_rfforward_priorvanilla_posteriors: The master equations this estimator does not run. Its siblings that take asset views do.
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).