Source code for factor_agent.derived
"""Derived columns: columns a daily factor's graph reads that its dataset does not hold.
A ``DerivedColumnsFactor`` is a KunQuant factor on the daily dataset whose
graph may read derived columns, filled before the graph runs by its
providers (``Provider``): period lags (``factor_agent.period_lag``), from
the daily panel. Each provider says which columns it fills, the dataset
columns it reads and the warm-up it needs, and fills the arrays; the factor
reads the longest warm-up any of them needs.
"""
from collections.abc import Iterable, Sequence
import numpy as np
import xarray as xr
from quantlab.dataset.base import BaseDataset
from quantlab.factor.kunquant import FactorKunQuant
[docs]
class Provider:
"""Fills derived columns of a ``DerivedColumnsFactor``.
``columns`` are the derived columns it fills; ``data_columns`` the
dataset columns it reads, ``panel_columns`` those of them it reads from
the panel only, never handed to the graph as arrays (a date key);
``warmup_bars`` the bars before the first computed bar it reads.
"""
#: The derived columns the provider fills.
columns: tuple[str, ...] = ()
#: The dataset columns it reads.
data_columns: tuple[str, ...] = ()
#: The dataset columns it reads from the panel only.
panel_columns: tuple[str, ...] = ()
#: The bars before the first computed bar it reads.
warmup_bars: int = 0
[docs]
def fill(
self, dataset: BaseDataset, panel: xr.Dataset, arrays: dict[str, np.ndarray],
symbols: np.ndarray, timestamps: np.ndarray,
) -> dict[str, np.ndarray]:
"""The arrays of ``columns``, on the axes of ``arrays`` (``timestamps`` by ``symbols``).
``panel`` is the dataset's panel under its shared names
(``to_shared_names``), warm-up included; ``arrays`` its columns as the
graph reads them.
"""
raise NotImplementedError
[docs]
def data_columns(inputs: Iterable[str], providers: Iterable[Provider]) -> tuple[str, ...]:
"""The dataset columns to read for graph ``inputs``: each derived column becomes its provider's, sorted."""
inputs, providers = set(inputs), list(providers)
columns = inputs.difference(*(provider.columns for provider in providers))
for provider in providers:
if inputs.intersection(provider.columns):
columns |= set(provider.data_columns)
return tuple(sorted(columns))
[docs]
class DerivedColumnsFactor(FactorKunQuant):
"""A KunQuant factor whose graph may read derived columns; see the module docstring.
A subclass says which providers fill the derived columns its graph reads
(``_providers``) and holds their dataset columns in ``config.data_columns``
(``data_columns``). The factor reads at least the warm-up its providers need.
A graph reading derived columns only (session columns, say) reads one
column of the dataset still, for the panel's dates and symbols.
"""
def _providers(self) -> Sequence[Provider]:
"""The providers of the derived columns the graph reads; none by default."""
return ()
@property
def warmup_bars(self) -> int:
"""``config.warmup_bars``, or the longest warm-up a provider needs when that is longer."""
return max([self.config.warmup_bars, *(provider.warmup_bars for provider in self._providers())])
def _input_variables(self) -> list[str]:
read = super()._input_variables()
if read:
return read
names = [n for n in self.config.dataset.head(0).collect_schema().names() if n not in ("timestamp", "symbol")]
return self.config.dataset.own_names(names[:1])
def _kunquant_inputs(self, inputs: xr.Dataset) -> tuple[dict[str, np.ndarray], np.ndarray, np.ndarray]:
providers = self._providers()
if not providers:
return super()._kunquant_inputs(inputs)
hidden = {column for provider in providers for column in provider.panel_columns}
values = tuple(c for c in self.config.data_columns if c not in hidden)
dataset = self.config.dataset
arrays, symbols, timestamps = dataset.to_kunquant(data_columns=values, panel=inputs)
panel = dataset.to_shared_names(inputs)
filled = {}
for provider in providers:
filled |= provider.fill(dataset, panel, arrays, symbols, timestamps)
return {**arrays, **filled}, symbols, timestamps