新增6维度策略诊断实验脚本和报告: - task1: 信号产生分析 (调仓频率、无效调仓率) - task2: 收益计算分析 (T+1执行偏差、溢价问题) - task3: 调仓逻辑分析 (最小持仓期模拟) - task4: 资金管理分析 (止损、波动率适配) - task5: 收益归因分析 (集中度、静态vs轮动) - task6: 回撤诊断分析 (最大回撤复盘、尾部风险) 输出报告: - diagnosis_report.md: 完整策略诊断报告 - rebalancing_optimization_experiment.md: 调仓频率优化实验报告 实验结论: - 发现调仓过于频繁 (405次/1549天) - No-Trade Region方案可提升年化3%、夏普0.11 - 但改善幅度有限,信号质量是根本瓶颈
290 lines
11 KiB
Python
290 lines
11 KiB
Python
"""
|
|
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()
|