"""Registry and factory for alphas. Built-in alphas live in :mod:`pipeline.alpha.library` and self-register via the :func:`register_alpha` decorator. External alphas authored anywhere can be made available with :func:`load_alpha_module` (a dotted module path or a ``.py`` file), which is how you test an alpha written outside this repo. """ import importlib import importlib.util import inspect from pathlib import Path from typing import Optional, Type from pipeline.alpha.base import BaseAlpha _REGISTRY: dict[str, Type[BaseAlpha]] = {} _builtins_loaded = False def register_alpha(cls: Type[BaseAlpha]) -> Type[BaseAlpha]: """Class decorator that registers an alpha under its :attr:`~BaseAlpha.name`. Raises: TypeError: If ``cls`` is not a ``BaseAlpha`` subclass. ValueError: If ``name`` is empty or already used by a different class. """ if not (isinstance(cls, type) and issubclass(cls, BaseAlpha)): raise TypeError(f"{cls!r} is not a BaseAlpha subclass") key = getattr(cls, "name", "") if not key: raise ValueError(f"{cls.__name__} must set a non-empty class attribute `name`") existing = _REGISTRY.get(key) if existing is not None and existing is not cls: raise ValueError( f"Alpha name '{key}' already registered by {existing.__name__}" ) _REGISTRY[key] = cls return cls def available_alphas() -> list[str]: """Sorted names of all registered alphas (built-ins are loaded lazily).""" _ensure_builtins() return sorted(_REGISTRY) def get_alpha(name: str, **params) -> BaseAlpha: """Instantiate a registered alpha by name. Only the parameters accepted by the alpha's ``__init__`` are forwarded, so a caller may pass a superset (e.g. both ``lookback`` and ``vol_window``) and each alpha picks what it needs. Raises: KeyError: If ``name`` is not registered. """ _ensure_builtins() if name not in _REGISTRY: raise KeyError(f"Unknown alpha '{name}'. Available: {sorted(_REGISTRY)}") cls = _REGISTRY[name] accepted = _accepted_params(cls) kwargs = params if accepted is None else {k: v for k, v in params.items() if k in accepted} return cls(**kwargs) def load_alpha_module(spec: str) -> None: """Import an external module so its ``@register_alpha`` classes register. Args: spec: A dotted module path (``my_pkg.my_alpha``) on ``sys.path``, or a filesystem path to a ``.py`` file (``/path/to/my_alpha.py``). Raises: FileNotFoundError: If a ``.py`` path is given but does not exist. """ looks_like_file = spec.endswith(".py") or Path(spec).expanduser().exists() if looks_like_file: path = Path(spec).expanduser().resolve() if not path.exists(): raise FileNotFoundError(f"Alpha module not found: {path}") module_spec = importlib.util.spec_from_file_location(path.stem, path) if module_spec is None or module_spec.loader is None: raise ImportError(f"Cannot load alpha module from {path}") module = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) else: importlib.import_module(spec) def _accepted_params(cls: Type[BaseAlpha]) -> Optional[set[str]]: """Param names ``cls.__init__`` accepts, or None if it takes ``**kwargs``.""" sig = inspect.signature(cls.__init__) if any(p.kind is p.VAR_KEYWORD for p in sig.parameters.values()): return None return {name for name in sig.parameters if name != "self"} def _ensure_builtins() -> None: global _builtins_loaded if not _builtins_loaded: import pipeline.alpha.library # noqa: F401 (importing registers built-ins) _builtins_loaded = True