Source code for quantlab.model.view

"""Predictor views: predictors that answer through the predictor they wrap.

A **predictor view** wraps another predictor (a model, an ensemble or
another view) and is a predictor itself: it drops into
``BacktestConfig.model`` in place of the predictor it wraps and changes only
what it says it changes about the predictions, such as which cells it masks
(``MembershipMaskedPredictor``), where they come from (``RecordedPredictor``)
or how they are smoothed in time (``SmoothedPredictor``). ``PredictorView``
forwards every member of the backtester's ``Predictor`` protocol
(``quantlab.backtest.base``) to the wrapped predictor once, so views of
views compose.

The protocol lives in the backtest layer, which the model layer does not
import (ADR 0022): the view forwards it structurally, and
``tests/test_predictor_view.py`` checks it forwards every protocol member.
"""

import dataclasses
from pathlib import Path
from typing import Self

import xarray as xr

from quantlab.core.component import Component
from quantlab.model.config import PredictorViewConfig


[docs] class PredictorView(Component): """Root class of the views of ``config.predictor``. Every protocol member is the wrapped predictor's, through the view: ========================= ============================================== ``labels`` the wrapped predictor's ``train_bounds`` the wrapped predictor's ``test_bounds`` the wrapped predictor's ``fitted_train_bounds`` the wrapped predictor's ``label_delays`` the wrapped predictor's ``label_scales`` the wrapped predictor's ``predict_window`` the wrapped predictor's, then ``_view_predictions`` ``collect``, ``load`` called on the wrapped predictor; return the view ``train`` the wrapped predictor's checkpoint ``check_checkpoint`` the wrapped predictor's ``config`` ``config_cls`` filled from the view's attributes of the same names ``from_config`` the config's fields, components rebuilt, passed to the constructor ========================= ============================================== A subclass declares ``config_cls`` (a subclass of ``PredictorViewConfig``), keeps each config field as an attribute of the same name, takes the fields as constructor arguments, and overrides ``_view_predictions`` to change the predictions; a view that predicts differently (a wider request, a record) overrides ``predict_window``, and one that changes a label's scale or the checkpoint handling overrides that member. Parameters ---------- predictor : Predictor The wrapped predictor. Attributes ---------- predictor : Predictor The wrapped predictor. Examples -------- With ``model`` a loaded predictor: >>> class Doubled(PredictorView): ... def _view_predictions(self, predictions): ... return predictions * 2 >>> view = Doubled(model) >>> view.train_bounds == model.train_bounds, view.label_scales == model.label_scales (True, True) >>> window = ("2024-01-01", "2024-01-12") >>> bool(view.predict_window(*window).equals(model.predict_window(*window) * 2)) True >>> view.load("ckpt.joblib") is view True """ #: The config class the view is serialised and rebuilt with. config_cls = PredictorViewConfig def __init__(self, predictor) -> None: """Initialize the view; see the class docstring for parameters.""" self.predictor = predictor def _view_predictions(self, predictions: xr.Dataset) -> xr.Dataset: """Return ``predictions``, the wrapped predictor's, as the view gives them.""" return predictions @property def labels(self) -> list: """The wrapped predictor's labels. Examples -------- >>> view.labels is model.labels True """ return self.predictor.labels @property def train_bounds(self) -> tuple: """The wrapped predictor's ``(start, end)`` training window. Examples -------- >>> view.train_bounds == model.train_bounds True """ return self.predictor.train_bounds @property def test_bounds(self) -> tuple: """The wrapped predictor's ``(start, end)`` test window. Examples -------- >>> view.test_bounds == model.test_bounds True """ return self.predictor.test_bounds @property def fitted_train_bounds(self) -> tuple: """The wrapped predictor's fitted ``(start, end)`` training window. Examples -------- >>> view.fitted_train_bounds == model.fitted_train_bounds True """ return self.predictor.fitted_train_bounds @property def label_delays(self) -> tuple[int, ...]: """The wrapped predictor's label delays; a view moves no bar. Examples -------- >>> view.label_delays == model.label_delays True """ return self.predictor.label_delays @property def label_scales(self) -> dict[str, str]: """The wrapped predictor's label scales. Examples -------- >>> view.label_scales == model.label_scales True """ return self.predictor.label_scales
[docs] def predict_window(self, start, end) -> xr.Dataset: """The wrapped predictor's predictions over the window, through ``_view_predictions``. Examples -------- >>> bool(view.predict_window(*window).equals(model.predict_window(*window) * 2)) True """ return self._view_predictions(self.predictor.predict_window(start, end))
[docs] def collect(self) -> Self: """Collect the wrapped predictor's training data; returns the view. Examples -------- >>> view.collect() is view True """ self.predictor.collect() return self
[docs] def train(self) -> Path: """Train the wrapped predictor; returns its checkpoint. Examples -------- >>> view.train() == model.train() True """ return self.predictor.train()
[docs] def load(self, path) -> Self: """Load ``path`` into the wrapped predictor; returns the view. Examples -------- >>> view.load("ckpt.joblib") is view True """ self.predictor.load(path) return self
[docs] def check_checkpoint(self, path) -> None: """Validate ``path`` with the wrapped predictor, without loading it. Examples -------- >>> view.check_checkpoint("ckpt.joblib") # raises if it does not fit """ self.predictor.check_checkpoint(path)
@property def config(self) -> PredictorViewConfig: """``config_cls`` filled from the view's attributes; ``get_config()`` serialises it. Examples -------- >>> view.config.predictor is model True """ return self.config_cls( **{ spec.name: getattr(self, spec.name) for spec in dataclasses.fields(self.config_cls) } )
[docs] @classmethod def from_config(cls, config: dict, run_dir=None) -> Self: """Rebuild the view and the predictor it wraps from ``get_config()``. The component fields are rebuilt by the component rule, with ``run_dir`` passed down, and every field is passed to the constructor by name. Examples -------- >>> from quantlab.model.predefined.smoothed import SmoothedPredictor >>> smoothed = SmoothedPredictor(None, halflife_bars=2.5) >>> rebuilt = SmoothedPredictor.from_config(smoothed.get_config()) >>> rebuilt.halflife_bars, rebuilt.warmup_bars, rebuilt.predictor (2.5, 25, None) """ return cls(**cls._rebuilt_fields(config, run_dir))