fix: preserve NaNs in cross-sectional ranks
This commit is contained in:
@@ -46,9 +46,13 @@ class MomentumQualityStrategy(Strategy):
|
|||||||
inv_dd = rolling_max_dd(data, self.quality_window)
|
inv_dd = rolling_max_dd(data, self.quality_window)
|
||||||
|
|
||||||
# --- Cross-sectional ranking ---
|
# --- Cross-sectional ranking ---
|
||||||
mom_rank = momentum.rank(axis=1, pct=True, na_option="bottom")
|
# na_option="keep" so NaN stocks stay NaN in the composite. With
|
||||||
con_rank = consistency.rank(axis=1, pct=True, na_option="bottom")
|
# "bottom" + default ascending=True, NaN entries receive pct=1.0 and
|
||||||
dd_rank = inv_dd.rank(axis=1, pct=True, na_option="bottom")
|
# the additive composite ends up maximal for NaN rows, silently
|
||||||
|
# selecting pre-IPO / delisted names as "top".
|
||||||
|
mom_rank = momentum.rank(axis=1, pct=True, na_option="keep")
|
||||||
|
con_rank = consistency.rank(axis=1, pct=True, na_option="keep")
|
||||||
|
dd_rank = inv_dd.rank(axis=1, pct=True, na_option="keep")
|
||||||
|
|
||||||
# Composite: momentum 50%, consistency 25%, drawdown 25%
|
# Composite: momentum 50%, consistency 25%, drawdown 25%
|
||||||
scores = 0.50 * mom_rank + 0.25 * con_rank + 0.25 * dd_rank
|
scores = 0.50 * mom_rank + 0.25 * con_rank + 0.25 * dd_rank
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ class MultiFactorStrategy(Strategy):
|
|||||||
value = stock.rolling(self.value_period).min() / stock
|
value = stock.rolling(self.value_period).min() / stock
|
||||||
|
|
||||||
# --- Cross-sectional ranking (each row ranked across assets) ---
|
# --- Cross-sectional ranking (each row ranked across assets) ---
|
||||||
mom_rank = momentum.rank(axis=1, pct=True, na_option="bottom")
|
# na_option="keep" so NaN stocks (pre-IPO / delisted / masked) stay
|
||||||
val_rank = value.rank(axis=1, pct=True, na_option="bottom")
|
# NaN in the composite score instead of being assigned pct=1.0.
|
||||||
|
mom_rank = momentum.rank(axis=1, pct=True, na_option="keep")
|
||||||
|
val_rank = value.rank(axis=1, pct=True, na_option="keep")
|
||||||
scores = mom_rank + val_rank # combined score, higher = better
|
scores = mom_rank + val_rank # combined score, higher = better
|
||||||
|
|
||||||
# --- Select top_n assets per row ---
|
# --- Select top_n assets per row ---
|
||||||
|
|||||||
@@ -36,9 +36,13 @@ class RecoveryMomentumStrategy(Strategy):
|
|||||||
# Factor 2: 12-1 month momentum
|
# Factor 2: 12-1 month momentum
|
||||||
momentum = data.shift(self.mom_skip).pct_change(self.mom_lookback - self.mom_skip)
|
momentum = data.shift(self.mom_skip).pct_change(self.mom_lookback - self.mom_skip)
|
||||||
|
|
||||||
# Cross-sectional percentile ranks
|
# Cross-sectional percentile ranks. na_option="keep" is critical:
|
||||||
rec_rank = recovery.rank(axis=1, pct=True, na_option="bottom")
|
# with "bottom" + ascending=True default, NaN stocks get pct=1.0, so
|
||||||
mom_rank = momentum.rank(axis=1, pct=True, na_option="bottom")
|
# non-member / delisted / pre-IPO names leak into the composite as
|
||||||
|
# "top" candidates. "keep" propagates NaN and the final ascending=False
|
||||||
|
# + "bottom" rank pushes them to the end where they are not selected.
|
||||||
|
rec_rank = recovery.rank(axis=1, pct=True, na_option="keep")
|
||||||
|
mom_rank = momentum.rank(axis=1, pct=True, na_option="keep")
|
||||||
|
|
||||||
# Composite score (50/50)
|
# Composite score (50/50)
|
||||||
composite = 0.5 * rec_rank + 0.5 * mom_rank
|
composite = 0.5 * rec_rank + 0.5 * mom_rank
|
||||||
|
|||||||
Reference in New Issue
Block a user