fix: GlobalRotationStrategy select_num 未生效

- 修复分散化选股逻辑:每个 group 选 Top 1 后,需要再按动量排序选 Top select_num
- 之前:所有 group 的 Top 1 都标记为信号(忽略 select_num)
- 现在:先从每个 group 选 Top 1,再从中按动量选 Top select_num 个
- 影响:配置 select_num=3 时,实际持仓 3 只而不是 4 只(group 数量)
This commit is contained in:
2026-05-25 01:23:00 +08:00
parent 2be81ba00d
commit e8e4e9c3ac
3 changed files with 266 additions and 36 deletions

View File

@@ -87,6 +87,24 @@ def main():
trading_calendar = strategy._get_trading_calendar()
print(f" A 股交易日: {len(trading_calendar)}")
# 准备收盘价和溢价率数据
print("[7.5] 准备价格和溢价率数据...")
index_close_dict = {} # 指数收盘价
etf_close_dict = {} # ETF 收盘价
etf_premium_dict = {} # ETF 溢价率(需要从 API 获取)
for signal_code, trade_code in signal_to_trade.items():
# 指数收盘价
if signal_code in data:
index_close_dict[signal_code] = data[signal_code]['close']
# ETF 收盘价
if trade_code in data:
etf_close_dict[signal_code] = data[trade_code]['close'] # 注意:用 signal_code 作为键
# 溢价率暂时设为 None需要额外 API 支持)
# TODO: 接入 ETF 净值数据计算溢价率
# 创建对齐器
aligner = CrossMarketAligner(target_calendar=trading_calendar)
@@ -192,6 +210,25 @@ def main():
assets = {}
all_codes = factor_df.columns.tolist()
# 对齐价格到 A 股日历
index_close_aligned = {}
etf_close_aligned = {}
for code in all_codes:
if code in index_close_dict:
index_close_aligned[code] = index_close_dict[code].reindex(common_dates, method='ffill')
if code in etf_close_dict:
etf_close_aligned[code] = etf_close_dict[code].reindex(common_dates, method='ffill')
# 计算指数和 ETF 收益率
index_returns = {}
etf_returns = {}
for code in all_codes:
if code in index_close_aligned:
index_returns[code] = index_close_aligned[code].pct_change(fill_method=None)
if code in etf_close_aligned:
etf_returns[code] = etf_close_aligned[code].pct_change(fill_method=None)
for code in all_codes:
asset = {}
@@ -206,49 +243,75 @@ def main():
asset['threshold'] = safe_val(threshold, 4)
asset['above_threshold'] = mom >= threshold if mom is not None else False
# 指数价格
if code in index_close_aligned:
idx_close = index_close_aligned[code].get(date)
asset['index_close'] = safe_val(idx_close, 2) if pd.notna(idx_close) else None
else:
asset['index_close'] = None
# ETF 价格
if code in etf_close_aligned:
etf_close = etf_close_aligned[code].get(date)
asset['etf_close'] = safe_val(etf_close, 3) if pd.notna(etf_close) else None
else:
asset['etf_close'] = None
# 指数收益率
if code in index_returns:
idx_ret = index_returns[code].get(date)
asset['index_return'] = safe_val(idx_ret, 6) if pd.notna(idx_ret) else None
else:
asset['index_return'] = None
# ETF 收益率(兼容 V1 命名etf_return_ctc
if code in etf_returns:
etf_ret = etf_returns[code].get(date)
asset['etf_return_ctc'] = safe_val(etf_ret, 6) if pd.notna(etf_ret) else None
else:
asset['etf_return_ctc'] = None
# 溢价率(暂时为 None
asset['premium'] = None
# 持仓状态
is_held = code in current_holdings
asset['is_held'] = is_held
asset['weight'] = safe_val(pos_row.get(code, 0), 4) if is_held else 0.0
if is_held and code in holdings_state:
hs = holdings_state[code]
asset['entry_date'] = hs['entry_date']
asset['entry_price'] = safe_val(hs['entry_price'], 4)
asset['entry_price_etf'] = safe_val(hs['entry_price'], 4)
asset['entry_price_idx'] = None # V2 暂不记录指数进场价
entry_dt = pd.Timestamp(hs['entry_date'])
trading_days_held = len(common_dates[(common_dates >= entry_dt) & (common_dates <= date)])
asset['holding_days'] = trading_days_held
# 累计收益
# 累计收益(区分 ETF 和指数,兼容 V1
if hs['entry_price'] and hs['entry_price'] > 0:
if code in close_dict:
cur = close_dict[code].get(date)
if cur and pd.notna(cur):
asset['cum_return'] = safe_val(float(cur) / hs['entry_price'] - 1, 4)
cum_ret = float(cur) / hs['entry_price'] - 1
asset['cum_return_etf'] = safe_val(cum_ret, 4)
asset['cum_return_idx'] = safe_val(cum_ret, 4) # V2 暂不区分
else:
asset['cum_return'] = None
asset['cum_return_etf'] = None
asset['cum_return_idx'] = None
else:
asset['cum_return'] = None
asset['cum_return_etf'] = None
asset['cum_return_idx'] = None
else:
asset['cum_return'] = None
# 当日收益贡献
if code in returns_df.columns:
asset['daily_return'] = safe_val(returns_df.loc[date, code], 6)
asset['return_contribution'] = safe_val(
pos_row.get(code, 0) * returns_df.loc[date, code], 6
)
else:
asset['daily_return'] = None
asset['return_contribution'] = None
asset['cum_return_etf'] = None
asset['cum_return_idx'] = None
else:
asset['entry_date'] = None
asset['entry_price'] = None
asset['entry_price_etf'] = None
asset['entry_price_idx'] = None
asset['holding_days'] = 0
asset['cum_return'] = None
asset['daily_return'] = None
asset['return_contribution'] = None
asset['cum_return_etf'] = None
asset['cum_return_idx'] = None
assets[code] = asset
@@ -269,32 +332,30 @@ def main():
days_list.append(day_record)
prev_holdings = current_holdings
# 10. 构建元数据
# 10. 构建元数据(兼容 V1 格式)
codes_meta = {}
for signal_code, asset in config.asset_pools.assets.items():
codes_meta[signal_code] = {
'name': asset.name,
'etf': asset.trade_source,
'group': asset.group
for code in all_codes:
asset_config = config.asset_pools.assets.get(code)
codes_meta[code] = {
'name': asset_config.name if asset_config else code,
'etf': asset_config.trade_source if asset_config else None,
'market': asset_config.group if asset_config else None # V1 使用 market 字段
}
output = {
'meta': {
'version': 'V2',
'strategy': 'GlobalRotationStrategy',
'mode': '指数信号 + ETF收益',
'mode': 'V2: 指数信号 + ETF收益',
'start_date': common_dates[0].strftime('%Y-%m-%d'),
'end_date': common_dates[-1].strftime('%Y-%m-%d'),
'total_days': len(common_dates),
'select_num': strategy.select_num,
'n_days': config.factor.n_days,
'trade_cost': strategy.trade_cost,
'dynamic_threshold': {
'bond_threshold': {
'enabled': strategy.use_dynamic_threshold,
'bond_code': bond_code,
'ratio': bond_ratio
},
'diversified': strategy.diversified,
'rebalance_count': int(rebalance_count),
'final_nav': safe_val(equity_curve.iloc[-1], 4),
'codes': codes_meta
},
'days': days_list