analyze_trades_time.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50 T+1 交易时间维度分析
  5. """
  6. import pandas as pd
  7. import numpy as np
  8. from datetime import datetime
  9. import warnings
  10. import sys
  11. warnings.filterwarnings('ignore')
  12. # Redirect stdout to suppress verbose output
  13. from io import StringIO
  14. old_stdout = sys.stdout
  15. sys.stdout = StringIO()
  16. from cyb50_30min_dual_direction import ConfigManager, IntradayDataFetcher, DualDirectionSignalGenerator, DualDirectionExecutor
  17. from t1_converter import simulate_t1_trades
  18. def load_local_data(csv_file='cyb50_30min_2023_to_20260325.csv'):
  19. df = pd.read_csv(csv_file)
  20. df['DateTime'] = pd.to_datetime(df['DateTime'])
  21. df.set_index('DateTime', inplace=True)
  22. df.sort_index(inplace=True)
  23. if 'Open' not in df.columns and 'o' in df.columns:
  24. df.rename(columns={'o':'Open','h':'High','l':'Low','c':'Close','v':'Volume','a':'Amount'}, inplace=True)
  25. for col in ['Open', 'High', 'Low', 'Close', 'Volume']:
  26. if col in df.columns:
  27. df[col] = pd.to_numeric(df[col], errors='coerce')
  28. df['Returns'] = df['Close'].pct_change()
  29. df['High_Low_Pct'] = (df['High'] - df['Low']) / df['Close'].shift(1)
  30. df['Close_Open_Pct'] = (df['Close'] - df['Open']) / df['Open']
  31. df.ffill(inplace=True)
  32. df.dropna(inplace=True)
  33. return df
  34. # 运行回测
  35. initial_capital = 1000000
  36. raw_data = load_local_data('cyb50_30min_2023_to_20260325.csv')
  37. config_manager = ConfigManager('config.json')
  38. fetcher = IntradayDataFetcher(config_manager)
  39. data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
  40. signal_generator = DualDirectionSignalGenerator()
  41. signals_df = signal_generator.generate_dual_direction_signals(data_with_indicators)
  42. executor = DualDirectionExecutor(initial_capital=initial_capital)
  43. results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
  44. long_trades = trades_df[trades_df['交易方向'] == '做多'].copy()
  45. t1_trades = simulate_t1_trades(data_with_indicators, long_trades, initial_capital)
  46. # Restore stdout
  47. sys.stdout = old_stdout
  48. # ============ 时间维度分析 ============
  49. print('='*80)
  50. print('创业板50 T+1 交易时间维度深度分析')
  51. print('='*80)
  52. # 准备数据
  53. t1_trades['开仓日期'] = pd.to_datetime(t1_trades['开仓时间']).dt.date
  54. t1_trades['开仓月份'] = pd.to_datetime(t1_trades['开仓时间']).dt.to_period('M')
  55. t1_trades['开仓季度'] = pd.to_datetime(t1_trades['开仓时间']).dt.to_period('Q')
  56. t1_trades['开仓年份'] = pd.to_datetime(t1_trades['开仓时间']).dt.year
  57. t1_trades['开仓小时'] = pd.to_datetime(t1_trades['开仓时间']).dt.hour
  58. t1_trades['星期几'] = pd.to_datetime(t1_trades['开仓时间']).dt.day_name()
  59. t1_trades['是否盈利'] = t1_trades['盈亏金额'] > 0
  60. t1_trades['T+1调整'] = t1_trades['T+1调整'].fillna('否')
  61. # ========== 1. 年度分析 ==========
  62. print('\n' + '='*80)
  63. print('【1】年度表现分析')
  64. print('='*80)
  65. yearly_stats = t1_trades.groupby('开仓年份').agg({
  66. '盈亏金额': ['count', 'sum', 'mean'],
  67. '是否盈利': 'sum'
  68. }).round(2)
  69. yearly_stats.columns = ['交易次数', '总盈亏', '平均盈亏', '盈利次数']
  70. yearly_stats['胜率'] = (yearly_stats['盈利次数'] / yearly_stats['交易次数'] * 100).round(1)
  71. yearly_stats['亏损次数'] = yearly_stats['交易次数'] - yearly_stats['盈利次数']
  72. # 计算累计资金
  73. cumulative = initial_capital
  74. yearly_stats['期初资金'] = 0.0
  75. yearly_stats['期末资金'] = 0.0
  76. for year in yearly_stats.index:
  77. yearly_stats.loc[year, '期初资金'] = float(cumulative)
  78. cumulative += yearly_stats.loc[year, '总盈亏']
  79. yearly_stats.loc[year, '期末资金'] = float(cumulative)
  80. yearly_stats['年度收益率'] = ((yearly_stats['期末资金'] / yearly_stats['期初资金'] - 1) * 100).round(2)
  81. print(yearly_stats[['交易次数', '盈利次数', '亏损次数', '胜率', '总盈亏', '年度收益率']].to_string())
  82. # ========== 2. 月度分析 ==========
  83. print('\n' + '='*80)
  84. print('【2】月度表现分析 (各年度月份对比)')
  85. print('='*80)
  86. monthly_stats = t1_trades.groupby(['开仓年份', pd.to_datetime(t1_trades['开仓时间']).dt.month]).agg({
  87. '盈亏金额': ['count', 'sum'],
  88. '是否盈利': 'sum'
  89. }).round(2)
  90. monthly_stats.columns = ['交易次数', '总盈亏', '盈利次数']
  91. monthly_stats['胜率'] = (monthly_stats['盈利次数'] / monthly_stats['交易次数'] * 100).round(1)
  92. monthly_stats = monthly_stats[monthly_stats['交易次数'] > 0]
  93. print('\n月度盈亏分布:')
  94. for year in sorted(t1_trades['开仓年份'].unique()):
  95. year_data = monthly_stats.loc[year] if year in monthly_stats.index else None
  96. if year_data is not None:
  97. print(f'\n{year}年:')
  98. print(year_data[['交易次数', '胜率', '总盈亏']].to_string())
  99. # 找出最佳/最差月份
  100. best_months = monthly_stats.nlargest(5, '总盈亏')[['交易次数', '胜率', '总盈亏']]
  101. worst_months = monthly_stats.nsmallest(5, '总盈亏')[['交易次数', '胜率', '总盈亏']]
  102. print('\n【最佳月份TOP5】')
  103. print(best_months.to_string())
  104. print('\n【最差月份TOP5】')
  105. print(worst_months.to_string())
  106. # ========== 3. T+1调整的影响分析 ==========
  107. print('\n' + '='*80)
  108. print('【3】T+1调整深度分析')
  109. print('='*80)
  110. # 按时间分析T+1调整
  111. t1_by_year = t1_trades.groupby(['开仓年份', 'T+1调整']).agg({
  112. '盈亏金额': ['count', 'sum', 'mean'],
  113. '是否盈利': 'sum'
  114. }).round(2)
  115. t1_by_year.columns = ['交易次数', '总盈亏', '平均盈亏', '盈利次数']
  116. t1_by_year['胜率'] = (t1_by_year['盈利次数'] / t1_by_year['交易次数'] * 100).round(1)
  117. print('\n按年度和T+1调整类型统计:')
  118. print(t1_by_year.to_string())
  119. # 分析T+1调整的盈亏分布
  120. print('\nT+1调整交易的盈亏分布:')
  121. t1_adj_trades = t1_trades[t1_trades['T+1调整'] == '是(T0→T1)']
  122. if len(t1_adj_trades) > 0:
  123. bins = [-np.inf, -50000, -20000, -10000, -5000, 0, 5000, 10000, 20000, 50000, np.inf]
  124. labels = ['< -5万', '-5万~-2万', '-2万~-1万', '-1万~-5千', '-5千~0',
  125. '0~5千', '5千~1万', '1万~2万', '2万~5万', '> 5万']
  126. t1_adj_trades['盈亏区间'] = pd.cut(t1_adj_trades['盈亏金额'], bins=bins, labels=labels)
  127. print(t1_adj_trades['盈亏区间'].value_counts().sort_index().to_string())
  128. # ========== 4. 日内时间分析 ==========
  129. print('\n' + '='*80)
  130. print('【4】日内开仓时间分析')
  131. print('='*80)
  132. hourly_stats = t1_trades.groupby('开仓小时').agg({
  133. '盈亏金额': ['count', 'sum', 'mean'],
  134. '是否盈利': 'sum'
  135. }).round(2)
  136. hourly_stats.columns = ['交易次数', '总盈亏', '平均盈亏', '盈利次数']
  137. hourly_stats['胜率'] = (hourly_stats['盈利次数'] / hourly_stats['交易次数'] * 100).round(1)
  138. print('\n按小时统计:')
  139. print(hourly_stats[['交易次数', '胜率', '平均盈亏', '总盈亏']].to_string())
  140. # ========== 5. 星期分析 ==========
  141. print('\n' + '='*80)
  142. print('【5】星期效应分析')
  143. print('='*80)
  144. # 星期顺序
  145. weekday_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
  146. weekday_names = {'Monday': '周一', 'Tuesday': '周二', 'Wednesday': '周三',
  147. 'Thursday': '周四', 'Friday': '周五'}
  148. weekday_stats = t1_trades.groupby('星期几').agg({
  149. '盈亏金额': ['count', 'sum', 'mean'],
  150. '是否盈利': 'sum'
  151. }).round(2)
  152. weekday_stats.columns = ['交易次数', '总盈亏', '平均盈亏', '盈利次数']
  153. weekday_stats['胜率'] = (weekday_stats['盈利次数'] / weekday_stats['交易次数'] * 100).round(1)
  154. # 按正确顺序排列
  155. weekday_stats = weekday_stats.reindex([d for d in weekday_order if d in weekday_stats.index])
  156. weekday_stats.index = [weekday_names.get(d, d) for d in weekday_stats.index]
  157. print(weekday_stats[['交易次数', '胜率', '平均盈亏', '总盈亏']].to_string())
  158. # ========== 6. 连续亏损分析 ==========
  159. print('\n' + '='*80)
  160. print('【6】连续交易表现分析')
  161. print('='*80)
  162. # 计算连续盈亏
  163. t1_trades_sorted = t1_trades.sort_values('开仓时间').reset_index(drop=True)
  164. t1_trades_sorted['连续盈亏'] = 0
  165. current_streak = 0
  166. streak_type = None
  167. streaks = []
  168. current_streak_info = {'类型': None, '长度': 0, '盈亏': 0, '开始': None, '结束': None}
  169. for i, row in t1_trades_sorted.iterrows():
  170. is_profit = row['盈亏金额'] > 0
  171. if current_streak_info['类型'] is None:
  172. current_streak_info['类型'] = '盈利' if is_profit else '亏损'
  173. current_streak_info['开始'] = row['开仓时间']
  174. if (is_profit and current_streak_info['类型'] == '盈利') or (not is_profit and current_streak_info['类型'] == '亏损'):
  175. current_streak_info['长度'] += 1
  176. current_streak_info['盈亏'] += row['盈亏金额']
  177. current_streak_info['结束'] = row['平仓时间']
  178. else:
  179. streaks.append(current_streak_info.copy())
  180. current_streak_info = {
  181. '类型': '盈利' if is_profit else '亏损',
  182. '长度': 1,
  183. '盈亏': row['盈亏金额'],
  184. '开始': row['开仓时间'],
  185. '结束': row['平仓时间']
  186. }
  187. if current_streak_info['类型'] is not None:
  188. streaks.append(current_streak_info)
  189. streaks_df = pd.DataFrame(streaks)
  190. # 分析连续亏损
  191. loss_streaks = streaks_df[streaks_df['类型'] == '亏损'].copy()
  192. if len(loss_streaks) > 0:
  193. print(f'\n连续亏损统计:')
  194. print(f' 总次数: {len(loss_streaks)}次')
  195. print(f' 平均长度: {loss_streaks["长度"].mean():.1f}笔')
  196. print(f' 最大连续亏损: {loss_streaks["长度"].max()}笔')
  197. print(f' 最长连续亏损详情:')
  198. worst_streak = loss_streaks.loc[loss_streaks['长度'].idxmax()]
  199. print(f' 时间: {worst_streak["开始"]} ~ {worst_streak["结束"]}')
  200. print(f' 连续{worst_streak["长度"]}笔亏损, 总亏损{worst_streak["盈亏"]:,.0f}元')
  201. print('\n连续亏损TOP5:')
  202. worst5 = loss_streaks.nsmallest(5, '盈亏')[['开始', '结束', '长度', '盈亏']]
  203. print(worst5.to_string(index=False))
  204. # ========== 7. 市场环境分析 ==========
  205. print('\n' + '='*80)
  206. print('【7】市场环境与交易表现')
  207. print('='*80)
  208. # 计算月度市场收益率
  209. monthly_market = data_with_indicators.resample('ME')['Close'].agg(['first', 'last'])
  210. monthly_market['月收益率'] = (monthly_market['last'] / monthly_market['first'] - 1) * 100
  211. # 合并交易数据
  212. t1_trades['月份'] = pd.to_datetime(t1_trades['开仓时间']).dt.to_period('M')
  213. monthly_trade_perf = t1_trades.groupby('月份')['盈亏金额'].sum()
  214. # 找出市场大跌月份
  215. bear_months = monthly_market[monthly_market['月收益率'] < -5].index
  216. print(f'\n市场大跌月份(跌幅>5%): {len(bear_months)}个')
  217. for m in bear_months[:5]:
  218. if m in monthly_trade_perf.index:
  219. print(f' {m}: 市场{monthly_market.loc[m, "月收益率"]:.1f}%, 策略{monthly_trade_perf[m]:+,.0f}元')
  220. # ========== 8. 亏损交易特征分析 ==========
  221. print('\n' + '='*80)
  222. print('【8】亏损交易深度分析')
  223. print('='*80)
  224. loss_trades = t1_trades[t1_trades['盈亏金额'] < 0].copy()
  225. if len(loss_trades) > 0:
  226. print(f'\n亏损交易总数: {len(loss_trades)}笔')
  227. print(f'总亏损金额: {loss_trades["盈亏金额"].sum():,.0f}元')
  228. print(f'平均单笔亏损: {loss_trades["盈亏金额"].mean():,.0f}元')
  229. print(f'最大单笔亏损: {loss_trades["盈亏金额"].min():,.0f}元')
  230. # 按T+1调整分析亏损
  231. loss_by_t1 = loss_trades.groupby('T+1调整').agg({
  232. '盈亏金额': ['count', 'sum', 'mean']
  233. }).round(2)
  234. loss_by_t1.columns = ['亏损笔数', '总亏损', '平均亏损']
  235. print('\n按T+1调整分类的亏损:')
  236. print(loss_by_t1.to_string())
  237. # 年度亏损分布
  238. loss_by_year = loss_trades.groupby('开仓年份')['盈亏金额'].sum()
  239. print('\n年度亏损分布:')
  240. print(loss_by_year.to_string())
  241. # ========== 9. 改进建议 ==========
  242. print('\n' + '='*80)
  243. print('【9】改进建议')
  244. print('='*80)
  245. print("""
  246. 基于以上分析,提出以下改进方向:
  247. 1. 【T+1规则适应】
  248. - 当前76笔T+1调整交易亏损26.9万,是主要亏损来源
  249. - 建议: 增加隔夜风险判断,避免在尾盘开仓
  250. - 建议: 降低T+1场景的仓位或暂停交易
  251. 2. 【时间过滤】
  252. - 分析显示某些月份/时段表现持续不佳
  253. - 建议: 在已知的大跌月份降低仓位或空仓
  254. - 建议: 优化开仓时间窗口
  255. 3. 【连续亏损保护】
  256. - 检测到连续亏损模式
  257. - 建议: 连续亏损3笔后暂停交易或降低仓位50%
  258. - 建议: 单日亏损超过阈值时当日停止开新仓
  259. 4. 【市场环境过滤】
  260. - 大跌月份亏损严重
  261. - 建议: 增加趋势过滤器,下跌趋势中减少交易频率
  262. - 建议: 结合大盘指数趋势进行交易决策
  263. 5. 【止盈止损优化】
  264. - 当前胜率40.8%偏低,盈亏比0.92<1
  265. - 建议: 优化止盈止损比例,提高盈亏比
  266. - 建议: 考虑移动止损保护盈利
  267. """)
  268. print('\n' + '='*80)
  269. print('分析完成')
  270. print('='*80)