auto_report.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 自动化交易报告系统
  5. 基于 cyb50_30min_dual_direction.py 完整策略
  6. """
  7. import sys
  8. sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
  9. import pandas as pd
  10. import numpy as np
  11. from datetime import datetime, timedelta
  12. import smtplib
  13. import ssl
  14. from email.mime.text import MIMEText
  15. from email.mime.multipart import MIMEMultipart
  16. from email.header import Header
  17. import warnings
  18. warnings.filterwarnings('ignore')
  19. # 导入原版策略模块
  20. from cyb50_30min_dual_direction import (
  21. ConfigManager, IntradayDataFetcher,
  22. DualDirectionSignalGenerator, DualDirectionExecutor
  23. )
  24. # ==================== 邮件配置 ====================
  25. EMAIL_CONFIG = {
  26. "smtp_server": "localhost",
  27. "smtp_port": 25,
  28. "sender_email": "catfly@erwin.wang",
  29. "receiver_emails": ["380880504@qq.com", "695047456@qq.com"]
  30. }
  31. def send_email(subject, html_content, text_content=""):
  32. """发送邮件"""
  33. try:
  34. msg = MIMEMultipart('alternative')
  35. msg['Subject'] = Header(subject, 'utf-8')
  36. msg['From'] = EMAIL_CONFIG['sender_email']
  37. msg['To'] = ', '.join(EMAIL_CONFIG['receiver_emails'])
  38. text_part = MIMEText(text_content, 'plain', 'utf-8')
  39. msg.attach(text_part)
  40. html_part = MIMEText(html_content, 'html', 'utf-8')
  41. msg.attach(html_part)
  42. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  43. server.sendmail(
  44. EMAIL_CONFIG['sender_email'],
  45. EMAIL_CONFIG['receiver_emails'],
  46. msg.as_string()
  47. )
  48. print(f"✅ 邮件发送成功: {subject}")
  49. return True
  50. except Exception as e:
  51. print(f"❌ 邮件发送失败: {e}")
  52. return False
  53. def check_today_trades(trades_df):
  54. """检查当天是否有交易
  55. 返回:
  56. has_today_trade: bool, 当天是否有交易
  57. today_trades: DataFrame, 当天的交易记录
  58. """
  59. if len(trades_df) == 0:
  60. return False, pd.DataFrame()
  61. # 获取今天的日期
  62. today = datetime.now().date()
  63. # 检查是否有今天的交易
  64. today_trades = trades_df[
  65. (pd.to_datetime(trades_df['开仓时间']).dt.date == today) |
  66. (pd.to_datetime(trades_df['平仓时间']).dt.date == today)
  67. ]
  68. has_today_trade = len(today_trades) > 0
  69. if has_today_trade:
  70. print(f"📊 当天交易数量: {len(today_trades)}笔")
  71. for _, trade in today_trades.iterrows():
  72. print(f" {trade['交易方向']} | {trade['开仓时间']} → {trade['平仓时间']} | {trade['盈亏金额']:+.0f}元")
  73. else:
  74. print("📭 当天无交易")
  75. return has_today_trade, today_trades
  76. def is_post_close_time():
  77. """检查当前是否是盘后时间(15:00-15:30)"""
  78. now = datetime.now()
  79. return now.hour == 15 and now.minute >= 0 and now.minute <= 30
  80. def generate_report(trades_df, results_df, initial_capital=1000000):
  81. """生成交易报告"""
  82. if len(trades_df) == 0:
  83. final_capital = initial_capital
  84. total_return = 0
  85. html = f"""
  86. <html><body>
  87. <h1>🚀 创业板50交易报告</h1>
  88. <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  89. <p>数据区间: 近2个月</p>
  90. <p><b>近2个月无交易信号触发</b></p>
  91. <p>初始资金: {initial_capital:,.0f}元</p>
  92. <p>最终资金: {final_capital:,.0f}元</p>
  93. <p>收益率: {total_return:+.2f}%</p>
  94. </body></html>
  95. """
  96. text = f"近2个月无交易信号\n初始资金: {initial_capital:,.0f}元\n最终资金: {final_capital:,.0f}元"
  97. return html, text, final_capital
  98. # 计算统计数据
  99. final_capital = results_df['net_value'].iloc[-1]
  100. total_return = (final_capital - initial_capital) / initial_capital * 100
  101. total_trades = len(trades_df)
  102. winning_trades = trades_df[trades_df['盈亏金额'] > 0]
  103. losing_trades = trades_df[trades_df['盈亏金额'] < 0]
  104. win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
  105. total_profit = winning_trades['盈亏金额'].sum() if len(winning_trades) > 0 else 0
  106. total_loss = abs(losing_trades['盈亏金额'].sum()) if len(losing_trades) > 0 else 0
  107. profit_factor = total_profit / total_loss if total_loss > 0 else 0
  108. long_trades = trades_df[trades_df['交易方向'] == '做多']
  109. short_trades = trades_df[trades_df['交易方向'] == '做空']
  110. # HTML报告
  111. html = f"""
  112. <html><head><style>
  113. body {{ font-family: Arial, sans-serif; margin: 20px; }}
  114. h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
  115. h2 {{ color: #555; margin-top: 30px; }}
  116. table {{ border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 14px; }}
  117. th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
  118. th {{ background-color: #007bff; color: white; }}
  119. tr:nth-child(even) {{ background-color: #f2f2f2; }}
  120. .positive {{ color: green; font-weight: bold; }}
  121. .negative {{ color: red; font-weight: bold; }}
  122. </style></head><body>
  123. <h1>🚀 创业板50交易报告</h1>
  124. <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  125. <p>数据区间: 近2个月</p>
  126. <h2>📊 总体绩效</h2>
  127. <table>
  128. <tr><th>指标</th><th>数值</th></tr>
  129. <tr><td>初始资金</td><td>{initial_capital:,.0f}元</td></tr>
  130. <tr><td>最终资金</td><td>{final_capital:,.0f}元</td></tr>
  131. <tr><td>总收益率</td><td class="{'positive' if total_return >= 0 else 'negative'}">{total_return:+.2f}%</td></tr>
  132. <tr><td>总交易次数</td><td>{total_trades}笔</td></tr>
  133. <tr><td>胜率</td><td>{win_rate:.1f}%</td></tr>
  134. <tr><td>盈亏比</td><td>{profit_factor:.2f}</td></tr>
  135. </table>
  136. <h2>🔄 多空统计</h2>
  137. <table>
  138. <tr><th>方向</th><th>交易次数</th><th>胜率</th><th>总盈亏</th></tr>
  139. <tr>
  140. <td>做多</td><td>{len(long_trades)}笔</td>
  141. <td>{(len(long_trades[long_trades['盈亏金额']>0])/len(long_trades)*100 if len(long_trades)>0 else 0):.1f}%</td>
  142. <td class="{'positive' if long_trades['盈亏金额'].sum() >= 0 else 'negative'}">{long_trades['盈亏金额'].sum():+,.0f}元</td>
  143. </tr>
  144. <tr>
  145. <td>做空</td><td>{len(short_trades)}笔</td>
  146. <td>{(len(short_trades[short_trades['盈亏金额']>0])/len(short_trades)*100 if len(short_trades)>0 else 0):.1f}%</td>
  147. <td class="{'positive' if short_trades['盈亏金额'].sum() >= 0 else 'negative'}">{short_trades['盈亏金额'].sum():+,.0f}元</td>
  148. </tr>
  149. </table>
  150. <h2>📝 最近30笔交易明细</h2>
  151. <table>
  152. <tr><th>方向</th><th>开仓时间</th><th>平仓时间</th><th>开仓价</th><th>平仓价</th>
  153. <th>盈亏</th><th>退出原因</th></tr>
  154. """
  155. for _, trade in trades_df.tail(30).iterrows():
  156. pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
  157. html += f"""
  158. <tr>
  159. <td>{trade['交易方向']}</td>
  160. <td>{trade['开仓时间']}</td>
  161. <td>{trade['平仓时间']}</td>
  162. <td>{trade['开仓价格']:.2f}</td>
  163. <td>{trade['平仓价格']:.2f}</td>
  164. <td class="{pnl_class}">{trade['盈亏金额']:+.0f}</td>
  165. <td>{trade['退出原因']}</td>
  166. </tr>
  167. """
  168. html += "</table></body></html>"
  169. # 纯文本报告
  170. text = f"""
  171. 创业板50交易报告
  172. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  173. 【总体绩效】
  174. 初始资金: {initial_capital:,.0f}元
  175. 最终资金: {final_capital:,.0f}元
  176. 总收益率: {total_return:+.2f}%
  177. 总交易次数: {total_trades}笔
  178. 胜率: {win_rate:.1f}%
  179. 盈亏比: {profit_factor:.2f}
  180. 【多空统计】
  181. 做多: {len(long_trades)}笔, 盈亏{long_trades['盈亏金额'].sum():+,.0f}元
  182. 做空: {len(short_trades)}笔, 盈亏{short_trades['盈亏金额'].sum():+,.0f}元
  183. 【最近5笔交易】
  184. {trades_df.tail(5)[['交易方向', '开仓时间', '平仓时间', '盈亏金额', '退出原因']].to_string(index=False)}
  185. """
  186. return html, text, final_capital
  187. def main():
  188. """主程序"""
  189. print("="*80)
  190. print("🚀 cat-fly 自动交易报告系统 (完整版)")
  191. print("="*80)
  192. print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  193. initial_capital = 1000000
  194. # 1. 获取数据
  195. print("\n📊 步骤1: 获取近2个月数据...")
  196. try:
  197. # 使用原版数据获取器
  198. fetcher = IntradayDataFetcher()
  199. end_date = datetime.now()
  200. start_date = end_date - timedelta(days=70) # 2个月+10天缓冲
  201. raw_data = fetcher.fetch_30min_data(start_date, end_date)
  202. if raw_data is None or len(raw_data) == 0:
  203. print("❌ 数据获取失败")
  204. return
  205. print(f"✅ 数据获取成功: {len(raw_data)}条K线")
  206. print(f" 数据区间: {raw_data.index[0]} ~ {raw_data.index[-1]}")
  207. except Exception as e:
  208. print(f"❌ 数据获取失败: {e}")
  209. return
  210. # 2. 生成信号
  211. print("\n📈 步骤2: 生成交易信号...")
  212. try:
  213. # 先计算技术指标(使用数据获取器的方法)
  214. print(" 计算技术指标...")
  215. data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
  216. # 生成信号
  217. signal_generator = DualDirectionSignalGenerator()
  218. signals_df = signal_generator.generate_dual_direction_signals(data_with_indicators)
  219. # 统计信号
  220. long_signals = len(signals_df[signals_df['Signal'] == 1])
  221. short_signals = len(signals_df[signals_df['Signal'] == -1])
  222. print(f"✅ 信号生成完成: 做多信号{long_signals}个, 做空信号{short_signals}个")
  223. except Exception as e:
  224. print(f"❌ 信号生成失败: {e}")
  225. return
  226. # 3. 执行回测
  227. print("\n💼 步骤3: 执行交易回测...")
  228. try:
  229. executor = DualDirectionExecutor(initial_capital=initial_capital)
  230. results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
  231. print(f"✅ 回测完成: 共执行{len(trades_df)}笔交易")
  232. if len(trades_df) > 0:
  233. final_value = results_df['net_value'].iloc[-1]
  234. print(f" 最终资金: {final_value:,.0f}元")
  235. print(f" 收益率: {(final_value/initial_capital-1)*100:+.2f}%")
  236. except Exception as e:
  237. print(f"❌ 回测执行失败: {e}")
  238. return
  239. # 4. 检查当天交易并决定是否发送邮件
  240. print("\n📊 步骤4: 检查当天交易情况...")
  241. has_today_trade, today_trades = check_today_trades(trades_df)
  242. # 判断是否应该发送邮件
  243. should_send = False
  244. send_reason = ""
  245. if has_today_trade:
  246. # 有当天交易,正常发送
  247. should_send = True
  248. send_reason = f"当天有{len(today_trades)}笔交易"
  249. elif is_post_close_time():
  250. # 没有当天交易,但在盘后时间(15:00-15:30),发送一次
  251. should_send = True
  252. send_reason = "盘后时间,当天无交易,发送例行报告"
  253. else:
  254. # 没有当天交易,也不在盘后时间,跳过发送
  255. should_send = False
  256. send_reason = "当天无交易,非盘后时间,跳过发送"
  257. print(f"\n📧 邮件发送决策: {send_reason}")
  258. if should_send:
  259. # 5. 生成报告
  260. print("\n📝 步骤5: 生成报告...")
  261. html_report, text_report, final_capital = generate_report(trades_df, results_df, initial_capital)
  262. # 6. 发送邮件
  263. print("\n📧 步骤6: 发送邮件...")
  264. total_trades = len(trades_df)
  265. total_return = (final_capital/initial_capital-1)*100
  266. subject = f"🚀 创业板50报告 {datetime.now().strftime('%m-%d %H:%M')} | 收益{total_return:+.2f}% | {total_trades}笔交易"
  267. send_email(subject, html_report, text_report)
  268. else:
  269. print("⏭️ 跳过邮件发送(当天无交易且非盘后时间)")
  270. print("\n✅ 全部完成!")
  271. print("="*80)
  272. if __name__ == "__main__":
  273. main()