t1_converter.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. CYB50 T+1 回测引擎 - 正确的资金管理版本 V2
  5. 核心逻辑:
  6. 1. 按时间顺序处理所有信号
  7. 2. 维护持仓状态(是否持仓、持仓期间)
  8. 3. 当新信号的开仓时间落在已有持仓期间时,跳过该信号
  9. 4. 正确处理T+1延期平仓
  10. """
  11. import pandas as pd
  12. import numpy as np
  13. from datetime import datetime, timedelta
  14. import sys
  15. sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
  16. from cyb50_30min_dual_direction import (
  17. ConfigManager, IntradayDataFetcher,
  18. DualDirectionSignalGenerator, DualDirectionExecutor
  19. )
  20. def simulate_t1_trades_v2(data_df, long_trades_df, initial_capital=1000000):
  21. """
  22. 正确的T+1回测 V2 - 正确处理时间重叠
  23. 参数:
  24. data_df: 包含所有价格数据的DataFrame
  25. long_trades_df: 原始做多交易列表(作为信号源)
  26. initial_capital: 初始资金
  27. 返回:
  28. t1_trades_df: T+1规则下实际执行的交易
  29. """
  30. print("\n" + "="*80)
  31. print("T+1回测引擎 V2 - 正确处理时间重叠")
  32. print("="*80)
  33. if len(long_trades_df) == 0:
  34. print("没有做多交易记录")
  35. return pd.DataFrame()
  36. # 准备信号列表
  37. signals = []
  38. for idx, trade in long_trades_df.iterrows():
  39. signals.append({
  40. 'signal_id': idx + 1,
  41. 'entry_time': trade['开仓时间'],
  42. 'entry_price': trade['开仓价格'],
  43. 'exit_time': trade['平仓时间'],
  44. 'exit_price': trade['平仓价格'],
  45. 'exit_reason': trade['退出原因'],
  46. 'entry_signals': trade.get('入场信号', '')
  47. })
  48. # 按开仓时间排序
  49. signals = sorted(signals, key=lambda x: x['entry_time'])
  50. t1_trades = []
  51. capital = initial_capital
  52. # 当前持仓状态
  53. current_position = None # None 或 {'entry_time', 'entry_price', 'can_sell_time', ...}
  54. print(f"\n初始资金: {initial_capital:,.0f}元")
  55. print(f"总信号数: {len(signals)}笔")
  56. print("\n" + "-"*80)
  57. for signal in signals:
  58. sig_entry = signal['entry_time']
  59. sig_exit = signal['exit_time']
  60. # 检查是否有持仓
  61. if current_position is not None:
  62. # 检查新信号是否在持仓期间
  63. hold_end = current_position['actual_exit_time']
  64. if sig_entry < hold_end:
  65. # 新信号在持仓期间,跳过
  66. print(f"\n[跳过] 信号 #{signal['signal_id']}: {sig_entry.strftime('%m-%d %H:%M')}")
  67. print(f" 原因: 当前持仓中 (持仓期: {current_position['entry_time'].strftime('%m-%d %H:%M')} → {hold_end.strftime('%m-%d %H:%M')})")
  68. continue
  69. else:
  70. # 新信号在持仓结束后,先结算当前持仓
  71. print(f"\n[结算前持仓] 信号 #{signal['signal_id']} 到来时,前持仓已结束")
  72. # 持仓已经结束,可以开新仓
  73. current_position = None
  74. # 现在可以开仓
  75. entry_time = sig_entry
  76. entry_price = signal['entry_price']
  77. # 计算可卖时间(T+1规则)
  78. can_sell_time = entry_time + timedelta(days=1)
  79. can_sell_time = can_sell_time.replace(hour=9, minute=30)
  80. # 确定实际平仓时间
  81. # 原始平仓时间 vs T+1最早可卖时间,取较晚者
  82. actual_exit_time = max(sig_exit, can_sell_time)
  83. # 查找实际平仓价格
  84. if actual_exit_time > sig_exit:
  85. # 需要延期,查找数据
  86. future_data = data_df[data_df.index >= actual_exit_time]
  87. if len(future_data) > 0:
  88. actual_exit_time = future_data.index[0]
  89. actual_exit_price = future_data.iloc[0]['Open']
  90. t1_adjusted = True
  91. else:
  92. # 无后续数据,使用原始
  93. actual_exit_time = sig_exit
  94. actual_exit_price = signal['exit_price']
  95. t1_adjusted = False
  96. else:
  97. actual_exit_price = signal['exit_price']
  98. t1_adjusted = False
  99. # 计算仓位(使用全部资金)
  100. position_size = int(capital / entry_price)
  101. if position_size <= 0:
  102. print(f"\n[跳过] 信号 #{signal['signal_id']}: {entry_time.strftime('%m-%d %H:%M')} - 资金不足")
  103. continue
  104. # 记录持仓
  105. current_position = {
  106. 'entry_time': entry_time,
  107. 'entry_price': entry_price,
  108. 'position_size': position_size,
  109. 'actual_exit_time': actual_exit_time,
  110. 'actual_exit_price': actual_exit_price,
  111. 't1_adjusted': t1_adjusted,
  112. 'original_exit_time': sig_exit,
  113. 'original_exit_price': signal['exit_price'],
  114. 'exit_reason': signal['exit_reason'],
  115. 'entry_signals': signal['entry_signals']
  116. }
  117. # 计算盈亏
  118. gross_pnl = (actual_exit_price - entry_price) * position_size
  119. pnl_pct = (actual_exit_price - entry_price) / entry_price * 100
  120. # 更新资金
  121. capital += gross_pnl
  122. # 记录交易
  123. # 计算原始盈亏(假设按原始时间平仓)
  124. if t1_adjusted:
  125. original_pnl = (signal['exit_price'] - entry_price) * position_size
  126. pnl_change = gross_pnl - original_pnl
  127. # 为T+1延期交易生成新的退出原因(基于实际平仓价格)
  128. stop_loss = entry_price * 0.992
  129. take_profit = entry_price * 1.02
  130. if actual_exit_price <= stop_loss:
  131. exit_reason = f"T+1止损(价格{actual_exit_price:.2f}触及止损线{stop_loss:.2f},实际亏损{abs(pnl_pct):.2f}%)"
  132. elif actual_exit_price >= take_profit:
  133. exit_reason = f"T+1止盈(价格{actual_exit_price:.2f}触及止盈线{take_profit:.2f},实际盈利{pnl_pct:.2f}%)"
  134. else:
  135. exit_reason = f"T+1平仓(价格{actual_exit_price:.2f},盈亏{pnl_pct:+.2f}%)"
  136. else:
  137. original_pnl = gross_pnl
  138. pnl_change = 0
  139. exit_reason = signal['exit_reason']
  140. trade_record = {
  141. '交易方向': '做多',
  142. '开仓时间': entry_time,
  143. '平仓时间': actual_exit_time,
  144. '开仓价格': entry_price,
  145. '平仓价格': actual_exit_price,
  146. '仓位': position_size,
  147. '盈亏金额': gross_pnl,
  148. '盈亏百分比': pnl_pct,
  149. '退出原因': exit_reason,
  150. '持仓周期数': int((actual_exit_time - entry_time).total_seconds() / 1800),
  151. '持仓小时数': (actual_exit_time - entry_time).total_seconds() / 3600,
  152. 'T+1调整': '是(T0→T1)' if t1_adjusted else '否',
  153. '原平仓时间': sig_exit,
  154. '原平仓价格': signal['exit_price'],
  155. '原盈亏': original_pnl,
  156. '盈亏变化': pnl_change,
  157. '入场信号': signal['entry_signals'],
  158. '平仓时资金': capital,
  159. }
  160. t1_trades.append(trade_record)
  161. # 打印
  162. status = "✅盈利" if gross_pnl > 0 else "❌亏损"
  163. adj_str = "[T+1调整] " if t1_adjusted else ""
  164. print(f"\n[执行] 信号 #{signal['signal_id']}: {entry_time.strftime('%m-%d %H:%M')} → {actual_exit_time.strftime('%m-%d %H:%M')}")
  165. print(f" {adj_str}价格: {entry_price:.2f} → {actual_exit_price:.2f}")
  166. print(f" 盈亏: {gross_pnl:+,.0f}元 ({pnl_pct:+.2f}%) {status}")
  167. print(f" 资金: {capital:,.0f}元")
  168. # 处理最后一笔持仓(如果数据结束前未平仓)
  169. if current_position is not None and current_position['actual_exit_time'] > data_df.index[-1]:
  170. final_price = data_df.iloc[-1]['Close']
  171. final_time = data_df.index[-1]
  172. entry_time = current_position['entry_time']
  173. entry_price = current_position['entry_price']
  174. position_size = current_position['position_size']
  175. gross_pnl = (final_price - entry_price) * position_size
  176. pnl_pct = (final_price - entry_price) / entry_price * 100
  177. capital += gross_pnl
  178. trade_record = {
  179. '交易方向': '做多',
  180. '开仓时间': entry_time,
  181. '平仓时间': final_time,
  182. '开仓价格': entry_price,
  183. '平仓价格': final_price,
  184. '仓位': position_size,
  185. '盈亏金额': gross_pnl,
  186. '盈亏百分比': pnl_pct,
  187. '退出原因': '回测强制平仓',
  188. '持仓周期数': int((final_time - entry_time).total_seconds() / 1800),
  189. '持仓小时数': (final_time - entry_time).total_seconds() / 3600,
  190. 'T+1调整': '否',
  191. '原平仓时间': final_time,
  192. '原平仓价格': final_price,
  193. '原盈亏': gross_pnl,
  194. '盈亏变化': 0,
  195. '入场信号': current_position['entry_signals'],
  196. '平仓时资金': capital,
  197. }
  198. t1_trades.append(trade_record)
  199. print(f"\n[强制平仓] {final_time.strftime('%m-%d %H:%M')} @ {final_price:.2f}")
  200. print(f" 盈亏: {gross_pnl:+,.0f}元")
  201. # 生成结果
  202. t1_trades_df = pd.DataFrame(t1_trades)
  203. print("\n" + "="*80)
  204. print("T+1回测 V2 完成")
  205. print("="*80)
  206. print(f"原始信号数: {len(signals)}笔")
  207. print(f"实际执行: {len(t1_trades)}笔")
  208. print(f"跳过信号: {len(signals) - len(t1_trades)}笔")
  209. print(f"最终资金: {capital:,.0f}元")
  210. print(f"总收益率: {(capital/initial_capital-1)*100:+.2f}%")
  211. return t1_trades_df
  212. def compare_results(original_trades, t1_trades, initial_capital=1000000):
  213. """对比原始交易和T+1交易"""
  214. print("\n" + "="*80)
  215. print("T+1转换前后对比")
  216. print("="*80)
  217. # 原始统计
  218. original_total_pnl = original_trades['盈亏金额'].sum()
  219. original_final = initial_capital + original_total_pnl
  220. original_return = (original_final / initial_capital - 1) * 100
  221. original_win_rate = (original_trades['盈亏金额'] > 0).sum() / len(original_trades) * 100
  222. # T+1统计
  223. t1_total_pnl = t1_trades['盈亏金额'].sum() if len(t1_trades) > 0 else 0
  224. t1_final = initial_capital + t1_total_pnl
  225. t1_return = (t1_final / initial_capital - 1) * 100
  226. t1_win_rate = (t1_trades['盈亏金额'] > 0).sum() / len(t1_trades) * 100 if len(t1_trades) > 0 else 0
  227. # T0交易统计
  228. t0_adjusted = t1_trades[t1_trades['T+1调整'] == '是(T0→T1)'] if len(t1_trades) > 0 else pd.DataFrame()
  229. print(f"\n【原始交易(T0规则)】")
  230. print(f" 交易次数: {len(original_trades)}")
  231. print(f" 总盈亏: {original_total_pnl:+,.2f}元")
  232. print(f" 最终资金: {original_final:,.2f}元")
  233. print(f" 收益率: {original_return:+.2f}%")
  234. print(f" 胜率: {original_win_rate:.1f}%")
  235. print(f"\n【T+1转换后】")
  236. print(f" 交易次数: {len(t1_trades)}")
  237. print(f" 总盈亏: {t1_total_pnl:+,.2f}元")
  238. print(f" 最终资金: {t1_final:,.2f}元")
  239. print(f" 收益率: {t1_return:+.2f}%")
  240. print(f" 胜率: {t1_win_rate:.1f}%")
  241. print(f"\n【T+1调整统计】")
  242. print(f" T0→T1调整交易数: {len(t0_adjusted)}笔")
  243. if len(t0_adjusted) > 0:
  244. print(f" 调整后盈亏变化: {t0_adjusted['盈亏变化'].sum():+,.2f}元")
  245. print(f" 平均每笔变化: {t0_adjusted['盈亏变化'].mean():+,.2f}元")
  246. print(f"\n【收益差异】")
  247. print(f" 收益率变化: {(t1_return - original_return):+.2f}%")
  248. print(f" 绝对盈亏差: {(t1_total_pnl - original_total_pnl):+,.2f}元")
  249. def main():
  250. """主程序 - 测试V2版本"""
  251. print("="*80)
  252. print("CYB50 T+1 回测引擎 V2")
  253. print("="*80)
  254. initial_capital = 1000000
  255. # 获取数据和原始交易
  256. print("\n【步骤1】获取数据...")
  257. config_manager = ConfigManager('config.json')
  258. fetcher = IntradayDataFetcher(config_manager)
  259. end_date = datetime.now()
  260. start_date = end_date - timedelta(days=70)
  261. raw_data = fetcher.fetch_30min_data(start_date, end_date)
  262. data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
  263. signal_generator = DualDirectionSignalGenerator()
  264. signals_df = signal_generator.generate_dual_direction_signals(data_with_indicators)
  265. executor = DualDirectionExecutor(initial_capital=initial_capital)
  266. results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
  267. long_trades = trades_df[trades_df['交易方向'] == '做多'].copy()
  268. print(f"✅ 原始做多信号: {len(long_trades)}笔")
  269. # 运行V2回测
  270. print("\n【步骤2】运行T+1回测 V2...")
  271. t1_trades = simulate_t1_trades_v2(data_with_indicators, long_trades, initial_capital)
  272. # 对比
  273. print("\n【步骤3】对比...")
  274. orig_pnl = long_trades['盈亏金额'].sum()
  275. t1_pnl = t1_trades['盈亏金额'].sum() if len(t1_trades) > 0 else 0
  276. print(f"原始交易: {len(long_trades)}笔, 盈亏 {orig_pnl:+,.0f}元")
  277. print(f"T+1交易: {len(t1_trades)}笔, 盈亏 {t1_pnl:+,.0f}元")
  278. print(f"差异: {t1_pnl - orig_pnl:+,.0f}元")
  279. # 检查时间重叠
  280. if len(t1_trades) > 0:
  281. print("\n【步骤4】验证时间无重叠...")
  282. prev_exit = None
  283. overlap = 0
  284. for _, row in t1_trades.iterrows():
  285. if prev_exit is not None and row['开仓时间'] < prev_exit:
  286. overlap += 1
  287. print(f" ⚠️ 重叠: {row['开仓时间'].strftime('%m-%d %H:%M')} 早于前笔平仓 {prev_exit.strftime('%m-%d %H:%M')}")
  288. prev_exit = row['平仓时间']
  289. if overlap == 0:
  290. print(" ✅ 无时间重叠")
  291. else:
  292. print(f" ❌ 发现 {overlap} 处重叠")
  293. # 保存
  294. if len(t1_trades) > 0:
  295. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  296. output_file = f'cyb50_t1_v2_{timestamp}.csv'
  297. export_df = t1_trades.copy()
  298. for col in ['开仓时间', '平仓时间']:
  299. export_df[col] = export_df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
  300. export_df.to_csv(output_file, index=False, encoding='utf-8-sig')
  301. print(f"\n✅ 已保存: {output_file}")
  302. print("\n" + "="*80)
  303. print("完成!")
  304. print("="*80)
  305. if __name__ == "__main__":
  306. main()