compare_yearly_returns.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 全策略年度收益对比(真实数据)
  5. 对比策略:趋势跟踪、双均线、动量、多因子、RSI
  6. """
  7. import pandas as pd
  8. import numpy as np
  9. import warnings
  10. warnings.filterwarnings('ignore')
  11. print("="*90)
  12. print("创业板50指数 - 全策略年度收益对比(真实数据)")
  13. print("="*90)
  14. # ==================== 1. 加载真实数据 ====================
  15. def load_real_data():
  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. for col in ['open', 'high', 'low', 'close', 'volume']:
  20. df[col] = pd.to_numeric(df[col], errors='coerce')
  21. return df
  22. # ==================== 2. 策略定义 ====================
  23. # 策略1: 趋势跟踪
  24. def strategy_trend(data, pos):
  25. close = data['close'].values
  26. high = data['high'].values
  27. low = data['low'].values
  28. if len(close) < 60:
  29. return 0, "INIT"
  30. ma10 = np.mean(close[-10:])
  31. ma30 = np.mean(close[-30:])
  32. ret10 = (close[-1] / close[-10] - 1) if len(close) >= 10 else 0
  33. high_20 = np.max(high[-20:])
  34. low_20 = np.min(low[-20:])
  35. curr = close[-1]
  36. buy = (curr > ma10 > ma30) and (curr >= high_20 * 0.995) and (ret10 > 0.02)
  37. sell = (curr < ma30) or (curr <= low_20 * 1.005)
  38. if buy and pos == 0:
  39. return 1.0, "ENTRY"
  40. elif sell and pos > 0:
  41. return 0.0, "EXIT"
  42. return pos, "HOLD" if pos > 0 else "EMPTY"
  43. # 策略2: 双均线
  44. def strategy_ma_cross(data, pos):
  45. close = data['close'].values
  46. if len(close) < 60:
  47. return 0, "INIT"
  48. ma20 = np.mean(close[-20:])
  49. ma60 = np.mean(close[-60:])
  50. curr = close[-1]
  51. if curr > ma20 > ma60:
  52. return 1.0, "BULL"
  53. elif curr < ma60:
  54. return 0.0, "BEAR"
  55. return pos, "HOLD"
  56. # 策略3: 动量
  57. def strategy_momentum(data, pos):
  58. close = data['close']
  59. if len(close) < 60:
  60. return 0, "INIT"
  61. ma5 = close.rolling(5).mean().iloc[-1]
  62. ma20 = close.rolling(20).mean().iloc[-1]
  63. ma60 = close.rolling(60).mean().iloc[-1]
  64. momentum = (close.iloc[-1] / close.iloc[-10] - 1) * 100
  65. trend_strong = (close.iloc[-1] > ma5) and (ma5 > ma20) and (ma20 > ma60)
  66. trend_weak = (close.iloc[-1] < ma5) and (ma5 < ma20)
  67. if trend_strong and momentum > 2:
  68. return 1.0, "STRONG_UP"
  69. elif trend_strong and momentum > 0:
  70. return 0.8, "UP"
  71. elif trend_weak or momentum < -3:
  72. return 0.0, "DOWN"
  73. return 0.5, "OSCILLATE"
  74. # 策略4: 多因子
  75. def strategy_multifactor(data, pos):
  76. c = data['close']
  77. h = data['high']
  78. l = data['low']
  79. if len(c) < 60:
  80. return 0, "INIT"
  81. # 趋势因子
  82. ma5 = c.rolling(5).mean()
  83. ma20 = c.rolling(20).mean()
  84. ma60 = c.rolling(60).mean()
  85. trend_score = 0
  86. if c.iloc[-1] > ma5.iloc[-1]: trend_score += 1
  87. if ma5.iloc[-1] > ma20.iloc[-1]: trend_score += 1
  88. if ma20.iloc[-1] > ma60.iloc[-1]: trend_score += 1
  89. trend_score = trend_score / 3
  90. # 动量因子
  91. ret20 = (c.iloc[-1] / c.iloc[-20] - 1) if len(c) >= 20 else 0
  92. mom_score = np.clip((ret20 + 0.2) / 0.4, 0, 1)
  93. # 波动率因子
  94. atr = pd.concat([h-l, (h-c.shift(1)).abs(), (l-c.shift(1)).abs()], axis=1).max(axis=1)
  95. atr_mean = atr.rolling(20).mean().iloc[-1]
  96. vol_pct = atr_mean / c.iloc[-1]
  97. vol_score = 1 - np.clip((vol_pct - 0.015) / 0.025, 0, 1)
  98. # 突破因子
  99. high_20 = h.rolling(20).max()
  100. breakout = 1 if c.iloc[-1] >= high_20.iloc[-1] * 0.99 else 0
  101. # 综合得分
  102. total_score = trend_score * 0.35 + mom_score * 0.25 + vol_score * 0.25 + breakout * 0.15
  103. if total_score > 0.7:
  104. return 1.0, "STRONG"
  105. elif total_score > 0.5:
  106. return 0.6, "MEDIUM"
  107. elif total_score > 0.3:
  108. return 0.3, "WEAK"
  109. return 0.0, "EMPTY"
  110. # 策略5: RSI
  111. def strategy_rsi(data, pos):
  112. close = data['close']
  113. if len(close) < 20:
  114. return 0, "INIT"
  115. delta = close.diff()
  116. gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
  117. loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
  118. rs = gain / loss
  119. rsi = 100 - (100 / (1 + rs))
  120. curr_rsi = rsi.iloc[-1]
  121. if pd.isna(curr_rsi):
  122. return 0, "INIT"
  123. if curr_rsi < 30:
  124. return 1.0, "OVERSOLD"
  125. elif curr_rsi > 70:
  126. return 0.0, "OVERBOUGHT"
  127. return pos, "HOLD"
  128. # ==================== 3. 回测引擎 ====================
  129. def backtest_yearly(data, strategy_func, year, warmup=60):
  130. """年度回测"""
  131. year_data = data[data.index.year == year].copy()
  132. if len(year_data) < warmup + 5:
  133. return None
  134. nav = 1.0
  135. position = 0
  136. for i in range(warmup, len(year_data)):
  137. curr_data = data.loc[:year_data.index[i]]
  138. new_pos, state = strategy_func(curr_data, position)
  139. if i > warmup:
  140. daily_ret = year_data['close'].iloc[i] / year_data['close'].iloc[i-1] - 1
  141. nav *= (1 + daily_ret * position)
  142. position = new_pos if new_pos is not None else position
  143. return (nav - 1) * 100 # 返回百分比收益
  144. # ==================== 4. 主程序 ====================
  145. def main():
  146. # 加载数据
  147. data = load_real_data()
  148. strategies = [
  149. ("趋势跟踪", strategy_trend),
  150. ("双均线", strategy_ma_cross),
  151. ("动量", strategy_momentum),
  152. ("多因子", strategy_multifactor),
  153. ("RSI", strategy_rsi),
  154. ]
  155. years = list(range(2018, 2026))
  156. # 收集结果
  157. results = {name: {} for name, _ in strategies}
  158. results["买入持有"] = {}
  159. print("\n开始年度回测...")
  160. print("-"*90)
  161. for year in years:
  162. year_data = data[data.index.year == year]
  163. if len(year_data) == 0:
  164. continue
  165. # 买入持有收益
  166. start_price = year_data['close'].iloc[60] # 考虑warmup
  167. end_price = year_data['close'].iloc[-1]
  168. buyhold_ret = (end_price / start_price - 1) * 100
  169. results["买入持有"][year] = buyhold_ret
  170. # 各策略收益
  171. for name, strategy_func in strategies:
  172. ret = backtest_yearly(data, strategy_func, year)
  173. results[name][year] = ret if ret is not None else 0
  174. # 打印结果
  175. print("\n" + "="*90)
  176. print("年度收益对比表 (%)")
  177. print("="*90)
  178. # 表头
  179. header = f"{'策略':<10}"
  180. for year in years:
  181. header += f" | {year:>8}"
  182. header += f" | {'平均':>8} | {'跑赢年数':>8}"
  183. print(header)
  184. print("-"*90)
  185. # 各策略
  186. all_strategies = list(results.keys())
  187. for name in all_strategies:
  188. row = f"{name:<10}"
  189. returns = []
  190. win_count = 0
  191. for year in years:
  192. if year in results[name]:
  193. ret = results[name][year]
  194. returns.append(ret)
  195. row += f" | {ret:>+7.1f}"
  196. # 判断是否跑赢指数
  197. if name != "买入持有" and year in results["买入持有"]:
  198. if ret > results["买入持有"][year]:
  199. win_count += 1
  200. else:
  201. row += f" | {'--':>8}"
  202. avg_ret = np.mean(returns) if returns else 0
  203. row += f" | {avg_ret:>+7.1f}"
  204. if name != "买入持有":
  205. row += f" | {win_count:>6}/8"
  206. else:
  207. row += f" | {'--':>8}"
  208. print(row)
  209. print("-"*90)
  210. # 超额收益统计
  211. print("\n" + "="*90)
  212. print("超额收益对比 (策略 - 买入持有)")
  213. print("="*90)
  214. header = f"{'策略':<10}"
  215. for year in years:
  216. header += f" | {year:>8}"
  217. header += f" | {'平均超额':>8}"
  218. print(header)
  219. print("-"*90)
  220. for name in all_strategies:
  221. if name == "买入持有":
  222. continue
  223. row = f"{name:<10}"
  224. excess_returns = []
  225. for year in years:
  226. if year in results[name] and year in results["买入持有"]:
  227. excess = results[name][year] - results["买入持有"][year]
  228. excess_returns.append(excess)
  229. marker = "⭐" if excess > 10 else "✓" if excess > 0 else ""
  230. row += f" | {excess:>+7.1f}{marker}"
  231. else:
  232. row += f" | {'--':>8}"
  233. avg_excess = np.mean(excess_returns) if excess_returns else 0
  234. row += f" | {avg_excess:>+7.1f}"
  235. print(row)
  236. print("-"*90)
  237. # 汇总评价
  238. print("\n" + "="*90)
  239. print("策略评价")
  240. print("="*90)
  241. for name in all_strategies:
  242. if name == "买入持有":
  243. continue
  244. returns = [results[name][y] for y in years if y in results[name]]
  245. excess_list = [results[name][y] - results["买入持有"][y] for y in years if y in results[name] and y in results["买入持有"]]
  246. win_years = sum(1 for e in excess_list if e > 0)
  247. avg_ret = np.mean(returns)
  248. avg_excess = np.mean(excess_list)
  249. print(f"\n【{name}】")
  250. print(f" 年均收益: {avg_ret:+.1f}%")
  251. print(f" 年均超额: {avg_excess:+.1f}%")
  252. print(f" 跑赢年数: {win_years}/8 ({win_years/8*100:.0f}%)")
  253. if avg_excess > 10:
  254. print(f" 评价: 优秀 ⭐⭐⭐")
  255. elif avg_excess > 5:
  256. print(f" 评价: 良好 ⭐⭐")
  257. elif avg_excess > 0:
  258. print(f" 评价: 一般 ⭐")
  259. else:
  260. print(f" 评价: 不佳")
  261. print("\n" + "="*90)
  262. if __name__ == "__main__":
  263. main()