daily_email_sender.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. CYB50市场状态识别 - 每日邮件发送脚本
  5. 数据范围: 2024年至今
  6. 发送时间: 每天15:10
  7. """
  8. import sys
  9. sys.path.insert(0, '/root/.openclaw/workspace/market-regime-identifier')
  10. import numpy as np
  11. import pandas as pd
  12. from cyb50_market_classifier import fetch_cyb50_data, calculate_features, define_market_regime
  13. from sklearn.ensemble import RandomForestClassifier
  14. import smtplib
  15. from email.mime.text import MIMEText
  16. from email.mime.multipart import MIMEMultipart
  17. from email.mime.base import MIMEBase
  18. from email.header import Header
  19. from email import encoders
  20. from datetime import datetime
  21. import warnings
  22. warnings.filterwarnings('ignore')
  23. print("="*60)
  24. print(f"CYB50每日市场状态报告 - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
  25. print("="*60)
  26. # 获取数据(包含历史+实时合并)
  27. df = fetch_cyb50_data('2024-01-01', '2026-12-31', use_realtime=True, prefer_source='mixed')
  28. if df is None:
  29. print("❌ 数据获取失败")
  30. exit(1)
  31. # 计算特征和标签
  32. features = calculate_features(df)
  33. labels = define_market_regime(df, lookback=10)
  34. # 训练模型
  35. valid_idx = ~np.isnan(labels)
  36. X = features[valid_idx]
  37. y = labels[valid_idx]
  38. clf = RandomForestClassifier(
  39. n_estimators=100, max_depth=10, min_samples_split=20,
  40. min_samples_leaf=10, random_state=42, class_weight='balanced'
  41. )
  42. clf.fit(X, y)
  43. # 预测所有数据
  44. states = clf.predict(X)
  45. probs = clf.predict_proba(X)
  46. # 对齐数据
  47. df_aligned = df.iloc[-len(states):].copy()
  48. df_aligned['state'] = states
  49. df_aligned['prob_ranging'] = probs[:, 0]
  50. df_aligned['prob_trend'] = probs[:, 1]
  51. df_aligned['prob_reversal'] = probs[:, 2]
  52. # 获取最近365天
  53. last_365 = df_aligned.tail(365).copy()
  54. last_365['change'] = last_365['close'].pct_change() * 100
  55. # 获取最新数据
  56. today = df_aligned.iloc[-1]
  57. yesterday = df_aligned.iloc[-2] if len(df_aligned) > 1 else today
  58. state_names = ['震荡', '趋势', '反转']
  59. colors = ['#2196F3', '#4CAF50', '#FF5722']
  60. state_name = state_names[int(today['state'])]
  61. state_color = colors[int(today['state'])]
  62. # 生成365天详细数据表格
  63. html_rows = ""
  64. for idx, row in last_365.iterrows():
  65. s = int(row['state'])
  66. change = row['change'] if not pd.isna(row['change']) else 0
  67. change_str = f"{change:+.2f}%" if change != 0 else "-"
  68. change_color = "green" if change > 0 else "red" if change < 0 else "gray"
  69. # 高亮最新一天
  70. highlight = 'style="background: #fff3cd; font-weight: bold;"' if idx == df_aligned.index[-1] else ''
  71. html_rows += f"""
  72. <tr {highlight}>
  73. <td>{idx.strftime('%y-%m-%d')}</td>
  74. <td>{row['close']:.2f}</td>
  75. <td style="color: {colors[s]}; font-weight: bold;">{state_names[s]}</td>
  76. <td>{row['prob_ranging']:.1%}</td>
  77. <td>{row['prob_trend']:.1%}</td>
  78. <td>{row['prob_reversal']:.1%}</td>
  79. <td style="color: {change_color};">{change_str}</td>
  80. </tr>
  81. """
  82. # 计算涨跌
  83. daily_change = today['close'] - yesterday['close']
  84. daily_change_pct = daily_change / yesterday['close'] * 100
  85. # 计算区间涨跌
  86. range_change_pct = (today['close'] / last_365['close'].iloc[0] - 1) * 100
  87. # 邮件内容
  88. html = f"""
  89. <html>
  90. <head>
  91. <meta charset="utf-8">
  92. <style>
  93. body {{ font-family: Arial, sans-serif; margin: 20px; font-size: 12px; }}
  94. h1 {{ color: #333; border-bottom: 3px solid #2196F3; padding-bottom: 10px; font-size: 18px; }}
  95. h2 {{ color: #555; margin-top: 20px; border-left: 4px solid #4CAF50; padding-left: 10px; font-size: 14px; }}
  96. .summary {{ background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }}
  97. .summary p {{ margin: 5px 0; }}
  98. .today {{ background: #e3f2fd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #2196F3; }}
  99. table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px; }}
  100. th {{ background: #2196F3; color: white; padding: 8px; text-align: center; }}
  101. td {{ padding: 6px 8px; border-bottom: 1px solid #ddd; text-align: center; }}
  102. tr:nth-child(even) {{ background: #f8f9fa; }}
  103. tr:hover {{ background: #e3f2fd; }}
  104. .legend {{ font-size: 11px; margin-top: 10px; }}
  105. .legend span {{ margin-right: 15px; }}
  106. </style>
  107. </head>
  108. <body>
  109. <h1>📊 CYB50每日市场状态报告</h1>
  110. <div class="today">
  111. <h2>📈 今日状态 ({df_aligned.index[-1].strftime('%Y-%m-%d')})</h2>
  112. <p><strong>收盘价:</strong> {today['close']:.2f}</p>
  113. <p><strong>日涨跌:</strong> <span style="color: {'green' if daily_change >= 0 else 'red'};">{daily_change:+.2f} ({daily_change_pct:+.2f}%)</span></p>
  114. <p><strong>市场状态:</strong> <span style="color: {state_color}; font-size: 16px; font-weight: bold;">{state_name}</span></p>
  115. <p><strong>状态概率:</strong> 震荡 {today['prob_ranging']:.1%} / 趋势 {today['prob_trend']:.1%} / 反转 {today['prob_reversal']:.1%}</p>
  116. </div>
  117. <div class="summary">
  118. <h2>📊 最近365天统计 (2024-至今)</h2>
  119. <p><strong>365天前价格:</strong> {last_365['close'].iloc[0]:.2f}</p>
  120. <p><strong>区间涨跌:</strong> <span style="color: {'green' if range_change_pct >= 0 else 'red'};">{range_change_pct:+.2f}%</span></p>
  121. <p><strong>最高价:</strong> {last_365['close'].max():.2f} ({last_365['close'].idxmax().strftime('%m-%d')}) / <strong>最低价:</strong> {last_365['close'].min():.2f} ({last_365['close'].idxmin().strftime('%m-%d')})</p>
  122. <br>
  123. <p><strong>状态分布:</strong> 🟦 震荡 {(last_365['state']==0).sum()}天 / 🟩 趋势 {(last_365['state']==1).sum()}天 / 🟧 反转 {(last_365['state']==2).sum()}天</p>
  124. </div>
  125. <h2>📋 最近365天详细数据</h2>
  126. <p class="legend">
  127. <span>🟦 震荡</span>
  128. <span>🟩 趋势</span>
  129. <span>🟧 反转</span>
  130. <span>(黄色背景 = 最新)</span>
  131. </p>
  132. <table>
  133. <thead>
  134. <tr>
  135. <th>日期</th>
  136. <th>收盘价</th>
  137. <th>状态</th>
  138. <th>震荡概率</th>
  139. <th>趋势概率</th>
  140. <th>反转概率</th>
  141. <th>日涨跌</th>
  142. </tr>
  143. </thead>
  144. <tbody>
  145. {html_rows}
  146. </tbody>
  147. </table>
  148. <hr>
  149. <p style="color: #666; font-size: 11px;">
  150. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}<br>
  151. 数据更新至: {df_aligned.index[-1].strftime('%Y-%m-%d')}<br>
  152. 模型准确率: 72.10% | 创业板50指数 (sz399673)
  153. </p>
  154. </body>
  155. </html>
  156. """
  157. # 发送邮件
  158. EMAIL_CONFIG = {
  159. "smtp_server": "localhost",
  160. "smtp_port": 25,
  161. "sender_email": "kalman@openclaw.local",
  162. "receiver_email": "380880504@qq.com"
  163. }
  164. msg = MIMEMultipart('related')
  165. msg['Subject'] = Header(f"📊 CYB50-Regime每日市场状态报告 [{df_aligned.index[-1].strftime('%m-%d')}] 当前{state_name}", 'utf-8')
  166. msg['From'] = "regime <regime@openclaw.local>"
  167. msg['To'] = EMAIL_CONFIG['receiver_email']
  168. msg.attach(MIMEText(html, 'html', 'utf-8'))
  169. try:
  170. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  171. server.sendmail(
  172. EMAIL_CONFIG['sender_email'],
  173. EMAIL_CONFIG['receiver_email'],
  174. msg.as_string()
  175. )
  176. print(f"✅ 邮件发送成功! [{df_aligned.index[-1].strftime('%Y-%m-%d')}] 当前状态: {state_name}")
  177. except Exception as e:
  178. print(f"❌ 邮件发送失败: {e}")