auto_report_realtime.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 只做多T+1自动化交易报告系统 (实时信号增强版)
  5. 在14:50等时间点会使用实时行情数据计算当前信号
  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_v2, 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 get_realtime_price():
  55. """获取创业板50实时价格"""
  56. try:
  57. import akshare as ak
  58. # 使用新浪接口获取实时行情(更稳定)
  59. df = ak.stock_zh_index_spot_sina()
  60. cyb50 = df[df['代码'] == 'sz399673']
  61. if len(cyb50) > 0:
  62. return {
  63. 'price': float(cyb50.iloc[0]['最新价']),
  64. 'open': float(cyb50.iloc[0]['今开']),
  65. 'high': float(cyb50.iloc[0]['最高']),
  66. 'low': float(cyb50.iloc[0]['最低']),
  67. 'volume': float(cyb50.iloc[0]['成交量']),
  68. 'change_pct': float(cyb50.iloc[0].get('涨跌幅', 0)),
  69. 'time': datetime.now().strftime('%H:%M:%S')
  70. }
  71. except Exception as e:
  72. print(f"获取实时行情失败: {e}")
  73. return None
  74. def calculate_realtime_signal(current_price, last_kline_data):
  75. """
  76. 基于最新价格和最后一根K线数据,估算当前信号状态
  77. """
  78. score = 0
  79. signals = []
  80. last_close = last_kline_data['Close']
  81. price_change_pct = (current_price - last_close) / last_close
  82. # 1. RSI估算
  83. estimated_rsi = last_kline_data['RSI'] + price_change_pct * 250
  84. if estimated_rsi < 30:
  85. score += 2
  86. signals.append(f"RSI超卖(估{estimated_rsi:.1f})")
  87. elif estimated_rsi < 35:
  88. score += 1
  89. signals.append(f"RSI偏弱(估{estimated_rsi:.1f})")
  90. # 2. KDJ估算
  91. estimated_j = last_kline_data['J'] + price_change_pct * 300
  92. if estimated_j < 0:
  93. score += 1
  94. signals.append(f"KDJ极端超卖(估J={estimated_j:.1f})")
  95. # 3. 布林带位置
  96. bb_lower = last_kline_data['BB_lower']
  97. if current_price <= bb_lower * 1.005:
  98. score += 2
  99. signals.append("触及下轨")
  100. elif current_price <= bb_lower * 1.02:
  101. score += 1
  102. signals.append("接近下轨")
  103. # 4. 价格动量
  104. if price_change_pct < -0.015:
  105. score += 1
  106. signals.append(f"动量超卖({price_change_pct*100:.2f}%)")
  107. # 5. MA趋势
  108. if last_kline_data['MA6'] > last_kline_data['MA12']:
  109. score += 1
  110. signals.append("MA短期上行")
  111. else:
  112. score -= 1
  113. signals.append("MA下降趋势惩罚")
  114. return {
  115. 'score': score,
  116. 'signals': signals,
  117. 'estimated_rsi': estimated_rsi,
  118. 'estimated_j': estimated_j,
  119. 'price_change_pct': price_change_pct,
  120. 'bb_lower': bb_lower,
  121. 'bb_upper': last_kline_data['BB_upper'],
  122. 'current_price': current_price,
  123. 'last_close': last_close,
  124. 'triggered': score >= 4
  125. }
  126. def is_pre_close_time():
  127. """检查是否是收盘前10分钟(14:50左右)"""
  128. now = datetime.now()
  129. return now.hour == 14 and now.minute >= 50
  130. def check_today_trades(trades_df):
  131. """检查当天是否有交易"""
  132. if len(trades_df) == 0:
  133. return False, pd.DataFrame()
  134. today = datetime.now().date()
  135. today_trades = trades_df[
  136. (pd.to_datetime(trades_df['开仓时间']).dt.date == today) |
  137. (pd.to_datetime(trades_df['平仓时间']).dt.date == today)
  138. ]
  139. has_today_trade = len(today_trades) > 0
  140. if has_today_trade:
  141. print(f"📊 当天交易数量: {len(today_trades)}笔")
  142. for _, trade in today_trades.iterrows():
  143. print(f" {trade['开仓时间']} → {trade['平仓时间']} | {trade['盈亏金额']:+.0f}元")
  144. else:
  145. print("📭 当天无交易")
  146. return has_today_trade, today_trades
  147. def is_post_close_time():
  148. """检查是否是盘后时间(15:00-15:30)"""
  149. now = datetime.now()
  150. return now.hour == 15 and now.minute >= 0 and now.minute <= 30
  151. def generate_report(trades_df, initial_capital=1000000, realtime_signal=None):
  152. """生成只做多T+1交易报告(增强版,包含实时信号)"""
  153. if len(trades_df) == 0:
  154. final_capital = initial_capital
  155. total_return = 0
  156. else:
  157. total_pnl = trades_df['盈亏金额'].sum()
  158. final_capital = initial_capital + total_pnl
  159. total_return = (final_capital - initial_capital) / initial_capital * 100
  160. total_trades = len(trades_df)
  161. winning_trades = trades_df[trades_df['盈亏金额'] > 0]
  162. losing_trades = trades_df[trades_df['盈亏金额'] < 0]
  163. win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
  164. total_profit = winning_trades['盈亏金额'].sum() if len(winning_trades) > 0 else 0
  165. total_loss = abs(losing_trades['盈亏金额'].sum()) if len(losing_trades) > 0 else 0
  166. profit_factor = total_profit / total_loss if total_loss > 0 else 0
  167. # T+1调整统计
  168. t1_adjusted = trades_df[trades_df['T+1调整'] == '是(T0→T1)']
  169. t1_count = len(t1_adjusted)
  170. t1_pnl = t1_adjusted['盈亏金额'].sum() if len(t1_adjusted) > 0 else 0
  171. # 构建HTML报告
  172. now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  173. # 实时信号部分
  174. realtime_html = ""
  175. if realtime_signal:
  176. signal_color = "green" if realtime_signal['triggered'] else "orange" if realtime_signal['score'] >= 3 else "gray"
  177. signal_text = "🟢 触发买入" if realtime_signal['triggered'] else "🟡 接近触发" if realtime_signal['score'] >= 3 else "⚪ 未触发"
  178. realtime_html = f"""
  179. <h2>🔔 实时信号检测 (收盘前)</h2>
  180. <table>
  181. <tr><th>指标</th><th>数值</th></tr>
  182. <tr><td>检测时间</td><td>{now_str}</td></tr>
  183. <tr><td>实时价格</td><td>{realtime_signal['current_price']:.2f}</td></tr>
  184. <tr><td>较上一K线</td><td>{realtime_signal['price_change_pct']*100:+.2f}%</td></tr>
  185. <tr><td>RSI(估算)</td><td>{realtime_signal['estimated_rsi']:.2f}</td></tr>
  186. <tr><td>KDJ J(估算)</td><td>{realtime_signal['estimated_j']:.2f}</td></tr>
  187. <tr><td>布林带下轨</td><td>{realtime_signal['bb_lower']:.2f}</td></tr>
  188. <tr><td>信号评分</td><td style="color: {signal_color}; font-weight: bold;">{realtime_signal['score']}/4</td></tr>
  189. <tr><td>触发信号</td><td>{', '.join(realtime_signal['signals']) if realtime_signal['signals'] else '无'}</td></tr>
  190. <tr><td>最终判断</td><td style="color: {signal_color}; font-size: 16px; font-weight: bold;">{signal_text}</td></tr>
  191. </table>
  192. """
  193. html = f"""
  194. <html><head><style>
  195. body {{ font-family: Arial, sans-serif; margin: 20px; }}
  196. h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
  197. h2 {{ color: #555; margin-top: 30px; }}
  198. table {{ border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 14px; }}
  199. th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
  200. th {{ background-color: #007bff; color: white; }}
  201. tr:nth-child(even) {{ background-color: #f2f2f2; }}
  202. .positive {{ color: green; font-weight: bold; }}
  203. .negative {{ color: red; font-weight: bold; }}
  204. .highlight {{ background-color: #fff3cd; }}
  205. </style></head><body>
  206. <h1>🚀 创业板50交易报告 (T+1)</h1>
  207. <p>生成时间: {now_str}</p>
  208. <p>数据区间: 近3个月</p>
  209. {realtime_html}
  210. <h2>📊 总体绩效</h2>
  211. <table>
  212. <tr><th>指标</th><th>数值</th></tr>
  213. <tr><td>初始资金</td><td>{initial_capital:,.0f}元</td></tr>
  214. <tr><td>最终资金</td><td>{final_capital:,.0f}元</td></tr>
  215. <tr><td>总收益率</td><td class="{'positive' if total_return >= 0 else 'negative'}">{total_return:+.2f}%</td></tr>
  216. <tr><td>总交易次数</td><td>{total_trades}笔</td></tr>
  217. <tr><td>胜率</td><td>{win_rate:.1f}%</td></tr>
  218. <tr><td>盈亏比</td><td>{profit_factor:.2f}</td></tr>
  219. </table>
  220. <h2>📝 最近交易明细</h2>
  221. <table>
  222. <tr><th>开仓时间</th><th>平仓时间</th><th>开仓价</th><th>平仓价</th>
  223. <th>盈亏</th><th>退出原因</th><th>T+1调整</th></tr>
  224. """
  225. for _, trade in trades_df.tail(10).iterrows():
  226. pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
  227. t1_flag = "✓" if trade['T+1调整'] == '是(T0→T1)' else ""
  228. html += f"""
  229. <tr>
  230. <td>{trade['开仓时间'].strftime('%m-%d %H:%M')}</td>
  231. <td>{trade['平仓时间'].strftime('%m-%d %H:%M')}</td>
  232. <td>{trade['开仓价格']:.2f}</td>
  233. <td>{trade['平仓价格']:.2f}</td>
  234. <td class="{pnl_class}">{trade['盈亏金额']:+.0f}</td>
  235. <td>{trade['退出原因']}</td>
  236. <td>{t1_flag}</td>
  237. </tr>
  238. """
  239. if len(trades_df) == 0:
  240. html += "<tr><td colspan='7'>近2个月无交易信号触发</td></tr>"
  241. html += "</table></body></html>"
  242. # 纯文本报告
  243. text = f"""
  244. 创业板50交易报告 (T+1)
  245. 生成时间: {now_str}
  246. {f"【实时信号检测】\n时间: {now_str}\n实时价格: {realtime_signal['current_price']:.2f}\n信号评分: {realtime_signal['score']}/4\n触发信号: {', '.join(realtime_signal['signals']) if realtime_signal['signals'] else '无'}\n判断: {'触发买入' if realtime_signal['triggered'] else '未触发'}\n" if realtime_signal else ""}
  247. 【总体绩效】
  248. 初始资金: {initial_capital:,.0f}元
  249. 最终资金: {final_capital:,.0f}元
  250. 总收益率: {total_return:+.2f}%
  251. 总交易次数: {total_trades}笔
  252. 胜率: {win_rate:.1f}%
  253. """
  254. return html, text, final_capital
  255. def main():
  256. """主程序 - 实时信号增强版"""
  257. print("="*80)
  258. print("🚀 创业板50只做多T+1自动交易报告系统 (实时信号增强版)")
  259. print("="*80)
  260. print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  261. initial_capital = 1000000
  262. realtime_signal = None
  263. # 判断是否是收盘前时间
  264. pre_close = is_pre_close_time()
  265. if pre_close:
  266. print("\n🔔 检测到收盘前时间(14:50+),将获取实时行情计算信号...")
  267. # 1. 获取数据
  268. print("\n📊 步骤1: 获取近3个月数据...")
  269. try:
  270. config_manager = ConfigManager('config.json')
  271. fetcher = IntradayDataFetcher(config_manager)
  272. end_date = datetime.now()
  273. start_date = end_date - timedelta(days=90)
  274. raw_data = fetcher.fetch_30min_data(start_date, end_date)
  275. if raw_data is None or len(raw_data) == 0:
  276. print("❌ 数据获取失败")
  277. return
  278. print(f"✅ 数据获取成功: {len(raw_data)}条K线")
  279. print(f" 数据区间: {raw_data.index[0]} ~ {raw_data.index[-1]}")
  280. except Exception as e:
  281. print(f"❌ 数据获取失败: {e}")
  282. import traceback
  283. traceback.print_exc()
  284. return
  285. # 2. 如果是收盘前时间,获取实时信号
  286. if pre_close:
  287. print("\n📈 步骤1.5: 获取实时行情并计算信号...")
  288. try:
  289. realtime = get_realtime_price()
  290. if realtime:
  291. print(f" 实时价格: {realtime['price']:.2f} (时间: {realtime['time']})")
  292. # 计算技术指标
  293. data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
  294. last_kline = data_with_indicators.iloc[-1]
  295. # 计算实时信号
  296. realtime_signal = calculate_realtime_signal(realtime['price'], last_kline)
  297. print(f" 实时信号评分: {realtime_signal['score']}/4")
  298. print(f" 触发信号: {', '.join(realtime_signal['signals']) if realtime_signal['signals'] else '无'}")
  299. print(f" 是否触发: {'是' if realtime_signal['triggered'] else '否'}")
  300. else:
  301. print(" ⚠️ 获取实时行情失败,跳过实时信号计算")
  302. except Exception as e:
  303. print(f" ⚠️ 实时信号计算失败: {e}")
  304. # 3. 生成多空双向信号并执行
  305. print("\n📈 步骤2: 运行多空双向策略获取做多交易...")
  306. try:
  307. data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
  308. signal_generator = DualDirectionSignalGenerator()
  309. signals_df = signal_generator.generate_dual_direction_signals(data_with_indicators)
  310. executor = DualDirectionExecutor(initial_capital=initial_capital)
  311. results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
  312. long_trades = trades_df[trades_df['交易方向'] == '做多'].copy()
  313. print(f"✅ 多空策略完成: 共{len(trades_df)}笔交易, 做多{len(long_trades)}笔")
  314. except Exception as e:
  315. print(f"❌ 多空策略执行失败: {e}")
  316. import traceback
  317. traceback.print_exc()
  318. return
  319. # 4. 应用T+1规则转换
  320. print("\n🔄 步骤3: 应用T+1规则转换...")
  321. try:
  322. t1_trades = simulate_t1_trades_v2(data_with_indicators, long_trades, initial_capital)
  323. print(f"✅ T+1转换完成: {len(t1_trades)}笔交易")
  324. except Exception as e:
  325. print(f"❌ T+1转换失败: {e}")
  326. import traceback
  327. traceback.print_exc()
  328. return
  329. # 5. 检查当天交易并决定是否发送邮件
  330. print("\n📊 步骤4: 检查当天交易情况...")
  331. has_today_trade, today_trades = check_today_trades(t1_trades)
  332. # 判断是否应该发送邮件
  333. should_send = False
  334. send_reason = ""
  335. if has_today_trade:
  336. should_send = True
  337. send_reason = f"当天有{len(today_trades)}笔交易"
  338. elif realtime_signal and realtime_signal['triggered']:
  339. should_send = True
  340. send_reason = "实时信号触发买入"
  341. elif pre_close:
  342. should_send = True
  343. send_reason = "收盘前例行报告(含实时信号)"
  344. elif is_post_close_time():
  345. should_send = True
  346. send_reason = "盘后时间,当天无交易,发送例行报告"
  347. else:
  348. should_send = False
  349. send_reason = "当天无交易,非关键时间点,跳过发送"
  350. print(f"\n📧 邮件发送决策: {send_reason}")
  351. if should_send:
  352. # 6. 生成报告(含实时信号)
  353. print("\n📝 步骤5: 生成报告...")
  354. html_report, text_report, final_capital = generate_report(t1_trades, initial_capital, realtime_signal)
  355. # 7. 发送邮件
  356. print("\n📧 步骤6: 发送邮件...")
  357. total_trades = len(t1_trades)
  358. total_return = (final_capital/initial_capital-1)*100
  359. # 邮件主题包含实时信号信息
  360. if realtime_signal:
  361. sig_emoji = "🟢" if realtime_signal['triggered'] else "⚪"
  362. subject = f"{sig_emoji} CYB50-T1报告 {datetime.now().strftime('%m-%d %H:%M')} | 实时{realtime_signal['score']}分 | 收益{total_return:+.2f}%"
  363. else:
  364. subject = f"🚀 CYB50-T1报告 {datetime.now().strftime('%m-%d %H:%M')} | 收益{total_return:+.2f}% | {total_trades}笔交易"
  365. send_email(subject, html_report, text_report)
  366. else:
  367. print("⏭️ 跳过邮件发送")
  368. print("\n✅ 全部完成!")
  369. print("="*80)
  370. if __name__ == "__main__":
  371. main()