""" Task 6: 回撤诊断 分析维度: 6.1 最大回撤复盘 - 2022-05 前后持仓与动量变化 6.2 近期回撤趋势 - 2026 年回撤分析 6.3 极端尾部风险 - 极端日归因 """ import sys from pathlib import Path from collections import defaultdict from typing import Dict, List 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, ) def analyze_max_drawdown(days: List[dict], nav: pd.DataFrame): """6.1 最大回撤复盘 (2022-05)""" print_section("6.1 最大回撤复盘 (2022-04 ~ 2022-06)") # 找到最大回撤的谷底 nav_df = nav.copy() dd = compute_drawdown(nav_df['nav']) trough_idx = dd.idxmin() trough_date = nav_df.iloc[trough_idx]['date'] trough_dd = dd.iloc[trough_idx] print(f" 最大回撤谷底: {trough_date.date()}") print(f" 最大回撤: {trough_dd:.2%}") # 找到回撤开始点(前高点) peak_idx = nav_df['nav'][:trough_idx+1].idxmax() peak_date = nav_df.iloc[peak_idx]['date'] peak_nav = nav_df.iloc[peak_idx]['nav'] print(f" 回撤起点(前高): {peak_date.date()}, NAV={peak_nav:.4f}") print(f" 回撤持续: {(trough_date - peak_date).days} 天") # 找到恢复点 recovery_mask = nav_df['nav'].iloc[trough_idx:] >= peak_nav if recovery_mask.any(): recovery_idx = recovery_mask.idxmax() recovery_date = nav_df.iloc[recovery_idx]['date'] print(f" 回撤恢复: {recovery_date.date()}, 恢复耗时: {(recovery_date - trough_date).days} 天") else: print(f" 回撤未恢复(截至数据结束)") # 2022-04 ~ 2022-06 详细持仓变化 print(f"\n 2022-04 ~ 2022-06 持仓与动量变化:") days_2022 = [d for d in days if d['date'] >= '2022-04-01' and d['date'] <= '2022-06-30'] prev_holdings = [] key_events = [] for day in days_2022: holdings = day.get('holdings', []) is_rebal = day.get('is_rebalance', False) if is_rebal: added = set(holdings) - set(prev_holdings) removed = set(prev_holdings) - set(holdings) # 收集动量得分 momentums = {} for code, asset in day.get('assets', {}).items(): m = asset.get('momentum') if m is not None: momentums[code] = m key_events.append({ 'date': day['date'], 'nav': day['nav'], 'daily_return': day['daily_return'], 'added': added, 'removed': removed, 'holdings': holdings, 'momentums': momentums, }) prev_holdings = holdings print(f" 期间调仓事件数: {len(key_events)}") for evt in key_events: mom_str = ', '.join(f"{c}={v:.4f}" for c, v in sorted(evt['momentums'].items(), key=lambda x: -x[1])[:5]) print(f" {evt['date']}: NAV={evt['nav']:.4f}, " f"日收益={evt['daily_return']:+.4%}") print(f" 调仓: +{evt['added']} -{evt['removed']}") print(f" 持仓: {evt['holdings']}") print(f" Top5动量: {mom_str}") # 分析为什么动态阈值没有触发防御 print(f"\n 动态阈值分析 (2022-04 ~ 2022-06):") for day in days_2022: assets = day.get('assets', {}) bond = assets.get('931862.CSI', {}) bond_m = bond.get('momentum') threshold = bond.get('threshold', 0) if bond_m is not None: holdings = day.get('holdings', []) has_bond = '931862.CSI' in holdings # 只在调仓日或每周输出一次 if day.get('is_rebalance') or day['date'].endswith('-01') or day['date'].endswith('-15'): non_bond_above = sum(1 for c, a in assets.items() if c != '931862.CSI' and a.get('above_threshold', False)) print(f" {day['date']}: 短债动量={bond_m:.6f}, " f"阈值={threshold:.6f}, 持仓={holdings}, " f"非债券>阈值: {non_bond_above}") return {'trough_date': str(trough_date.date()), 'max_dd': float(trough_dd)} def analyze_recent_drawdown(days: List[dict], nav: pd.DataFrame): """6.2 2026 年近期回撤分析""" print_section("6.2 2026 年近期回撤分析") nav_2026 = nav[nav['date'].dt.year == 2026].copy() if len(nav_2026) == 0: print(" 无 2026 年数据") return {} dd_2026 = compute_drawdown(nav_2026['nav']) max_dd_2026 = dd_2026.min() trough_idx = dd_2026.idxmin() trough_date = nav_2026.iloc[trough_idx - nav_2026.index[0]]['date'] if trough_idx >= nav_2026.index[0] else None print(f" 2026 年最大回撤: {max_dd_2026:.2%}") # 2026 年月度收益 nav_2026['month'] = nav_2026['date'].dt.month print(f"\n 2026 年月度收益:") for month, grp in nav_2026.groupby('month'): m_ret = grp['nav'].iloc[-1] / grp['nav'].iloc[0] - 1 m_dd = compute_drawdown(grp['nav']).min() print(f" {month}月: 收益={m_ret:+.2%}, 月内最大回撤={m_dd:.2%}") # 2026 年极端日 extreme_2026 = nav_2026[(nav_2026['daily_return'] > 0.03) | (nav_2026['daily_return'] < -0.03)] print(f"\n 2026 年极端日(|收益|>3%):") days_2026_detail = [d for d in days if d['date'].startswith('2026')] for _, row in extreme_2026.iterrows(): date_str = row['date'].strftime('%Y-%m-%d') day_detail = None for d in days_2026_detail: if d['date'] == date_str: day_detail = d break holdings_info = "" if day_detail: for code, asset in day_detail.get('assets', {}).items(): if asset.get('is_held'): etf_ret = asset.get('etf_return_ctc', 0) holdings_info += f" {code}({etf_ret:+.2%})" print(f" {date_str}: {row['daily_return']:+.4%} |{holdings_info}") # NAV 绝对回撤金额分析 print(f"\n 绝对回撤金额分析:") initial_nav = nav['nav'].iloc[0] current_nav = nav['nav'].iloc[-1] peak_nav = nav['nav'].max() print(f" 初始 NAV: {initial_nav:.4f}") print(f" 当前 NAV: {current_nav:.4f}") print(f" 峰值 NAV: {peak_nav:.4f}") print(f" 峰值回撤绝对值: {peak_nav * abs(max_dd_2026):.4f} NAV 单位") print(f" 对比: 2020 年全年收益 NAV = " f"{nav[nav['date'].dt.year == 2020]['nav'].iloc[-1] - nav[nav['date'].dt.year == 2020]['nav'].iloc[0]:.4f}") return {'max_dd_2026': float(max_dd_2026)} def analyze_extreme_tail(days: List[dict], nav: pd.DataFrame): """6.3 极端尾部风险""" print_section("6.3 极端尾部风险分析") returns = nav['daily_return'].values # 收益分布统计 print(" 日收益分布:") for pct in [1, 5, 10, 25, 50, 75, 90, 95, 99]: val = np.percentile(returns, pct) print(f" P{pct}: {val:+.4%}") # 尾部风险指标 tail_5pct = returns[returns <= np.percentile(returns, 5)] print(f"\n 尾部风险 (5% 分位以下):") print(f" CVaR(5%): {np.mean(tail_5pct):+.4%}") print(f" Worst: {np.min(returns):+.4%}") # 极端日归因 print(f"\n 极端亏损日归因 (日收益 < -5%):") extreme_loss = nav[nav['daily_return'] < -0.05] for _, row in extreme_loss.iterrows(): date_str = row['date'].strftime('%Y-%m-%d') day_detail = None for d in days: if d['date'] == date_str: day_detail = d break if day_detail: print(f"\n {date_str}: 日收益={row['daily_return']:+.4%}, NAV={row['nav']:.4f}") for code, asset in day_detail.get('assets', {}).items(): if asset.get('is_held'): etf_ret = asset.get('etf_return_ctc', 0) idx_ret = asset.get('index_return', 0) premium = asset.get('premium', 0) print(f" {code}: ETF收益={etf_ret:+.4%}, " f"指数收益={idx_ret:+.4%}, 溢价率={premium:+.4%}" if premium else f" {code}: ETF收益={etf_ret:+.4%}, 指数收益={idx_ret:+.4%}") # 连续亏损天数分析 streak = 0 max_streak = 0 streaks = [] for r in returns: if r < 0: streak += 1 else: if streak > 0: streaks.append(streak) streak = 0 if streak > 0: streaks.append(streak) max_streak = max(streaks) if streaks else 0 print(f"\n 连续亏损分析:") print(f" 最大连续亏损天数: {max_streak}") print(f" 连续亏损>=3天的次数: {sum(1 for s in streaks if s >= 3)}") print(f" 连续亏损>=5天的次数: {sum(1 for s in streaks if s >= 5)}") # 连续亏损段的累计收益 if streaks: loss_streak_rets = [] current_streak_ret = 0 in_streak = False for r in returns: if r < 0: current_streak_ret += r in_streak = True else: if in_streak and current_streak_ret < -0.05: loss_streak_rets.append(current_streak_ret) current_streak_ret = 0 in_streak = False if loss_streak_rets: print(f"\n 显著连续亏损段(累计<-5%):") for ret in sorted(loss_streak_rets): print(f" 累计亏损: {ret:+.4%}") return {'max_streak': max_streak, 'cvar_5pct': float(np.mean(tail_5pct))} def main(): print_section("Task 6: 回撤诊断") nav = load_nav() signals = load_signals() days = load_detail_days() meta = load_detail_meta() print(f" 数据期间: {meta['start_date']} ~ {meta['end_date']}") results = {} # 6.1 最大回撤复盘 results['max_dd'] = analyze_max_drawdown(days, nav) # 6.2 近期回撤 results['recent_dd'] = analyze_recent_drawdown(days, nav) # 6.3 尾部风险 results['tail'] = analyze_extreme_tail(days, nav) print_section("Task 6 总结") print(f" 1. 最大回撤 {results['max_dd']['max_dd']:.2%} 发生在 {results['max_dd']['trough_date']}") print(f" 2. 2026 年最大回撤: {results['recent_dd'].get('max_dd_2026', 0):.2%}") print(f" 3. CVaR(5%): {results['tail']['cvar_5pct']:+.4%}, " f"最大连续亏损: {results['tail']['max_streak']} 天") return results if __name__ == '__main__': main()