Augmented Black-Litterman Prior
PortfolioOptimisers.AugmentedBlackLittermanPrior — Type
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_FAugmented 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/Tby 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.muandpr.sigma. - $\hat{\boldsymbol{\mu}}_f$, $\hat{\mathbf{\Sigma}}_f$: $K \times 1$ and $K \times K$ posterior factor moments,
pr.fpr.muandpr.fpr.sigma. - $\lambda$, $\boldsymbol{w}$: The risk-aversion coefficient
pe.land the equilibrium weightspe.w, read byequilibrium_muand only wherepe.lis set. - $r_{f}$: Risk-free rate, added once by
apply_rfto the equilibrium mean. It is absent wherepe.lisnothing, 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_viewsresolves againstsets.dict[sets.xkey],f_viewsagainst the time-series factor axissets.dict[sets.tfkey], and each axis must name the columns ofXandFrespectively, in order. Only the axis aLinearConstraintEstimatoractually resolves names against is required — views supplied as aBlackLittermanViewsresult 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 vectorassets × 1. Ifnothing, 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 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
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) -> AugmentedBlackLittermanPriorKeywords 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:
muandsigmaare the asset half of the augmented posterior; the factor blockfpris the factor half, so both are posterior.cholis dropped on both sides, because the posterior covariances supersede the ones they factorise.w,ens,kldandowcome from the asset prior, andfpr'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.Zcomes from the asset prior only: the factor prior's would be factors × features and would not describe this asset axis.
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.
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
- If
wis notnothing,!isempty(w). - If either
a_viewsorf_viewsis aLinearConstraintEstimator,!isnothing(sets). - If
a_views_confis notnothing, validated withassert_bl_views_conf. - If
f_views_confis notnothing, validated withassert_bl_views_conf. - If
tauis notnothing,tau > 0.
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 viafactory.f_pe: Recursively updated viafactory.re: 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:
a_pe: Recursively viewed viaport_opt_view.re: Recursively viewed viaport_opt_view.sets: Sliced to the selected indices viaport_opt_view.w: Sliced to the selected indices viaport_opt_view.
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 ┴ 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_FEmpiricalPriorBlackLittermanViewsUniverseSetsLowOrderPriorpriorfactoryport_opt_view
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.
PortfolioOptimisers.prior — Function
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: 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. It reachespe.a_pe, the prior over the assets.pe.f_peis 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: 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.a_viewsis aLinearConstraintEstimator,length(pe.sets.dict[pe.sets.xkey]) == size(X, 2). - If
pe.f_viewsis aLinearConstraintEstimator,haskey(pe.sets.dict, pe.sets.tfkey)andlength(pe.sets.dict[pe.sets.tfkey]) == size(F, 2), both viafactor_universe.
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 asmuandsigma, the asset prior's observation weighting and diagnostics, its feature matrix, the regression result, and a factor blockfprholding the factor half of the same posterior.cholisnothingon both blocks.
Algorithm
- Orient
XandFwithdims_oriented, toobservations × assetsandobservations × factors. - When
pe.a_viewsresolves names, check that the declared asset axis is as long asXis wide. - When
pe.f_viewsresolves names, check the declared factor axis against the width ofFwithfactor_universe. Each axis is checked only by the views that resolve names against it, so a pair of precomputedBlackLittermanViewsneeds nosetsat all. - Fit
pe.a_peonX, givinga_prior. - Derive the Investable Mask and the reduced asset view universe with
investable_views, refuse a precomputed asset view matrix over a gapped universe withassert_bl_precomputed_universe, and viewa_priorat the mask withinvestable_prior.Nis the reduced asset count from here on. - Fit
pe.f_peonF, givingf_prior. - Regress the reduced
X—reduce_columnsat the mask — onFwithfactor_reconstructionunderpe.re, givingrrand the reconstructed returnsposterior_X, both on the reduced asset axis. - Assemble the asset views with
bl_prerollat 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, andbl_view_blockthen 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. - Build $\boldsymbol{\Sigma}_{aug}$, whose off-diagonal blocks are the model-implied cross-covariance $\mathbf{M}\boldsymbol{\Sigma}_f$ and its transpose.
- Stack $\mathbf{P}_{aug}$ block-diagonally, $\boldsymbol{q}_{aug}$ and $\boldsymbol{\Omega}_{aug}$ to match, the asset rows above the factor rows.
- Put the stacked prior mean on the total-return scale the views are written on, giving
aug_prior_mu. Whenpe.lisnothingthis is the stacked wrapped means, which are on that scale already. Whenpe.lis set it is the equilibrium mean ofequilibrium_mu, a bare risk premium, overpe.wsliced to the reduced axis byinvestable_weights_view, pluspe.rfbyapply_rfand plusrr.bon the asset half. - Run the master equations with
vanilla_posteriorsover the augmented space, giving the augmented posterior pair. When neither half stated a surviving view the stack has nothing to condition on, andbl_posteriorshands 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. - Process the augmented posterior covariance in place with
matrix_processing!, underpe.mpand the two return matrices side by side. - 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. - Truncate the factor half from
N+1:N+K, and forward the factor block withforward_prior, droppingchol. 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. - Announce the departures once with
announce_bl_departures. - Build the carrier directly, taking
w, its diagnostics andZfroma_prior, and writing every asset-axis block back onto the full universe on the way: the moment pair withexpand_moment, the reconstruction withexpand_columnsand the regression withexpand_regression.
Related
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).