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>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user