Asset sets matrix

PortfolioOptimisers.UniverseSetsType
struct UniverseSets{__T_xkey, __T_uxkey, __T_tfkey, __T_utfkey, __T_cfkey, __T_ucfkey, __T_nikey, __T_dict} <: AbstractEstimator

Declares 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 in dict identifying the primary asset list. Required, and the axis a view slices.
  • uxkey: Key prefix for unique-entry asset group variants in dict.
  • tfkey: Key in dict identifying the time-series factor list — the columns of rd.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 in dict. Validated at construction, never recomputed by a view.
  • cfkey: Key in dict identifying the cross-sectional factor list — the exposures a cross-sectional regression fits one loading vector per observation against. Optional, and validated exactly as tfkey is; 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 in dict. Validated at construction, never recomputed by a view.
  • nikey: Key in dict identifying 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 a UniverseSets that 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}) -> UniverseSets

Keywords correspond to the struct's fields.

Validation

  • !isempty(dict).
  • haskey(dict, xkey).
  • No two of xkey, uxkey, tfkey, utfkey, cfkey, ucfkey, nikey may be a prefix of one another (42 ordered checks, which also rules out any two being equal).
  • If a key in dict starts with the same value as xkey, length(dict[k]) == length(dict[xkey]).
  • If a key in dict starts with the same value as uxkey, there must be a corresponding key in dict where the uxkey prefix is replaced by the xkey prefix, and its length must equal length(dict[xkey]).
  • If a key in dict starts with the same value as tfkey, haskey(dict, tfkey) and length(dict[k]) == length(dict[tfkey]).
  • If a key in dict starts with the same value as utfkey, there must be a corresponding key in dict where the utfkey prefix is replaced by the tfkey prefix, and its length must equal length(dict[tfkey]).
  • If a key in dict starts with the same value as cfkey, haskey(dict, cfkey) and length(dict[k]) == length(dict[cfkey]).
  • If a key in dict starts with the same value as ucfkey, there must be a corresponding key in dict where the ucfkey prefix is replaced by the cfkey prefix, and its length must equal length(dict[cfkey]).
  • If dict carries nikey, its entries are unique, and none of them is also in dict[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 of dict is sliced to the selected assets, and every uxkey-prefixed entry is rebuilt from the sliced partition it names.
  • The tfkey-, utfkey-, cfkey- and ucfkey-prefixed entries, and every plain group, are carried through unchanged. port_opt_view states why each axis is exempt.
  • The nikey entry 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

source
PortfolioOptimisers.AssetSetsMatrixEstimatorType
struct AssetSetsMatrixEstimator{__T_val} <: AbstractConstraintEstimator

Names 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) -> AssetSetsMatrixEstimator

Keywords 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  1

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.1, Equations 9.2-9.4.
source
PortfolioOptimisers.asset_sets_matrixFunction
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

  1. Read the taxonomy column all_sets from sets.dict[smtx], through taxonomy_column.
  2. Check that all_sets has the length of the asset universe.
  3. Take unique_sets = unique(all_sets), the distinct group values in order of first appearance. Each one defines a group, and the order of unique_sets is the order of the groups in the result.
  4. For each group val of unique_sets, write the indicator all_sets .== val into column i of a BitMatrix A, giving an assets × groups matrix.
  5. 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: A UniverseSets object specifying the asset universe and groupings.

Validation

  • haskey(sets.dict, smtx), via taxonomy_column.
  • Throws an AssertionError if the length of sets.dict[smtx] does not match the asset universe.

Returns

  • A: The transpose of a BitMatrix, of size (number of groups) × (number of assets), where A[i, j] == 1 if asset j belongs to group i. The orientation is groups × assets. The row order is unique(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  1

Related

source
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) or nothing.
  • args...: Additional positional arguments (ignored).

Returns

  • smtx::Option{<:MatNum}: The input matrix or nothing, unchanged.

Related

source
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

Validation

  • Delegated to the group name key method, which validates smtx.val (see asset_sets_matrix).

Returns

  • A: The transpose of a BitMatrix, of size (number of groups) × (number of assets), exactly as the group name key method returns it.

Related

source
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

Validation

  • Delegated per entry to the method that entry dispatches to.

Returns

  • A: One result per entry of smtx, in the order of smtx. Each entry keeps whatever its own method returns, so a groups × assets transpose and a passed-through matrix can sit side by side.

Related

source
PortfolioOptimisers.panel_inputFunction
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:

  1. Read the key's values off sets.dict, which raises a KeyError when the key is absent.
  2. Choose the input type: the forced one from a Pair, and otherwise CategoricalPanelInput for a string element type and NumericPanelInput for a number element type. Refuse anything else.
  3. Name the field with panel_input_name, unless name overrides it.
  4. Build the input.

The vector form takes one step:

  1. Map the scalar form over the entries.

Arguments

  • sets: The universe sets holding the key.
  • key: The key, or key => InputType to force the type.
  • keys: A vector of either form.
  • name: Panel Field name, or nothing to derive it from the key.
  • levels: Category levels, or nothing to let the builder sort them. Categorical only.
  • alg: Fill policy for the raw values.

Validation

  • haskey(sets.dict, key). Raises a KeyError.
  • 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

source
PortfolioOptimisers.port_opt_viewMethod
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

  1. For a matrix, return view(smtx, :, i), the columns of the selected assets.
  2. 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: A groups × assets membership matrix, or a vector whose entries are each such a matrix or an AssetSetsMatrixEstimator.
  • 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 of smtx, in a concretely typed array.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).