feat(strategy): 分组选股增强-大类冠军二次过滤确保组合动量达标

核心改进:
- 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问题
This commit is contained in:
2026-05-16 20:38:57 +08:00
parent 788120387a
commit a475e1b314
3 changed files with 24 additions and 4 deletions

View File

@@ -85,6 +85,9 @@ max_days: 60
select_num: 3
# 强制分散化:每个大类只选 Top 1
diversified: true
# 动量最低阈值:标的动量得分需>=此值才考虑入选(年化收益率*R²
# 设置为0表示过滤负动量标的更高阈值虽能改善回撤但可能错过正动量机会
min_score: 0.0
# ==================== 调仓控制 ====================
# 最低调仓周期(交易日):持仓至少持有 N 天后才允许换仓

View File

@@ -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'))

View File

@@ -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]]