Augmented Black-Litterman Prior

PortfolioOptimisers.AugmentedBlackLittermanPriorType
struct AugmentedBlackLittermanPrior{__T_a_pe, __T_f_pe, __T_mp, __T_re, __T_a_views, __T_f_views, __T_sets, __T_a_views_conf, __T_f_views_conf, __T_w, __T_rf, __T_l, __T_tau, __T_cache} <: AbstractLowOrderPriorEstimator_F

Augmented Black-Litterman prior estimator for asset returns.

AugmentedBlackLittermanPrior is a low order prior estimator that computes the mean and covariance of asset returns using an augmented Black-Litterman model. It combines asset and factor prior estimators, matrix post-processing, regression and variance estimators, asset and factor views over one dual-axis universe sets, view confidences, weights, risk-free rate, leverage, and a blending parameter tau. This estimator supports both direct and constraint-based views, flexible confidence specification, and matrix processing, and incorporates joint asset-factor Bayesian updating for posterior inference.

Mathematical definition

Factor model linking assets and factors via regression:

\[\begin{align} \mathbf{X} &\approx \mathbf{F}\mathbf{M}^{\intercal} + \mathbf{1}\boldsymbol{b}^{\intercal}\,. \end{align}\]

Augmented prior moments (stacking asset and factor priors):

\[\begin{align} \boldsymbol{\mu}_{aug} &= \begin{pmatrix}\boldsymbol{\mu}_a \\ \boldsymbol{\mu}_f\end{pmatrix} \quad\text{or}\quad \lambda\begin{pmatrix}\boldsymbol{\Sigma}_a \\ \boldsymbol{\Sigma}_f\mathbf{M}^{\intercal}\end{pmatrix}\boldsymbol{w} + \begin{pmatrix}\boldsymbol{b} \\ \mathbf{0}\end{pmatrix} + r_{f}\,, \\ \boldsymbol{\Sigma}_{aug} &= \begin{pmatrix}\boldsymbol{\Sigma}_a & \mathbf{M}\boldsymbol{\Sigma}_f \\ \boldsymbol{\Sigma}_f\mathbf{M}^{\intercal} & \boldsymbol{\Sigma}_f\end{pmatrix}\,. \end{align}\]

The left prior mean is the stacked pair of wrapped means, and it is what pe.l === nothing uses. The right one is the equilibrium alternative pe.l selects, and it carries two corrections the wrapped pair does not need: the asset-side intercept $\boldsymbol{b}$, and the risk-free rate. Both are levels the wrapped means already contain, and both must be in $\boldsymbol{\mu}_{aug}$ rather than added afterwards, because the update blends this mean against $\boldsymbol{q}_{aug}$ by forming the residual $\boldsymbol{q}_{aug} - \mathbf{P}_{aug}\boldsymbol{\mu}_{aug}$.

The off-diagonal blocks are the cross-covariance the factor model implies, $\mathrm{cov}(\mathbf{X}, \mathbf{F}) = \mathbf{M}\boldsymbol{\Sigma}_f$. They are built from the factor covariance and not from the asset one, so they carry factor variance alone while the leading block carries factor and residual variance. That asymmetry is what opens the gap described in the second warning below.

The views are stacked block-diagonally, each set over its own axis:

\[\begin{align} \mathbf{P}_{aug} &= \begin{pmatrix}\mathbf{P} & \mathbf{0} \\ \mathbf{0} & \mathbf{P}_f\end{pmatrix}\,, \quad \boldsymbol{q}_{aug} = \begin{pmatrix}\boldsymbol{q} \\ \boldsymbol{q}_f\end{pmatrix}\,, \quad \boldsymbol{\Omega}_{aug} = \begin{pmatrix}\boldsymbol{\Omega} & \mathbf{0} \\ \mathbf{0} & \boldsymbol{\Omega}_f\end{pmatrix}\,. \end{align}\]

Black-Litterman posterior on the augmented space, by the ordinary master equations:

\[\begin{align} \boldsymbol{\mu}_{post} &= \boldsymbol{\mu}_{aug} + \tau\boldsymbol{\Sigma}_{aug}\mathbf{P}_{aug}^{\intercal}\left(\tau\mathbf{P}_{aug}\boldsymbol{\Sigma}_{aug}\mathbf{P}_{aug}^{\intercal} + \boldsymbol{\Omega}_{aug}\right)^{-1}\!\left(\boldsymbol{q}_{aug} - \mathbf{P}_{aug}\boldsymbol{\mu}_{aug}\right)\,, \\ \boldsymbol{\Sigma}_{post} &= \boldsymbol{\Sigma}_{aug} + \tau\boldsymbol{\Sigma}_{aug} - \tau\boldsymbol{\Sigma}_{aug}\mathbf{P}_{aug}^{\intercal}\left(\tau\mathbf{P}_{aug}\boldsymbol{\Sigma}_{aug}\mathbf{P}_{aug}^{\intercal} + \boldsymbol{\Omega}_{aug}\right)^{-1}\!\mathbf{P}_{aug}\tau\boldsymbol{\Sigma}_{aug}\,. \end{align}\]

The two halves are then read off by truncation, and nothing else. The intercept and the risk-free rate are in $\boldsymbol{\mu}_{aug}$ above, where the views are blended against them:

\[\begin{align} \hat{\boldsymbol{\mu}} &= \left(\boldsymbol{\mu}_{post}\right)_{1:N}\,, \quad \hat{\mathbf{\Sigma}} = \left(\boldsymbol{\Sigma}_{post}\right)_{1:N,\,1:N}\,, \\ \hat{\boldsymbol{\mu}}_f &= \left(\boldsymbol{\mu}_{post}\right)_{N+1:N+K}\,, \quad \hat{\mathbf{\Sigma}}_f = \left(\boldsymbol{\Sigma}_{post}\right)_{N+1:N+K,\,N+1:N+K}\,. \end{align}\]

Where:

  • $T$, $N$, $K$: The number of observations, of assets, and of factors.
  • $\mathbf{X}$: $T \times N$ asset returns matrix.
  • $\mathbf{F}$: $T \times K$ factor returns matrix.
  • $\mathbf{M}$: $N \times K$ factor loadings (regression coefficients), pr.rr.M.
  • $\boldsymbol{b}$: $N \times 1$ regression intercept vector, pr.rr.b.
  • $\boldsymbol{\mu}_a$, $\boldsymbol{\Sigma}_a$: $N \times 1$ and $N \times N$ asset prior mean and covariance, from a_pe.
  • $\boldsymbol{\mu}_f$, $\boldsymbol{\Sigma}_f$: $K \times 1$ and $K \times K$ factor prior mean and covariance, from f_pe.
  • $\boldsymbol{\mu}_{aug}$, $\boldsymbol{\Sigma}_{aug}$: Augmented (joint asset-factor) prior moments, of length and order $N + K$.
  • $\boldsymbol{\mu}_{post}$, $\boldsymbol{\Sigma}_{post}$: Augmented posterior moments, of the same shape.
  • $\tau$: Scaling parameter for the prior uncertainty, 1/T by default.
  • $\mathbf{P}$, $\boldsymbol{q}$, $\boldsymbol{\Omega}$: Asset views matrix, returns vector and uncertainty matrix, over the asset axis.
  • $\mathbf{P}_f$, $\boldsymbol{q}_f$, $\boldsymbol{\Omega}_f$: The same three over the factor axis.
  • $\mathbf{P}_{aug}$, $\boldsymbol{q}_{aug}$, $\boldsymbol{\Omega}_{aug}$: The same three stacked over the augmented axis, the asset rows above the factor rows.
  • $\hat{\boldsymbol{\mu}}$, $\hat{\mathbf{\Sigma}}$: $N \times 1$ and $N \times N$ posterior asset moments, pr.mu and pr.sigma.
  • $\hat{\boldsymbol{\mu}}_f$, $\hat{\mathbf{\Sigma}}_f$: $K \times 1$ and $K \times K$ posterior factor moments, pr.fpr.mu and pr.fpr.sigma.
  • $\lambda$, $\boldsymbol{w}$: The risk-aversion coefficient pe.l and the equilibrium weights pe.w, read by equilibrium_mu and only where pe.l is set.
  • $r_{f}$: Risk-free rate, added once by apply_rf to the equilibrium mean. It is absent where pe.l is nothing, because the wrapped means are total returns already.

Fields

  • a_pe: Asset prior estimator.
  • f_pe: Factor prior estimator.
  • mp: Matrix processing estimator.
  • re: Regression estimator.
  • a_views: Asset views estimator or result.
  • f_views: Factor views estimator or result.
  • sets: Universe sets. This estimator reads two declared axes: a_views resolves against sets.dict[sets.xkey], f_views against the time-series factor axis sets.dict[sets.tfkey], and each axis must name the columns of X and F respectively, in order. Only the axis a LinearConstraintEstimator actually resolves names against is required — views supplied as a BlackLittermanViews result carry their own matrix and need no universe. A view slices the asset axis and leaves the factor entries untouched, which is why this field is @vprop.
  • a_views_conf: Asset views confidence estimator or result.
  • f_views_conf: Factor 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.
  • 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

AugmentedBlackLittermanPrior(;    a_pe::AbstractLowOrderPriorEstimator_A_AF = EmpiricalPrior(),    f_pe::AbstractLowOrderPriorEstimator_A_AF = EmpiricalPrior(),    mp::AbstractMatrixProcessingEstimator = MatrixProcessing(),    re::AbstractTimeSeriesRegressionEstimator = StepwiseRegression(),    a_views::Lc_BLV,    f_views::Lc_BLV,    sets::Option{<:UniverseSets} = nothing,    a_views_conf::Option{<:Num_VecNum} = nothing,    f_views_conf::Option{<:Num_VecNum} = nothing,    w::Option{<:VecNum} = nothing,    rf::Number = 0.0,    l::Option{<:Number} = nothing,    tau::Option{<:Number} = nothing,    cache::Option{<:AbstractPartialFitState} = nothing) -> AugmentedBlackLittermanPrior

Keywords correspond to the struct's fields.

Composition: what this estimator forwards

This estimator merges two priors rather than forwarding one along its own axis, so it builds its carrier directly; the same posterior-versus-structural pattern still governs which source each field takes. It solves one augmented Black-Litterman system over [assets; factors] and reports both halves:

  • mu and sigma are the asset half of the augmented posterior; the factor block fpr is the factor half, so both are posterior. chol is dropped on both sides, because the posterior covariances supersede the ones they factorise.
  • w, ens, kld and ow come from the asset prior, and fpr's own come from the factor prior. Two priors disagreeing about observation weights is a legitimate configuration, and the nested block is what keeps the two weightings distinguishable rather than forcing a choice.
  • Z comes from the asset prior only: the factor prior's would be factors × features and would not describe this asset axis.
Warning

The returned mu and sigma are the augmented posterior, but w is the asset prior's observation weighting, forwarded unchanged (and fpr.w is the factor prior's). 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 asset prior, not of the posterior. Measured: pr.w, pr.ens, pr.kld and pr.ow are the identical objects the asset prior carried, and pr.fpr.w the identical object the factor prior carried.

Warning

pr.mu != pr.rr.M * pr.fpr.mu + pr.rr.b, even though both blocks are posterior. Two independent causes open the gap, and both are properties of the update. The intercept is not one of them: it enters the prior stack, and the update is affine in that stack, so it reaches both sides of the identity together.

Idiosyncratic variance, on every branch. The augmented covariance stacks the full asset covariance sigma_a — factor and residual variance — against a cross-covariance M * sigma_f that is pure factor. The update therefore moves the asset half by tau * sigma_a * P'(…) and the factor half by tau * sigma_f * M' * P'(…), and for the two to stay related by M it would need sigma_a == M * sigma_f * M'. That holds only when the factor model is exact. This part scales with the residual variance, and both view sets contribute to it.

A non-zero rf, on the l branch alone. apply_rf puts the rate on the whole equilibrium stack, so the asset half gains rf and the factor half gains rf too. The identity carries the factor half through the loadings, which turns that rf into rf * s for the row sums s of M, so the two sides agree only where an asset's loadings sum to one. The gap this opens is rf * (1 - s), exactly. It is zero at the default rf = 0.0, and where pe.l is nothing the field is never read.

The two priors satisfy the identity before the update only when both means are the plain sample mean, because least squares with an intercept zeroes the unweighted residual mean. One shared non-uniform weighting is not enough, because the weighted residual mean is not zero, and two different weightings part further. On an exact factor model a shared non-uniform weighting does satisfy it, because there is no residual to weight.

FactorBlackLittermanPrior and BayesianBlackLittermanPrior satisfy the identity exactly, because they update the factor distribution alone and project it onto the assets rather than updating an asset block alongside it. BlackLittermanPrior breaks it for the opposite reason — it takes asset views only and never computes a posterior factor distribution at all, so pr.fpr is nothing and the right-hand side cannot be formed.

One sets, two axes

This is the only estimator whose views land on both distributions, and the two axes it needs are the two UniverseSets declares: a_views resolves against sets.dict[sets.xkey], f_views against sets.dict[sets.tfkey]. Before the axis was declared this took two separate sets objects, and the factor-flavoured one had to be exempted from port_opt_view by hand — a missing annotation was all that stood between a view and a factor universe sliced by asset indices. With one dual-axis object the exemption is a property of the data: the field is @vprop, the slice moves the asset entries and the factor entries come back untouched.

Each axis is required only by the views that resolve names against it. A BlackLittermanViews result carries its own P and needs no universe at all, so asset-views-only and factor-views-only mandates are both expressible with a single sets — and a pair of precomputed view sets needs none, and answers the same as the pair supplied with sets. port_opt_view over an asset selection leaves the factor axis of sets whole while its asset axis and w fall to the selection.

Validation

The length of w is not validated here. It 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.

Propagated parameters

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

  • a_pe: Recursively updated via factory.
  • f_pe: Recursively updated via factory.
  • re: 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> AugmentedBlackLittermanPrior(;                                    sets = UniverseSets(;                                                        dict = Dict("nx" => ["A", "B", "C"],                                                                    "nf" => ["F1", "F2"])),                                    a_views = LinearConstraintEstimator(;                                                                        val = ["A == 0.03",                                                                               "B + C == 0.04"]),                                    f_views = LinearConstraintEstimator(;                                                                        val = ["F1 == 0.01",                                                                               "F2 == 0.02"]))AugmentedBlackLittermanPrior          a_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_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            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()       a_views ┼ LinearConstraintEstimator               │   val ┼ Vector{String}: ["A == 0.03", "B + C == 0.04"]               │   key ┴ nothing       f_views ┼ LinearConstraintEstimator               │   val ┼ Vector{String}: ["F1 == 0.01", "F2 == 0.02"]               │   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"])  a_views_conf ┼ nothing  f_views_conf ┼ nothing             w ┼ nothing            rf ┼ Float64: 0.0             l ┼ nothing           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

  • [77] W. Cheung. The augmented Black-Litterman model: a ranking-free approach to factor-based portfolio construction and beyond. Quantitative Finance 13, 301–316 (2013).
  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 5.2, Equations 5.17 to 5.19.
source
PortfolioOptimisers.priorFunction
prior(pe::AugmentedBlackLittermanPrior, X::MatNum, F::MatNum,
      pnl::Option{<:AssetPanel} = nothing; dims::Int = 1,
      strict::Bool = false, kwargs...)

Compute augmented Black-Litterman prior moments for asset returns.

prior estimates the mean and covariance of asset returns using the augmented Black-Litterman model, combining asset and factor prior estimators, matrix post-processing, regression and variance estimators, asset and factor views over one dual-axis universe sets, view confidences, weights, risk-free rate, leverage, and a blending parameter tau. This method supports both direct and constraint-based views, flexible confidence specification, and matrix processing, and incorporates joint asset-factor 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 X. The mean handed to the update is a total return, and both halves are reported on that scale. pe.rf reaches the update on the pe.l branch alone, where it converts the equilibrium risk premium, and it goes on the whole stack. Writing $\mathbf{G}$ for the augmented update gain and $\mathbf{1}$ for the vector of ones, the answer therefore moves against the same estimator at rf = 0 by rf * (I - G P_aug) * 1, read on each half. Where pe.l is nothing the field is never read, and two fits differing only in rf agree exactly on both halves.

Arguments

  • pe: Augmented 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. It reaches pe.a_pe, the prior over the assets. pe.f_pe is fitted on the factors, whose axis no panel describes, so the panel does not reach 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

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 asset half of the augmented posterior as mu and sigma, the asset prior's observation weighting and diagnostics, its feature matrix, the regression result, and a factor block fpr holding the factor half of the same posterior. chol is nothing on both blocks.

Algorithm

  1. Orient X and F with dims_oriented, to observations × assets and observations × factors.
  2. When pe.a_views resolves names, check that the declared asset axis is as long as X is wide.
  3. When pe.f_views resolves names, check the declared factor axis against the width of F with factor_universe. Each axis is checked only by the views that resolve names against it, so a pair of precomputed BlackLittermanViews needs no sets at all.
  4. Fit pe.a_pe on X, giving a_prior.
  5. Derive the Investable Mask and the reduced asset view universe with investable_views, refuse a precomputed asset view matrix over a gapped universe with assert_bl_precomputed_universe, and view a_prior at the mask with investable_prior. N is the reduced asset count from here on.
  6. Fit pe.f_pe on F, giving f_prior.
  7. Regress the reduced Xreduce_columns at the mask — on F with factor_reconstruction under pe.re, giving rr and the reconstructed returns posterior_X, both on the reduced asset axis.
  8. Assemble the asset views with bl_preroll at the default :xkey, over the reduced asset prior covariance, and the factor views at :tfkey, over the factor prior covariance. Either half can be emptied, and bl_view_block then gives that half no row rather than collapsing the stack — the joint posterior is still conditioned by whatever the other half kept. Only the asset half can be emptied by a departure: a factor axis holds no asset name. When both halves empty there is nothing left to condition on, and step 12 takes the joint prior.
  9. Build $\boldsymbol{\Sigma}_{aug}$, whose off-diagonal blocks are the model-implied cross-covariance $\mathbf{M}\boldsymbol{\Sigma}_f$ and its transpose.
  10. Stack $\mathbf{P}_{aug}$ block-diagonally, $\boldsymbol{q}_{aug}$ and $\boldsymbol{\Omega}_{aug}$ to match, the asset rows above the factor rows.
  11. Put the stacked prior mean on the total-return scale the views are written on, giving aug_prior_mu. When pe.l is nothing this is the stacked wrapped means, which are on that scale already. When pe.l is set it is the equilibrium mean of equilibrium_mu, a bare risk premium, over pe.w sliced to the reduced axis by investable_weights_view, plus pe.rf by apply_rf and plus rr.b on the asset half.
  12. Run the master equations with vanilla_posteriors over the augmented space, giving the augmented posterior pair. When neither half stated a surviving view the stack has nothing to condition on, and bl_posteriors hands back the joint prior instead: an empty $\mathbf{P}_{aug}$ run through the master equations would add $\tau\boldsymbol{\Sigma}_{aug}$ and widen the joint covariance on the strength of views that are not there.
  13. Process the augmented posterior covariance in place with matrix_processing!, under pe.mp and the two return matrices side by side.
  14. Truncate the asset half from 1:N. Nothing is added to it: the intercept and the rate went into the prior mean at step 11, and the update is affine in that mean.
  15. Truncate the factor half from N+1:N+K, and forward the factor block with forward_prior, dropping chol. The half takes no intercept, because the intercept is the regression's and hence asset-only, no rate, because the stack reached the update carrying the one it needed, and no second processing pass, because a principal submatrix of a processed matrix is already processed. It is not expanded: the reduction never touched the factor axis.
  16. Announce the departures once with announce_bl_departures.
  17. Build the carrier directly, taking w, its diagnostics and Z from a_prior, and writing every asset-axis block back onto the full universe on the way: the moment pair with expand_moment, the reconstruction with expand_columns and the regression with expand_regression.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[77]
W. Cheung. The augmented Black-Litterman model: a ranking-free approach to factor-based portfolio construction and beyond. Quantitative Finance 13, 301–316 (2013).