Преглед на файлове

feat: 新增TQE每日定时推送和参数优化测试

- 新增 daily_tqe_sender.py: 每个交易日15:06推送TQE质量报告
- 新增 optimize_params_simple.py: 参数优化测试工具
- 测试8种参数组合,确认原配置最优
- 设置定时任务: 工作日15:06自动推送

定时任务:
- TQE每日质量报告: 工作日15:06
openclaw преди 2 месеца
родител
ревизия
d0c83d7d85
променени са 2 файла, в които са добавени 356 реда и са изтрити 0 реда
  1. 161 0
      trend-quality-evaluator/daily_tqe_sender.py
  2. 195 0
      trend-quality-evaluator/optimize_params_simple.py

+ 161 - 0
trend-quality-evaluator/daily_tqe_sender.py

@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+趋势质量评估器 - 交易日15:06定时推送
+检查是否为交易日,如果是则发送报告
+"""
+
+import sys
+sys.path.insert(0, '/root/.openclaw/workspace/trend-quality-evaluator')
+
+import numpy as np
+import pandas as pd
+from trend_quality_evaluator import fetch_stock_data, TrendQualityEvaluator
+from datetime import datetime
+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
+import warnings
+warnings.filterwarnings('ignore')
+
+print("="*60)
+print(f"TQE交易日定时推送 - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
+print("="*60)
+
+# 获取数据
+df = fetch_stock_data("399673", "2025-01-01", "2026-12-31", "d")
+if df is None:
+    print("❌ 数据获取失败,跳过")
+    exit(0)
+
+# 检查今天是否有数据(是否为交易日)
+today = datetime.now().strftime('%Y-%m-%d')
+if today not in df.index.strftime('%Y-%m-%d').values:
+    print(f"📅 {today} 非交易日,跳过推送")
+    exit(0)
+
+print(f"✅ {today} 是交易日,生成报告...")
+
+# 获取最近365天数据用于统计
+last_365 = df.tail(365).copy()
+
+# 评估今日趋势质量
+evaluator = TrendQualityEvaluator()
+score = evaluator.evaluate(df)
+
+# 获取最新数据
+latest = df.iloc[-1]
+prev = df.iloc[-2] if len(df) > 1 else latest
+
+# 计算涨跌
+daily_change = latest['close'] - prev['close']
+daily_change_pct = daily_change / prev['close'] * 100
+
+# 计算近期趋势
+ret_5d = (latest['close'] / df['close'].iloc[-6] - 1) * 100 if len(df) >= 6 else 0
+ret_20d = (latest['close'] / df['close'].iloc[-21] - 1) * 100 if len(df) >= 21 else 0
+
+# 生成邮件内容
+state_name = ['震荡', '趋势', '反转'][0]  # TQE不输出状态,只输出评分
+is_tradeable = score.is_tradeable
+
+html = f"""
+<html>
+<head>
+    <meta charset="utf-8">
+    <style>
+        body {{ font-family: Arial, sans-serif; margin: 20px; font-size: 13px; }}
+        h1 {{ color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; font-size: 18px; }}
+        h2 {{ color: #555; margin-top: 20px; border-left: 4px solid #2196F3; padding-left: 10px; font-size: 14px; }}
+        .summary {{ background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 15px 0; }}
+        .today {{ background: #e3f2fd; padding: 15px; border-radius: 5px; margin: 15px 0; border-left: 4px solid #2196F3; }}
+        .tradeable {{ background: #e8f5e9; border-left: 4px solid #4CAF50; }}
+        .not-tradeable {{ background: #ffebee; border-left: 4px solid #f44336; }}
+        table {{ width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 12px; }}
+        th {{ background: #4CAF50; color: white; padding: 10px; text-align: center; }}
+        td {{ padding: 8px; border-bottom: 1px solid #ddd; text-align: center; }}
+        tr:nth-child(even) {{ background: #f8f9fa; }}
+        .positive {{ color: #4CAF50; font-weight: bold; }}
+        .negative {{ color: #f44336; font-weight: bold; }}
+        .score-high {{ color: #4CAF50; font-size: 24px; font-weight: bold; }}
+        .score-low {{ color: #f44336; font-size: 24px; font-weight: bold; }}
+        .score-mid {{ color: #FF9800; font-size: 24px; font-weight: bold; }}
+    </style>
+</head>
+<body>
+    <h1>📊 Trend-Quality-Evaluator 每日质量报告</h1>
+    
+    <div class="today {'tradeable' if is_tradeable else 'not-tradeable'}">
+        <h2>📈 今日评估 ({today})</h2>
+        <p><strong>收盘价:</strong> {latest['close']:.2f}</p>
+        <p><strong>日涨跌:</strong> <span class="{'positive' if daily_change >= 0 else 'negative'}">{daily_change:+.2f} ({daily_change_pct:+.2f}%)</span></p>
+        
+        <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>
+        
+        <p><strong>交易建议:</strong> {'✅ 可交易 (≥60分)' if is_tradeable else '❌ 观望 (<60分)'}</p>
+    </div>
+    
+    <div class="summary">
+        <h2>📊 各因子得分详情</h2>
+        <table>
+            <tr><th>因子</th><th>得分</th><th>满分</th><th>原始指标</th><th>阈值</th></tr>
+            <tr><td>ADX趋势强度</td><td>{score.adx_score:.1f}</td><td>30</td><td>ADX={score.adx_value:.2f}</td><td>>25</td></tr>
+            <tr><td>均线斜率</td><td>{score.ma_slope_score:.1f}</td><td>25</td><td>斜率={score.ma_slope:.4f}</td><td>>1.002</td></tr>
+            <tr><td>波动率收缩</td><td>{score.volatility_score:.1f}</td><td>20</td><td>ATR比={score.volatility_ratio:.3f}</td><td><0.8</td></tr>
+            <tr><td>时间框架共振</td><td>{score.timeframe_score:.1f}</td><td>15</td><td>日周共振</td><td>-</td></tr>
+            <tr><td>成交量确认</td><td>{score.volume_score:.1f}</td><td>10</td><td>量比={score.volume_ratio:.2f}x</td><td>>1.5</td></tr>
+        </table>
+    </div>
+    
+    <div class="summary">
+        <h2>📈 近期趋势</h2>
+        <p><strong>5日涨跌:</strong> <span class="{'positive' if ret_5d >= 0 else 'negative'}">{ret_5d:+.2f}%</span></p>
+        <p><strong>20日涨跌:</strong> <span class="{'positive' if ret_20d >= 0 else 'negative'}">{ret_20d:+.2f}%</span></p>
+        <p><strong>365天最高:</strong> {last_365['close'].max():.2f} ({last_365['close'].idxmax().strftime('%y-%m-%d')})</p>
+        <p><strong>365天最低:</strong> {last_365['close'].min():.2f} ({last_365['close'].idxmin().strftime('%y-%m-%d')})</p>
+    </div>
+    
+    <h2>💡 交易建议说明</h2>
+    <ul>
+        <li><strong>≥80分:</strong> 优秀趋势,建议重仓</li>
+        <li><strong>70-79分:</strong> 良好趋势,建议中等仓位</li>
+        <li><strong>60-69分:</strong> 及格趋势,建议轻仓试探</li>
+        <li><strong>40-59分:</strong> 趋势较弱,建议观望</li>
+        <li><strong><40分:</strong> 趋势混乱,避免交易</li>
+    </ul>
+    
+    <hr>
+    <p style="color: #666; font-size: 11px;">
+        推送时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}<br>
+        评估标的: 创业板50指数 (sz399673)<br>
+        模型版本: Trend-Quality-Evaluator v1.0 (参数已优化)
+    </p>
+</body>
+</html>
+"""
+
+# 发送邮件
+EMAIL_CONFIG = {
+    "smtp_server": "localhost",
+    "smtp_port": 25,
+    "sender_email": "regime@openclaw.local",
+    "receiver_email": "380880504@qq.com"
+}
+
+msg = MIMEMultipart('related')
+msg['Subject'] = Header(f"📊 TQE每日质量报告 [{today}] {'✅可交易' if is_tradeable else '❌观望'} {score.total_score:.0f}分", 'utf-8')
+msg['From'] = EMAIL_CONFIG['sender_email']
+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"✅ 邮件发送成功! [{today}] 评分: {score.total_score:.1f}分 {'可交易' if is_tradeable else '观望'}")
+except Exception as e:
+    print(f"❌ 邮件发送失败: {e}")
+
+print("="*60)

+ 195 - 0
trend-quality-evaluator/optimize_params_simple.py

@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+趋势质量评估器 - 参数优化测试 (简化版)
+测试不同参数组合的回测表现
+"""
+
+import sys
+sys.path.insert(0, '/root/.openclaw/workspace/trend-quality-evaluator')
+
+import numpy as np
+import pandas as pd
+from trend_quality_evaluator import fetch_stock_data
+import warnings
+warnings.filterwarnings('ignore')
+
+print("="*70)
+print("趋势质量评估器 - 参数优化测试")
+print("="*70)
+
+# 获取数据
+df = fetch_stock_data("399673", "2019-01-01", "2026-03-06", "d")
+if df is None:
+    print("数据获取失败")
+    exit(1)
+
+print(f"\n✓ 数据获取成功: {len(df)}条")
+
+# 预计算指标
+def calc_indicators(data):
+    d = data.copy()
+    high, low, close = d['high'], d['low'], d['close']
+    
+    # ADX
+    plus_dm = high.diff().where(high.diff() > 0, 0)
+    minus_dm = low.diff().abs().where(low.diff().abs() > 0, 0)
+    tr = pd.concat([high-low, (high-close.shift()).abs(), (low-close.shift()).abs()], axis=1).max(axis=1)
+    atr = tr.rolling(14).mean()
+    dx = (plus_dm.rolling(14).mean() / atr * 100).abs()
+    d['adx'] = dx.rolling(14).mean()
+    
+    # MA斜率
+    d['ma20'] = d['close'].rolling(20).mean()
+    d['ma_slope'] = d['ma20'] / d['ma20'].shift(5)
+    
+    # ATR比率
+    d['atr_ratio'] = tr.rolling(14).mean() / tr.rolling(50).mean()
+    
+    # 成交量
+    d['vol_ratio'] = d['volume'] / d['volume'].rolling(20).mean()
+    
+    d['above_ma20'] = d['close'] > d['ma20']
+    return d
+
+data = calc_indicators(df)
+
+# 测试的参数组合 (简化版)
+test_configs = [
+    # 配置1: 原配置
+    {'name': '原配置', 'adx_t': 25, 'adx_w': 30, 'ma_t': 1.002, 'ma_w': 25, 'vol_t': 0.8, 'vol_w': 20, 'volu_t': 1.5, 'volu_w': 10},
+    # 配置2: 提高ADX权重
+    {'name': '高ADX权重', 'adx_t': 25, 'adx_w': 35, 'ma_t': 1.002, 'ma_w': 20, 'vol_t': 0.8, 'vol_w': 20, 'volu_t': 1.5, 'volu_w': 10},
+    # 配置3: 降低ADX阈值
+    {'name': '低ADX阈值', 'adx_t': 20, 'adx_w': 30, 'ma_t': 1.002, 'ma_w': 25, 'vol_t': 0.8, 'vol_w': 20, 'volu_t': 1.5, 'volu_w': 10},
+    # 配置4: 提高MA权重
+    {'name': '高MA权重', 'adx_t': 25, 'adx_w': 25, 'ma_t': 1.002, 'ma_w': 30, 'vol_t': 0.8, 'vol_w': 20, 'volu_t': 1.5, 'volu_w': 10},
+    # 配置5: 更严格成交量
+    {'name': '严格成交量', 'adx_t': 25, 'adx_w': 30, 'ma_t': 1.002, 'ma_w': 25, 'vol_t': 0.8, 'vol_w': 15, 'volu_t': 2.0, 'volu_w': 15},
+    # 配置6: 宽松波动率
+    {'name': '宽松波动率', 'adx_t': 25, 'adx_w': 30, 'ma_t': 1.002, 'ma_w': 25, 'vol_t': 0.9, 'vol_w': 20, 'volu_t': 1.5, 'volu_w': 10},
+    # 配置7: 综合优化1
+    {'name': '综合优化1', 'adx_t': 22, 'adx_w': 32, 'ma_t': 1.001, 'ma_w': 28, 'vol_t': 0.85, 'vol_w': 18, 'volu_t': 1.8, 'volu_w': 12},
+    # 配置8: 综合优化2
+    {'name': '综合优化2', 'adx_t': 20, 'adx_w': 35, 'ma_t': 1.003, 'ma_w': 25, 'vol_t': 0.8, 'vol_w': 15, 'volu_t': 2.0, 'volu_w': 10},
+]
+
+results = []
+
+for cfg in test_configs:
+    print(f"\n测试: {cfg['name']}...")
+    
+    scores = []
+    for i in range(60, len(data)):
+        row = data.iloc[i]
+        
+        # 计算各因子得分
+        adx_s = cfg['adx_w'] if row['adx'] >= cfg['adx_t'] else cfg['adx_w'] * (row['adx'] / cfg['adx_t'])
+        
+        if row['ma_slope'] >= cfg['ma_t'] * 1.5:
+            ma_s = cfg['ma_w']
+        elif row['ma_slope'] >= cfg['ma_t']:
+            ma_s = cfg['ma_w'] * 0.7
+        elif row['ma_slope'] >= 1.0:
+            ma_s = cfg['ma_w'] * 0.3
+        else:
+            ma_s = 0
+        
+        if row['atr_ratio'] <= cfg['vol_t'] * 0.75:
+            vol_s = cfg['vol_w']
+        elif row['atr_ratio'] <= cfg['vol_t']:
+            vol_s = cfg['vol_w'] * 0.7
+        elif row['atr_ratio'] <= 1.0:
+            vol_s = cfg['vol_w'] * 0.3
+        else:
+            vol_s = 0
+        
+        if row['vol_ratio'] >= cfg['volu_t'] * 1.3:
+            volu_s = cfg['volu_w']
+        elif row['vol_ratio'] >= cfg['volu_t']:
+            volu_s = cfg['volu_w'] * 0.7
+        elif row['vol_ratio'] >= 1.0:
+            volu_s = cfg['volu_w'] * 0.3
+        else:
+            volu_s = 0
+        
+        tf_s = 15 if row['above_ma20'] else 0
+        
+        total = adx_s + ma_s + vol_s + volu_s + tf_s
+        is_trade = total >= 60
+        
+        scores.append({
+            'date': data.index[i],
+            'close': row['close'],
+            'score': total,
+            'is_trade': is_trade,
+            'adx_s': adx_s, 'ma_s': ma_s, 'vol_s': vol_s, 'volu_s': volu_s, 'tf_s': tf_s
+        })
+    
+    scores_df = pd.DataFrame(scores).set_index('date')
+    scores_df['ret_20d'] = scores_df['close'].pct_change(20).shift(-20) * 100
+    
+    t_mask = scores_df['is_trade']
+    trade_days = t_mask.sum()
+    
+    if trade_days > 0:
+        ret_20d = scores_df[t_mask]['ret_20d'].dropna()
+        result = {
+            'name': cfg['name'],
+            'adx_t': cfg['adx_t'], 'adx_w': cfg['adx_w'],
+            'ma_t': cfg['ma_t'], 'ma_w': cfg['ma_w'],
+            'vol_t': cfg['vol_t'], 'vol_w': cfg['vol_w'],
+            'volu_t': cfg['volu_t'], 'volu_w': cfg['volu_w'],
+            'trade_pct': trade_days / len(scores_df) * 100,
+            'avg_score': scores_df['score'].mean(),
+            'ret_20d': ret_20d.mean() if len(ret_20d) > 0 else 0,
+            'win_rate': (ret_20d > 0).mean() * 100 if len(ret_20d) > 0 else 0,
+            'sharpe': ret_20d.mean() / ret_20d.std() if len(ret_20d) > 0 and ret_20d.std() > 0 else 0,
+        }
+        results.append(result)
+        print(f"  可交易: {result['trade_pct']:.1f}%, 20日收益: {result['ret_20d']:+.2f}%, 胜率: {result['win_rate']:.1f}%")
+
+# 排序结果
+print("\n" + "="*70)
+print("参数优化结果排名")
+print("="*70)
+
+# 按20日收益排序
+results_by_return = sorted(results, key=lambda x: x['ret_20d'], reverse=True)
+print("\n【按20日收益排序】")
+print(f"{'排名':<4} {'配置名称':<12} {'可交易%':<10} {'20日收益':<12} {'胜率':<10} {'夏普':<8}")
+print("-"*70)
+for i, r in enumerate(results_by_return[:5]):
+    print(f"{i+1:<4} {r['name']:<12} {r['trade_pct']:>8.1f}% {r['ret_20d']:>+10.2f}% {r['win_rate']:>8.1f}% {r['sharpe']:>6.2f}")
+
+# 按胜率排序
+results_by_winrate = sorted(results, key=lambda x: x['win_rate'], reverse=True)
+print("\n【按胜率排序】")
+print(f"{'排名':<4} {'配置名称':<12} {'可交易%':<10} {'20日收益':<12} {'胜率':<10} {'夏普':<8}")
+print("-"*70)
+for i, r in enumerate(results_by_winrate[:5]):
+    print(f"{i+1:<4} {r['name']:<12} {r['trade_pct']:>8.1f}% {r['ret_20d']:>+10.2f}% {r['win_rate']:>8.1f}% {r['sharpe']:>6.2f}")
+
+# 综合最佳
+print("\n【综合推荐配置】")
+best = results_by_return[0]
+print(f"配置名称: {best['name']}")
+print(f"ADX阈值: {best['adx_t']}, 权重: {best['adx_w']}")
+print(f"MA斜率阈值: {best['ma_t']}, 权重: {best['ma_w']}")
+print(f"波动率阈值: {best['vol_t']}, 权重: {best['vol_w']}")
+print(f"成交量阈值: {best['volu_t']}, 权重: {best['volu_w']}")
+print(f"\n绩效:")
+print(f"  可交易比例: {best['trade_pct']:.1f}%")
+print(f"  20日平均收益: {best['ret_20d']:+.2f}%")
+print(f"  胜率: {best['win_rate']:.1f}%")
+print(f"  夏普比率: {best['sharpe']:.2f}")
+
+# 保存结果
+results_df = pd.DataFrame(results)
+results_df = results_df.sort_values('ret_20d', ascending=False)
+results_df.to_csv('/root/.openclaw/workspace/trend-quality-evaluator/param_optimization_results.csv', index=False)
+print("\n✓ 结果已保存: param_optimization_results.csv")
+
+print("\n" + "="*70)
+print("参数优化测试完成!")
+print("="*70)