Factor risk contribution

PortfolioOptimisers.FactorRiskContributionResultType
struct FactorRiskContributionResult{__T_jr, __T_r, __T_rr, __T_frc_plr, __T_fb} <: RiskJuMPOptimisationResult

Result type for Factor Risk Contribution portfolio optimisation.

Fields

  • r: The risk measure the optimisation ran under, or a vector of them, stored resolved — a Deferred Quantity has already been fitted and an unstated slot has already taken the prior's field. A resolved measure is fitted state, not configuration, so it belongs on the Result. Pass it back as expected_risk(res.r, res.w, res.pr; sca = res.sca).
  • rr: Regression result.
  • frc_plr: Factor risk contribution placeholder result.
  • fb: The fallback chain that answered this result: the (estimator, result) pair of every attempt optimise made before this one, in the order they ran, or nothing when the estimator it was asked of answered (see FbChain).

Property access delegates to the embedded JuMPOptimisationResult; unknown properties forward into rr first, then through jr (including the virtual :w and the pa fall-through).

Constructors

FactorRiskContributionResult(;    jr::JuMPOptimisationResult, r::BaseRM_VecBaseRM, rr::AbstractLoadingsRegressionResult,    frc_plr::Option{<:AbstractPhylogenyConstraintResult}, fb::Option{<:OptE_Opt_FbChain}) -> FactorRiskContributionResult

Keywords correspond to the struct's fields.

Related

source
PortfolioOptimisers.FactorRiskContributionType
struct FactorRiskContribution{__T_opt, __T_re, __T_r, __T_obj, __T_frc_ple, __T_sets, __T_wi, __T_flag, __T_fb} <: RiskJuMPOptimisationEstimator

Solves a mean-risk problem whose decision variable is the vector of factor exposures rather than the vector of asset weights.

The asset weights are recovered from the exposures through the factor loadings, so a constraint written on the decision variable is a constraint on a factor. This is the change of basis alone: FactorRiskContribution sets no risk budget of its own. A target contribution per factor is stated through the risk measure's own rc constraints, exactly as it is for assets.

Mathematical definition

The factor model of the loadings, fitted by re or carried by the prior:

\[\begin{align} \mathbf{R} &= \mathbf{F} \mathbf{B}^\intercal + \mathbf{E}\,. \end{align}\]

The portfolio's factor exposures are $\boldsymbol{y}_{f} = \mathbf{B}^\intercal \boldsymbol{w}$, and the weights are recovered from them by the Moore-Penrose pseudoinverse:

\[\begin{align} \boldsymbol{w} &= (\mathbf{B}^\intercal)^{+} \boldsymbol{y}_{f}\,, \\ \boldsymbol{w} &= (\mathbf{B}^\intercal)^{+} \boldsymbol{y}_{f} + (\tilde{\mathbf{B}}^\intercal)^{+} \tilde{\boldsymbol{y}}_{af}\,, \quad \tilde{\mathbf{B}} = \ker(\mathbf{B}^\intercal)\,. \end{align}\]

The first form is the model built when flag = false, and the second when flag = true. The second adds the $N - N_{f}$ directions the loadings do not span, so the weight vector is no longer confined to the factor subspace.

The problem is then the mean-risk problem of MeanRisk over $\boldsymbol{y}_{f}$:

\[\begin{align} \underset{\boldsymbol{y}_{f}}{\min} \; f(\boldsymbol{w}(\boldsymbol{y}_{f})) \quad \text{s.t.} \quad \boldsymbol{w}(\boldsymbol{y}_{f}) \in \mathcal{W}\,. \end{align}\]

The risk contribution of factor $j$, which factor_risk_contribution reports, follows from the Euler decomposition in that basis:

\[\begin{align} RC_j(\boldsymbol{y}_{f}) &= \left[ \frac{\partial \mathcal{R}(\boldsymbol{w})}{\partial \boldsymbol{w}} (\mathbf{B}^\intercal)^{+} \right]_{j} \left[ \mathbf{B}^\intercal \boldsymbol{w} \right]_{j}\,. \end{align}\]

Where:

  • $\mathbf{R}$: Asset returns matrix.
  • $\mathbf{F}$: Factor returns matrix.
  • $\mathbf{B}$: Loading matrix, of size $N \times N_{f}$.
  • $\mathbf{E}$: Residual matrix.
  • $\boldsymbol{w}$: Portfolio weight vector.
  • $\boldsymbol{y}_{f}$: Factor exposure vector, the decision variable.
  • $\tilde{\boldsymbol{y}}_{af}$: Exposures to the additional directions, which carry no economic interpretation.
  • $(\cdot)^{+}$: Moore-Penrose pseudoinverse.
  • $f(\boldsymbol{w})$: Objective function (depends on ObjectiveFunction).
  • $\mathcal{W}$: Feasible weight set defined by portfolio constraints.
  • $\mathcal{R}(\boldsymbol{w})$: Portfolio risk measure.
  • $RC_j$: Risk contribution of factor $j$.

Fields

  • opt: JuMP optimiser configuration.
  • re: Regression estimator.
  • r: Risk measure or vector of risk measures.
  • obj: Portfolio objective function.
  • frc_ple: Factor risk contribution placeholder constraints.
  • sets: Sets used to map estimator values to assets.
  • wi: Initial portfolio weights for warm-starting the solver.
  • flag: Algorithm selection flag.
  • fb: Fallback result or estimator.

Constructors

FactorRiskContribution(;    opt::JuMPOptimiser,    re::TD{<:RegE_Reg} = StepwiseRegression(),    r::TD{<:RM_VecRM} = Variance(),    obj::TD{<:ObjectiveFunction} = MinimumRisk(),    frc_ple::TD_Option{<:PlCE_PlC_VecPlCE_PlC} = nothing,    sets::TD_Option{<:UniverseSets} = nothing,    wi::TD_Option{<:VecNum} = nothing,    flag::Bool = false,    fb::TDO_Option{<:OptE_Opt} = nothing) -> FactorRiskContribution

Keywords correspond to the struct's fields. Fields typed TD, TD_Option or TDO_Option may hold a TimeDependent per-fold schedule instead of a static value: the factor model, risk measure, objective, placeholder constraints, asset sets, warm start and fallback are problem definition, so a cross-validation fold loop resolves them per fold, and a fold-less optimise runs with each at its static default. flag is execution control and stays static.

Validation

  • If r is a vector: !isempty(r).
  • If wi is a vector: !isempty(wi).
  • fb schedules: bind !== :nearest.
  • A risk expression that is identically zero is refused. The problem is stated in the factor basis, so a model with no risk term bounds nothing. A NoRisk measure and settings.rke = false on every measure are the two routes to it.

Propagated parameters

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

  • opt: Recursively updated via factory.
  • r: Recursively updated via factory.
  • fb: Recursively updated via factory.

View parameters

FactorRiskContribution defines its own port_opt_view method rather than deriving one from field tags.

  • The method reads the returns matrix X as its third argument. When opt.pe already holds a prior result, the method replaces X with opt.pe.X, so the children are viewed against the prior's own observations rather than the caller's matrix.
  • opt and r recurse through port_opt_view with that matrix. re recurses with the index alone, which slices its loadings to the selected assets and leaves the factor axis whole.
  • wi is carried through unchanged, because it holds initial factor weights. The optimisation re-bases the weight variable onto the factor axis, so the asset selection does not index wi.
  • obj, frc_ple, sets, flag and fb are carried through unchanged.

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 10.2.1.
  • [132] T. Roncalli and G. Weisang. Risk parity portfolios with risk factors. Available at SSRN 2155159 (2012).
  • [133] A. Meucci. Risk contributions from generic user-defined factors. Available at SSRN 930034 (2007).
source
PortfolioOptimisers.factoryMethod
factory(a::Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                 <:AbstractResult}, args...; kwargs...) -> a
factory(a::AbstractVector{<:Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                                  <:AbstractResult}}, args...; kwargs...) -> Vector

No-op factory function for constructing objects with a uniform interface.

Defining methods which dispatch on the first argument allows for a consistent factory interface across different types.

factory and port_opt_view are the two propagation mechanisms in this library. They are duals: factory threads runtime values (prior moments, observation weights, previous portfolio weights) down through a composed struct tree; port_opt_view threads an index selection (a subset of assets or observations) down through the same tree.

The vector method is the one forwarding contract for every vector-valued propagation field: it applies factory to each element and forwards args... and kwargs... unchanged, so a family that admits a vector of estimators, algorithms, or results needs no method of its own. A family that needs more than the forward, such as a concrete element type (concrete_typed_array_if_abstract), defines its own more specific method.

Algorithm

The scalar method:

  1. Return a unchanged, and drop args... and kwargs.... This method is the leaf of the recursion, and it is what makes an untagged type safe to call the verb on.

The vector method:

  1. For each element ai of a, call factory on ai, and forward args... and kwargs... unchanged.
  2. Collect the results into a new vector, in the order of a, and return it.

A @propagatable struct with at least one @fprop- or @wprop-tagged field carries a generated method that dominates the scalar method. That method rebuilds the struct with its keyword constructor, sending each @fprop field through factory_child and each @wprop field through _wprop.

Arguments

  • a: Indicates no object should be constructed, or a vector whose elements are rebuilt one by one.
  • args...: Arbitrary positional arguments (ignored by the scalar method, forwarded by the vector method).
  • kwargs...: Arbitrary keyword arguments (ignored by the scalar method, forwarded by the vector method).

Returns

  • a: The input unchanged.
  • v::Vector: The element-wise rebuilds, for the vector method.

Examples

julia> factory(nothing, 1, 2; x = 3)julia> factory(MeanValue())MeanValue  w ┴ nothing

Related

source
factory(res::NonFiniteAllocationOptimisationResult, fb::Option{<:OptE_Opt_FbChain})

Rebuild a continuous optimisation result with an updated fallback record fb.

Every optimisation result carries fb as its last field, so the generic rebuild copies all fields unchanged except the trailing fb. Concrete result types may override this method when rebuilding requires more than swapping fb. optimise is the one caller, and it hands in the FbChain it walked.

Related

source
factory(
    opt::Union{NonFiniteAllocationOptimisationEstimator, NonFiniteAllocationOptimisationResult},
    _
) -> RandomWeighted{_A, var"#s185", _B, _C, _D, _E, _F, Bool} where {_A, var"#s185"<:AbstractRNG, _B, _C, _D, _E, _F}

Return opt unchanged.

Default pass-through factory for optimisation estimators and results. Overridden for estimators that carry parameters requiring update at each optimisation step.

Related

source
PortfolioOptimisers.port_opt_viewMethod
port_opt_view(
    frc::FactorRiskContribution,
    i,
    X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}},
    args...
) -> FactorRiskContribution{JuMPOptimiser{__T_pe, __T_slv, __T_wb, __T_bgt, __T_sbgt, __T_gbgt, __T_xbgt, __T_lt, __T_st, __T_lcse, __T_cte, __T_gcarde, __T_sgcarde, __T_smtx, __T_sgmtx, __T_slt, __T_sst, __T_sglt, __T_sgst, __T_tn, __T_fees, __T_sets, __T_tr, __T_ple, __T_ret, __T_sca, __T_ccnt, __T_cobj, __T_sc, __T_so, __T_ss, __T_card, __T_scard, __T_l2c, __T_lpc, __T_linfc, __T_l1, __T_l2, __T_lp, __T_linf, __T_brt, __T_x_src, __T_strict, __T_cache}, _A, _B, _C, _D, _E, _F, Bool} where {__T_pe, __T_slv, __T_wb, __T_bgt, __T_sbgt, __T_gbgt, __T_xbgt, __T_lt, __T_st, __T_lcse, __T_cte, __T_gcarde, __T_sgcarde, __T_smtx, __T_sgmtx, __T_slt, __T_sst, __T_sglt, __T_sgst, __T_tn, __T_fees, __T_sets, __T_tr, __T_ple, __T_ret, __T_sca, __T_ccnt, __T_cobj, __T_sc, __T_so, __T_ss, __T_card, __T_scard, __T_l2c, __T_lpc, __T_linfc, __T_l1, __T_l2, __T_lp, __T_linf, __T_brt, __T_x_src, __T_strict, __T_cache, _A, _B, _C, _D, _E, _F}

Return a cluster-sliced copy of FactorRiskContribution for asset index set i and returns matrix X.

source
PortfolioOptimisers.optimiseMethod
optimise(frc::FactorRiskContribution{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any,
                  <:Any, <:Any, Nothing
              },
         rd::ReturnsResult; dims::Int = 1,
         str_names::Bool = false, save::Bool = true, kwargs...) -> FactorRiskContributionResult

Run the Factor Risk Contribution portfolio optimisation.

Arguments

  • frc: The factor risk contribution optimiser to use.
  • rd: The returns result to use. If isa(frc.opt.pe, AbstractPriorResult), rd is not necessary if doing a standalone optimisation, but may be required/desired by fallbacks and/or clusterisation.
  • dims: The dimension along which observations advance in time.
  • str_names: Whether to use string names for the assets in the optimisation.
  • save: Whether to save the JuMP model in the optimisation result.
  • kwargs: Additional keyword arguments passed to the optimisation function.

Validation

  • No field in the tree of frc holds an Online. An ArgumentError naming the field is thrown otherwise, through assert_batch_entry: a plain optimise is a batch fit, and a wrapper resolves only at the warm-up of the fold loop's online arm.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[132]
T. Roncalli and G. Weisang. Risk Parity Portfolios with Risk Factors. Available at SSRN 2155159 (2012).
[133]
A. Meucci. Risk Contributions from Generic User-Defined Factors. Available at SSRN 930034 (2007).