Cross-Sectional Weights: private API
Functions
PortfolioOptimisers.cross_sectional_cap_weights — Function
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
- When
pis zero, return the mask as a floating point matrix, giving every eligible pair the weight one. - Otherwise check
mcapagainst the mask, and writemcap[t, i]^pinto every eligible pair, leaving zero elsewhere.
Arguments
p::Real: Exponent applied to the market capitalisation.mcap::Option{<:MatNum}: Market capitalisation matrixobservations × assets, ornothingwhenpis zero.mask::AbstractMatrix{Bool}: Eligibility maskobservations × assets.
Validation
!isempty(mask).!isnothing(mcap)whenpis not zero.size(mcap) == size(mask).- Every eligible pair carries a finite, non-negative capitalisation. The
DomainErrornames the observation and the asset.
Returns
W0::Matrix{<:Number}: Weightsobservations × assets, zero outsidemask.
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.0Related
PortfolioOptimisers.cross_sectional_lagged_inverse_variance — Function
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
- Take the variance series of
epsthroughvariance_series. - Fill the result with
NaN, which is what row1and every pair outsidemaskkeep. Both blend components then normalise over the same universe. - Write the reciprocal of row
t - 1of the series into every eligible pair of rowt, for every later observation.
Arguments
ve: Variance estimator.eps::MatNum: First-pass residual matrixobservations × assets.mask::AbstractMatrix{Bool}: Eligibility maskobservations × assets.kwargs...: Additional keyword arguments passed tovariance_series.
Validation
!isempty(eps).size(eps) == size(mask).
Returns
IV::Matrix{<:Number}: Inverse variancesobservations × assets,NaNwhere no estimate exists and outsidemask.
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.125Related
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
- For each observation, gather the entries in the estimation universe that are not
NaN. - Take the quantiles
winsof that set, and clamp the whole row between them. - Write an all-
NaNrow where the set carries no finite entry, because no bound exists there.
Arguments
IV::Matrix{<:Number}: Inverse variancesobservations × assets, changed in place.W0::MatNum: First-pass weightsobservations × 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.IVcarries 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.0Related
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
- For each observation, record whether the row carries a finite entry.
- Take the median of the entries that are not
NaN, and cap the row atratiotimes it. - Divide the row by the sum of its entries that are not
NaN.
Arguments
IV::Matrix{<:Number}: Inverse variancesobservations × assets, changed in place.ratio::Real: Largest multiple of the cross-sectional median an entry may take.
Returns
ready::BitVector: One entry per observation,truewhere the row carries a usable inverse variance estimate. A row that answersfalseis 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.6Related