""" Task 3: 调仓逻辑问题诊断 分析维度: 3.1 最小持仓期模拟 - 对比 3/5/10 天最小持仓期的效果 3.2 等权 vs 波动率加权 - 评估风险贡献偏斜 3.3 分组竞争机制 - 对比"取消分组"vs"当前分组"的收益差异 """ import ast import sys from pathlib import Path from collections import defaultdict from typing import Dict, List, Tuple import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from rotation.experiments.common import ( load_nav, load_signals, load_detail_days, load_detail_meta, print_section, compute_drawdown, compute_sharpe, compute_annual_return, ) def simulate_min_hold(days: List[dict], min_hold: int) -> dict: """模拟最小持仓期:在调仓后至少持有 min_hold 天。 简化模型:遍历每日记录,如果距上次调仓不足 min_hold 天,则忽略信号变化。 返回模拟后的统计指标。 """ if not days: return {} trade_cost = 0.001 current_holdings = list(days[0].get('holdings', [])) nav = 1.0 days_since_rebalance = 0 rebalance_count = 0 simulated_returns = [] # 新列表,不引用 days 内部数据 for i, day in enumerate(days): daily_return = day.get('daily_return', 0) new_holdings = day.get('holdings', []) is_orig_rebalance = day.get('is_rebalance', False) # 模拟:如果距上次调仓不足 min_hold 天,不执行调仓 should_rebalance = is_orig_rebalance and days_since_rebalance >= min_hold if should_rebalance: # 执行调仓:使用原始收益(已包含交易成本) nav *= (1 + daily_return) rebalance_count += 1 days_since_rebalance = 0 elif is_orig_rebalance and days_since_rebalance < min_hold: # 信号变化但被最小持仓期阻止:加回被扣除的交易成本 approx_return = daily_return + trade_cost nav *= (1 + approx_return) days_since_rebalance += 1 simulated_returns.append(approx_return) continue else: nav *= (1 + daily_return) days_since_rebalance += 1 simulated_returns.append(daily_return) n = len(days) total_return = nav - 1 annual_return = (1 + total_return) ** (252 / n) - 1 if n > 0 else 0 ret_series = pd.Series(simulated_returns) # 近似 NAV 序列用于回撤计算 nav_series = pd.Series(simulated_returns).add(1).cumprod() max_dd = compute_drawdown(nav_series).min() sharpe = compute_sharpe(ret_series) return { 'min_hold': min_hold, 'total_return': total_return, 'annual_return': annual_return, 'max_drawdown': max_dd, 'sharpe': sharpe, 'rebalance_count': rebalance_count, } def analyze_min_hold_days(days: List[dict]): """3.1 最小持仓期模拟""" print_section("3.1 最小持仓期模拟") results = [] for min_hold in [1, 3, 5, 10]: r = simulate_min_hold(days, min_hold) results.append(r) print(f" 最小持仓期={min_hold}天: 累计={r['total_return']:+.2%}, " f"年化={r['annual_return']:+.2%}, 最大回撤={r['max_drawdown']:.2%}, " f"夏普={r['sharpe']:.2f}, 调仓={r['rebalance_count']}次") return results def analyze_volatility_weighting(days: List[dict]): """3.2 等权 vs 波动率加权 - 风险贡献分析""" print_section("3.2 风险贡献分析 (等权 vs 波动率加权)") # 收集每个资产在被持有期间的日收益 asset_returns = defaultdict(list) for day in days: for code, asset in day.get('assets', {}).items(): if asset.get('is_held') and asset.get('etf_return_ctc') is not None: asset_returns[code].append(asset['etf_return_ctc']) print(" 各资产持有期间日收益波动率:") volatilities = {} for code in sorted(asset_returns.keys()): rets = asset_returns[code] if len(rets) < 10: continue vol = np.std(rets) * np.sqrt(252) mean_ret = np.mean(rets) * 252 volatilities[code] = vol # 等权下的风险贡献(简化:假设等权 1/N) print(f" {code}: 年化波动率={vol:.2%}, 年化收益={mean_ret:+.2%}, " f"持有天数={len(rets)}, Sharpe={mean_ret/vol:.2f}" if vol > 0 else f" {code}: 年化波动率={vol:.2%}, 持有天数={len(rets)}") # 计算等权组合的风险贡献 print(f"\n 等权组合风险贡献估算 (假设持有 Top3 等权):") # 找最常见的 3 资产组合 combo_counter = defaultdict(int) for day in days: holdings = tuple(sorted(day.get('holdings', []))) if holdings: combo_counter[holdings] += 1 top_combos = sorted(combo_counter.items(), key=lambda x: -x[1])[:5] print(" 最常见的持仓组合:") for combo, count in top_combos: print(f" {combo}: {count} 天 ({count/len(days)*100:.1f}%)") # 波动率倒数加权 vs 等权的理论风险贡献对比 if len(volatilities) >= 3: codes_with_vol = {c: v for c, v in volatilities.items() if v > 0 and c != '931862.CSI'} if len(codes_with_vol) >= 3: codes_list = list(codes_with_vol.keys()) vols = np.array([codes_with_vol[c] for c in codes_list]) n = len(codes_list) # 等权 eq_weights = np.ones(n) / n eq_risk_contrib = eq_weights * vols # 简化 eq_risk_pct = eq_risk_contrib / eq_risk_contrib.sum() * 100 # 波动率倒数加权 inv_vol = 1.0 / vols iv_weights = inv_vol / inv_vol.sum() iv_risk_contrib = iv_weights * vols iv_risk_pct = iv_risk_contrib / iv_risk_contrib.sum() * 100 print(f"\n 风险贡献对比 (全部非债券资产):") print(f" {'资产':<15} {'波动率':>8} {'等权风险%':>10} {'反波动率风险%':>14}") for i, code in enumerate(codes_list): print(f" {code:<15} {vols[i]:>7.2%} {eq_risk_pct[i]:>9.1f}% {iv_risk_pct[i]:>13.1f}%") return {'volatilities': volatilities} def analyze_group_mechanism(days: List[dict], meta: dict): """3.3 分组竞争机制分析""" print_section("3.3 分组竞争机制分析") # 从 config 获取分组信息 group_map = { '399006.SZ': 'A', 'H30269.CSI': 'A', 'NDX': 'US', 'N225': 'JP', 'GDAXI': 'EU', 'HSI': 'HK', 'HSTECH.HK': 'HK', 'GC=F': 'COMMODITY', 'CL=F': 'COMMODITY', 'HG=F': 'COMMODITY', '931862.CSI': 'BOND', } # 统计每组被选中的频率 group_hold_count = defaultdict(int) total_days = 0 for day in days: total_days += 1 holdings = day.get('holdings', []) groups_held = set() for code in holdings: g = group_map.get(code, 'UNKNOWN') if g != 'BOND': groups_held.add(g) group_hold_count[g] += 1 print(" 各组被选中天数 (每次调仓选3个):") for g in ['A', 'US', 'JP', 'EU', 'HK', 'COMMODITY']: count = group_hold_count.get(g, 0) print(f" {g}: {count} 天 ({count/total_days*100:.1f}%)") # 分析同组两个标的都强但只能选一个的情况 # 以 A 组为例 (399006.SZ + H30269.CSI) print(f"\n A 组内部竞争分析 (399006.SZ vs H30269.CSI):") both_above = 0 a_wins = 0 h_wins = 0 for day in days: assets = day.get('assets', {}) a_asset = assets.get('399006.SZ', {}) h_asset = assets.get('H30269.CSI', {}) a_m = a_asset.get('momentum') h_m = h_asset.get('momentum') threshold = a_asset.get('threshold', 0) if a_m is not None and h_m is not None and a_m >= threshold and h_m >= threshold: both_above += 1 if a_m > h_m: a_wins += 1 else: h_wins += 1 print(f" 两标的动量都超过阈值的天数: {both_above}") print(f" 399006.SZ 胜出: {a_wins} ({a_wins/both_above*100:.1f}%)" if both_above > 0 else "") print(f" H30269.CSI 胜出: {h_wins} ({h_wins/both_above*100:.1f}%)" if both_above > 0 else "") # HK 组分析 print(f"\n HK 组内部竞争分析 (HSI vs HSTECH.HK):") both_above_hk = 0 hsi_wins = 0 hstech_wins = 0 for day in days: assets = day.get('assets', {}) hsi = assets.get('HSI', {}) hstech = assets.get('HSTECH.HK', {}) hsi_m = hsi.get('momentum') hstech_m = hstech.get('momentum') threshold = hsi.get('threshold', 0) if hsi_m is not None and hstech_m is not None and hsi_m >= threshold and hstech_m >= threshold: both_above_hk += 1 if hsi_m > hstech_m: hsi_wins += 1 else: hstech_wins += 1 print(f" 两标的动量都超过阈值的天数: {both_above_hk}") if both_above_hk > 0: print(f" HSI 胜出: {hsi_wins} ({hsi_wins/both_above_hk*100:.1f}%)") print(f" HSTECH 胜出: {hstech_wins} ({hstech_wins/both_above_hk*100:.1f}%)") # 商品组分析(3个标的) print(f"\n COMMODITY 组分析 (GC=F vs CL=F vs HG=F):") commodity_counts = defaultdict(int) for day in days: assets = day.get('assets', {}) valid = {} threshold = 0 for c in ['GC=F', 'CL=F', 'HG=F']: a = assets.get(c, {}) m = a.get('momentum') threshold = a.get('threshold', 0) if m is not None and m >= threshold: valid[c] = m if valid: winner = max(valid, key=valid.get) commodity_counts[winner] += 1 for c in ['GC=F', 'CL=F', 'HG=F']: count = commodity_counts.get(c, 0) total_valid = sum(commodity_counts.values()) print(f" {c} 胜出: {count} 天 ({count/total_valid*100:.1f}%)" if total_valid > 0 else f" {c}: 无有效数据") return {'group_hold_count': dict(group_hold_count)} def main(): print_section("Task 3: 调仓逻辑问题诊断") nav = load_nav() signals = load_signals() days = load_detail_days() meta = load_detail_meta() print(f" 数据期间: {meta['start_date']} ~ {meta['end_date']}") results = {} # 3.1 最小持仓期 results['min_hold'] = analyze_min_hold_days(days) # 3.2 波动率加权 results['vol_weight'] = analyze_volatility_weighting(days) # 3.3 分组机制 results['group'] = analyze_group_mechanism(days, meta) print_section("Task 3 总结") print(" 1. 最小持仓期增加可减少无效调仓,但可能错过趋势转换") print(" 2. 等权配置导致高波动资产主导组合风险,波动率加权可平衡") print(" 3. 分组机制确保地域分散,但可能牺牲集中优势") return results if __name__ == '__main__': main()