backtest_report.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 趋势质量评估器 - 历史回测 (2017-至今)
  5. 生成完整回测报告
  6. """
  7. import sys
  8. sys.path.insert(0, '/root/.openclaw/workspace/trend-quality-evaluator')
  9. import numpy as np
  10. import pandas as pd
  11. from trend_quality_evaluator import fetch_stock_data, TrendQualityEvaluator
  12. from datetime import datetime
  13. import warnings
  14. warnings.filterwarnings('ignore')
  15. print("="*70)
  16. print("趋势质量评估器 - 历史回测报告")
  17. print("回测区间: 2017-01-01 至 2026-03-06")
  18. print("="*70)
  19. # 获取数据
  20. df = fetch_stock_data("399673", "2017-01-01", "2026-03-06", "d")
  21. if df is None:
  22. print("数据获取失败")
  23. exit(1)
  24. print(f"\n✓ 数据获取成功: {len(df)}条")
  25. print(f" 日期范围: {df.index[0].date()} ~ {df.index[-1].date()}")
  26. # 初始化评估器
  27. evaluator = TrendQualityEvaluator()
  28. # 逐日评估
  29. print("\n正在进行历史回测...")
  30. results = []
  31. # 需要至少60天数据计算MA60
  32. start_idx = 60
  33. for i in range(start_idx, len(df)):
  34. current_df = df.iloc[:i+1]
  35. try:
  36. score = evaluator.evaluate(current_df)
  37. results.append({
  38. 'date': df.index[i],
  39. 'close': df['close'].iloc[i],
  40. 'total_score': score.total_score,
  41. 'adx_score': score.adx_score,
  42. 'ma_slope_score': score.ma_slope_score,
  43. 'volatility_score': score.volatility_score,
  44. 'timeframe_score': score.timeframe_score,
  45. 'volume_score': score.volume_score,
  46. 'is_tradeable': score.is_tradeable,
  47. 'adx_value': score.adx_value,
  48. 'ma_slope': score.ma_slope,
  49. 'volatility_ratio': score.volatility_ratio,
  50. 'volume_ratio': score.volume_ratio
  51. })
  52. except Exception as e:
  53. pass
  54. if i % 100 == 0:
  55. print(f" 进度: {i}/{len(df)} ({i/len(df)*100:.1f}%)")
  56. results_df = pd.DataFrame(results)
  57. results_df = results_df.set_index('date')
  58. print(f"\n✓ 回测完成: {len(results_df)}个交易日")
  59. # 计算未来收益率(用于验证评分有效性)
  60. print("\n计算未来收益验证...")
  61. results_df['future_5d_return'] = results_df['close'].pct_change(5).shift(-5) * 100
  62. results_df['future_10d_return'] = results_df['close'].pct_change(10).shift(-10) * 100
  63. results_df['future_20d_return'] = results_df['close'].pct_change(20).shift(-20) * 100
  64. # ============================================
  65. # 生成报告
  66. # ============================================
  67. print("\n" + "="*70)
  68. print("回测报告")
  69. print("="*70)
  70. # 1. 评分分布统计
  71. print("\n【一、评分分布统计】")
  72. print("-"*50)
  73. total_days = len(results_df)
  74. tradeable_days = results_df['is_tradeable'].sum()
  75. print(f"总交易天数: {total_days}")
  76. print(f"可交易天数(≥60分): {tradeable_days} ({tradeable_days/total_days*100:.1f}%)")
  77. print(f"不可交易天数(<60分): {total_days-tradeable_days} ({(total_days-tradeable_days)/total_days*100:.1f}%)")
  78. # 分数段分布
  79. print(f"\n分数段分布:")
  80. bins = [0, 40, 60, 70, 80, 100]
  81. labels = ['0-40(混乱)', '40-60(较差)', '60-70(及格)', '70-80(良好)', '80-100(优秀)']
  82. results_df['score_bin'] = pd.cut(results_df['total_score'], bins=bins, labels=labels, include_lowest=True)
  83. score_dist = results_df['score_bin'].value_counts().sort_index()
  84. for label, count in score_dist.items():
  85. pct = count / total_days * 100
  86. bar = '█' * int(pct / 2)
  87. print(f" {label:15s}: {count:4d}天 ({pct:5.1f}%) {bar}")
  88. # 2. 各因子得分统计
  89. print("\n【二、各因子得分统计】")
  90. print("-"*50)
  91. factors = [
  92. ('adx_score', 'ADX趋势强度', 30),
  93. ('ma_slope_score', '均线斜率', 25),
  94. ('volatility_score', '波动率收缩', 20),
  95. ('timeframe_score', '时间框架共振', 15),
  96. ('volume_score', '成交量确认', 10)
  97. ]
  98. for col, name, max_score in factors:
  99. mean_score = results_df[col].mean()
  100. pct_of_max = mean_score / max_score * 100
  101. print(f"{name:15s}: 平均{mean_score:5.1f}/{max_score}分 ({pct_of_max:4.1f}%)")
  102. # 3. 评分有效性验证
  103. print("\n【三、评分有效性验证】")
  104. print("-"*50)
  105. print("不同评分区间的未来收益表现:")
  106. print(f"{'评分区间':<15} {'5日后收益':<12} {'10日后收益':<12} {'20日后收益':<12} {'样本数':<8}")
  107. print("-"*60)
  108. for label in labels:
  109. mask = results_df['score_bin'] == label
  110. if mask.sum() > 0:
  111. r5 = results_df[mask]['future_5d_return'].mean()
  112. r10 = results_df[mask]['future_10d_return'].mean()
  113. r20 = results_df[mask]['future_20d_return'].mean()
  114. count = mask.sum()
  115. print(f"{label:<15} {r5:>+10.2f}% {r10:>+10.2f}% {r20:>+10.2f}% {count:>6d}")
  116. # 4. 可交易 vs 不可交易对比
  117. print("\n【四、可交易性验证】")
  118. print("-"*50)
  119. tradeable_mask = results_df['is_tradeable']
  120. not_tradeable_mask = ~results_df['is_tradeable']
  121. print(f"{'指标':<20} {'可交易(≥60分)':<15} {'不可交易(<60分)':<15} {'差异':<10}")
  122. print("-"*60)
  123. for period, col in [('5日', 'future_5d_return'), ('10日', 'future_10d_return'), ('20日', 'future_20d_return')]:
  124. t_mean = results_df[tradeable_mask][col].mean()
  125. nt_mean = results_df[not_tradeable_mask][col].mean()
  126. diff = t_mean - nt_mean
  127. print(f"{period+'收益':<20} {t_mean:>+13.2f}% {nt_mean:>+14.2f}% {diff:>+9.2f}%")
  128. # 5. 年度最佳/最差月份
  129. print("\n【五、年度统计】")
  130. print("-"*50)
  131. results_df['year'] = results_df.index.year
  132. yearly_stats = results_df.groupby('year').agg({
  133. 'total_score': 'mean',
  134. 'is_tradeable': 'sum',
  135. 'future_20d_return': 'mean'
  136. }).round(2)
  137. yearly_stats['tradeable_pct'] = (yearly_stats['is_tradeable'] / results_df.groupby('year').size() * 100).round(1)
  138. print(f"{'年份':<8} {'平均评分':<10} {'可交易天数':<12} {'可交易比例':<12} {'20日收益':<10}")
  139. print("-"*60)
  140. for year, row in yearly_stats.iterrows():
  141. print(f"{year:<8} {row['total_score']:>8.1f} {int(row['is_tradeable']):>8}天 {row['tradeable_pct']:>8.1f}% {row['future_20d_return']:>+7.2f}%")
  142. # 6. 最新评分
  143. print("\n【六、最新评分】")
  144. print("-"*50)
  145. latest = results_df.iloc[-1]
  146. print(f"日期: {results_df.index[-1].strftime('%Y-%m-%d')}")
  147. print(f"收盘价: {latest['close']:.2f}")
  148. print(f"总分: {latest['total_score']:.1f}分")
  149. print(f"是否可交易: {'✅ 是' if latest['is_tradeable'] else '❌ 否'}")
  150. print(f"\n各因子得分:")
  151. for col, name, max_score in factors:
  152. print(f" {name:15s}: {latest[col]:.1f}/{max_score}分")
  153. # 7. 关键发现
  154. print("\n【七、关键发现】")
  155. print("-"*50)
  156. # 计算胜率
  157. tradeable_returns = results_df[tradeable_mask]['future_20d_return'].dropna()
  158. win_rate = (tradeable_returns > 0).mean() * 100
  159. print(f"1. 可交易信号胜率(20日): {win_rate:.1f}%")
  160. # 最佳评分日期
  161. best_day = results_df.loc[results_df['total_score'].idxmax()]
  162. print(f"2. 历史最高评分: {best_day['total_score']:.1f}分 ({results_df['total_score'].idxmax().strftime('%Y-%m-%d')})")
  163. # 评分与收益相关性
  164. corr_20d = results_df['total_score'].corr(results_df['future_20d_return'])
  165. print(f"3. 评分与20日收益相关性: {corr_20d:.3f}")
  166. # 连续可交易日统计
  167. results_df['tradeable_change'] = results_df['is_tradeable'].astype(int).diff()
  168. entry_points = results_df[results_df['tradeable_change'] == 1].index
  169. print(f"4. 历史可交易入场信号次数: {len(entry_points)}次")
  170. print("\n" + "="*70)
  171. print("回测报告生成完成")
  172. print("="*70)
  173. # 保存结果
  174. results_df.to_csv('/root/.openclaw/workspace/trend-quality-evaluator/backtest_results.csv')
  175. print("\n✓ 详细数据已保存: backtest_results.csv")