Source code for quantlab.factor.predefined.roster

"""Roster factor: another factor read on a roster only.

A model built for one universe reads a market-wide factor store (the
BarraStyle store of every listed stock) on its own **Roster**: the undated
list of symbols its panel may hold. ``RosterFactor`` wraps any factor and
takes the roster dataset's whole symbol axis; the outputs keep the wrapped
factor's names, so the wrapper replaces the factor in a model's config
without anything else changing. Who is a member on a given date is decided
elsewhere (the membership a predictor is masked with), never by the roster.

The wrapper is a factor view (``quantlab.factor.view``): it reads the
wrapped factor's store, and only the factor owning every variable of that
store writes it (ADR 0029).
"""

import pandas as pd
from loguru import logger

from quantlab.factor.config import RosterConfig
from quantlab.factor.view import FactorView
from quantlab.utils.symbol_axis import on_roster


[docs] class RosterFactor(FactorView): """The outputs of ``config.factor`` on the symbols of ``config.roster``. The roster is the symbol axis of the roster dataset's store, read on every call, so it follows the store as members are added. It is cast to the type of the wrapped panel's symbol axis (an integer permaticker axis against a text one) and the symbols the wrapped panel lacks are dropped, the count logged on a read or a computation: a symbol without values is a fact of the data, not an error. The kept symbols are in the wrapped panel's order. ``read`` reads the wrapped factor's store on the kept symbols, so only those are read and fingerprinted. ``compute`` computes the wrapped factor on its whole dataset and cuts afterwards: a cross-sectional factor standardizes over the whole market, so computing it on the roster alone would change its values. The store queries are a view's (see ``FactorView``): ``store_path`` is ``None``, ``store_uses()`` reads the wrapped factor's store, ``store_range()`` is the wrapped factor's, ``owns_store()`` is false and ``build`` and ``extend`` are refused, so a live day extends the wrapped store only through its owner. Parameters ---------- config : RosterConfig The wrapped ``factor`` and the ``roster`` dataset. Examples -------- With ``styles`` a factor pinned to ``b`` of a store holding ``a, b, c`` on symbols 1..5, and ``roster`` a dataset on symbols 2 and 4: >>> factor = RosterFactor(RosterConfig(factor=styles, roster=roster)) >>> panel = factor.read("2024-01-05", "2024-01-10") >>> list(panel.data_vars), panel["symbol"].values.tolist() (['b'], ['2', '4']) >>> factor.stored_symbols() ['2', '4'] """ #: The config class ``from_config`` rebuilds this factor with. config_cls = RosterConfig # Narrower type annotation for readers and type checkers only. config: RosterConfig def _kept_symbols(self, axis: pd.Index, log: bool) -> list: """Return the symbols of ``axis`` on the roster, in ``axis`` order. The roster is cast to ``axis``'s type first; with ``log``, the count of roster symbols ``axis`` lacks is logged. """ listed = pd.Index(self.config.roster.stored_symbols()) kept = on_roster(axis, listed) dropped = listed.nunique() - len(kept) if log and dropped: logger.info( f"{self.class_name}: {dropped} of {listed.nunique()} roster symbol(s) " f"are not in {self.config.factor.class_name}'s panel and are left out." ) return kept