The Listing Span

The Listing Span

A Listing Span is the interval of the price clock over which an asset is listed, one per asset column. The Span Rule derives it from the position of a gap: a leading run of gaps is an asset not yet listed, a trailing run is a delisting, and an interior gap is a suspension or a holiday on an asset that is still listed and still held. So a caller holding only prices can state a universe, because the position already carries the distinction a listing calendar would supply.

listing_span derives the span, and universe_masks projects it onto the returns clock and intersects it with finiteness, giving the two universe masks an AssetPanel carries. A return consumes the earlier price of its pair, so the projection is [first + 1, last] under padding and [first, last - 1] without it — the active mask bounds exactly the run of a column's finite returns, and an inception emits no Held Gap. The two masks then differ only inside an interior gap, which is what the fold reports and zeroes.

Both verbs work on bare arrays and carry no estimator. The type bound is AbstractMatrix{Bool}, so a caller's own declaration — a listing calendar, or a constituency that leaves and rejoins — enters at the same point and replaces the derived active mask outright. The estimation mask is never the caller's to state and is always re-derived, which is what makes emsk ⊆ amsk hold by construction. See docs/adr/0129-the-ingestion-layer-seams-at-the-listing-span-and-the-clock-draws-the-pipeline-boundary.md and docs/adr/0131-a-return-needs-two-consecutive-prices-and-a-gap-return-writes-only-the-cells-that-lack-them.md.

PortfolioOptimisers.listing_spanFunction
listing_span(X::AbstractMatrix{<:Union{Missing, <:Real}}) -> ListingSpan

Derive the Listing Span of every asset column of a price panel by the Span Rule.

The rule reads a gap's position in a price column, which is what lets a caller holding only prices state a universe: a leading run of gaps is an asset not yet listed, a trailing run is a delisting, and an interior gap — priced on both sides — is a suspension or a holiday on an asset that is still listed and still held. The first two fall outside the span and the third inside it, so the span is exactly the interval from the first priced observation to the last. A cell counts as priced when it is neither missing nor non-finite, which unifies the two conventions a source spells an absent price with.

The derivation runs once over the whole panel rather than per window. A listing calendar is a fact about the instruments, not an estimate from returns, which is what licenses that; a window-local derivation instead reads a delisting straddling the window end as dead rather than held.

Algorithm

  1. For each asset column, find the first and the last observation whose price is neither missing nor non-finite.
  2. A column with no priced observation takes the empty interval, first = 1 and last = 0.
  3. Return the two bound vectors as a PortfolioOptimisers.ListingSpan over the price clock.

Arguments

  • X: The price panel, observations × assets. An absent price is spelled missing or non-finite.

Validation

Returns

  • span::ListingSpan: The Listing Span of each asset column, on the price clock.

Examples

julia> X = [NaN 1.0 3.0            1.0 NaN 3.1            1.1 2.0 NaN];julia> span = listing_span(X)ListingSpan(3 × 3)julia> Matrix(span)3×3 Matrix{Bool}: 0  1  1 1  1  1 1  1  0

Related

source
PortfolioOptimisers.universe_masksFunction
universe_masks(
    span::AbstractMatrix{Bool},
    R::AbstractMatrix{<:Union{Missing, <:Real}}
) -> Tuple{AbstractMatrix{Bool}, BitMatrix}

Project a listing statement onto the returns clock and intersect it with finiteness, giving an Asset Panel's two universe masks.

The active mask says which assets are in the universe at each observation, and it is span projected by PortfolioOptimisers.project_span. A caller who passes their own AbstractMatrix{Bool} rather than a derived PortfolioOptimisers.ListingSpan therefore replaces the Span Rule's answer outright, and is never second-guessed. The estimation mask is the active mask intersected with the finiteness of the returns: an asset inside a Held Gap is active — still listed, still held — but has no return at that observation, so it cannot enter that observation's cross-section. It is never the caller's to state and is always re-derived, which is what makes emsk ⊆ amsk hold by construction rather than by refusal, and what keeps the library from quietly disagreeing with the numbers it emitted.

The estimation mask is a snapshot of what the conversion produced, not a view over whatever R later holds, so a value-level step that rewrites a return does not move the estimation universe under a fold already scored against it.

Which padding convention the conversion used is read off the two row counts, and this is the one place that knows: size(span, 1) == size(R, 1) is the padded case, in which the first observation survives with a non-finite return, and size(span, 1) == size(R, 1) + 1 is the unpadded case.

Algorithm

  1. Read the padding convention off the row counts of span and R.
  2. Project span onto the returns clock with PortfolioOptimisers.project_span, giving the active mask.
  3. Intersect the active mask with the finiteness of R, giving the estimation mask.

Arguments

  • span: The listing statement, price observations × assets. A PortfolioOptimisers.ListingSpan from listing_span, or a caller's own declaration.
  • R: The returns panel the conversion produced, observations × assets. An absent return is spelled missing or non-finite.

Validation

  • size(span, 2) == size(R, 2). Raises a DimensionMismatch.
  • size(span, 1) is size(R, 1) or size(R, 1) + 1. Raises a DimensionMismatch.

Returns

  • amsk: The active mask, observations × assets on the returns clock. A PortfolioOptimisers.ListingSpan when span is one, and a BitMatrix otherwise.
  • emsk::BitMatrix: The estimation mask, observations × assets on the returns clock.

Examples

julia> X = [NaN 1.0 3.0            1.0 NaN 3.1            1.1 2.0 NaN];julia> R = [NaN NaN NaN            NaN NaN 0.1            0.1 NaN NaN];julia> amsk, emsk = universe_masks(listing_span(X), R);julia> Matrix(amsk)3×3 Matrix{Bool}: 0  0  0 0  1  1 1  1  0julia> emsk3×3 BitMatrix: 0  0  0 0  0  1 1  0  0

Related

source