1caa63faeb
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>
58 lines
2.1 KiB
Python
58 lines
2.1 KiB
Python
"""Base class for alphas.
|
||
|
||
An alpha maps a wide close matrix (date index × symbol_id columns) to signed
|
||
position weights. Subclasses implement :meth:`signal` — the raw, unnormalized
|
||
score. The base class turns a signal into cross-sectionally z-scored weights
|
||
via :meth:`to_weights` (override it for a different normalization).
|
||
"""
|
||
from abc import ABC, abstractmethod
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
|
||
class BaseAlpha(ABC):
|
||
"""A position-weight alpha over a cross-section of stocks.
|
||
|
||
Concrete subclasses must set a unique class-level :attr:`name` (the registry
|
||
key) and implement :meth:`signal`. Construct subclasses with their own typed
|
||
parameters (e.g. ``lookback``); the factory passes only the parameters a
|
||
given ``__init__`` accepts.
|
||
"""
|
||
|
||
#: Unique registry key. Every concrete alpha must set this to a non-empty str.
|
||
name: str = ""
|
||
|
||
@abstractmethod
|
||
def signal(self, close: pd.DataFrame) -> pd.DataFrame:
|
||
"""Compute the raw signal.
|
||
|
||
Args:
|
||
close: Wide close prices, date index × ``symbol_id`` columns.
|
||
|
||
Returns:
|
||
A wide DataFrame aligned to ``close`` where higher values indicate a
|
||
stronger long. Use NaN where the signal is undefined.
|
||
"""
|
||
|
||
def to_weights(self, signal: pd.DataFrame) -> pd.DataFrame:
|
||
"""Cross-sectionally z-score a signal into signed position weights.
|
||
|
||
Each date is demeaned and scaled by its cross-sectional std; undefined
|
||
cells become a 0 weight. Override for a custom scheme (rank, neutralized,
|
||
capped, etc.).
|
||
"""
|
||
signal = signal.dropna(how="all")
|
||
demeaned = signal.subtract(signal.mean(axis=1), axis=0)
|
||
std = signal.std(axis=1).replace(0, np.nan)
|
||
weights = demeaned.divide(std, axis=0)
|
||
return weights.fillna(0.0)
|
||
|
||
def weights(self, close: pd.DataFrame) -> pd.DataFrame:
|
||
"""Full pipeline for one alpha: raw signal → normalized weights."""
|
||
return self.to_weights(self.signal(close))
|
||
|
||
def __repr__(self) -> str:
|
||
params = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
|
||
return f"{type(self).__name__}({params})"
|