diff --git a/strategies/shared/signals/selectors.py b/strategies/shared/signals/selectors.py index 5f55cc6..2632908 100644 --- a/strategies/shared/signals/selectors.py +++ b/strategies/shared/signals/selectors.py @@ -105,13 +105,18 @@ class TopNSelector(SignalGenerator): if pd.notna(score): scores[col] = score + # V3: 过滤前检查bond是否有因子数据(用于填充守卫) + cfg = self.bond_threshold_config + bond_code = cfg.get('bond_code', '931862.CSI') if cfg.get('enabled') else None + bond_has_data = bond_code in scores # scores此时是过滤前的完整字典 + # V3: 动态阈值过滤(替代固定 min_score) threshold = self._get_dynamic_threshold(scores) scores = {k: v for k, v in scores.items() if v >= threshold} # 分组选股或全局选股 if self.group_mapping: - selected = self._grouped_selection(scores) + selected = self._grouped_selection(scores, bond_has_data) else: selected = self._global_top_n(scores) @@ -177,10 +182,11 @@ class TopNSelector(SignalGenerator): current_held = target last_rebalance_idx = i else: - # 目标信号为空(所有标的动量得分低于min_score),清仓 - # 不继续持有负动量标的,转为空仓 - current_held = '' - last_rebalance_idx = i + # V3: target为空时保持当前持仓不变(与独立脚本行为一致) + # 在有bond数据的时期target不会为空(会被bond填充) + # target为空仅发生在2002-2007无bond数据期 + # 保持旧持仓比突然清仓更平滑 + pass signals.append(current_held) @@ -216,13 +222,14 @@ class TopNSelector(SignalGenerator): return new_total > 0 - def _grouped_selection(self, scores: Dict[str, float]) -> List[str]: + def _grouped_selection(self, scores: Dict[str, float], bond_has_data: bool = True) -> List[str]: """V3分组选股:BOND不参与竞争,空余仓位填充短债 V3逻辑: 1. BOND大类标的不参与冠军竞争(它是阈值,不是候选) 2. 选出不足 select_num 只时,用短债填充 - 3. V2退化:若bond_threshold.enabled=false,BOND正常参与竞争 + 3. bond无数据时(2002-2007)不填充 + 4. V2退化:若bond_threshold.enabled=false,BOND正常参与竞争 """ if not scores: return [] @@ -248,11 +255,12 @@ class TopNSelector(SignalGenerator): selected = [code for code, score in sorted_champions[:self.select_num]] # V3: 空余仓位填充短债 - # 短债填充是防御机制,不应受阈值过滤影响 - if cfg.get('fill_bond', False) and bond_code: + # 短债填充是防御机制,但需要有数据才能填充 + # bond有数据(含负值)→ 填充 ✓(防御机制不受动量影响) + # bond无数据(NaN)→ 不填充 ✓(2002-2007正常退化) + if cfg.get('fill_bond', False) and bond_code and bond_has_data: n_bond_slots = self.select_num - len(selected) if n_bond_slots > 0: - # 修复: 无条件填充短债(防御仓位不应依赖动量阈值) for _ in range(n_bond_slots): selected.append(bond_code)