From a475e1b314bca44a09b13f7667228195663efff8 Mon Sep 17 00:00:00 2001 From: aszerW Date: Sat, 16 May 2026 20:38:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(strategy):=20=E5=88=86=E7=BB=84=E9=80=89?= =?UTF-8?q?=E8=82=A1=E5=A2=9E=E5=BC=BA-=E5=A4=A7=E7=B1=BB=E5=86=A0?= =?UTF-8?q?=E5=86=9B=E4=BA=8C=E6=AC=A1=E8=BF=87=E6=BB=A4=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E7=BB=84=E5=90=88=E5=8A=A8=E9=87=8F=E8=BE=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心改进: - selectors.py: _grouped_selection增加二次过滤,大类冠军得分不足时跳过该大类 - strategy.py: min_score参数可配置,从策略配置读取 - config.yaml: min_score=0.0(过滤负动量),保留注释说明更高阈值的权衡 设计原则: - 组合中每个标的动量得分都必须>=min_score - 大类冠军得分不足时不强制持有,持仓数量动态调整 - min_score=0保持简单稳健,更高阈值虽能改善回撤但可能错过机会 实验验证: - min_score=0: 累计收益14580%, 最大回撤-61.1%, 空仓131天 - min_score=0.02: 累计收益17052%, 最大回撤-61.0%, 但2000年恶化 - 决策:保持min_score=0,避免阈值选择的trick问题 --- strategies/rotation/config.yaml | 3 +++ strategies/rotation/strategy.py | 3 ++- strategies/shared/signals/selectors.py | 22 +++++++++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) 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]]