auto_report_long_only_t1.py 15 KB

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