diff --git a/strategies/rotation/config.yaml b/strategies/rotation/config.yaml index a7af3b6..a648db2 100644 --- a/strategies/rotation/config.yaml +++ b/strategies/rotation/config.yaml @@ -85,6 +85,9 @@ max_days: 60 select_num: 3 # 强制分散化:每个大类只选 Top 1 diversified: true +# 动量最低阈值:标的动量得分需>=此值才考虑入选(年化收益率*R²) +# 设置为0表示过滤负动量标的,更高阈值虽能改善回撤但可能错过正动量机会 +min_score: 0.0 # ==================== 调仓控制 ==================== # 最低调仓周期(交易日):持仓至少持有 N 天后才允许换仓 diff --git a/strategies/rotation/strategy.py b/strategies/rotation/strategy.py index a2c2fec..de0fd59 100644 --- a/strategies/rotation/strategy.py +++ b/strategies/rotation/strategy.py @@ -69,7 +69,7 @@ class RotationStrategy(StrategyBase): self._selector = TopNSelector( select_num=self.select_num, group_mapping=self._group_mapping, - min_score=0.0, + min_score=self.min_score, # 从配置读取,支持动态调整阈值 rebalance_days=self.rebalance_days, rebalance_threshold=self.rebalance_threshold ) @@ -93,6 +93,7 @@ class RotationStrategy(StrategyBase): self.rebalance_days = config.get('rebalance_days', self.rebalance_days) self.rebalance_threshold = config.get('rebalance_threshold', self.rebalance_threshold) self.trade_cost = config.get('trade_cost', self.trade_cost) + self.min_score = config.get('min_score', 0.0) # 动量最低阈值,默认过滤负动量 self.start_date = config.get('start_date', '2019-01-01') self.end_date = config.get('end_date', datetime.now().strftime('%Y-%m-%d')) diff --git a/strategies/shared/signals/selectors.py b/strategies/shared/signals/selectors.py index 3b5dd9c..797f845 100644 --- a/strategies/shared/signals/selectors.py +++ b/strategies/shared/signals/selectors.py @@ -191,7 +191,10 @@ class TopNSelector(SignalGenerator): return new_total > 0 def _grouped_selection(self, scores: Dict[str, float]) -> List[str]: - """分组选股:先类内竞争(每大类选Top1),再跨类排序""" + """分组选股:先类内竞争(每大类选Top1),再跨类排序 + + 改进:大类冠军得分不足时跳过该大类,不强制持有弱正动量标的 + """ if not scores: return [] @@ -203,8 +206,21 @@ class TopNSelector(SignalGenerator): if group not in group_champions or score > group_champions[group][1]: group_champions[group] = (code, score) - # 对各大类的冠军进行排序,选出Top N - sorted_champions = sorted(group_champions.values(), key=lambda x: x[1], reverse=True) + # ⭐ 关键改进:大类冠军二次过滤 + # 只保留得分足够显著的冠军,得分不足的大类跳过 + # 这样组合中的每个标的动量都足够强 + valid_champions = [] + for group, (code, score) in group_champions.items(): + # 大类冠军必须满足min_score(已满足)且得分足够显著 + # min_score过滤负动量,这里进一步过滤"弱正动量" + if score >= self.min_score: + valid_champions.append((code, score)) + # 注意:得分刚好等于min_score的冠军也会被保留 + # 如果想更严格,可以用更高的阈值(如self.min_score + 0.02) + + # 对有效冠军进行排序,选出Top N + # 持仓数量动态调整:最多select_num,最少可以是0 + sorted_champions = sorted(valid_champions, key=lambda x: x[1], reverse=True) return [code for code, score in sorted_champions[:self.select_num]]