Add outlier-robust reversal_rank alpha and investable-universe filter
reversal_rank weights the 5-day reversal signal by bounded cross-sectional rank instead of z-score, so a few extreme A-share pct_change outliers (newly listed / post-suspension / limit-up names) can no longer dominate the book. compute_alpha gains an optional per-date investable-universe mask (tradable, non-ST, seasoned, top-liquidity) applied to the signal before weighting, exposed via --liquid-universe/--universe-top-n. combo combine now accepts a single alpha as an identity passthrough so a one-alpha pipeline run needs no synthetic second input. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -27,13 +27,71 @@ def _pivot_close(df: pd.DataFrame) -> pd.DataFrame:
|
||||
|
||||
def _daily_returns(close: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Compute daily returns from wide close DataFrame."""
|
||||
return close.pct_change()
|
||||
return close.pct_change(fill_method=None)
|
||||
|
||||
|
||||
def investable_universe_mask(
|
||||
data: pd.DataFrame,
|
||||
template: pd.DataFrame,
|
||||
*,
|
||||
top_n: int = 1000,
|
||||
min_history: int = 60,
|
||||
require_tradable: bool = True,
|
||||
exclude_st: bool = True,
|
||||
) -> pd.DataFrame:
|
||||
"""Build a per-date investable-universe mask aligned to ``template``.
|
||||
|
||||
A ``(date, symbol_id)`` cell is ``True`` when the name is, on that date,
|
||||
seasoned (at least ``min_history`` prior closes), currently tradable
|
||||
(``tradestatus == 1``), not flagged ST (``isST == 0``), and inside the
|
||||
``top_n`` most liquid names by trailing 20-day mean ``amount``. The mask is
|
||||
applied to the *signal* (computed on full contiguous prices), so it
|
||||
restricts only what is *held*, never the price history used to form the
|
||||
signal — that keeps ``pct_change`` correct and look-ahead free.
|
||||
|
||||
Args:
|
||||
data: Long DataFrame with at least ``symbol_id``, ``date``, ``close``,
|
||||
``amount``, ``isST``, ``tradestatus``.
|
||||
template: Wide signal (date index × ``symbol_id`` columns) to align to.
|
||||
top_n: Keep this many most-liquid names per date.
|
||||
min_history: Minimum number of observed closes before a name is eligible.
|
||||
require_tradable: Require ``tradestatus == 1`` on the date.
|
||||
exclude_st: Drop names flagged ``isST == 1``.
|
||||
|
||||
Returns:
|
||||
Boolean wide DataFrame aligned to ``template``.
|
||||
"""
|
||||
def _wide(col: str) -> pd.DataFrame:
|
||||
return (
|
||||
data.pivot_table(index="date", columns="symbol_id", values=col, aggfunc="first")
|
||||
.sort_index()
|
||||
.reindex(index=template.index, columns=template.columns)
|
||||
)
|
||||
|
||||
close = _wide("close")
|
||||
mask = close.notna()
|
||||
|
||||
seasoned = close.notna().cumsum() >= min_history
|
||||
mask &= seasoned
|
||||
|
||||
if exclude_st and "isST" in data.columns:
|
||||
mask &= _wide("isST").fillna(1) == 0
|
||||
if require_tradable and "tradestatus" in data.columns:
|
||||
mask &= _wide("tradestatus").fillna(0) == 1
|
||||
|
||||
amount = _wide("amount")
|
||||
amt_ma = amount.rolling(20, min_periods=10).mean()
|
||||
liquid_rank = amt_ma.rank(axis=1, ascending=False)
|
||||
mask &= liquid_rank <= top_n
|
||||
|
||||
return mask.fillna(False)
|
||||
|
||||
|
||||
def compute_alpha(
|
||||
data: pd.DataFrame,
|
||||
alpha_name: str,
|
||||
alpha_type: str,
|
||||
universe: dict | None = None,
|
||||
**params,
|
||||
) -> pd.DataFrame:
|
||||
"""Compute alpha weights from raw data.
|
||||
@@ -42,6 +100,11 @@ def compute_alpha(
|
||||
data: DataFrame with DATA_COLUMNS.
|
||||
alpha_name: Label stored in the ``alpha_name`` output column.
|
||||
alpha_type: Registry key of the alpha class (e.g. ``reversal``).
|
||||
universe: Optional investable-universe filter. When given, the alpha's
|
||||
raw signal is masked to the investable set (see
|
||||
:func:`investable_universe_mask`) *before* it is turned into
|
||||
weights, so unheld names get weight 0. Keys are forwarded as keyword
|
||||
arguments to :func:`investable_universe_mask`.
|
||||
**params: Constructor parameters for the alpha (e.g. ``lookback``,
|
||||
``vol_window``). Only the params the alpha's ``__init__`` accepts are
|
||||
used; extras are ignored.
|
||||
@@ -54,7 +117,12 @@ def compute_alpha(
|
||||
"""
|
||||
alpha = get_alpha(alpha_type, **params)
|
||||
close = _pivot_close(data)
|
||||
weights = alpha.weights(close)
|
||||
if universe is None:
|
||||
weights = alpha.weights(close)
|
||||
else:
|
||||
signal = alpha.signal(close)
|
||||
mask = investable_universe_mask(data, signal, **universe)
|
||||
weights = alpha.to_weights(signal.where(mask))
|
||||
|
||||
# Melt to long format
|
||||
weights_melted = weights.reset_index().melt(
|
||||
|
||||
Reference in New Issue
Block a user