Files
chinese-equity-quant/pipeline/alpha/registry.py
T
Yuxuan Yan 1caa63faeb refactor: class-based alpha factory + month-partitioned data pipeline
Replace the old signal/strategy/backtest modules with a decoupled
data → alpha → combo pipeline (parquet between phases, .pq extension).

Alphas:
- BaseAlpha + @register_alpha factory/plugin registry; one file per
  built-in (reversal, reversal_vol, momentum); external alphas via
  --alpha-module. Alphas are z-scored position weights, not predictors.

Data:
- baostock primary / akshare fallback, treated consistently.
- New --universe all (~5000 A-shares via query_all_stock, filtered).
- login-once batch downloader; empty-string OHLCV coerced to NaN.
- Month-partitioned dataset {output_dir}/{universe}/month=YYYY-MM/*.pq
  with chunked durability flushes; --data-path is the dataset dir.

CLI logs at INFO by default (--log-level) so progress is visible.
Docs (README, CLAUDE.md) updated incl. pipeline diagram and roadmap
TODOs for portfolio construction / backtest / paper trading.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 14:07:07 +08:00

103 lines
3.7 KiB
Python

"""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