| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- CYB50市场状态识别 - 每日邮件发送脚本
- 数据范围: 2024年至今
- 发送时间: 每天15:10
- """
- import sys
- sys.path.insert(0, '/root/.openclaw/workspace/market-regime-identifier')
- import numpy as np
- import pandas as pd
- from cyb50_market_classifier import fetch_cyb50_data, calculate_features, define_market_regime
- from sklearn.ensemble import RandomForestClassifier
- import smtplib
- from email.mime.text import MIMEText
- from email.mime.multipart import MIMEMultipart
- from email.mime.base import MIMEBase
- from email.header import Header
- from email import encoders
- from datetime import datetime
- import warnings
- warnings.filterwarnings('ignore')
- print("="*60)
- print(f"CYB50每日市场状态报告 - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
- print("="*60)
- # 获取数据(包含历史+实时合并)
- df = fetch_cyb50_data('2024-01-01', '2026-12-31', use_realtime=True, prefer_source='mixed')
- if df is None:
- print("❌ 数据获取失败")
- exit(1)
- # 计算特征和标签
- features = calculate_features(df)
- labels = define_market_regime(df, lookback=10)
- # 训练模型
- valid_idx = ~np.isnan(labels)
- X = features[valid_idx]
- y = labels[valid_idx]
- clf = RandomForestClassifier(
- n_estimators=100, max_depth=10, min_samples_split=20,
- min_samples_leaf=10, random_state=42, class_weight='balanced'
- )
- clf.fit(X, y)
- # 预测所有数据
- states = clf.predict(X)
- probs = clf.predict_proba(X)
- # 对齐数据
- df_aligned = df.iloc[-len(states):].copy()
- df_aligned['state'] = states
- df_aligned['prob_ranging'] = probs[:, 0]
- df_aligned['prob_trend'] = probs[:, 1]
- df_aligned['prob_reversal'] = probs[:, 2]
- # 获取最近365天
- last_365 = df_aligned.tail(365).copy()
- last_365['change'] = last_365['close'].pct_change() * 100
- # 获取最新数据
- today = df_aligned.iloc[-1]
- yesterday = df_aligned.iloc[-2] if len(df_aligned) > 1 else today
- state_names = ['震荡', '趋势', '反转']
- colors = ['#2196F3', '#4CAF50', '#FF5722']
- state_name = state_names[int(today['state'])]
- state_color = colors[int(today['state'])]
- # 生成365天详细数据表格
- html_rows = ""
- for idx, row in last_365.iterrows():
- s = int(row['state'])
- change = row['change'] if not pd.isna(row['change']) else 0
- change_str = f"{change:+.2f}%" if change != 0 else "-"
- change_color = "green" if change > 0 else "red" if change < 0 else "gray"
-
- # 高亮最新一天
- highlight = 'style="background: #fff3cd; font-weight: bold;"' if idx == df_aligned.index[-1] else ''
-
- html_rows += f"""
- <tr {highlight}>
- <td>{idx.strftime('%y-%m-%d')}</td>
- <td>{row['close']:.2f}</td>
- <td style="color: {colors[s]}; font-weight: bold;">{state_names[s]}</td>
- <td>{row['prob_ranging']:.1%}</td>
- <td>{row['prob_trend']:.1%}</td>
- <td>{row['prob_reversal']:.1%}</td>
- <td style="color: {change_color};">{change_str}</td>
- </tr>
- """
- # 计算涨跌
- daily_change = today['close'] - yesterday['close']
- daily_change_pct = daily_change / yesterday['close'] * 100
- # 计算区间涨跌
- range_change_pct = (today['close'] / last_365['close'].iloc[0] - 1) * 100
- # 邮件内容
- html = f"""
- <html>
- <head>
- <meta charset="utf-8">
- <style>
- body {{ font-family: Arial, sans-serif; margin: 20px; font-size: 12px; }}
- h1 {{ color: #333; border-bottom: 3px solid #2196F3; padding-bottom: 10px; font-size: 18px; }}
- h2 {{ color: #555; margin-top: 20px; border-left: 4px solid #4CAF50; padding-left: 10px; font-size: 14px; }}
- .summary {{ background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }}
- .summary p {{ margin: 5px 0; }}
- .today {{ background: #e3f2fd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #2196F3; }}
- table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px; }}
- th {{ background: #2196F3; color: white; padding: 8px; text-align: center; }}
- td {{ padding: 6px 8px; border-bottom: 1px solid #ddd; text-align: center; }}
- tr:nth-child(even) {{ background: #f8f9fa; }}
- tr:hover {{ background: #e3f2fd; }}
- .legend {{ font-size: 11px; margin-top: 10px; }}
- .legend span {{ margin-right: 15px; }}
- </style>
- </head>
- <body>
- <h1>📊 CYB50每日市场状态报告</h1>
-
- <div class="today">
- <h2>📈 今日状态 ({df_aligned.index[-1].strftime('%Y-%m-%d')})</h2>
- <p><strong>收盘价:</strong> {today['close']:.2f}</p>
- <p><strong>日涨跌:</strong> <span style="color: {'green' if daily_change >= 0 else 'red'};">{daily_change:+.2f} ({daily_change_pct:+.2f}%)</span></p>
- <p><strong>市场状态:</strong> <span style="color: {state_color}; font-size: 16px; font-weight: bold;">{state_name}</span></p>
- <p><strong>状态概率:</strong> 震荡 {today['prob_ranging']:.1%} / 趋势 {today['prob_trend']:.1%} / 反转 {today['prob_reversal']:.1%}</p>
- </div>
-
- <div class="summary">
- <h2>📊 最近365天统计 (2024-至今)</h2>
- <p><strong>365天前价格:</strong> {last_365['close'].iloc[0]:.2f}</p>
- <p><strong>区间涨跌:</strong> <span style="color: {'green' if range_change_pct >= 0 else 'red'};">{range_change_pct:+.2f}%</span></p>
- <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>
- <br>
- <p><strong>状态分布:</strong> 🟦 震荡 {(last_365['state']==0).sum()}天 / 🟩 趋势 {(last_365['state']==1).sum()}天 / 🟧 反转 {(last_365['state']==2).sum()}天</p>
- </div>
-
- <h2>📋 最近365天详细数据</h2>
- <p class="legend">
- <span>🟦 震荡</span>
- <span>🟩 趋势</span>
- <span>🟧 反转</span>
- <span>(黄色背景 = 最新)</span>
- </p>
- <table>
- <thead>
- <tr>
- <th>日期</th>
- <th>收盘价</th>
- <th>状态</th>
- <th>震荡概率</th>
- <th>趋势概率</th>
- <th>反转概率</th>
- <th>日涨跌</th>
- </tr>
- </thead>
- <tbody>
- {html_rows}
- </tbody>
- </table>
-
- <hr>
- <p style="color: #666; font-size: 11px;">
- 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}<br>
- 数据更新至: {df_aligned.index[-1].strftime('%Y-%m-%d')}<br>
- 模型准确率: 72.10% | 创业板50指数 (sz399673)
- </p>
- </body>
- </html>
- """
- # 发送邮件
- EMAIL_CONFIG = {
- "smtp_server": "localhost",
- "smtp_port": 25,
- "sender_email": "kalman@openclaw.local",
- "receiver_email": "380880504@qq.com"
- }
- msg = MIMEMultipart('related')
- msg['Subject'] = Header(f"📊 CYB50-Regime每日市场状态报告 [{df_aligned.index[-1].strftime('%m-%d')}] 当前{state_name}", 'utf-8')
- msg['From'] = "regime <regime@openclaw.local>"
- msg['To'] = EMAIL_CONFIG['receiver_email']
- msg.attach(MIMEText(html, 'html', 'utf-8'))
- try:
- with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
- server.sendmail(
- EMAIL_CONFIG['sender_email'],
- EMAIL_CONFIG['receiver_email'],
- msg.as_string()
- )
- print(f"✅ 邮件发送成功! [{df_aligned.index[-1].strftime('%Y-%m-%d')}] 当前状态: {state_name}")
- except Exception as e:
- print(f"❌ 邮件发送失败: {e}")
|