t1_converter_correct.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. CYB50 T+1 回测引擎 - 正确的资金管理版本
  5. 核心规则:
  6. 1. T+1规则:买入当天不能卖出
  7. 2. 资金占用:持仓期间资金被占用,不能开新仓
  8. 3. 100%仓位:每次开仓使用全部可用资金
  9. 4. 信号连续性:如果前一交易持仓中,新信号出现时如何处理?
  10. 回测逻辑:
  11. - 遍历原始做多交易信号
  12. - 维护持仓状态和资金
  13. - 检查每笔交易的资金可用性
  14. - 记录实际执行的交易
  15. """
  16. import pandas as pd
  17. import numpy as np
  18. from datetime import datetime, timedelta
  19. import sys
  20. sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
  21. from cyb50_30min_dual_direction import (
  22. ConfigManager, IntradayDataFetcher,
  23. DualDirectionSignalGenerator, DualDirectionExecutor
  24. )
  25. def simulate_t1_trades_correct(data_df, long_trades_df, initial_capital=1000000):
  26. """
  27. 正确的T+1回测 - 带资金管理
  28. 参数:
  29. data_df: 包含所有价格数据的DataFrame
  30. long_trades_df: 原始做多交易列表(作为信号源)
  31. initial_capital: 初始资金
  32. 返回:
  33. t1_trades_df: T+1规则下实际执行的交易
  34. """
  35. print("\n" + "="*80)
  36. print("T+1回测引擎 - 正确资金管理版本")
  37. print("="*80)
  38. if len(long_trades_df) == 0:
  39. print("没有做多交易记录")
  40. return pd.DataFrame()
  41. # 按开仓时间排序
  42. signals = long_trades_df.sort_values('开仓时间').reset_index(drop=True)
  43. # 状态变量
  44. capital = initial_capital # 总资金
  45. available_capital = initial_capital # 可用资金(未持仓时等于总资金)
  46. position = 0 # 持仓数量
  47. entry_price = 0 # 开仓价格
  48. entry_time = None # 开仓时间
  49. can_sell_time = None # 最早可卖出时间(T+1规则)
  50. entry_signals_str = '' # 入场信号描述
  51. t1_trades = []
  52. skipped_trades = []
  53. print(f"\n初始资金: {initial_capital:,.0f}元")
  54. print(f"总信号数: {len(signals)}笔")
  55. print("\n" + "-"*80)
  56. for idx, signal in signals.iterrows():
  57. signal_entry_time = signal['开仓时间']
  58. signal_entry_price = signal['开仓价格']
  59. signal_exit_time = signal['平仓时间']
  60. signal_exit_price = signal['平仓价格']
  61. signal_exit_reason = signal['退出原因']
  62. signal_entry_signals = signal.get('入场信号', '')
  63. # 情况1:当前无持仓
  64. if position == 0:
  65. # 检查可用资金是否足够
  66. if available_capital <= 0:
  67. print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 资金不足")
  68. skipped_trades.append({
  69. '信号序号': idx+1,
  70. '信号时间': signal_entry_time,
  71. '跳过原因': '资金不足',
  72. '备注': f'可用资金: {available_capital:,.0f}元'
  73. })
  74. continue
  75. # 开仓 - 使用全部可用资金
  76. position_size = int(available_capital / signal_entry_price)
  77. if position_size <= 0:
  78. print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 股价过高无法开仓")
  79. continue
  80. cost = position_size * signal_entry_price
  81. # 更新状态
  82. position = position_size
  83. entry_price = signal_entry_price
  84. entry_time = signal_entry_time
  85. can_sell_time = signal_entry_time + timedelta(days=1) # T+1:次日才能卖
  86. can_sell_time = can_sell_time.replace(hour=9, minute=30) # 假设次日9:30可卖
  87. entry_signals_str = signal_entry_signals
  88. available_capital = 0 # 资金全部占用
  89. print(f"\n[开仓] 信号 #{idx+1}: {entry_time.strftime('%m-%d %H:%M')}")
  90. print(f" 价格: {entry_price:.2f}, 数量: {position}股, 成本: {cost:,.0f}元")
  91. print(f" 最早可卖: {can_sell_time.strftime('%m-%d %H:%M')} (T+1)")
  92. # 确定平仓时间和价格
  93. # 原始信号可能是T0(当天),但T+1要延期
  94. actual_exit_time = max(signal_exit_time, can_sell_time)
  95. # 如果延期,需要从data_df中找到对应的价格
  96. if actual_exit_time > signal_exit_time:
  97. # 查找延期后的平仓价格
  98. future_data = data_df[data_df.index >= can_sell_time]
  99. if len(future_data) > 0:
  100. # 使用最早可用时间的价格
  101. actual_exit_time = future_data.index[0]
  102. actual_exit_price = future_data.iloc[0]['Open']
  103. t1_adjusted = True
  104. print(f" [T+1调整] 原始平仓: {signal_exit_time.strftime('%m-%d %H:%M')} → 新平仓: {actual_exit_time.strftime('%m-%d %H:%M')}")
  105. else:
  106. # 没有后续数据,使用原始平仓
  107. actual_exit_time = signal_exit_time
  108. actual_exit_price = signal_exit_price
  109. t1_adjusted = False
  110. print(f" [警告] 无法找到T+1数据,使用原始平仓时间")
  111. else:
  112. actual_exit_price = signal_exit_price
  113. t1_adjusted = False
  114. print(f" [正常持仓] 平仓: {actual_exit_time.strftime('%m-%d %H:%M')}")
  115. # 计算盈亏
  116. gross_pnl = (actual_exit_price - entry_price) * position
  117. pnl_pct = (actual_exit_price - entry_price) / entry_price * 100
  118. # 更新资金
  119. capital += gross_pnl
  120. available_capital = capital # 平仓后资金释放
  121. # 记录交易
  122. trade_record = {
  123. '交易方向': '做多',
  124. '开仓时间': entry_time,
  125. '平仓时间': actual_exit_time,
  126. '开仓价格': entry_price,
  127. '平仓价格': actual_exit_price,
  128. '仓位': position,
  129. '盈亏金额': gross_pnl,
  130. '盈亏百分比': pnl_pct,
  131. '退出原因': signal_exit_reason if not t1_adjusted else f"T+1延期-{signal_exit_reason}",
  132. '持仓周期数': int((actual_exit_time - entry_time).total_seconds() / 1800),
  133. '持仓小时数': (actual_exit_time - entry_time).total_seconds() / 3600,
  134. 'T+1调整': '是(T0→T1)' if t1_adjusted else '否',
  135. '原平仓时间': signal_exit_time,
  136. '原平仓价格': signal_exit_price,
  137. '入场信号': entry_signals_str,
  138. '平仓时资金': capital,
  139. }
  140. t1_trades.append(trade_record)
  141. status = "✅盈利" if gross_pnl > 0 else "❌亏损"
  142. print(f" [平仓] {actual_exit_time.strftime('%m-%d %H:%M')} @ {actual_exit_price:.2f}")
  143. print(f" {status}: {gross_pnl:+,.0f}元 ({pnl_pct:+.2f}%)")
  144. print(f" 当前总资金: {capital:,.0f}元")
  145. # 重置持仓状态
  146. position = 0
  147. entry_price = 0
  148. entry_time = None
  149. can_sell_time = None
  150. # 情况2:当前有持仓
  151. else:
  152. # 检查新信号是否在持仓期间
  153. if signal_entry_time < entry_time:
  154. print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 时间早于当前持仓")
  155. continue
  156. # 信号出现时已有持仓,跳过该信号
  157. print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 当前持仓中 (持仓从 {entry_time.strftime('%m-%d %H:%M')} 开始)")
  158. skipped_trades.append({
  159. '信号序号': idx+1,
  160. '信号时间': signal_entry_time,
  161. '跳过原因': '持仓中无法开仓',
  162. '备注': f'当前持仓: {entry_time.strftime("%m-%d %H:%M")} 买入'
  163. })
  164. continue
  165. # 处理最后一笔持仓(如果存在)
  166. if position > 0:
  167. final_price = data_df.iloc[-1]['Close']
  168. final_time = data_df.index[-1]
  169. gross_pnl = (final_price - entry_price) * position
  170. pnl_pct = (final_price - entry_price) / entry_price * 100
  171. capital += gross_pnl
  172. trade_record = {
  173. '交易方向': '做多',
  174. '开仓时间': entry_time,
  175. '平仓时间': final_time,
  176. '开仓价格': entry_price,
  177. '平仓价格': final_price,
  178. '仓位': position,
  179. '盈亏金额': gross_pnl,
  180. '盈亏百分比': pnl_pct,
  181. '退出原因': f'回测强制平仓(最终价格{final_price:.2f})',
  182. '持仓周期数': int((final_time - entry_time).total_seconds() / 1800),
  183. '持仓小时数': (final_time - entry_time).total_seconds() / 3600,
  184. 'T+1调整': '否',
  185. '原平仓时间': final_time,
  186. '原平仓价格': final_price,
  187. '入场信号': entry_signals_str,
  188. '平仓时资金': capital,
  189. }
  190. t1_trades.append(trade_record)
  191. print(f"\n[强制平仓] {final_time.strftime('%m-%d %H:%M')} @ {final_price:.2f}")
  192. print(f" 盈亏: {gross_pnl:+,.0f}元 ({pnl_pct:+.2f}%)")
  193. # 生成结果
  194. t1_trades_df = pd.DataFrame(t1_trades)
  195. print("\n" + "="*80)
  196. print("T+1回测完成")
  197. print("="*80)
  198. print(f"原始信号数: {len(signals)}笔")
  199. print(f"实际执行: {len(t1_trades)}笔")
  200. print(f"跳过信号: {len(skipped_trades)}笔")
  201. print(f"最终资金: {capital:,.0f}元")
  202. print(f"总收益率: {(capital/initial_capital-1)*100:+.2f}%")
  203. if len(skipped_trades) > 0:
  204. print("\n【跳过的信号】")
  205. for st in skipped_trades:
  206. print(f" 信号#{st['信号序号']} {st['信号时间'].strftime('%m-%d %H:%M')}: {st['跳过原因']}")
  207. return t1_trades_df
  208. def compare_with_original(original_trades, t1_trades, initial_capital=1000000):
  209. """对比原始交易和T+1交易"""
  210. print("\n" + "="*80)
  211. print("对比分析")
  212. print("="*80)
  213. # 原始统计
  214. orig_pnl = original_trades['盈亏金额'].sum()
  215. orig_final = initial_capital + orig_pnl
  216. # T+1统计
  217. t1_pnl = t1_trades['盈亏金额'].sum() if len(t1_trades) > 0 else 0
  218. t1_final = initial_capital + t1_pnl
  219. print(f"\n【原始交易(假设T0,无资金限制)】")
  220. print(f" 交易次数: {len(original_trades)}")
  221. print(f" 总盈亏: {orig_pnl:+,.0f}元")
  222. print(f" 最终资金: {orig_final:,.0f}元")
  223. print(f" 收益率: {(orig_final/initial_capital-1)*100:+.2f}%")
  224. print(f"\n【T+1回测(正确资金管理)】")
  225. print(f" 交易次数: {len(t1_trades)}")
  226. print(f" 总盈亏: {t1_pnl:+,.0f}元")
  227. print(f" 最终资金: {t1_final:,.0f}元")
  228. print(f" 收益率: {(t1_final/initial_capital-1)*100:+.2f}%")
  229. print(f"\n【差异分析】")
  230. print(f" 交易次数差异: {len(original_trades) - len(t1_trades)}笔 (被T+1规则过滤)")
  231. print(f" 盈亏差异: {t1_pnl - orig_pnl:+,.0f}元")
  232. print(f" 收益率差异: {(t1_final - orig_final)/initial_capital*100:+.2f}%")
  233. def main():
  234. """主程序 - 测试正确版本"""
  235. print("="*80)
  236. print("CYB50 T+1 回测引擎 - 正确资金管理版本")
  237. print("="*80)
  238. initial_capital = 1000000
  239. # 1. 获取数据和原始交易
  240. print("\n【步骤1】获取数据和原始交易信号...")
  241. config_manager = ConfigManager('config.json')
  242. fetcher = IntradayDataFetcher(config_manager)
  243. end_date = datetime.now()
  244. start_date = end_date - timedelta(days=70)
  245. raw_data = fetcher.fetch_30min_data(start_date, end_date)
  246. data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
  247. signal_generator = DualDirectionSignalGenerator()
  248. signals_df = signal_generator.generate_dual_direction_signals(data_with_indicators)
  249. executor = DualDirectionExecutor(initial_capital=initial_capital)
  250. results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
  251. long_trades = trades_df[trades_df['交易方向'] == '做多'].copy()
  252. print(f"✅ 获取到 {len(long_trades)} 笔做多交易信号")
  253. # 2. 运行正确的T+1回测
  254. print("\n【步骤2】运行正确的T+1回测...")
  255. t1_trades = simulate_t1_trades_correct(data_with_indicators, long_trades, initial_capital)
  256. # 3. 对比分析
  257. print("\n【步骤3】对比分析...")
  258. compare_with_original(long_trades, t1_trades, initial_capital)
  259. # 4. 导出结果
  260. if len(t1_trades) > 0:
  261. print("\n【步骤4】导出结果...")
  262. export_df = t1_trades.copy()
  263. for col in ['开仓时间', '平仓时间', '原平仓时间']:
  264. if col in export_df.columns:
  265. export_df[col] = export_df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
  266. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  267. output_file = f'cyb50_t1_correct_{timestamp}.csv'
  268. export_df.to_csv(output_file, index=False, encoding='utf-8-sig')
  269. print(f"✅ 已保存: {output_file}")
  270. print("\n" + "="*80)
  271. print("完成!")
  272. print("="*80)
  273. if __name__ == "__main__":
  274. main()