Asset sets matrix
PortfolioOptimisers.UniverseSets — Type
struct UniverseSets{__T_xkey, __T_uxkey, __T_tfkey, __T_utfkey, __T_cfkey, __T_ucfkey, __T_nikey, __T_dict} <: AbstractEstimatorDeclares the universes a portfolio problem is written against, and any groupings or partitions of them.
Constraint generation and the estimator routines read it to expand group references, to map a group name to its member list, and to validate membership.
It declares every axis it carries: xkey/uxkey for assets, tfkey/utfkey for time-series factors, cfkey/ucfkey for cross-sectional factors, and nikey for the Non-Investable Axis. Assets are the primary axis — haskey(dict, xkey) is required, and it is the axis a view slices. The factor axes are optional: requiring either would invalidate every sets object built for a problem with no factor model, so a consumer that needs one and does not find it throws at the point of need rather than at construction.
There are two factor axes because the two factor families name different things. A time-series regression fits one loading vector per asset over the observations, so its factors are the columns of rd.F and a caller copies rd.nf into the dict under tfkey. A cross-sectional regression fits one loading vector per observation across the assets, so its factors are the exposures the fit was built from, and they exist only inside the fitted block. One axis carrying both would make a single key mean two different lists on one problem. A consumer never chooses between them by hand: factor_axis_key reads the key off the loadings result it already holds.
If a key in dict starts with the same value as xkey, it means that the corresponding group must have the same length as the asset universe, dict[xkey]. This is useful for defining partitions of the asset universe, for example when using asset_sets_matrix with NestedClustered.
If a key in dict starts with the same value as uxkey, it identifies a unique-entry group variant. The corresponding xkey-prefixed group must exist in dict with the same length as the asset universe, and is used to match each asset to a unique entry from the uxkey-prefixed group. This enables constraint generation using unique entries even in NestedClustered optimisations.
The tfkey/utfkey prefixes mean the same thing on the time-series factor axis, and cfkey/ucfkey mean the same thing again on the cross-sectional one. They buy something different from the asset pair. On the asset side the conventions serve views; factors are never sliced by an asset index, so on either factor side they buy length validation at construction and one shared mental model. The two factor axes are validated alike, and neither is validated against the other: a problem may declare one, both, or neither.
A taxonomy reaches the Asset Panel through panel_input, which reads one xkey-prefixed key as one Panel Field. The panel names its own columns, so no key declares a feature axis.
nikey declares the Non-Investable Axis: the names the Investable Mask left out, which is the axis a forced liquidation is priced on. It is unlike the other six in three ways, and each is deliberate. It is minted, not authored — a door writes it after it reduces an optimiser to the Investable Mask, so a caller who states one by hand is overwritten there; authoring one is still the way to resolve a liquidation rate outside a door. It is dropped by every view, so a sets that carries it was reduced by exactly one door for exactly one problem, and a cluster of a nested optimisation can never inherit its parent's departures and charge them again. And it is bare — no prefixed partition and no unique-entry twin — because its entries are unique by construction, and a plain group already reaches it.
A key matching none of the seven prefixes is a plain group: expanded by name and axis-blind, which is why a factor group needs no machinery of its own, and why a group resolves on the Non-Investable Axis with no nikey-prefixed machinery at all.
Fields
xkey: Key indictidentifying the primary asset list. Required, and the axis a view slices.
uxkey: Key prefix for unique-entry asset group variants indict.
tfkey: Key indictidentifying the time-series factor list — the columns ofrd.F, which a time-series regression fits one loading vector per asset against. Optional — a consumer that needs it and does not find it throws at the point of need.
utfkey: Key prefix for unique-entry time-series factor group variants indict. Validated at construction, never recomputed by a view.
cfkey: Key indictidentifying the cross-sectional factor list — the exposures a cross-sectional regression fits one loading vector per observation against. Optional, and validated exactly astfkeyis; the two axes are never validated against each other, so a problem may declare one, both, or neither.
ucfkey: Key prefix for unique-entry cross-sectional factor group variants indict. Validated at construction, never recomputed by a view.
nikey: Key indictidentifying the Non-Investable Axis — the names the Investable Mask left out. Optional, and minted by a door rather than authored: a view drops it, so aUniverseSetsthat carries one was reduced by exactly one door, for exactly that problem. Bare: it names assets and admits no prefixed partition, because its entries are unique by construction and nothing resolves a group over it that a plain, axis-blind group does not already reach.
dict: Dictionary mapping group identifiers to member labels.
Constructors
UniverseSets(; xkey::AbstractString = "nx", uxkey::AbstractString = "ux", tfkey::AbstractString = "nf", utfkey::AbstractString = "uf", cfkey::AbstractString = "ncf", ucfkey::AbstractString = "ucf", nikey::AbstractString = "ni", dict::AbstractDict{<:AbstractString, <:Any}) -> UniverseSetsKeywords correspond to the struct's fields.
Validation
!isempty(dict).haskey(dict, xkey).- No two of
xkey,uxkey,tfkey,utfkey,cfkey,ucfkey,nikeymay be a prefix of one another (42 ordered checks, which also rules out any two being equal). - If a key in
dictstarts with the same value asxkey,length(dict[k]) == length(dict[xkey]). - If a key in
dictstarts with the same value asuxkey, there must be a corresponding key indictwhere theuxkeyprefix is replaced by thexkeyprefix, and its length must equallength(dict[xkey]). - If a key in
dictstarts with the same value astfkey,haskey(dict, tfkey)andlength(dict[k]) == length(dict[tfkey]). - If a key in
dictstarts with the same value asutfkey, there must be a corresponding key indictwhere theutfkeyprefix is replaced by thetfkeyprefix, and its length must equallength(dict[tfkey]). - If a key in
dictstarts with the same value ascfkey,haskey(dict, cfkey)andlength(dict[k]) == length(dict[cfkey]). - If a key in
dictstarts with the same value asucfkey, there must be a corresponding key indictwhere theucfkeyprefix is replaced by thecfkeyprefix, and its length must equallength(dict[cfkey]). - If
dictcarriesnikey, its entries are unique, and none of them is also indict[xkey]. An asset is investable or it is not, and a name on both axes would be priced twice — once as a holding and once as a forced exit.
View parameters
UniverseSets defines its own port_opt_view method rather than deriving one from field tags.
- The method reads the asset index alone. It drops every further positional argument, because no axis but the asset axis is sliced.
- Every
xkey-prefixed entry ofdictis sliced to the selected assets, and everyuxkey-prefixed entry is rebuilt from the sliced partition it names. - The
tfkey-,utfkey-,cfkey- anducfkey-prefixed entries, and every plain group, are carried through unchanged.port_opt_viewstates why each axis is exempt. - The
nikeyentry is dropped, because only a door mints one. The key itself is matched exactly rather than by prefix, so a plain group whose name merely starts with it survives. - The seven key prefixes are carried through unchanged, so the viewed value declares the same axes as the original.
Examples
julia> UniverseSets(; xkey = "nx", dict = Dict("nx" => ["A", "B", "C"], "group1" => ["A", "B"]))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("group1" => ["A", "B"], "nx" => ["A", "B", "C"])Related
PortfolioOptimisers.AssetSetsMatrixEstimator — Type
struct AssetSetsMatrixEstimator{__T_val} <: AbstractConstraintEstimatorNames the group name key a binary asset-group membership matrix is built from.
The key is read out of a UniverseSets by asset_sets_matrix, which returns one row per distinct group value and one column per asset. A row of that matrix is the set indicator a group weight constraint sums the weights over.
Fields
val: Group name key for asset set membership matrix extraction.
Constructors
AssetSetsMatrixEstimator(; val::AbstractString) -> AssetSetsMatrixEstimatorKeywords correspond to the struct's fields.
Validation
!isempty(val).
Examples
julia> sets = UniverseSets(; xkey = "nx", dict = Dict("nx" => ["A", "B", "C"], "nx_sector" => ["Tech", "Tech", "Finance"]));julia> est = AssetSetsMatrixEstimator(; val = "nx_sector")AssetSetsMatrixEstimator val ┴ String: "nx_sector"julia> asset_sets_matrix(est, sets)2×3 transpose(::BitMatrix) with eltype Bool: 1 1 0 0 0 1Related
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1, Equations 9.2-9.4.
PortfolioOptimisers.asset_sets_matrix — Function
asset_sets_matrix(
smtx::AbstractString,
sets::UniverseSets
) -> LinearAlgebra.Transpose{Bool, BitMatrix}
Construct a binary asset-group membership matrix from asset set groupings.
asset_sets_matrix generates a binary (0/1) matrix indicating asset membership in groups or categories, based on the key or group name smtx in the provided UniverseSets. Each row corresponds to a unique group value, and each column to an asset in the universe. This is used in constraint generation and portfolio construction workflows that require mapping assets to groups or categories.
Algorithm
- Read the taxonomy column
all_setsfromsets.dict[smtx], throughtaxonomy_column. - Check that
all_setshas the length of the asset universe. - Take
unique_sets = unique(all_sets), the distinct group values in order of first appearance. Each one defines a group, and the order ofunique_setsis the order of the groups in the result. - For each group
valofunique_sets, write the indicatorall_sets .== valinto columniof aBitMatrixA, giving anassets × groupsmatrix. - Return
transpose(A), which turns the groups into the rows and the assets into the columns.
Arguments
smtx: The key or group name to extract from the asset sets.sets: AUniverseSetsobject specifying the asset universe and groupings.
Validation
haskey(sets.dict, smtx), viataxonomy_column.- Throws an
AssertionErrorif the length ofsets.dict[smtx]does not match the asset universe.
Returns
A: Thetransposeof aBitMatrix, of size (number of groups) × (number of assets), whereA[i, j] == 1if assetjbelongs to groupi. The orientation isgroups × assets. The row order isunique(sets.dict[smtx]), the distinct group values in order of first appearance.
Examples
julia> sets = UniverseSets(; xkey = "nx", dict = Dict("nx" => ["A", "B", "C"], "nx_sector" => ["Tech", "Tech", "Finance"]));julia> asset_sets_matrix("nx_sector", sets)2×3 transpose(::BitMatrix) with eltype Bool: 1 1 0 0 0 1Related
asset_sets_matrix(smtx::Option{<:MatNum}, args...)No-op fallback for asset set membership matrix construction.
This method returns the input matrix smtx unchanged. It is used as a fallback when the asset set membership matrix is already provided as an MatNum or is nothing, enabling composability and uniform interface handling in constraint generation workflows.
Arguments
smtx: An existing asset set membership matrix (MatNum) ornothing.args...: Additional positional arguments (ignored).
Returns
smtx::Option{<:MatNum}: The input matrix ornothing, unchanged.
Related
asset_sets_matrix(smtx::AssetSetsMatrixEstimator, sets::UniverseSets)This method is a wrapper calling:
asset_sets_matrix(smtx.val, sets)It is used for type stability and to provide a uniform interface for processing constraint estimators, as well as simplifying the use of multiple estimators simulatneously.
Arguments
smtx: AnAssetSetsMatrixEstimatornaming the group name key to read.sets: AUniverseSetsobject specifying the asset universe and groupings.
Validation
- Delegated to the group name key method, which validates
smtx.val(seeasset_sets_matrix).
Returns
A: Thetransposeof aBitMatrix, of size (number of groups) × (number of assets), exactly as the group name key method returns it.
Related
asset_sets_matrix(smtx::VecMatNum_ASetMatE,
sets::UniverseSets)Broadcasts asset_sets_matrix over the vector.
Provides a uniform interface for processing multiple constraint estimators simulatneously.
Arguments
smtx: A vector whose entries are each anAssetSetsMatrixEstimatoror an already-built numeric matrix.sets: AUniverseSetsobject specifying the asset universe and groupings.
Validation
- Delegated per entry to the method that entry dispatches to.
Returns
A: One result per entry ofsmtx, in the order ofsmtx. Each entry keeps whatever its own method returns, so agroups × assetstranspose and a passed-through matrix can sit side by side.
Related
PortfolioOptimisers.panel_input — Function
panel_input(sets::UniverseSets, key::AbstractString; name = nothing, levels = nothing,
alg = NoPanelFill())
panel_input(sets::UniverseSets, key::Pair; name = nothing, levels = nothing,
alg = NoPanelFill())
panel_input(sets::UniverseSets, keys::AbstractVector)Turn a UniverseSets key into the raw Panel Field input asset_panel reads.
A key is one vector over the asset axis, which is the static raw form of one Panel Field. This is the one bridge between a taxonomy and the panel: a string-valued key becomes a CategoricalPanelInput and a number-valued key a NumericPanelInput, by dispatch on the key's element type. A key of mixed element type is refused, because neither reading is right and guessing one is worse than saying so.
An entry key => InputType forces the type, in both the scalar and the vector form: "nx_rating" => CategoricalPanelInput reads a numeric rating as a label rather than as a number. There is no kind keyword; the pair is the whole mechanism.
The field is named by panel_input_name. A nested taxonomy is several keys, so several fields, and the vector form maps the same rule over them.
The result is static: a taxonomy has no observation axis. It joins a time-varying panel by the lazy lift asset_panel applies, so a taxonomy sits beside a fundamentals table in one panel.
The vector form takes no per-field keyword
name, levels and alg describe one field, so the vector form does not take them: a key that needs one of them goes through the scalar form, and the two results go into the same asset_panel call. This is why the vector form is a convenience rather than the primary route.
Algorithm
The scalar form takes four steps:
- Read the key's values off
sets.dict, which raises aKeyErrorwhen the key is absent. - Choose the input type: the forced one from a
Pair, and otherwiseCategoricalPanelInputfor a string element type andNumericPanelInputfor a number element type. Refuse anything else. - Name the field with
panel_input_name, unlessnameoverrides it. - Build the input.
The vector form takes one step:
- Map the scalar form over the entries.
Arguments
sets: The universe sets holding the key.key: The key, orkey => InputTypeto force the type.keys: A vector of either form.name: Panel Field name, ornothingto derive it from the key.levels: Category levels, ornothingto let the builder sort them. Categorical only.alg: Fill policy for the raw values.
Validation
haskey(sets.dict, key). Raises aKeyError.- The key's element type is a string or a number throughout. Raises an
ArgumentError.
Returns
- From the scalar form, one
AbstractPanelFieldInput. From the vector form, one per entry.
Examples
julia> sets = UniverseSets(; dict = Dict("nx" => ["A", "B", "C"], "nx_sector" => ["Fin", "Tech", "Fin"]));julia> inp = panel_input(sets, "nx_sector");julia> inp.name"sector"julia> pnl = asset_panel([inp]);julia> panel_feature_matrix(pnl)[1]2-element Vector{String}: "sector=Fin" "sector=Tech"Related
PortfolioOptimisers.port_opt_view — Method
port_opt_view(smtx::MatNum, i, args...; kwargs...)
port_opt_view(smtx::VecMatNum_ASetMatE, i, args...; kwargs...)
port_opt_view(smtx::AbstractVector{<:AssetSetsMatrixEstimator}, i, args...; kwargs...)Take an asset view of an asset-group membership matrix, or of a vector of matrices and estimators.
The matrix method slices columns, so it expects the groups × assets matrix asset_sets_matrix returns, whose column axis is the assets. An estimator names a key rather than holding data, and the key is resolved after the view, so an estimator entry is carried through unchanged.
Algorithm
- For a matrix, return
view(smtx, :, i), the columns of the selected assets. - For a vector, take the view of each entry through the method that entry dispatches to, then narrow the element type with
concrete_typed_array_if_abstract.
Arguments
smtx: Agroups × assetsmembership matrix, or a vector whose entries are each such a matrix or anAssetSetsMatrixEstimator.i: The asset index or range to slice.args...: Additional positional arguments (ignored).kwargs...: Additional keyword arguments (ignored).
Returns
- For a matrix, a column view of size
groups × length(i). For a vector, one result per entry, in the order ofsmtx, in a concretely typed array.
Related
References
- [5]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).