cyb50_real_backtest.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 基于真实历史数据的回测
  5. 数据来源:baostock (sz.399673)
  6. 数据区间:2017-01-03 ~ 2025-12-31
  7. """
  8. import pandas as pd
  9. import numpy as np
  10. import matplotlib
  11. matplotlib.use('Agg')
  12. import matplotlib.pyplot as plt
  13. import warnings
  14. warnings.filterwarnings('ignore')
  15. def load_real_data():
  16. """加载真实数据"""
  17. df = pd.read_csv('cyb50_baostock.csv')
  18. df['date'] = pd.to_datetime(df['date'])
  19. df = df.set_index('date').sort_index()
  20. # 转换数据类型
  21. for col in ['open', 'high', 'low', 'close', 'volume']:
  22. df[col] = pd.to_numeric(df[col], errors='coerce')
  23. print(f"真实数据加载成功!")
  24. print(f"数据区间: {df.index[0].date()} ~ {df.index[-1].date()}")
  25. print(f"总交易日: {len(df)}")
  26. print(f"价格范围: {df['close'].min():.0f} ~ {df['close'].max():.0f}")
  27. # 统计特征
  28. returns = df['close'].pct_change().dropna()
  29. print(f"\n数据统计特征:")
  30. print(f" 日收益均值: {returns.mean()*100:.4f}%")
  31. print(f" 日收益标准差: {returns.std()*100:.2f}%")
  32. print(f" 年化收益: {returns.mean()*252*100:.1f}%")
  33. print(f" 年化波动: {returns.std()*np.sqrt(252)*100:.1f}%")
  34. return df
  35. class RealDataStrategy:
  36. """趋势策略 - 针对真实数据优化"""
  37. def __init__(self):
  38. self.pos = 0
  39. self.entry = 0
  40. self.peak = 0
  41. self.trades = []
  42. def generate_signal(self, data):
  43. """生成交易信号"""
  44. close = data['close'].values
  45. high = data['high'].values
  46. low = data['low'].values
  47. if len(close) < 60:
  48. return 0, "INIT"
  49. # 计算指标
  50. ma10 = np.mean(close[-10:])
  51. ma30 = np.mean(close[-30:])
  52. # 10日和30日涨跌幅
  53. ret10 = (close[-1] / close[-10] - 1) if len(close) >= 10 else 0
  54. # 突破检测
  55. high_20 = np.max(high[-20:])
  56. low_20 = np.min(low[-20:])
  57. curr = close[-1]
  58. # 买入条件:价格>MA10>MA30 + 创20日新高 + 正动量
  59. buy_signal = (curr > ma10 > ma30) and (curr >= high_20 * 0.995) and (ret10 > 0.02)
  60. # 卖出条件:跌破MA30或创20日新低
  61. sell_signal = (curr < ma30) or (curr <= low_20 * 1.005)
  62. if buy_signal and self.pos == 0:
  63. target_pos = 1.0
  64. elif sell_signal and self.pos > 0:
  65. target_pos = 0.0
  66. else:
  67. target_pos = self.pos
  68. # 移动止损(10%)
  69. if self.pos > 0:
  70. if curr > self.peak:
  71. self.peak = curr
  72. if curr < self.peak * 0.90:
  73. target_pos = 0.0
  74. # 状态更新
  75. if target_pos > 0 and self.pos == 0:
  76. self.entry = curr
  77. self.peak = curr
  78. state = "ENTRY"
  79. self.trades.append({'type': 'buy', 'price': curr, 'date': data.index[-1]})
  80. elif target_pos == 0 and self.pos > 0:
  81. self.trades.append({'type': 'sell', 'price': curr, 'date': data.index[-1], 'pnl': (curr - self.entry) / self.entry})
  82. self.entry = 0
  83. self.peak = 0
  84. state = "EXIT"
  85. elif target_pos > 0:
  86. state = "HOLD"
  87. else:
  88. state = "EMPTY"
  89. self.pos = target_pos
  90. return target_pos, state
  91. def backtest(data, strategy, start_date, end_date, warmup=60):
  92. """回测引擎"""
  93. data = data[(data.index >= start_date) & (data.index <= end_date)]
  94. results = []
  95. nav = 1.0
  96. for i in range(warmup, len(data)):
  97. curr_data = data.iloc[:i+1]
  98. pos, state = strategy.generate_signal(curr_data)
  99. if i > warmup:
  100. daily_ret = data['close'].iloc[i] / data['close'].iloc[i-1] - 1
  101. strategy_ret = daily_ret * results[-1]['pos']
  102. nav *= (1 + strategy_ret)
  103. results.append({
  104. 'date': data.index[i],
  105. 'pos': pos,
  106. 'nav': nav,
  107. 'state': state,
  108. 'close': data['close'].iloc[i]
  109. })
  110. df = pd.DataFrame(results).set_index('date')
  111. df['index_nav'] = df['close'] / df['close'].iloc[0]
  112. return df
  113. def calculate_metrics(nav, index_nav):
  114. """计算绩效指标"""
  115. s_returns = nav.pct_change().dropna()
  116. total_return = nav.iloc[-1] - 1
  117. days = len(nav)
  118. annual_return = (1 + total_return) ** (252 / days) - 1
  119. index_return = index_nav.iloc[-1] - 1
  120. index_annual = (1 + index_return) ** (252 / days) - 1
  121. running_max = nav.expanding().max()
  122. max_dd = ((nav - running_max) / running_max).min()
  123. volatility = s_returns.std() * np.sqrt(252)
  124. sharpe = (annual_return - 0.03) / volatility if volatility > 0 else 0
  125. calmar = annual_return / abs(max_dd) if max_dd != 0 else 0
  126. win_rate = (s_returns > 0).mean()
  127. return {
  128. 'annual_return': annual_return,
  129. 'index_annual': index_annual,
  130. 'excess_annual': annual_return - index_annual,
  131. 'max_drawdown': max_dd,
  132. 'volatility': volatility,
  133. 'sharpe': sharpe,
  134. 'calmar': calmar,
  135. 'win_rate': win_rate,
  136. 'total_return': total_return,
  137. 'index_return': index_return
  138. }
  139. def plot_results(results, title, filename):
  140. """绘制回测结果"""
  141. fig, axes = plt.subplots(3, 1, figsize=(14, 10))
  142. # 净值曲线
  143. ax1 = axes[0]
  144. ax1.plot(results.index, results['nav'], 'r-', linewidth=2, label='Strategy')
  145. ax1.plot(results.index, results['index_nav'], 'gray', linewidth=1, alpha=0.7, label='Index (CYB50)')
  146. ax1.set_title(title, fontsize=14)
  147. ax1.set_ylabel('NAV')
  148. ax1.legend()
  149. ax1.grid(True, alpha=0.3)
  150. # 仓位
  151. ax2 = axes[1]
  152. ax2.fill_between(results.index, 0, results['pos'], alpha=0.5, color='green')
  153. ax2.set_ylabel('Position')
  154. ax2.set_ylim(0, 1.1)
  155. ax2.grid(True, alpha=0.3)
  156. # 回撤
  157. ax3 = axes[2]
  158. running_max = results['nav'].expanding().max()
  159. drawdown = (results['nav'] - running_max) / running_max
  160. ax3.fill_between(results.index, drawdown, 0, alpha=0.3, color='red')
  161. ax3.set_ylabel('Drawdown')
  162. ax3.set_xlabel('Date')
  163. ax3.grid(True, alpha=0.3)
  164. plt.tight_layout()
  165. plt.savefig(filename, dpi=150)
  166. print(f" 图表已保存: {filename}")
  167. def main():
  168. print("="*70)
  169. print("创业板50指数 - 真实历史数据回测")
  170. print("数据来源: baostock (sz.399673)")
  171. print("="*70)
  172. # 加载真实数据
  173. print("\n[1] 加载真实数据...")
  174. data = load_real_data()
  175. # 训练阶段
  176. print("\n[2] 训练阶段 (2018-2023)...")
  177. strategy = RealDataStrategy()
  178. train_results = backtest(data, strategy, '2018-01-01', '2023-12-31')
  179. train_metrics = calculate_metrics(train_results['nav'], train_results['index_nav'])
  180. print(f"\n ╔══════════════════════════════════════╗")
  181. print(f" ║ 训 练 集 结 果 ║")
  182. print(f" ╠══════════════════════════════════════╣")
  183. print(f" ║ 策略总收益: {train_metrics['total_return']*100:8.1f}% ║")
  184. print(f" ║ 指数总收益: {train_metrics['index_return']*100:8.1f}% ║")
  185. print(f" ║ ───────────────────────────────── ║")
  186. print(f" ║ 策略年化: {train_metrics['annual_return']*100:8.1f}% ║")
  187. print(f" ║ 指数年化: {train_metrics['index_annual']*100:8.1f}% ║")
  188. print(f" ║ 超额收益: {train_metrics['excess_annual']*100:8.1f}% ║")
  189. print(f" ║ ───────────────────────────────── ║")
  190. print(f" ║ 最大回撤: {train_metrics['max_drawdown']*100:8.1f}% ║")
  191. print(f" ║ 年化波动: {train_metrics['volatility']*100:8.1f}% ║")
  192. print(f" ║ 夏普比率: {train_metrics['sharpe']:8.2f} ║")
  193. print(f" ║ 卡玛比率: {train_metrics['calmar']:8.2f} ║")
  194. print(f" ║ 胜率: {train_metrics['win_rate']*100:8.1f}% ║")
  195. print(f" ╚══════════════════════════════════════╝")
  196. plot_results(train_results, "Training Set 2018-2023 (Real Data)", "train_real_data.png")
  197. # 验证阶段
  198. print("\n[3] 验证阶段 (2024-2025)...")
  199. strategy_val = RealDataStrategy()
  200. val_results = backtest(data, strategy_val, '2024-01-01', '2025-12-31')
  201. val_metrics = calculate_metrics(val_results['nav'], val_results['index_nav'])
  202. print(f"\n ╔══════════════════════════════════════╗")
  203. print(f" ║ 验 证 集 结 果 ║")
  204. print(f" ╠══════════════════════════════════════╣")
  205. print(f" ║ 策略总收益: {val_metrics['total_return']*100:8.1f}% ║")
  206. print(f" ║ 指数总收益: {val_metrics['index_return']*100:8.1f}% ║")
  207. print(f" ║ ───────────────────────────────── ║")
  208. print(f" ║ 策略年化: {val_metrics['annual_return']*100:8.1f}% ║")
  209. print(f" ║ 指数年化: {val_metrics['index_annual']*100:8.1f}% ║")
  210. print(f" ║ 超额收益: {val_metrics['excess_annual']*100:8.1f}% ║")
  211. print(f" ║ ───────────────────────────────── ║")
  212. print(f" ║ 最大回撤: {val_metrics['max_drawdown']*100:8.1f}% ║")
  213. print(f" ║ 夏普比率: {val_metrics['sharpe']:8.2f} ║")
  214. print(f" ╚══════════════════════════════════════╝")
  215. plot_results(val_results, "Validation Set 2024-2025 (Real Data)", "val_real_data.png")
  216. # 综合评价
  217. print("\n[4] 综合评价:")
  218. decay = (train_metrics['annual_return'] - val_metrics['annual_return']) / train_metrics['annual_return'] * 100 if train_metrics['annual_return'] > 0 else 0
  219. print(f" 年化收益衰减: {decay:.1f}%")
  220. if train_metrics['annual_return'] >= 0.15:
  221. print(" ✅ 训练集年化≥15%")
  222. else:
  223. print(" ⚠️ 训练集收益一般")
  224. if val_metrics['annual_return'] >= 0.10:
  225. print(" ✅ 验证集年化≥10%")
  226. elif val_metrics['annual_return'] > 0:
  227. print(" ⚠️ 验证集正收益但未达10%")
  228. else:
  229. print(" ❌ 验证集亏损")
  230. if decay < 50:
  231. print(" ✅ 策略稳健(衰减<50%)")
  232. else:
  233. print(" ⚠️ 策略有过拟合风险")
  234. print("\n" + "="*70)
  235. if train_metrics['annual_return'] >= 0.15 and val_metrics['annual_return'] > 0 and decay < 60:
  236. print("✅ 基于真实数据的策略验证通过!")
  237. elif train_metrics['annual_return'] >= 0.10 and val_metrics['annual_return'] > 0:
  238. print("⚠️ 策略尚可,建议继续优化")
  239. else:
  240. print("❌ 策略在真实数据上表现不佳,需重新设计")
  241. print("="*70)
  242. if __name__ == "__main__":
  243. main()