auto_report_long_only_t1.py 15 KB

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