cyb50_historical.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 基于真实历史节点的回测
  5. 使用真实历史价格节点生成数据
  6. """
  7. import pandas as pd
  8. import numpy as np
  9. import matplotlib
  10. matplotlib.use('Agg')
  11. import matplotlib.pyplot as plt
  12. import warnings
  13. warnings.filterwarnings('ignore')
  14. def load_real_data():
  15. """加载创业板50指数真实数据 - cyb50_baostock.csv"""
  16. df = pd.read_csv('cyb50_baostock.csv')
  17. df['date'] = pd.to_datetime(df['date'])
  18. df = df.set_index('date').sort_index()
  19. # 转换数据类型
  20. for col in ['open', 'high', 'low', 'close', 'volume']:
  21. df[col] = pd.to_numeric(df[col], errors='coerce')
  22. print(f"真实数据加载成功: {df.index[0].date()} ~ {df.index[-1].date()}")
  23. return df
  24. class HistoricalStrategy:
  25. """趋势策略 - 针对真实历史数据优化"""
  26. def __init__(self):
  27. self.pos = 0
  28. self.entry = 0
  29. self.peak = 0
  30. def signal(self, data):
  31. c = data['close'].values
  32. if len(c) < 60:
  33. return 0
  34. # 更长周期的指标(避免频繁交易)
  35. ma20 = np.mean(c[-20:])
  36. ma60 = np.mean(c[-60:])
  37. # 20日涨跌幅
  38. ret20 = (c[-1] / c[-20] - 1)
  39. # 买入:长期趋势向上 + 中期趋势向上
  40. if c[-1] > ma20 > ma60 and ret20 > 0.05: # 5%以上动量
  41. return 1.0
  42. # 卖出:跌破60日均线或大跌
  43. elif c[-1] < ma60 or ret20 < -0.08:
  44. return 0.0
  45. else:
  46. return self.pos
  47. def generate(self, data):
  48. new_pos = self.signal(data)
  49. curr = data['close'].iloc[-1]
  50. # 更宽松的止损(15%)
  51. if self.pos > 0:
  52. if curr > self.peak:
  53. self.peak = curr
  54. if curr < self.peak * 0.85: # 15%止损
  55. new_pos = 0
  56. if new_pos > 0 and self.pos == 0:
  57. self.entry = curr
  58. self.peak = curr
  59. state = "BUY"
  60. elif new_pos == 0 and self.pos > 0:
  61. self.entry = 0
  62. self.peak = 0
  63. state = "SELL"
  64. else:
  65. state = "HOLD" if new_pos > 0 else "EMPTY"
  66. self.pos = new_pos
  67. return new_pos, state
  68. def backtest(data, strategy, start, end, warmup=60):
  69. data = data[(data.index >= start) & (data.index <= end)]
  70. results = []
  71. nav = 1.0
  72. for i in range(warmup, len(data)):
  73. curr = data.iloc[:i+1]
  74. pos, state = strategy.generate(curr)
  75. if i > warmup:
  76. ret = data['close'].iloc[i] / data['close'].iloc[i-1] - 1
  77. nav *= (1 + ret * results[-1]['pos'])
  78. results.append({
  79. 'date': data.index[i],
  80. 'pos': pos,
  81. 'nav': nav,
  82. 'state': state,
  83. 'close': data['close'].iloc[i]
  84. })
  85. df = pd.DataFrame(results).set_index('date')
  86. df['idx_nav'] = df['close'] / df['close'].iloc[0]
  87. return df
  88. def metrics(nav, idx_nav):
  89. s_ret = nav.pct_change().dropna()
  90. total = nav.iloc[-1] - 1
  91. days = len(nav)
  92. annual = (1 + total) ** (252/days) - 1
  93. idx_total = idx_nav.iloc[-1] - 1
  94. idx_annual = (1 + idx_total) ** (252/days) - 1
  95. running_max = nav.expanding().max()
  96. max_dd = ((nav - running_max) / running_max).min()
  97. vol = s_ret.std() * np.sqrt(252)
  98. sharpe = (annual - 0.03) / vol if vol > 0 else 0
  99. calmar = annual / abs(max_dd) if max_dd != 0 else 0
  100. return {
  101. 'annual': annual, 'idx_annual': idx_annual,
  102. 'excess': annual - idx_annual, 'max_dd': max_dd,
  103. 'sharpe': sharpe, 'calmar': calmar,
  104. 'total': total, 'idx_total': idx_total
  105. }
  106. def plot(df, title, fn):
  107. fig, ax = plt.subplots(2, 1, figsize=(14, 8))
  108. ax[0].plot(df.index, df['nav'], 'r-', lw=2, label='Strategy')
  109. ax[0].plot(df.index, df['idx_nav'], 'gray', lw=1, alpha=0.7, label='Index')
  110. ax[0].set_title(title, fontsize=14)
  111. ax[0].legend()
  112. ax[0].grid(True, alpha=0.3)
  113. ax[1].fill_between(df.index, 0, df['pos'], alpha=0.5, color='green')
  114. ax[1].set_ylim(0, 1.1)
  115. ax[1].set_ylabel('Position')
  116. ax[1].grid(True, alpha=0.3)
  117. plt.tight_layout()
  118. plt.savefig(fn, dpi=150)
  119. print(f" 图表: {fn}")
  120. def main():
  121. print("="*70)
  122. print("创业板50 - 基于真实历史节点的回测")
  123. print("="*70)
  124. data = load_real_data()
  125. print(f"\n数据: {data.index[0].date()} ~ {data.index[-1].date()}")
  126. print(f"价格范围: {data['close'].min():.0f} ~ {data['close'].max():.0f}")
  127. # 训练
  128. print("\n【训练集 2018-2023】")
  129. s = HistoricalStrategy()
  130. train = backtest(data, s, '2018-01-01', '2023-12-31')
  131. m = metrics(train['nav'], train['idx_nav'])
  132. print(f" 策略收益: {m['total']*100:.1f}% (年化{m['annual']*100:.1f}%)")
  133. print(f" 指数收益: {m['idx_total']*100:.1f}% (年化{m['idx_annual']*100:.1f}%)")
  134. print(f" 超额: {m['excess']*100:.1f}%")
  135. print(f" 最大回撤: {m['max_dd']*100:.1f}%")
  136. print(f" 夏普: {m['sharpe']:.2f}")
  137. plot(train, "Training (2018-2023)", "train_historical.png")
  138. # 验证
  139. print("\n【验证集 2024-2025】")
  140. s2 = HistoricalStrategy()
  141. val = backtest(data, s2, '2024-01-01', '2025-12-31')
  142. m2 = metrics(val['nav'], val['idx_nav'])
  143. print(f" 策略收益: {m2['total']*100:.1f}% (年化{m2['annual']*100:.1f}%)")
  144. print(f" 指数收益: {m2['idx_total']*100:.1f}% (年化{m2['idx_annual']*100:.1f}%)")
  145. print(f" 超额: {m2['excess']*100:.1f}%")
  146. print(f" 最大回撤: {m2['max_dd']*100:.1f}%")
  147. plot(val, "Validation (2024-2025)", "val_historical.png")
  148. # 保存数据
  149. data.to_csv('cyb50_historical_data.csv')
  150. print("\n真实历史数据已保存: cyb50_historical_data.csv")
  151. print("\n" + "="*70)
  152. if __name__ == "__main__":
  153. main()