Cross-Sectional Weights

Types

PortfolioOptimisers.AbstractCrossSectionalWeightsAlgorithmType
abstract type AbstractCrossSectionalWeightsAlgorithm <: AbstractAlgorithm

Abstract supertype for all cross-sectional regression weight policy types.

A member says what weight an asset carries in the cross-sectional fit of an observation, and whether that weight is decided once or twice. A one-pass member reads the cross-section alone. A two-pass member reads the residuals of a first fit, so the caller runs the regression again with the weights the member returns.

All concrete and/or abstract types representing cross-sectional weight policies should be subtypes of AbstractCrossSectionalWeightsAlgorithm.

Interfaces

In order to implement a new concrete type that works seamlessly with the library, subtype AbstractCrossSectionalWeightsAlgorithm and implement the following methods:

cs_weights_initial

Arguments

  • alg: The concrete subtype instance.
  • mcap: Market capitalisation matrix observations × assets, or nothing.
  • mask: Eligibility mask observations × assets.

Returns

  • W0::Matrix{<:Number}: First-pass weights observations × assets, zero outside mask.

cs_weights_refine

Arguments

  • alg: The concrete subtype instance.
  • W0: First-pass weights observations × assets.
  • eps: First-pass residual matrix observations × assets.
  • ve: Variance estimator.
  • mask: Eligibility mask observations × assets.

Returns

  • W1::Matrix{<:Number}: Second-pass weights observations × assets, zero outside mask.

needs_second_pass

Arguments

  • alg: The concrete subtype instance.

Returns

Examples

julia> struct MyWeights <: PortfolioOptimisers.AbstractCrossSectionalWeightsAlgorithm endjulia> PortfolioOptimisers.needs_second_pass(MyWeights())false

Related

source
PortfolioOptimisers.MarketCapWeightsType
struct MarketCapWeights{__T_p} <: AbstractCrossSectionalWeightsAlgorithm

Weights an asset by a power of its market capitalisation, in one pass.

The power sets how concentrated the cross-section is: 0 gives every eligible asset the same weight, 1 gives raw capitalisation weights, and a value between the two shrinks the concentration of the largest assets.

Mathematical definition

\[\begin{align} w_{t,i} &= \begin{cases} m_{t,i}^{p} & \text{if } (t, i) \in \mathcal{M} \\ 0 & \text{otherwise} \end{cases}\,. \end{align}\]

Where:

  • $w_{t,i}$: Weight of asset $i$ at observation $t$.
  • $m_{t,i}$: Market capitalisation of asset $i$ at observation $t$.
  • $p$: Market capitalisation power.
  • $\mathcal{M}$: Eligibility mask.

The weights are relative, so they need not sum to one. A weighted least squares is invariant to their scale.

Fields

  • p: Exponent applied to the market capitalisation. A value of 0 needs no capitalisation matrix, because every eligible asset takes the same weight.

Constructors

MarketCapWeights(; p::Real = 0.5) -> MarketCapWeights

Keywords correspond to the struct's fields.

Validation

  • isfinite(p).
  • p >= 0.

Examples

julia> MarketCapWeights()MarketCapWeights  p ┴ Float64: 0.5

Related

source
PortfolioOptimisers.BlendedInverseVarianceWeightsType
struct BlendedInverseVarianceWeights{__T_p, __T_lambda, __T_ratio, __T_wins} <: AbstractCrossSectionalWeightsAlgorithm

Blends market capitalisation weights with inverse idiosyncratic variance weights, in two passes.

The blend is a two-step feasible generalised least squares: a first fit under capitalisation weights gives the residuals, a variance estimated from those residuals gives the second component, and the caller fits again. The variance paired with observation t reads residuals up to t - 1 only, so a weight never reads the return it multiplies.

Mathematical definition

\[\begin{align} u_{t,i} &= \min\left(\mathrm{clip}\left(\frac{1}{v_{t-1,i}},\, q_{t}^{\mathrm{lo}},\, q_{t}^{\mathrm{hi}}\right),\, \rho \, \mathrm{med}_{j}\left(u_{t,j}\right)\right) \\ w_{t,i} &= \lambda \frac{u_{t,i}}{\sum_{j} u_{t,j}} + \left(1 - \lambda\right) \frac{m_{t,i}^{p}}{\sum_{j} m_{t,j}^{p}}\,. \end{align}\]

Where:

  • $w_{t,i}$: Weight of asset $i$ at observation $t$.
  • $v_{t-1,i}$: Idiosyncratic variance of asset $i$ after observation $t - 1$.
  • $q_{t}^{\mathrm{lo}}$, $q_{t}^{\mathrm{hi}}$: Cross-sectional quantiles of the inverse variances at the levels wins.
  • $\rho$: Median ratio cap.
  • $\lambda$: Blend coefficient.
  • $m_{t,i}$: Market capitalisation of asset $i$ at observation $t$.
  • $p$: Market capitalisation power.

Both components are normalised over the same eligible set before the blend, so the realised blend equals the nominal $\lambda$. An observation with no variance estimate yet takes the capitalisation weights alone.

Fields

  • p: Exponent applied to the market capitalisation of the first blend component. A value of 0 needs no capitalisation matrix, because every eligible asset takes the same weight.
  • lambda: Blend coefficient. It is the share of the inverse variance component in the second-pass weights, so 0 recovers the capitalisation weights and 1 drops them.
  • ratio: Largest multiple of the cross-sectional median an inverse variance weight may take. It caps the influence of an asset whose estimated variance is very small.
  • wins: Lower and upper quantile levels of the cross-sectional winsorisation of the inverse variances, applied before the median cap.

Constructors

BlendedInverseVarianceWeights(;    p::Real = 0.5,    lambda::Real,    ratio::Real = 20.0,    wins::Tuple{<:Real, <:Real} = (0.025, 0.975)) -> BlendedInverseVarianceWeights

Keywords correspond to the struct's fields. lambda takes no default, because naming this member is the request for the second pass. A caller that wants one pass writes MarketCapWeights instead.

Validation

  • isfinite(p) and p >= 0.
  • 0 <= lambda <= 1.
  • isfinite(ratio) and ratio > 0.
  • 0 <= wins[1] < wins[2] <= 1.

Examples

julia> BlendedInverseVarianceWeights(; lambda = 0.5)BlendedInverseVarianceWeights       p ┼ Float64: 0.5  lambda ┼ Float64: 0.5   ratio ┼ Float64: 20.0    wins ┴ Tuple{Float64, Float64}: (0.025, 0.975)

Related

source

Functions

PortfolioOptimisers.cs_weights_initialFunction
cs_weights_initial(alg::MarketCapWeights, mcap::Option{<:MatNum},
                   mask::AbstractMatrix{Bool}) -> Matrix{<:Number}
cs_weights_initial(alg::BlendedInverseVarianceWeights, mcap::Option{<:MatNum},
                   mask::AbstractMatrix{Bool}) -> Matrix{<:Number}

Return the first-pass cross-sectional regression weights of a weight policy.

Both members read the capitalisation the same way, so both call cross_sectional_cap_weights. They part on what the caller does next: needs_second_pass is false for one and true for the other.

Arguments

  • alg: Cross-sectional weight policy.
  • mcap::Option{<:MatNum}: Market capitalisation matrix observations × assets, or nothing when the policy's p is zero.
  • mask::AbstractMatrix{Bool}: Eligibility mask observations × assets.

Validation

Returns

  • W0::Matrix{<:Number}: First-pass weights observations × assets, zero outside mask.

Examples

julia> PortfolioOptimisers.cs_weights_initial(MarketCapWeights(; p = 1.0), [2.0 3.0 5.0],                                              [true true false])1×3 Matrix{Float64}: 2.0  3.0  0.0

Related

source
PortfolioOptimisers.cs_weights_refineFunction
cs_weights_refine(alg::BlendedInverseVarianceWeights, W0::MatNum, eps::MatNum,
                  ve::AbstractCovarianceEstimator, mask::AbstractMatrix{Bool};
                  kwargs...) -> Matrix{<:Number}

Return the second-pass cross-sectional regression weights of a weight policy.

The verb takes the first-pass weights and residuals as arguments rather than reading them off the policy, because a weight policy is configuration and holds no fitted data. A caller runs it only when needs_second_pass answers true. An observation whose eligible first-pass weights sum to zero returns NaN rather than a weight, because both components are normalised. An eligible pair carries a positive weight whenever alg.p is zero or its market capitalisation is positive, so a caller reaches that state only by handing in a weight matrix that cs_weights_initial did not build.

Algorithm

  1. Take the lagged inverse variances through cross_sectional_lagged_inverse_variance.
  2. Clamp them to the cross-sectional quantiles through cross_sectional_winsorise!.
  3. Cap and normalise them through cross_sectional_median_cap!, which also names the observations that carry an estimate.
  4. Normalise the first-pass weights over the same universe.
  5. Write zero over every entry that is still NaN, so a missing entry does not contribute.
  6. Write the normalised first-pass weights into every observation that carries no estimate.
  7. Blend the two components by alg.lambda, and write zero outside mask.

Arguments

  • alg: Cross-sectional weight policy.
  • W0::MatNum: First-pass weights observations × assets.
  • eps::MatNum: First-pass residual matrix observations × assets.
  • ve: Variance estimator.
  • mask::AbstractMatrix{Bool}: Eligibility mask observations × assets.
  • kwargs...: Additional keyword arguments passed to variance_series.

Validation

Returns

  • W1::Matrix{<:Number}: Second-pass weights observations × assets, zero outside mask.

Examples

julia> eps = [1.0 2.0; 3.0 6.0; 2.0 4.0];julia> alg = BlendedInverseVarianceWeights(; p = 0.0, lambda = 1.0);julia> W0 = PortfolioOptimisers.cs_weights_initial(alg, nothing, trues(3, 2));julia> PortfolioOptimisers.cs_weights_refine(alg, W0, eps, SimpleVariance(), trues(3, 2))3×2 Matrix{Float64}: 0.5    0.5 0.5    0.5 0.785  0.215

Related

source
PortfolioOptimisers.needs_second_passFunction
needs_second_pass(alg::AbstractCrossSectionalWeightsAlgorithm) -> Bool

Return whether a weight policy asks the caller to fit the cross-sectional regression twice.

The trait keeps the fit loop in the caller, which reads top to bottom, so the weight family and the regression family never name each other's types. It follows needs_previous_weights, which the tracking family uses the same way.

Arguments

  • alg: Cross-sectional weight policy.

Returns

  • val::Bool: true when the caller must run cs_weights_refine and fit again, false when the first-pass weights are the answer.

Examples

julia> PortfolioOptimisers.needs_second_pass(MarketCapWeights())falsejulia> PortfolioOptimisers.needs_second_pass(BlendedInverseVarianceWeights(; lambda = 0.5))true

Related

source