backtest_dual_direction_correct.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. #!/usr/bin/env python3
  2. """
  3. 基于 cyb50_30min_dual_direction.py 的只做多T+1回测
  4. 使用与 auto_report_long_only_t1.py 完全相同的策略逻辑
  5. """
  6. import csv
  7. import json
  8. from datetime import datetime, timedelta
  9. from collections import deque
  10. import os
  11. class DualDirectionLongOnlyBacktest:
  12. """
  13. 只做多回测 - 基于 cyb50_30min_dual_direction 策略
  14. """
  15. def __init__(self, initial_capital=1000000):
  16. self.initial_capital = initial_capital
  17. self.position_size_pct = 1.0 # 满仓
  18. self.stop_loss_pct = 0.008 # 0.8%止损
  19. self.take_profit_pct = 0.02 # 2%止盈
  20. self.max_hold_bars = 16 # 最大8小时
  21. # 统计数据
  22. self.long_signal_count = 0
  23. self.trades = []
  24. self.capital = initial_capital
  25. def calculate_indicators(self, data):
  26. """计算技术指标"""
  27. print("计算技术指标...")
  28. # 为每行数据添加指标
  29. for i, row in enumerate(data):
  30. if i < 24: # 需要至少24个周期的历史数据
  31. row['RSI'] = 50
  32. row['MACD'] = 0
  33. row['MACD_hist'] = 0
  34. row['K'] = 50
  35. row['D'] = 50
  36. row['J'] = 50
  37. row['BB_middle'] = row['Close']
  38. row['BB_upper'] = row['Close'] * 1.02
  39. row['BB_lower'] = row['Close'] * 0.98
  40. row['Volume_Ratio'] = 1.0
  41. row['Price_Momentum'] = 0
  42. row['Close_Open_Pct'] = 0
  43. row['MA6'] = row['Close']
  44. row['MA12'] = row['Close']
  45. continue
  46. closes = [data[j]['Close'] for j in range(i-23, i+1)]
  47. highs = [data[j]['High'] for j in range(i-23, i+1)]
  48. lows = [data[j]['Low'] for j in range(i-23, i+1)]
  49. volumes = [data[j]['Volume'] for j in range(i-23, i+1)]
  50. # MA
  51. row['MA6'] = sum(closes[-6:]) / 6
  52. row['MA12'] = sum(closes[-12:]) / 12
  53. # RSI
  54. gains = []
  55. losses = []
  56. for j in range(1, 15):
  57. change = closes[-j] - closes[-j-1]
  58. gains.append(max(0, change))
  59. losses.append(max(0, -change))
  60. avg_gain = sum(gains) / 14
  61. avg_loss = sum(losses) / 14
  62. if avg_loss == 0:
  63. row['RSI'] = 100
  64. else:
  65. rs = avg_gain / avg_loss
  66. row['RSI'] = 100 - (100 / (1 + rs))
  67. # 布林带
  68. bb_middle = sum(closes[-20:]) / 20
  69. variance = sum((c - bb_middle) ** 2 for c in closes[-20:]) / 20
  70. bb_std = variance ** 0.5
  71. row['BB_middle'] = bb_middle
  72. row['BB_upper'] = bb_middle + bb_std * 2
  73. row['BB_lower'] = bb_middle - bb_std * 2
  74. # 简化MACD
  75. ema12 = sum(closes[-12:]) / 12
  76. ema26 = sum(closes[-26:]) / 26 if len(closes) >= 26 else sum(closes) / len(closes)
  77. row['MACD'] = ema12 - ema26
  78. row['MACD_hist'] = row['MACD'] # 简化
  79. # KDJ (简化)
  80. low_9 = min(lows[-9:])
  81. high_9 = max(highs[-9:])
  82. if high_9 == low_9:
  83. rsv = 50
  84. else:
  85. rsv = (row['Close'] - low_9) / (high_9 - low_9) * 100
  86. row['K'] = rsv
  87. row['D'] = rsv
  88. row['J'] = 3 * rsv - 2 * rsv
  89. # 成交量比率
  90. vol_ma = sum(volumes[-12:]) / 12
  91. row['Volume_Ratio'] = row['Volume'] / vol_ma if vol_ma > 0 else 1
  92. # 价格动量
  93. row['Price_Momentum'] = (row['Close'] - closes[-6]) / closes[-6] if closes[-6] > 0 else 0
  94. row['Close_Open_Pct'] = (row['Close'] - row['Open']) / row['Open'] if row['Open'] > 0 else 0
  95. print(f" 指标计算完成,共{len(data)}条")
  96. return data
  97. def calculate_long_score(self, row, prev_rows):
  98. """计算做多信号强度 - 完全按照 DualDirection 逻辑"""
  99. long_score = 0
  100. long_signals = []
  101. # 1. RSI超卖做多
  102. if row['RSI'] < 30:
  103. long_score += 2
  104. long_signals.append("RSI超卖")
  105. elif row['RSI'] < 35:
  106. long_score += 1
  107. long_signals.append("RSI偏弱")
  108. # 2. 价格触及布林带下轨
  109. if row['Close'] <= row['BB_lower'] * 1.01:
  110. long_score += 2
  111. long_signals.append("触及下轨")
  112. elif row['Close'] <= row['BB_lower'] * 1.03:
  113. long_score += 1
  114. long_signals.append("接近下轨")
  115. # 3. MACD金叉或柱状图转正
  116. if len(prev_rows) > 0:
  117. prev_macd_hist = prev_rows[-1]['MACD_hist']
  118. if row['MACD_hist'] > 0 and prev_macd_hist <= 0:
  119. long_score += 2
  120. long_signals.append("MACD金叉")
  121. elif row['MACD_hist'] > prev_macd_hist:
  122. long_score += 1
  123. long_signals.append("MACD改善")
  124. # 4. 价格动量向上
  125. if row['Price_Momentum'] > 0.005:
  126. long_score += 1
  127. long_signals.append("动量向上")
  128. # 5. 成交量放大
  129. if row['Volume_Ratio'] > 1.5:
  130. long_score += 1
  131. long_signals.append("放量")
  132. return long_score, long_signals
  133. def run_backtest(self, data_file):
  134. """运行回测"""
  135. print("="*70)
  136. print("DualDirection 策略 - 只做多T+1回测")
  137. print("="*70)
  138. print(f"\n参数设置:")
  139. print(f" 初始资金: {self.initial_capital:,.0f}元")
  140. print(f" 仓位比例: {self.position_size_pct*100:.0f}%")
  141. print(f" 止损: {self.stop_loss_pct*100:.1f}%")
  142. print(f" 止盈: {self.take_profit_pct*100:.1f}%")
  143. print(f" 最大持仓: {self.max_hold_bars}周期(8小时)")
  144. # 1. 加载数据
  145. print(f"\n[1/3] 加载数据: {data_file}")
  146. data = []
  147. with open(data_file, 'r', encoding='utf-8-sig') as f:
  148. reader = csv.DictReader(f)
  149. for row in reader:
  150. data.append({
  151. 'DateTime': row['DateTime'],
  152. 'Open': float(row['Open']),
  153. 'High': float(row['High']),
  154. 'Low': float(row['Low']),
  155. 'Close': float(row['Close']),
  156. 'Volume': float(row['Volume'])
  157. })
  158. print(f" 加载完成: {len(data)}条")
  159. print(f" 时间范围: {data[0]['DateTime']} ~ {data[-1]['DateTime']}")
  160. # 2. 计算指标
  161. print("\n[2/3] 计算技术指标...")
  162. data = self.calculate_indicators(data)
  163. # 3. 生成信号并执行回测
  164. print("\n[3/3] 生成信号并执行回测...")
  165. position = 0
  166. entry_price = 0
  167. entry_time = None
  168. entry_idx = 0
  169. for i in range(24, len(data)):
  170. row = data[i]
  171. current_time = row['DateTime']
  172. current_price = row['Close']
  173. # 计算做多信号
  174. prev_rows = data[max(0, i-5):i]
  175. long_score, long_signals = self.calculate_long_score(row, prev_rows)
  176. # 持仓管理
  177. if position > 0:
  178. holding_bars = i - entry_idx
  179. pnl_pct = (current_price - entry_price) / entry_price
  180. exit_reason = None
  181. # 止损 -0.8%
  182. if pnl_pct <= -self.stop_loss_pct:
  183. exit_reason = f"止损({current_price:.2f})"
  184. # 止盈 +2%
  185. elif pnl_pct >= self.take_profit_pct:
  186. exit_reason = f"止盈({current_price:.2f})"
  187. # 最大持仓时间
  188. elif holding_bars >= self.max_hold_bars:
  189. exit_reason = f"时间平仓({holding_bars}周期)"
  190. # RSI超买
  191. elif row['RSI'] > 75:
  192. exit_reason = f"RSI超买({row['RSI']:.1f})"
  193. if exit_reason:
  194. pnl = (current_price - entry_price) * position
  195. self.capital += pnl
  196. self.trades.append({
  197. 'action': 'CLOSE',
  198. 'time': current_time,
  199. 'price': current_price,
  200. 'shares': position,
  201. 'pnl': pnl,
  202. 'pnl_pct': pnl_pct * 100,
  203. 'reason': exit_reason
  204. })
  205. position = 0
  206. entry_price = 0
  207. entry_time = None
  208. # 开仓判断 - 信号强度>=4
  209. elif long_score >= 4 and position == 0:
  210. position_value = self.capital * self.position_size_pct
  211. position = position_value / current_price
  212. entry_price = current_price
  213. entry_time = current_time
  214. entry_idx = i
  215. self.long_signal_count += 1
  216. self.trades.append({
  217. 'action': 'OPEN',
  218. 'time': current_time,
  219. 'price': current_price,
  220. 'shares': position,
  221. 'value': position_value,
  222. 'reason': f"做多信号(强度{long_score}): {'+'.join(long_signals[:3])}"
  223. })
  224. print(f" 做多信号: {self.long_signal_count}个")
  225. print(f" 实际交易: {len([t for t in self.trades if t['action']=='CLOSE'])}笔")
  226. return self.generate_report()
  227. def generate_report(self):
  228. """生成报告"""
  229. closed_trades = [t for t in self.trades if t['action'] == 'CLOSE']
  230. if not closed_trades:
  231. print("\n无交易记录")
  232. return None
  233. # 计算统计
  234. wins = [t for t in closed_trades if t['pnl'] > 0]
  235. losses = [t for t in closed_trades if t['pnl'] <= 0]
  236. total_pnl = sum(t['pnl'] for t in closed_trades)
  237. final_capital = self.initial_capital + total_pnl
  238. total_return = (final_capital / self.initial_capital - 1) * 100
  239. win_rate = len(wins) / len(closed_trades) * 100
  240. total_profit = sum(t['pnl'] for t in wins) if wins else 0
  241. total_loss = abs(sum(t['pnl'] for t in losses)) if losses else 0
  242. profit_factor = total_profit / total_loss if total_loss > 0 else 0
  243. # 计算最大回撤
  244. peak = self.initial_capital
  245. max_dd = 0
  246. current = self.initial_capital
  247. for t in closed_trades:
  248. current += t['pnl']
  249. if current > peak:
  250. peak = current
  251. dd = (peak - current) / peak * 100
  252. if dd > max_dd:
  253. max_dd = dd
  254. print("\n" + "="*70)
  255. print("回测报告 - DualDirection 只做多T+1")
  256. print("="*70)
  257. print(f"\n【整体表现】")
  258. print(f" 初始资金: {self.initial_capital:,.2f}元")
  259. print(f" 最终资金: {final_capital:,.2f}元")
  260. print(f" 净盈亏: {total_pnl:+,.2f}元")
  261. print(f" 总收益率: {total_return:+.2f}%")
  262. print(f" 最大回撤: {max_dd:.2f}%")
  263. print(f"\n【交易统计】")
  264. print(f" 总交易: {len(closed_trades)}笔")
  265. print(f" 盈利: {len(wins)}笔")
  266. print(f" 亏损: {len(losses)}笔")
  267. print(f" 胜率: {win_rate:.2f}%")
  268. print(f" 盈亏比: {profit_factor:.2f}")
  269. print(f" 总盈利: {total_profit:,.2f}元")
  270. print(f" 总亏损: {total_loss:,.2f}元")
  271. print(f"\n【最近10笔交易】")
  272. for t in closed_trades[-10:]:
  273. print(f" {t['time']} | {t['pnl']:+10,.2f}元 ({t['pnl_pct']:+.2f}%) | {t['reason']}")
  274. print("="*70)
  275. return {
  276. 'total_return': total_return,
  277. 'win_rate': win_rate,
  278. 'profit_factor': profit_factor,
  279. 'trade_count': len(closed_trades),
  280. 'max_drawdown': max_dd
  281. }
  282. if __name__ == '__main__':
  283. backtest = DualDirectionLongOnlyBacktest(initial_capital=1000000)
  284. result = backtest.run_backtest('cyb50_30min_2023_to_20260325.csv')