fix: V3动态阈值根因2子问题和根因3修复
根因2子问题: bond无数据时不填充 - 问题: be8ca02无条件填充导致2002-2007年信号中出现931862.CSI - 但执行器找不到收益数据,2/3仓位收益为0,引发-31.92%回撤 - 修复: generate()中过滤前检查bond_has_data,传入_grouped_selection - 填充条件改为: bond_code存在 AND bond_has_data - bond有数据(含负值)→填充 ✓; bond无数据(NaN)→不填充 ✓ 根因3: 空target时保持旧持仓而非清仓 - 问题: _apply_rebalance_control中target为空时清仓 - 但独立脚本的行为是保持旧持仓 - 修复: else分支改为pass,保持current_held不变 - 在有bond数据期target不会为空,空target仅发生在2002-2007 修复后验证结果: - CAGR: 27.81% (预期28.03%, 差异-0.22%) - 最大回撤: -24.36% (预期-24.35%, 差异≈0) ✓ - 夏普: 1.40 (预期1.40) ✓ - 回撤区间: 2015-05~2016-06 (与预期一致) ✓ - Calmar: 1.14 (预期1.15) 净值: 334.46 → 394.80 (+18%)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user