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:
Yuxuan Yan
2026-06-11 17:40:28 +08:00
parent 0a6f367fbf
commit 07ed6ad917
6 changed files with 219 additions and 8 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Outlier-robust short-horizon reversal alpha."""
import pandas as pd
from pipeline.alpha.base import BaseAlpha
from pipeline.alpha.registry import register_alpha
@register_alpha
class ReversalRankAlpha(BaseAlpha):
"""Reversal weighted by cross-sectional rank instead of z-score.
The signal is the same trailing-return reversal as :class:`ReversalAlpha`,
but :meth:`to_weights` converts it with a cross-sectional rank that is then
demeaned. Rank weighting is bounded and monotone, so it does not dump the
book into a handful of extreme movers the way raw z-scoring does — the
failure mode that makes plain ``reversal`` collapse on the A-share universe,
where newly listed / post-suspension / limit-up names produce huge
``pct_change`` outliers.
"""
name = "reversal_rank"
def __init__(self, lookback: int = 5):
self.lookback = lookback
def signal(self, close: pd.DataFrame) -> pd.DataFrame:
return -close.pct_change(self.lookback, fill_method=None)
def to_weights(self, signal: pd.DataFrame) -> pd.DataFrame:
signal = signal.dropna(how="all")
ranks = signal.rank(axis=1)
weights = ranks.subtract(ranks.mean(axis=1), axis=0)
return weights.fillna(0.0)