07ed6ad917
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>
34 lines
1.2 KiB
Python
34 lines
1.2 KiB
Python
"""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)
|