daily_tqe_sender.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 趋势质量评估器 - 交易日15:06定时推送
  5. 检查是否为交易日,如果是则发送报告
  6. """
  7. import sys
  8. sys.path.insert(0, '/root/.openclaw/workspace/trend-quality-evaluator')
  9. import numpy as np
  10. import pandas as pd
  11. from trend_quality_evaluator import fetch_stock_data, TrendQualityEvaluator
  12. from datetime import datetime
  13. import smtplib
  14. from email.mime.text import MIMEText
  15. from email.mime.multipart import MIMEMultipart
  16. from email.mime.base import MIMEBase
  17. from email.header import Header
  18. from email import encoders
  19. import warnings
  20. warnings.filterwarnings('ignore')
  21. print("="*60)
  22. print(f"TQE交易日定时推送 - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
  23. print("="*60)
  24. # 获取数据
  25. df = fetch_stock_data("399673", "2025-01-01", "2026-12-31", "d")
  26. if df is None:
  27. print("❌ 数据获取失败,跳过")
  28. exit(0)
  29. # 检查今天是否有数据(是否为交易日)
  30. today = datetime.now().strftime('%Y-%m-%d')
  31. if today not in df.index.strftime('%Y-%m-%d').values:
  32. print(f"📅 {today} 非交易日,跳过推送")
  33. exit(0)
  34. print(f"✅ {today} 是交易日,生成报告...")
  35. # 获取最近365天数据用于统计
  36. last_365 = df.tail(365).copy()
  37. # 评估今日趋势质量
  38. evaluator = TrendQualityEvaluator()
  39. score = evaluator.evaluate(df)
  40. # 获取最新数据
  41. latest = df.iloc[-1]
  42. prev = df.iloc[-2] if len(df) > 1 else latest
  43. # 计算涨跌
  44. daily_change = latest['close'] - prev['close']
  45. daily_change_pct = daily_change / prev['close'] * 100
  46. # 计算近期趋势
  47. ret_5d = (latest['close'] / df['close'].iloc[-6] - 1) * 100 if len(df) >= 6 else 0
  48. ret_20d = (latest['close'] / df['close'].iloc[-21] - 1) * 100 if len(df) >= 21 else 0
  49. # 生成邮件内容
  50. state_name = ['震荡', '趋势', '反转'][0] # TQE不输出状态,只输出评分
  51. is_tradeable = score.is_tradeable
  52. html = f"""
  53. <html>
  54. <head>
  55. <meta charset="utf-8">
  56. <style>
  57. body {{ font-family: Arial, sans-serif; margin: 20px; font-size: 13px; }}
  58. h1 {{ color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; font-size: 18px; }}
  59. h2 {{ color: #555; margin-top: 20px; border-left: 4px solid #2196F3; padding-left: 10px; font-size: 14px; }}
  60. .summary {{ background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 15px 0; }}
  61. .today {{ background: #e3f2fd; padding: 15px; border-radius: 5px; margin: 15px 0; border-left: 4px solid #2196F3; }}
  62. .tradeable {{ background: #e8f5e9; border-left: 4px solid #4CAF50; }}
  63. .not-tradeable {{ background: #ffebee; border-left: 4px solid #f44336; }}
  64. table {{ width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 12px; }}
  65. th {{ background: #4CAF50; color: white; padding: 10px; text-align: center; }}
  66. td {{ padding: 8px; border-bottom: 1px solid #ddd; text-align: center; }}
  67. tr:nth-child(even) {{ background: #f8f9fa; }}
  68. .positive {{ color: #4CAF50; font-weight: bold; }}
  69. .negative {{ color: #f44336; font-weight: bold; }}
  70. .score-high {{ color: #4CAF50; font-size: 24px; font-weight: bold; }}
  71. .score-low {{ color: #f44336; font-size: 24px; font-weight: bold; }}
  72. .score-mid {{ color: #FF9800; font-size: 24px; font-weight: bold; }}
  73. </style>
  74. </head>
  75. <body>
  76. <h1>📊 Trend-Quality-Evaluator 每日质量报告</h1>
  77. <div class="today {'tradeable' if is_tradeable else 'not-tradeable'}">
  78. <h2>📈 今日评估 ({today})</h2>
  79. <p><strong>收盘价:</strong> {latest['close']:.2f}</p>
  80. <p><strong>日涨跌:</strong> <span class="{'positive' if daily_change >= 0 else 'negative'}">{daily_change:+.2f} ({daily_change_pct:+.2f}%)</span></p>
  81. <p><strong>趋势质量评分:</strong> <span class="{'score-high' if score.total_score >= 80 else 'score-mid' if score.total_score >= 60 else 'score-low'}">{score.total_score:.1f}分</span></p>
  82. <p><strong>交易建议:</strong> {'✅ 可交易 (≥60分)' if is_tradeable else '❌ 观望 (<60分)'}</p>
  83. </div>
  84. <div class="summary">
  85. <h2>📊 各因子得分详情</h2>
  86. <table>
  87. <tr><th>因子</th><th>得分</th><th>满分</th><th>原始指标</th><th>阈值</th></tr>
  88. <tr><td>ADX趋势强度</td><td>{score.adx_score:.1f}</td><td>30</td><td>ADX={score.adx_value:.2f}</td><td>>25</td></tr>
  89. <tr><td>均线斜率</td><td>{score.ma_slope_score:.1f}</td><td>25</td><td>斜率={score.ma_slope:.4f}</td><td>>1.002</td></tr>
  90. <tr><td>波动率收缩</td><td>{score.volatility_score:.1f}</td><td>20</td><td>ATR比={score.volatility_ratio:.3f}</td><td><0.8</td></tr>
  91. <tr><td>时间框架共振</td><td>{score.timeframe_score:.1f}</td><td>15</td><td>日周共振</td><td>-</td></tr>
  92. <tr><td>成交量确认</td><td>{score.volume_score:.1f}</td><td>10</td><td>量比={score.volume_ratio:.2f}x</td><td>>1.5</td></tr>
  93. </table>
  94. </div>
  95. <div class="summary">
  96. <h2>📈 近期趋势</h2>
  97. <p><strong>5日涨跌:</strong> <span class="{'positive' if ret_5d >= 0 else 'negative'}">{ret_5d:+.2f}%</span></p>
  98. <p><strong>20日涨跌:</strong> <span class="{'positive' if ret_20d >= 0 else 'negative'}">{ret_20d:+.2f}%</span></p>
  99. <p><strong>365天最高:</strong> {last_365['close'].max():.2f} ({last_365['close'].idxmax().strftime('%y-%m-%d')})</p>
  100. <p><strong>365天最低:</strong> {last_365['close'].min():.2f} ({last_365['close'].idxmin().strftime('%y-%m-%d')})</p>
  101. </div>
  102. <h2>💡 交易建议说明</h2>
  103. <ul>
  104. <li><strong>≥80分:</strong> 优秀趋势,建议重仓</li>
  105. <li><strong>70-79分:</strong> 良好趋势,建议中等仓位</li>
  106. <li><strong>60-69分:</strong> 及格趋势,建议轻仓试探</li>
  107. <li><strong>40-59分:</strong> 趋势较弱,建议观望</li>
  108. <li><strong><40分:</strong> 趋势混乱,避免交易</li>
  109. </ul>
  110. <hr>
  111. <p style="color: #666; font-size: 11px;">
  112. 推送时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}<br>
  113. 评估标的: 创业板50指数 (sz399673)<br>
  114. 模型版本: Trend-Quality-Evaluator v1.0 (参数已优化)
  115. </p>
  116. </body>
  117. </html>
  118. """
  119. # 发送邮件
  120. EMAIL_CONFIG = {
  121. "smtp_server": "localhost",
  122. "smtp_port": 25,
  123. "sender_email": "regime@erwin.wang",
  124. "receiver_email": "380880504@qq.com"
  125. }
  126. msg = MIMEMultipart('related')
  127. msg['Subject'] = Header(f"📊 TQE每日质量报告 [{today}] {'✅可交易' if is_tradeable else '❌观望'} {score.total_score:.0f}分", 'utf-8')
  128. msg['From'] = EMAIL_CONFIG['sender_email']
  129. msg['To'] = EMAIL_CONFIG['receiver_email']
  130. msg.attach(MIMEText(html, 'html', 'utf-8'))
  131. try:
  132. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  133. server.sendmail(EMAIL_CONFIG['sender_email'], EMAIL_CONFIG['receiver_email'], msg.as_string())
  134. print(f"✅ 邮件发送成功! [{today}] 评分: {score.total_score:.1f}分 {'可交易' if is_tradeable else '观望'}")
  135. except Exception as e:
  136. print(f"❌ 邮件发送失败: {e}")
  137. print("="*60)