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