Cross-Sectional Weights: private API

Functions

PortfolioOptimisers.cross_sectional_cap_weightsFunction
cross_sectional_cap_weights(
    p::Real,
    mcap::Union{Nothing, AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}},
    mask::AbstractMatrix{Bool}
) -> AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}

Return the market capitalisation weights of an eligibility mask, raised to a power.

The two members of the family share this body, so the formula is written once. A power of zero short-circuits, because the capitalisation matrix is then unread and a caller that owns no capitalisation data may pass nothing.

Algorithm

  1. When p is zero, return the mask as a floating point matrix, giving every eligible pair the weight one.
  2. Otherwise check mcap against the mask, and write mcap[t, i]^p into every eligible pair, leaving zero elsewhere.

Arguments

  • p::Real: Exponent applied to the market capitalisation.
  • mcap::Option{<:MatNum}: Market capitalisation matrix observations × assets, or nothing when p is zero.
  • mask::AbstractMatrix{Bool}: Eligibility mask observations × assets.

Validation

  • !isempty(mask).
  • !isnothing(mcap) when p is not zero.
  • size(mcap) == size(mask).
  • Every eligible pair carries a finite, non-negative capitalisation. The DomainError names the observation and the asset.

Returns

  • W0::Matrix{<:Number}: Weights observations × assets, zero outside mask.

Examples

julia> mask = [true true false];julia> PortfolioOptimisers.cross_sectional_cap_weights(0.5, [4.0 9.0 1.0], mask)1×3 Matrix{Float64}: 2.0  3.0  0.0julia> PortfolioOptimisers.cross_sectional_cap_weights(0.0, nothing, mask)1×3 Matrix{Float64}: 1.0  1.0  0.0

Related

source
PortfolioOptimisers.cross_sectional_lagged_inverse_varianceFunction
cross_sectional_lagged_inverse_variance(
    ve::AbstractCovarianceEstimator,
    eps::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}},
    mask::AbstractMatrix{Bool};
    kwargs...
) -> AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}}

Return the lagged inverse idiosyncratic variances of a residual matrix.

Row t reads the variance series at row t - 1, so an entry never carries the squared residual of its own observation. Row 1 is NaN, because no residual precedes the first observation.

Algorithm

  1. Take the variance series of eps through variance_series.
  2. Fill the result with NaN, which is what row 1 and every pair outside mask keep. Both blend components then normalise over the same universe.
  3. Write the reciprocal of row t - 1 of the series into every eligible pair of row t, for every later observation.

Arguments

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

Validation

  • !isempty(eps).
  • size(eps) == size(mask).

Returns

  • IV::Matrix{<:Number}: Inverse variances observations × assets, NaN where no estimate exists and outside mask.

Examples

julia> eps = [1.0 2.0; 3.0 6.0; 2.0 4.0];julia> PortfolioOptimisers.cross_sectional_lagged_inverse_variance(SimpleVariance(), eps,                                                                   trues(3, 2))3×2 Matrix{Float64}: NaN    NaN NaN    NaN   0.5    0.125

Related

source
PortfolioOptimisers.cross_sectional_winsorise!Function
cross_sectional_winsorise!(
    IV::Matrix{<:Number},
    W0::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}},
    wins::Tuple{Real, Real}
)

Clamp the inverse variance weights of each observation to two cross-sectional quantiles, in place.

The clamp protects the blend from an asset whose estimated variance is very small, which would otherwise carry almost the whole cross-section. The quantiles come from the estimation universe, which is the set of pairs with a positive first-pass weight, and the clamp reaches the whole row, so a pair outside that universe still leaves inside the same bounds.

Algorithm

  1. For each observation, gather the entries in the estimation universe that are not NaN.
  2. Take the quantiles wins of that set, and clamp the whole row between them.
  3. Write an all-NaN row where the set carries no finite entry, because no bound exists there.

Arguments

  • IV::Matrix{<:Number}: Inverse variances observations × assets, changed in place.
  • W0::MatNum: First-pass weights observations × assets. Only the sign of an entry is read, which names the estimation universe.
  • wins::Tuple{<:Real, <:Real}: Lower and upper quantile levels of the clamp.

Validation

  • size(IV) == size(W0).

Returns

  • nothing. IV carries the clamped weights.

Examples

julia> IV = [1.0 3.0 100.0];julia> PortfolioOptimisers.cross_sectional_winsorise!(IV, [1.0 1.0 0.0], (0.0, 1.0))julia> IV1×3 Matrix{Float64}: 1.0  3.0  3.0

Related

source
PortfolioOptimisers.cross_sectional_median_cap!Function
cross_sectional_median_cap!(
    IV::Matrix{<:Number},
    ratio::Real
) -> BitVector

Cap the inverse variance weights of each observation at a multiple of their median, then normalise them, in place.

The cap is the second of the two bounds, and it is what answers an asset whose estimated variance is exactly zero: its entry is infinite, and it leaves at ratio times the median, so the observation still returns a weight. The normalisation puts the row on the same scale as the capitalisation component, so the blend of the two realises the shrinkage the caller wrote.

Algorithm

  1. For each observation, record whether the row carries a finite entry.
  2. Take the median of the entries that are not NaN, and cap the row at ratio times it.
  3. Divide the row by the sum of its entries that are not NaN.

Arguments

  • IV::Matrix{<:Number}: Inverse variances observations × assets, changed in place.
  • ratio::Real: Largest multiple of the cross-sectional median an entry may take.

Returns

  • ready::BitVector: One entry per observation, true where the row carries a usable inverse variance estimate. A row that answers false is untouched.

Examples

julia> IV = [1.0 3.0 100.0];julia> PortfolioOptimisers.cross_sectional_median_cap!(IV, 2.0)1-element BitVector: 1julia> IV1×3 Matrix{Float64}: 0.1  0.3  0.6

Related

source