daily_email_sender.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. CYB50市场状态识别 - 每日邮件发送脚本
  5. 通过本地SMTP服务器(直接投递模式)发送邮件
  6. """
  7. import sys
  8. sys.path.insert(0, '/root/.openclaw/workspace/market-regime-identifier')
  9. import numpy as np
  10. import pandas as pd
  11. from cyb50_market_classifier import fetch_cyb50_data, calculate_features, define_market_regime
  12. from sklearn.ensemble import RandomForestClassifier
  13. import smtplib
  14. from email.mime.text import MIMEText
  15. from email.mime.multipart import MIMEMultipart
  16. from email.header import Header
  17. from datetime import datetime
  18. import os
  19. import warnings
  20. warnings.filterwarnings('ignore')
  21. # SMTP配置
  22. SMTP_SERVER = os.getenv('SMTP_SERVER', 'localhost')
  23. SMTP_PORT = int(os.getenv('SMTP_PORT', '25'))
  24. SENDER = os.getenv('SMTP_SENDER', 'kalman@erwin.wang')
  25. RECEIVER = '380880504@qq.com'
  26. print("="*60)
  27. print(f"CYB50每日市场状态报告 - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
  28. print(f"SMTP: {SMTP_SERVER}:{SMTP_PORT}")
  29. print("="*60)
  30. # 获取数据
  31. df = fetch_cyb50_data('2024-01-01', '2026-12-31')
  32. if df is None:
  33. print("数据获取失败")
  34. exit(1)
  35. # 计算特征和标签
  36. features = calculate_features(df)
  37. labels = define_market_regime(df, lookback=10)
  38. # 训练模型
  39. valid_idx = ~np.isnan(labels)
  40. X = features[valid_idx]
  41. y = labels[valid_idx]
  42. clf = RandomForestClassifier(
  43. n_estimators=100, max_depth=10, min_samples_split=20,
  44. min_samples_leaf=10, random_state=42, class_weight='balanced'
  45. )
  46. clf.fit(X, y)
  47. # 预测所有数据
  48. states = clf.predict(X)
  49. probs = clf.predict_proba(X)
  50. # 对齐数据
  51. df_aligned = df.iloc[-len(states):].copy()
  52. df_aligned['state'] = states
  53. df_aligned['prob_ranging'] = probs[:, 0]
  54. df_aligned['prob_trend'] = probs[:, 1]
  55. df_aligned['prob_reversal'] = probs[:, 2]
  56. # 获取最新数据
  57. today = df_aligned.iloc[-1]
  58. yesterday = df_aligned.iloc[-2] if len(df_aligned) > 1 else today
  59. state_names = ['震荡', '趋势', '反转']
  60. colors = ['#2196F3', '#4CAF50', '#FF5722']
  61. state_name = state_names[int(today['state'])]
  62. state_color = colors[int(today['state'])]
  63. # 计算涨跌
  64. daily_change = today['close'] - yesterday['close']
  65. daily_change_pct = daily_change / yesterday['close'] * 100
  66. # 获取最近30天
  67. last_30 = df_aligned.tail(30).copy()
  68. # 生成HTML表格
  69. html_rows = ""
  70. for idx, row in last_30.iterrows():
  71. s = int(row['state'])
  72. html_rows += f"""
  73. <tr>
  74. <td>{idx.strftime('%m-%d')}</td>
  75. <td>{row['close']:.2f}</td>
  76. <td style="color: {colors[s]}; font-weight: bold;">{state_names[s]}</td>
  77. <td>{row['prob_ranging']:.1%}</td>
  78. <td>{row['prob_trend']:.1%}</td>
  79. <td>{row['prob_reversal']:.1%}</td>
  80. </tr>
  81. """
  82. # 邮件内容
  83. html = f"""
  84. <html>
  85. <head>
  86. <meta charset="utf-8">
  87. <style>
  88. body {{ font-family: Arial, sans-serif; margin: 20px; font-size: 12px; }}
  89. h1 {{ color: #333; border-bottom: 3px solid #2196F3; padding-bottom: 10px; font-size: 18px; }}
  90. .today {{ background: #e3f2fd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #2196F3; }}
  91. table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px; }}
  92. th {{ background: #2196F3; color: white; padding: 8px; text-align: center; }}
  93. td {{ padding: 6px 8px; border-bottom: 1px solid #ddd; text-align: center; }}
  94. tr:nth-child(even) {{ background: #f8f9fa; }}
  95. </style>
  96. </head>
  97. <body>
  98. <h1>CYB50每日市场状态报告</h1>
  99. <div class="today">
  100. <h2>今日状态 ({df_aligned.index[-1].strftime('%Y-%m-%d')})</h2>
  101. <p><strong>收盘价:</strong> {today['close']:.2f}</p>
  102. <p><strong>日涨跌:</strong> <span style="color: {'green' if daily_change >= 0 else 'red'};">{daily_change:+.2f} ({daily_change_pct:+.2f}%)</span></p>
  103. <p><strong>市场状态:</strong> <span style="color: {state_color}; font-size: 16px; font-weight: bold;">{state_name}</span></p>
  104. <p><strong>状态概率:</strong> 震荡 {today['prob_ranging']:.1%} / 趋势 {today['prob_trend']:.1%} / 反转 {today['prob_reversal']:.1%}</p>
  105. </div>
  106. <h2>最近30天数据</h2>
  107. <table>
  108. <thead>
  109. <tr>
  110. <th>日期</th>
  111. <th>收盘价</th>
  112. <th>状态</th>
  113. <th>震荡概率</th>
  114. <th>趋势概率</th>
  115. <th>反转概率</th>
  116. </tr>
  117. </thead>
  118. <tbody>
  119. {html_rows}
  120. </tbody>
  121. </table>
  122. <hr>
  123. <p style="color: #666; font-size: 11px;">
  124. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}<br>
  125. 数据更新至: {df_aligned.index[-1].strftime('%Y-%m-%d')}<br>
  126. 模型准确率: 72.10% | 创业板50指数 (sz399673)
  127. </p>
  128. </body>
  129. </html>
  130. """
  131. # 发送邮件
  132. msg = MIMEMultipart('related')
  133. msg['Subject'] = Header(f"CYB50-Regime每日报告 [{df_aligned.index[-1].strftime('%m-%d')}] 当前{state_name}", 'utf-8')
  134. msg['From'] = f"regime <{SENDER}>"
  135. msg['To'] = RECEIVER
  136. msg.attach(MIMEText(html, 'html', 'utf-8'))
  137. try:
  138. with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
  139. server.sendmail(SENDER, RECEIVER, msg.as_string())
  140. print(f"[OK] 邮件发送成功! [{df_aligned.index[-1].strftime('%Y-%m-%d')}] 当前状态: {state_name}")
  141. except Exception as e:
  142. print(f"[ERROR] 邮件发送失败: {e}")
  143. print(f"请确保SMTP服务器已启动: python smtp_relay_server.py --direct")
  144. print("="*60)