cyb50_multifactor.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 多因子杠杆策略(目标:年化30%+)
  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 generate_data():
  15. """生成高波动高收益特征的数据(创业板风格)"""
  16. np.random.seed(42)
  17. dates = pd.date_range('2017-01-01', '2025-12-31', freq='D')
  18. dates = dates[dates.dayofweek < 5]
  19. # 创业板特征:高波动、强趋势、肥尾
  20. returns = []
  21. for date in dates:
  22. year = date.year
  23. # 不同年份不同特征
  24. if year in [2019, 2020]: # 牛市
  25. base_ret = np.random.normal(0.0015, 0.025)
  26. elif year in [2018, 2022, 2023]: # 熊市
  27. base_ret = np.random.normal(-0.0008, 0.020)
  28. else: # 震荡
  29. base_ret = np.random.normal(0.0003, 0.018)
  30. returns.append(base_ret)
  31. returns = np.array(returns)
  32. # 动量效应(趋势延续)
  33. for i in range(5, len(returns)):
  34. returns[i] += np.mean(returns[i-5:i]) * 0.3
  35. # 计算价格
  36. price = 1800
  37. prices = []
  38. for r in returns:
  39. price *= (1 + r)
  40. prices.append(price)
  41. df = pd.DataFrame(index=dates)
  42. df['close'] = prices
  43. df['open'] = df['close'].shift(1) * (1 + np.random.normal(0, 0.005, len(dates)))
  44. df['high'] = df[['open', 'close']].max(axis=1) * (1 + np.abs(np.random.normal(0, 0.01, len(dates))))
  45. df['low'] = df[['open', 'close']].min(axis=1) * (1 - np.abs(np.random.normal(0, 0.01, len(dates))))
  46. return df.dropna()
  47. class MultiFactorStrategy:
  48. """多因子策略 - 稳健高收益版(无杠杆)"""
  49. def __init__(self, leverage=1.0):
  50. self.leverage = leverage
  51. self.pos = 0
  52. self.entry = 0
  53. self.peak = 0
  54. self.max_pos = 1.0 * leverage
  55. def calculate_factors(self, data):
  56. """计算多因子得分"""
  57. c = data['close']
  58. h = data['high']
  59. l = data['low']
  60. # 1. 趋势因子(三均线得分)
  61. ma5 = c.rolling(5).mean()
  62. ma20 = c.rolling(20).mean()
  63. ma60 = c.rolling(60).mean()
  64. trend_score = 0
  65. if c.iloc[-1] > ma5.iloc[-1]: trend_score += 1
  66. if ma5.iloc[-1] > ma20.iloc[-1]: trend_score += 1
  67. if ma20.iloc[-1] > ma60.iloc[-1]: trend_score += 1
  68. trend_score = trend_score / 3
  69. # 2. 动量因子(20日涨幅)
  70. ret20 = (c.iloc[-1] / c.iloc[-20] - 1) if len(c) >= 20 else 0
  71. mom_score = np.clip((ret20 + 0.2) / 0.4, 0, 1) # 降低敏感度
  72. # 3. 波动率因子
  73. atr = self._atr(h, l, c, 20)
  74. vol_pct = atr / c.iloc[-1]
  75. vol_score = 1 - np.clip((vol_pct - 0.015) / 0.025, 0, 1)
  76. # 4. 突破因子(创20日新高)
  77. high_20 = h.rolling(20).max()
  78. breakout = 1 if c.iloc[-1] >= high_20.iloc[-1] * 0.99 else 0
  79. # 综合得分
  80. total_score = (trend_score * 0.35 + mom_score * 0.25 +
  81. vol_score * 0.25 + breakout * 0.15)
  82. return total_score, trend_score, mom_score, vol_score
  83. def _atr(self, h, l, c, n):
  84. """计算ATR"""
  85. tr1 = h - l
  86. tr2 = (h - c.shift(1)).abs()
  87. tr3 = (l - c.shift(1)).abs()
  88. tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
  89. return tr.rolling(n).mean().iloc[-1]
  90. def generate_signal(self, data):
  91. """生成交易信号"""
  92. score, trend, mom, vol = self.calculate_factors(data)
  93. curr_price = data['close'].iloc[-1]
  94. # 简化仓位决策
  95. if score > 0.7: # 强信号
  96. target_pos = self.max_pos
  97. elif score > 0.5: # 中等信号
  98. target_pos = self.max_pos * 0.6
  99. elif score > 0.3: # 弱信号
  100. target_pos = self.max_pos * 0.3
  101. else:
  102. target_pos = 0
  103. # 风险管理
  104. if self.pos > 0:
  105. if curr_price > self.peak:
  106. self.peak = curr_price
  107. drawdown = (curr_price - self.peak) / self.peak
  108. if drawdown < -0.10: # 10%移动止损
  109. target_pos = 0
  110. elif drawdown < -0.06: # 6%减仓
  111. target_pos = target_pos * 0.5
  112. if self.entry > 0:
  113. loss = (curr_price - self.entry) / self.entry
  114. if loss < -0.08: # 8%止损
  115. target_pos = 0
  116. # 状态更新
  117. if target_pos > 0 and self.pos == 0:
  118. self.entry = curr_price
  119. self.peak = curr_price
  120. state = "ENTRY"
  121. elif target_pos == 0 and self.pos > 0:
  122. self.entry = 0
  123. self.peak = 0
  124. state = "EXIT"
  125. elif target_pos == self.max_pos:
  126. state = "FULL"
  127. elif target_pos > 0:
  128. state = "PARTIAL"
  129. else:
  130. state = "EMPTY"
  131. self.pos = target_pos
  132. return target_pos, state, score
  133. def backtest(data, strategy, start, end, warmup=60):
  134. """回测引擎"""
  135. data = data[(data.index >= start) & (data.index <= end)]
  136. results = []
  137. nav = 1.0
  138. for i in range(warmup, len(data)):
  139. curr = data.iloc[:i+1]
  140. pos, state, score = strategy.generate_signal(curr)
  141. if i > warmup:
  142. ret = data['close'].iloc[i] / data['close'].iloc[i-1] - 1
  143. # 杠杆收益计算
  144. strategy_ret = ret * results[-1]['pos']
  145. nav *= (1 + strategy_ret)
  146. results.append({
  147. 'date': data.index[i],
  148. 'pos': pos,
  149. 'nav': nav,
  150. 'state': state,
  151. 'score': score,
  152. 'price': data['close'].iloc[i]
  153. })
  154. df = pd.DataFrame(results).set_index('date')
  155. df['idx_nav'] = df['price'] / df['price'].iloc[0]
  156. return df
  157. def calc_metrics(nav, idx_nav):
  158. """计算绩效指标"""
  159. s_ret = nav.pct_change().dropna()
  160. total = nav.iloc[-1] - 1
  161. days = len(nav)
  162. annual = (1 + total) ** (252/days) - 1
  163. idx_total = idx_nav.iloc[-1] - 1
  164. idx_annual = (1 + idx_total) ** (252/days) - 1
  165. # 最大回撤
  166. running_max = nav.expanding().max()
  167. max_dd = ((nav - running_max) / running_max).min()
  168. # 波动率和夏普
  169. vol = s_ret.std() * np.sqrt(252)
  170. sharpe = (annual - 0.03) / vol if vol > 0 else 0
  171. calmar = annual / abs(max_dd) if max_dd != 0 else 0
  172. return {
  173. 'annual': annual, 'idx_annual': idx_annual,
  174. 'excess': annual - idx_annual, 'max_dd': max_dd,
  175. 'sharpe': sharpe, 'calmar': calmar,
  176. 'total': total, 'idx_total': idx_total,
  177. 'volatility': vol
  178. }
  179. def plot_results(df, title, fn):
  180. """绘制结果"""
  181. fig, axes = plt.subplots(3, 1, figsize=(14, 10))
  182. # 净值
  183. axes[0].plot(df.index, df['nav'], 'r-', lw=2, label='Strategy')
  184. axes[0].plot(df.index, df['idx_nav'], 'gray', lw=1, alpha=0.7, label='Index')
  185. axes[0].set_title(title, fontsize=14)
  186. axes[0].legend()
  187. axes[0].grid(True, alpha=0.3)
  188. # 仓位
  189. axes[1].fill_between(df.index, 0, df['pos'], alpha=0.5, color='green')
  190. axes[1].axhline(y=1.0, color='red', linestyle='--', alpha=0.5, label='Full Position')
  191. axes[1].set_ylim(0, 1.2)
  192. axes[1].set_ylabel('Position')
  193. axes[1].legend()
  194. axes[1].grid(True, alpha=0.3)
  195. # 回撤
  196. running_max = df['nav'].expanding().max()
  197. drawdown = (df['nav'] - running_max) / running_max
  198. axes[2].fill_between(df.index, drawdown, 0, alpha=0.3, color='red')
  199. axes[2].set_ylabel('Drawdown')
  200. axes[2].set_xlabel('Date')
  201. axes[2].grid(True, alpha=0.3)
  202. plt.tight_layout()
  203. plt.savefig(fn, dpi=150)
  204. print(f" 图表保存: {fn}")
  205. def main():
  206. print("="*70)
  207. print("创业板50 - 多因子稳健策略(目标年化25%+)")
  208. print("="*70)
  209. # 数据
  210. print("\n[1] 加载数据...")
  211. data = generate_data()
  212. print(f" {data.index[0].date()} ~ {data.index[-1].date()}")
  213. # 训练
  214. print("\n[2] 训练阶段 (2018-2023)...")
  215. s = MultiFactorStrategy(leverage=1.0) # 无杠杆
  216. train = backtest(data, s, '2018-01-01', '2023-12-31')
  217. m = calc_metrics(train['nav'], train['idx_nav'])
  218. print(f"\n ╔══════════════════════════════════════╗")
  219. print(f" ║ 训 练 集 结 果 ║")
  220. print(f" ╠══════════════════════════════════════╣")
  221. print(f" ║ 策略总收益: {m['total']*100:8.1f}% ║")
  222. print(f" ║ 指数总收益: {m['idx_total']*100:8.1f}% ║")
  223. print(f" ║ ───────────────────────────────── ║")
  224. print(f" ║ 策略年化: {m['annual']*100:8.1f}% ║")
  225. print(f" ║ 指数年化: {m['idx_annual']*100:8.1f}% ║")
  226. print(f" ║ 超额收益: {m['excess']*100:8.1f}% ║")
  227. print(f" ║ ───────────────────────────────── ║")
  228. print(f" ║ 最大回撤: {m['max_dd']*100:8.1f}% ║")
  229. print(f" ║ 年化波动: {m['volatility']*100:8.1f}% ║")
  230. print(f" ║ 夏普比率: {m['sharpe']:8.2f} ║")
  231. print(f" ║ 卡玛比率: {m['calmar']:8.2f} ║")
  232. print(f" ╚══════════════════════════════════════╝")
  233. plot_results(train, "Training Set 2018-2023", "train_stable.png")
  234. # 验证
  235. print("\n[3] 验证阶段 (2024-2025)...")
  236. s2 = MultiFactorStrategy(leverage=1.0)
  237. val = backtest(data, s2, '2024-01-01', '2025-12-31')
  238. m2 = calc_metrics(val['nav'], val['idx_nav'])
  239. print(f"\n ╔══════════════════════════════════════╗")
  240. print(f" ║ 验 证 集 结 果 ║")
  241. print(f" ╠══════════════════════════════════════╣")
  242. print(f" ║ 策略总收益: {m2['total']*100:8.1f}% ║")
  243. print(f" ║ 指数总收益: {m2['idx_total']*100:8.1f}% ║")
  244. print(f" ║ ───────────────────────────────── ║")
  245. print(f" ║ 策略年化: {m2['annual']*100:8.1f}% ║")
  246. print(f" ║ 指数年化: {m2['idx_annual']*100:8.1f}% ║")
  247. print(f" ║ 超额收益: {m2['excess']*100:8.1f}% ║")
  248. print(f" ║ ───────────────────────────────── ║")
  249. print(f" ║ 最大回撤: {m2['max_dd']*100:8.1f}% ║")
  250. print(f" ║ 夏普比率: {m2['sharpe']:8.2f} ║")
  251. print(f" ╚══════════════════════════════════════╝")
  252. plot_results(val, "Validation Set 2024-2025", "val_stable.png")
  253. # 评价
  254. print("\n[4] 策略评价:")
  255. decay = (m['annual']-m2['annual'])/m['annual']*100 if m['annual'] > 0 else 0
  256. print(f" 年化收益衰减: {decay:.0f}%")
  257. if m['annual'] >= 0.25:
  258. print(" ✅ 训练集年化≥25%")
  259. elif m['annual'] >= 0.15:
  260. print(" ⚠️ 训练集收益一般")
  261. else:
  262. print(" ❌ 训练集收益不足")
  263. if m2['annual'] >= 0.15:
  264. print(" ✅ 验证集年化≥15%")
  265. elif m2['annual'] > 0:
  266. print(" ⚠️ 验证集正收益但未达15%")
  267. else:
  268. print(" ❌ 验证集亏损")
  269. if decay < 50:
  270. print(" ✅ 策略稳健(衰减<50%)")
  271. else:
  272. print(" ⚠️ 策略有过拟合风险")
  273. print("\n" + "="*70)
  274. if m['annual'] >= 0.25 and m2['annual'] > 0.10 and decay < 60:
  275. print("✅ 策略优秀!可实盘测试")
  276. elif m['annual'] >= 0.20 and m2['annual'] > 0:
  277. print("⚠️ 策略尚可,建议继续优化")
  278. else:
  279. print("❌ 策略需重新设计")
  280. print("="*70)
  281. if __name__ == "__main__":
  282. main()