auto_report_long_only_t1.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 只做多T+1自动化交易报告系统
  5. 基于 cyb50_30min_long_only_t1.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. from t1_converter import simulate_t1_trades, compare_results
  25. # ==================== 邮件配置 ====================
  26. EMAIL_CONFIG = {
  27. "smtp_server": "localhost",
  28. "smtp_port": 25,
  29. "sender_email": "cyb50-t1@erwin.wang",
  30. "receiver_emails": ["380880504@qq.com", "1095512042@qq.com"]
  31. }
  32. def send_email(subject, html_content, text_content=""):
  33. """发送邮件"""
  34. try:
  35. msg = MIMEMultipart('alternative')
  36. msg['Subject'] = Header(subject, 'utf-8')
  37. msg['From'] = EMAIL_CONFIG['sender_email']
  38. msg['To'] = ', '.join(EMAIL_CONFIG['receiver_emails'])
  39. text_part = MIMEText(text_content, 'plain', 'utf-8')
  40. msg.attach(text_part)
  41. html_part = MIMEText(html_content, 'html', 'utf-8')
  42. msg.attach(html_part)
  43. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  44. server.sendmail(
  45. EMAIL_CONFIG['sender_email'],
  46. EMAIL_CONFIG['receiver_emails'],
  47. msg.as_string()
  48. )
  49. print(f"✅ 邮件发送成功: {subject}")
  50. return True
  51. except Exception as e:
  52. print(f"❌ 邮件发送失败: {e}")
  53. return False
  54. def check_today_trades(trades_df):
  55. """检查当天是否有交易
  56. 返回:
  57. has_today_trade: bool, 当天是否有交易
  58. today_trades: DataFrame, 当天的交易记录
  59. """
  60. if len(trades_df) == 0:
  61. return False, pd.DataFrame()
  62. # 获取今天的日期
  63. today = datetime.now().date()
  64. # 检查是否有今天的交易
  65. today_trades = trades_df[
  66. (pd.to_datetime(trades_df['开仓时间']).dt.date == today) |
  67. (pd.to_datetime(trades_df['平仓时间']).dt.date == today)
  68. ]
  69. has_today_trade = len(today_trades) > 0
  70. if has_today_trade:
  71. print(f"📊 当天交易数量: {len(today_trades)}笔")
  72. for _, trade in today_trades.iterrows():
  73. print(f" {trade['开仓时间']} → {trade['平仓时间']} | {trade['盈亏金额']:+.0f}元")
  74. else:
  75. print("📭 当天无交易")
  76. return has_today_trade, today_trades
  77. def is_post_close_time():
  78. """检查当前是否是盘后时间(15:00-15:30)"""
  79. now = datetime.now()
  80. return now.hour == 15 and now.minute >= 0 and now.minute <= 30
  81. def generate_report(trades_df, initial_capital=1000000):
  82. """生成只做多T+1交易报告(基于多空版本转换后的数据)"""
  83. if len(trades_df) == 0:
  84. final_capital = initial_capital
  85. total_return = 0
  86. html = f"""
  87. <html><head><style>
  88. body {{ font-family: Arial, sans-serif; margin: 20px; }}
  89. h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
  90. h2 {{ color: #555; margin-top: 30px; }}
  91. table {{ border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 14px; }}
  92. th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
  93. th {{ background-color: #007bff; color: white; }}
  94. tr:nth-child(even) {{ background-color: #f2f2f2; }}
  95. .positive {{ color: green; font-weight: bold; }}
  96. .negative {{ color: red; font-weight: bold; }}
  97. </style></head><body>
  98. <h1>🚀 创业板50交易报告 (T+1)</h1>
  99. <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  100. <p>数据区间: 近2个月</p>
  101. <p><b>近2个月无交易信号触发</b></p>
  102. <p>初始资金: {initial_capital:,.0f}元</p>
  103. <p>最终资金: {final_capital:,.0f}元</p>
  104. <p>收益率: {total_return:+.2f}%</p>
  105. </body></html>
  106. """
  107. text = f"近2个月无交易信号\n初始资金: {initial_capital:,.0f}元\n最终资金: {final_capital:,.0f}元"
  108. return html, text, final_capital
  109. # 计算统计数据 - 使用实际盈亏总和计算最终资金(更准确)
  110. total_pnl = trades_df['盈亏金额'].sum()
  111. final_capital = initial_capital + total_pnl
  112. total_return = (final_capital - initial_capital) / initial_capital * 100
  113. total_trades = len(trades_df)
  114. winning_trades = trades_df[trades_df['盈亏金额'] > 0]
  115. losing_trades = trades_df[trades_df['盈亏金额'] < 0]
  116. win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
  117. total_profit = winning_trades['盈亏金额'].sum() if len(winning_trades) > 0 else 0
  118. total_loss = abs(losing_trades['盈亏金额'].sum()) if len(losing_trades) > 0 else 0
  119. profit_factor = total_profit / total_loss if total_loss > 0 else 0
  120. # T+1调整统计
  121. t1_adjusted = trades_df[trades_df['T+1调整'] == '是(T0→T1)']
  122. t1_count = len(t1_adjusted)
  123. t1_win = len(t1_adjusted[t1_adjusted['盈亏金额'] > 0]) if len(t1_adjusted) > 0 else 0
  124. t1_pnl = t1_adjusted['盈亏金额'].sum() if len(t1_adjusted) > 0 else 0
  125. # 非T+1调整交易
  126. t1_normal = trades_df[trades_df['T+1调整'] != '是(T0→T1)']
  127. normal_count = len(t1_normal)
  128. normal_win = len(t1_normal[t1_normal['盈亏金额'] > 0]) if len(t1_normal) > 0 else 0
  129. normal_pnl = t1_normal['盈亏金额'].sum() if len(t1_normal) > 0 else 0
  130. # HTML报告 - 使用原版cat-fly蓝色主题
  131. html = f"""
  132. <html><head><style>
  133. body {{ font-family: Arial, sans-serif; margin: 20px; }}
  134. h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
  135. h2 {{ color: #555; margin-top: 30px; }}
  136. table {{ border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 14px; }}
  137. th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
  138. th {{ background-color: #007bff; color: white; }}
  139. tr:nth-child(even) {{ background-color: #f2f2f2; }}
  140. .positive {{ color: green; font-weight: bold; }}
  141. .negative {{ color: red; font-weight: bold; }}
  142. .highlight {{ background-color: #fff3cd; }}
  143. </style></head><body>
  144. <h1>🚀 创业板50交易报告 (T+1)</h1>
  145. <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  146. <p>数据区间: 近2个月</p>
  147. <p><b>数据来源: 多空双向策略的做多交易 + T+1规则转换</b></p>
  148. <h2>📊 总体绩效</h2>
  149. <table>
  150. <tr><th>指标</th><th>数值</th></tr>
  151. <tr><td>初始资金</td><td>{initial_capital:,.0f}元</td></tr>
  152. <tr><td>最终资金</td><td>{final_capital:,.0f}元</td></tr>
  153. <tr><td>总收益率</td><td class="{'positive' if total_return >= 0 else 'negative'}">{total_return:+.2f}%</td></tr>
  154. <tr><td>总交易次数</td><td>{total_trades}笔</td></tr>
  155. <tr><td>胜率</td><td>{win_rate:.1f}%</td></tr>
  156. <tr><td>盈亏比</td><td>{profit_factor:.2f}</td></tr>
  157. </table>
  158. <h2>🔄 交易统计 (T+1调整影响)</h2>
  159. <table>
  160. <tr><th>类型</th><th>交易次数</th><th>胜率</th><th>总盈亏</th></tr>
  161. <tr>
  162. <td>正常交易</td><td>{normal_count}笔</td>
  163. <td>{(normal_win/normal_count*100 if normal_count>0 else 0):.1f}%</td>
  164. <td class="{'positive' if normal_pnl >= 0 else 'negative'}">{normal_pnl:+,.0f}元</td>
  165. </tr>
  166. <tr class="{'highlight' if t1_count > 0 else ''}">
  167. <td>T+1调整(T0→T1)</td><td>{t1_count}笔</td>
  168. <td>{(t1_win/t1_count*100 if t1_count>0 else 0):.1f}%</td>
  169. <td class="{'positive' if t1_pnl >= 0 else 'negative'}">{t1_pnl:+,.0f}元</td>
  170. </tr>
  171. </table>
  172. <h2>📈 T+1调整明细</h2>
  173. <table>
  174. <tr><th>开仓时间</th><th>原平仓</th><th>新平仓</th><th>原盈亏</th><th>新盈亏</th><th>变化</th></tr>
  175. """
  176. for _, trade in t1_adjusted.iterrows():
  177. change = trade['盈亏变化']
  178. change_class = "positive" if change >= 0 else "negative"
  179. html += f"""
  180. <tr>
  181. <td>{trade['开仓时间'].strftime('%m-%d %H:%M')}</td>
  182. <td>{trade['原平仓时间'].strftime('%m-%d %H:%M')}</td>
  183. <td>{trade['平仓时间'].strftime('%m-%d %H:%M')}</td>
  184. <td>{trade['原盈亏']:+.0f}</td>
  185. <td class="{'positive' if trade['盈亏金额'] >= 0 else 'negative'}">{trade['盈亏金额']:+.0f}</td>
  186. <td class="{change_class}">{change:+.0f}</td>
  187. </tr>
  188. """
  189. if len(t1_adjusted) == 0:
  190. html += "<tr><td colspan='6'>无T+1调整交易</td></tr>"
  191. html += """
  192. </table>
  193. <h2>📝 最近20笔交易明细</h2>
  194. <table>
  195. <tr><th>开仓时间</th><th>平仓时间</th><th>开仓价</th><th>平仓价</th>
  196. <th>盈亏</th><th>退出原因</th><th>T+1调整</th></tr>
  197. """
  198. for _, trade in trades_df.tail(20).iterrows():
  199. pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
  200. t1_flag = "✓" if trade['T+1调整'] == '是(T0→T1)' else ""
  201. html += f"""
  202. <tr>
  203. <td>{trade['开仓时间'].strftime('%m-%d %H:%M')}</td>
  204. <td>{trade['平仓时间'].strftime('%m-%d %H:%M')}</td>
  205. <td>{trade['开仓价格']:.2f}</td>
  206. <td>{trade['平仓价格']:.2f}</td>
  207. <td class="{pnl_class}">{trade['盈亏金额']:+.0f}</td>
  208. <td>{trade['退出原因']}</td>
  209. <td>{t1_flag}</td>
  210. </tr>
  211. """
  212. html += "</table></body></html>"
  213. # 纯文本报告
  214. text = f"""
  215. 创业板50交易报告 (T+1)
  216. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  217. 数据来源: 多空双向策略的做多交易 + T+1规则转换
  218. 【总体绩效】
  219. 初始资金: {initial_capital:,.0f}元
  220. 最终资金: {final_capital:,.0f}元
  221. 总收益率: {total_return:+.2f}%
  222. 总交易次数: {total_trades}笔
  223. 胜率: {win_rate:.1f}%
  224. 盈亏比: {profit_factor:.2f}
  225. 【T+1调整统计】
  226. 正常交易: {normal_count}笔, 盈亏{normal_pnl:+,.0f}元
  227. T+1调整(T0→T1): {t1_count}笔, 盈亏{t1_pnl:+,.0f}元
  228. 【最近20笔交易】
  229. {trades_df.tail(20)[['开仓时间', '平仓时间', '盈亏金额', '退出原因']].to_string(index=False)}
  230. """
  231. return html, text, final_capital
  232. def main():
  233. """主程序 - 使用多空版本做多交易 + T+1规则转换"""
  234. print("="*80)
  235. print("🚀 创业板50只做多T+1自动交易报告系统")
  236. print("(基于多空双向策略的做多交易 + T+1规则转换)")
  237. print("="*80)
  238. print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  239. initial_capital = 1000000
  240. # 1. 获取数据
  241. print("\n📊 步骤1: 获取近2个月数据...")
  242. try:
  243. config_manager = ConfigManager('config.json')
  244. fetcher = IntradayDataFetcher(config_manager)
  245. end_date = datetime.now()
  246. start_date = end_date - timedelta(days=70) # 2个月+10天缓冲
  247. raw_data = fetcher.fetch_30min_data(start_date, end_date)
  248. if raw_data is None or len(raw_data) == 0:
  249. print("❌ 数据获取失败")
  250. return
  251. print(f"✅ 数据获取成功: {len(raw_data)}条K线")
  252. print(f" 数据区间: {raw_data.index[0]} ~ {raw_data.index[-1]}")
  253. except Exception as e:
  254. print(f"❌ 数据获取失败: {e}")
  255. import traceback
  256. traceback.print_exc()
  257. return
  258. # 2. 生成多空双向信号并执行
  259. print("\n📈 步骤2: 运行多空双向策略获取做多交易...")
  260. try:
  261. # 计算技术指标
  262. print(" 计算技术指标...")
  263. data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
  264. # 生成多空双向信号
  265. signal_generator = DualDirectionSignalGenerator()
  266. signals_df = signal_generator.generate_dual_direction_signals(data_with_indicators)
  267. # 执行多空双向交易
  268. executor = DualDirectionExecutor(initial_capital=initial_capital)
  269. results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
  270. # 提取做多交易
  271. long_trades = trades_df[trades_df['交易方向'] == '做多'].copy()
  272. print(f"✅ 多空策略完成: 共{len(trades_df)}笔交易, 做多{len(long_trades)}笔")
  273. except Exception as e:
  274. print(f"❌ 多空策略执行失败: {e}")
  275. import traceback
  276. traceback.print_exc()
  277. return
  278. # 3. 应用T+1规则转换
  279. print("\n🔄 步骤3: 应用T+1规则转换...")
  280. try:
  281. t1_trades = simulate_t1_trades(data_with_indicators, long_trades, initial_capital)
  282. print(f"✅ T+1转换完成: {len(t1_trades)}笔交易")
  283. # 统计T+1调整
  284. t1_adjusted = t1_trades[t1_trades['T+1调整'] == '是(T0→T1)']
  285. print(f" T+1调整交易: {len(t1_adjusted)}笔")
  286. except Exception as e:
  287. print(f"❌ T+1转换失败: {e}")
  288. import traceback
  289. traceback.print_exc()
  290. return
  291. # 4. 检查当天交易并决定是否发送邮件
  292. print("\n📊 步骤4: 检查当天交易情况...")
  293. has_today_trade, today_trades = check_today_trades(t1_trades)
  294. # 判断是否应该发送邮件
  295. should_send = False
  296. send_reason = ""
  297. if has_today_trade:
  298. # 有当天交易,正常发送
  299. should_send = True
  300. send_reason = f"当天有{len(today_trades)}笔交易"
  301. elif is_post_close_time():
  302. # 没有当天交易,但在盘后时间(15:00-15:30),发送一次
  303. should_send = True
  304. send_reason = "盘后时间,当天无交易,发送例行报告"
  305. else:
  306. # 没有当天交易,也不在盘后时间,跳过发送
  307. should_send = False
  308. send_reason = "当天无交易,非盘后时间,跳过发送"
  309. print(f"\n📧 邮件发送决策: {send_reason}")
  310. if should_send:
  311. # 5. 生成报告
  312. print("\n📝 步骤5: 生成报告...")
  313. html_report, text_report, final_capital = generate_report(t1_trades, initial_capital)
  314. # 6. 发送邮件
  315. print("\n📧 步骤6: 发送邮件...")
  316. total_trades = len(t1_trades)
  317. total_return = (final_capital/initial_capital-1)*100
  318. subject = f"🚀 CYB50-T1报告 {datetime.now().strftime('%m-%d %H:%M')} | 收益{total_return:+.2f}% | {total_trades}笔交易"
  319. send_email(subject, html_report, text_report)
  320. else:
  321. print("⏭️ 跳过邮件发送(当天无交易且非盘后时间)")
  322. print("\n✅ 全部完成!")
  323. print("="*80)
  324. if __name__ == "__main__":
  325. main()