Explorar el Código

feat: 添加实时信号检测功能

- auto_report_realtime.py: 增强版报告系统,14:50获取实时行情计算信号
- realtime_signal.py: 独立实时信号检测工具
- cron_jobs_realtime.json: 更新后的定时任务配置
- README_REALTIME.md: 使用说明文档

功能:
1. 收盘前(14:50)获取实时行情,估算当前买入信号
2. 基于最后K线技术指标+实时价格进行信号估算
3. 实时信号触发时强制发送邮件
4. 邮件主题显示实时信号分数
openclaw hace 1 mes
padre
commit
973bb9b2a0

+ 119 - 0
cat-fly/README_REALTIME.md

@@ -0,0 +1,119 @@
+# Auto Report 实时信号增强版
+
+## 文件说明
+
+- `auto_report_long_only_t1.py` - 原始版本,使用30分钟K线数据
+- `auto_report_realtime.py` - 增强版本,在14:50等关键时间点会获取实时行情计算信号
+- `realtime_signal.py` - 独立的实时信号检测工具
+
+## 主要改进
+
+### 1. 收盘前实时信号检测 (14:50)
+
+原版问题:
+- 14:50运行时只能拿到14:30的K线数据
+- 14:30-15:00这根K线要到15:00才收盘
+- 无法反映14:30-14:50之间的价格变化
+
+改进方案:
+- 在14:50获取实时行情
+- 基于14:30的技术指标 + 当前实时价格,估算当前信号状态
+- RSI、KDJ等指标根据价格变化进行估算
+- 邮件报告中包含实时信号检测结果
+
+### 2. 邮件触发逻辑优化
+
+原版:
+- 有交易 或 盘后时间 → 发送邮件
+- 14:50无交易时可能跳过发送
+
+增强版:
+- 有交易 → 发送邮件
+- 实时信号触发 → 发送邮件(即使无交易)
+- 收盘前时间(14:50) → 发送邮件(包含实时信号)
+- 盘后时间 → 发送例行报告
+
+### 3. 邮件主题优化
+
+实时信号版本会在邮件主题中显示实时信号分数:
+- `🟢 CYB50-T1报告 04-01 14:50 | 实时5分 | 收益+2.50%` (触发买入)
+- `⚪ CYB50-T1报告 04-01 14:50 | 实时2分 | 收益+2.50%` (未触发)
+
+## 使用方法
+
+### 方法1:直接运行实时版本
+
+```bash
+cd /root/.openclaw/workspace/cat-fly
+python3 auto_report_realtime.py
+```
+
+在14:50左右运行会自动获取实时行情并计算信号。
+
+### 方法2:独立实时信号检测
+
+```bash
+cd /root/.openclaw/workspace/cat-fly
+python3 realtime_signal.py
+```
+
+仅检测当前信号状态,不生成完整报告。
+
+### 方法3:更新定时任务
+
+修改 `cron_jobs.json`,将14:50的任务指向新版本:
+
+```json
+{
+  "id": "catfly-afternoon-1450-realtime",
+  "name": "catfly-afternoon-1450-realtime",
+  "schedule": {"expr": "50 14 * * 1-5", "tz": "Asia/Shanghai"},
+  "payload": {"script": "auto_report_realtime.py"}
+}
+```
+
+## 实时信号计算逻辑
+
+基于最后一根完整K线(14:30)的技术指标,结合实时价格进行估算:
+
+1. **RSI估算**:价格每变化1%,RSI约变化2.5个点
+2. **KDJ估算**:J值对价格敏感,价格每变化1%,J约变化3个点
+3. **布林带**:使用实时价格判断与下轨的距离
+4. **动量**:使用实时价格相对上一K线收盘的变化率
+5. **MA趋势**:沿用上一K线的MA趋势判断
+
+## 注意事项
+
+1. 实时信号是**估算值**,基于价格与技术指标的近似线性关系
+2. 实际交易信号仍以完整K线收盘后的计算为准
+3. 实时信号主要用于**提前预警**,在收盘前10分钟给出参考
+4. 网络不稳定时可能无法获取实时行情,会回退到使用K线数据
+
+## 测试
+
+可以在任意时间测试实时信号检测:
+
+```bash
+python3 realtime_signal.py
+```
+
+输出示例:
+```
+================================================================================
+实时信号评估结果
+================================================================================
+当前价格: 3367.82
+最后一根K线收盘: 3387.91
+价格变化: -0.59%
+
+技术指标估算:
+  RSI(估算): 39.97
+  KDJ J(估算): 12.69
+  布林带: [3353.48, 3504.82]
+
+信号评分: 2/4 (阈值: 4分触发买入)
+触发信号: 触及下轨, 日内低位, MA下降趋势惩罚
+
+⚪ 建议: 未触发买入信号
+================================================================================
+```

+ 446 - 0
cat-fly/auto_report_realtime.py

@@ -0,0 +1,446 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+创业板50指数 - 只做多T+1自动化交易报告系统 (实时信号增强版)
+在14:50等时间点会使用实时行情数据计算当前信号
+"""
+
+import sys
+sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
+
+import pandas as pd
+import numpy as np
+from datetime import datetime, timedelta
+import smtplib
+import ssl
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.header import Header
+import warnings
+warnings.filterwarnings('ignore')
+
+# 导入策略模块
+from cyb50_30min_dual_direction import (
+    ConfigManager, IntradayDataFetcher, 
+    DualDirectionSignalGenerator, DualDirectionExecutor
+)
+from t1_converter import simulate_t1_trades_v2, compare_results
+
+# ==================== 邮件配置 ====================
+EMAIL_CONFIG = {
+    "smtp_server": "localhost",
+    "smtp_port": 25,
+    "sender_email": "cyb50-t1@erwin.wang",
+    "receiver_emails": ["380880504@qq.com", "1095512042@qq.com"]
+}
+
+def send_email(subject, html_content, text_content=""):
+    """发送邮件"""
+    try:
+        msg = MIMEMultipart('alternative')
+        msg['Subject'] = Header(subject, 'utf-8')
+        msg['From'] = EMAIL_CONFIG['sender_email']
+        msg['To'] = ', '.join(EMAIL_CONFIG['receiver_emails'])
+        
+        text_part = MIMEText(text_content, 'plain', 'utf-8')
+        msg.attach(text_part)
+        
+        html_part = MIMEText(html_content, 'html', 'utf-8')
+        msg.attach(html_part)
+        
+        with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
+            server.sendmail(
+                EMAIL_CONFIG['sender_email'],
+                EMAIL_CONFIG['receiver_emails'],
+                msg.as_string()
+            )
+        print(f"✅ 邮件发送成功: {subject}")
+        return True
+    except Exception as e:
+        print(f"❌ 邮件发送失败: {e}")
+        return False
+
+
+def get_realtime_price():
+    """获取创业板50实时价格"""
+    try:
+        import akshare as ak
+        # 使用新浪接口获取实时行情(更稳定)
+        df = ak.stock_zh_index_spot_sina()
+        cyb50 = df[df['代码'] == 'sz399673']
+        if len(cyb50) > 0:
+            return {
+                'price': float(cyb50.iloc[0]['最新价']),
+                'open': float(cyb50.iloc[0]['今开']),
+                'high': float(cyb50.iloc[0]['最高']),
+                'low': float(cyb50.iloc[0]['最低']),
+                'volume': float(cyb50.iloc[0]['成交量']),
+                'change_pct': float(cyb50.iloc[0].get('涨跌幅', 0)),
+                'time': datetime.now().strftime('%H:%M:%S')
+            }
+    except Exception as e:
+        print(f"获取实时行情失败: {e}")
+    return None
+
+
+def calculate_realtime_signal(current_price, last_kline_data):
+    """
+    基于最新价格和最后一根K线数据,估算当前信号状态
+    """
+    score = 0
+    signals = []
+    
+    last_close = last_kline_data['Close']
+    price_change_pct = (current_price - last_close) / last_close
+    
+    # 1. RSI估算
+    estimated_rsi = last_kline_data['RSI'] + price_change_pct * 250
+    if estimated_rsi < 30:
+        score += 2
+        signals.append(f"RSI超卖(估{estimated_rsi:.1f})")
+    elif estimated_rsi < 35:
+        score += 1
+        signals.append(f"RSI偏弱(估{estimated_rsi:.1f})")
+    
+    # 2. KDJ估算
+    estimated_j = last_kline_data['J'] + price_change_pct * 300
+    if estimated_j < 0:
+        score += 1
+        signals.append(f"KDJ极端超卖(估J={estimated_j:.1f})")
+    
+    # 3. 布林带位置
+    bb_lower = last_kline_data['BB_lower']
+    if current_price <= bb_lower * 1.005:
+        score += 2
+        signals.append("触及下轨")
+    elif current_price <= bb_lower * 1.02:
+        score += 1
+        signals.append("接近下轨")
+    
+    # 4. 价格动量
+    if price_change_pct < -0.015:
+        score += 1
+        signals.append(f"动量超卖({price_change_pct*100:.2f}%)")
+    
+    # 5. MA趋势
+    if last_kline_data['MA6'] > last_kline_data['MA12']:
+        score += 1
+        signals.append("MA短期上行")
+    else:
+        score -= 1
+        signals.append("MA下降趋势惩罚")
+    
+    return {
+        'score': score,
+        'signals': signals,
+        'estimated_rsi': estimated_rsi,
+        'estimated_j': estimated_j,
+        'price_change_pct': price_change_pct,
+        'bb_lower': bb_lower,
+        'bb_upper': last_kline_data['BB_upper'],
+        'current_price': current_price,
+        'last_close': last_close,
+        'triggered': score >= 4
+    }
+
+
+def is_pre_close_time():
+    """检查是否是收盘前10分钟(14:50左右)"""
+    now = datetime.now()
+    return now.hour == 14 and now.minute >= 50
+
+
+def check_today_trades(trades_df):
+    """检查当天是否有交易"""
+    if len(trades_df) == 0:
+        return False, pd.DataFrame()
+    
+    today = datetime.now().date()
+    today_trades = trades_df[
+        (pd.to_datetime(trades_df['开仓时间']).dt.date == today) | 
+        (pd.to_datetime(trades_df['平仓时间']).dt.date == today)
+    ]
+    
+    has_today_trade = len(today_trades) > 0
+    
+    if has_today_trade:
+        print(f"📊 当天交易数量: {len(today_trades)}笔")
+        for _, trade in today_trades.iterrows():
+            print(f"   {trade['开仓时间']} → {trade['平仓时间']} | {trade['盈亏金额']:+.0f}元")
+    else:
+        print("📭 当天无交易")
+    
+    return has_today_trade, today_trades
+
+
+def is_post_close_time():
+    """检查是否是盘后时间(15:00-15:30)"""
+    now = datetime.now()
+    return now.hour == 15 and now.minute >= 0 and now.minute <= 30
+
+
+def generate_report(trades_df, initial_capital=1000000, realtime_signal=None):
+    """生成只做多T+1交易报告(增强版,包含实时信号)"""
+    
+    if len(trades_df) == 0:
+        final_capital = initial_capital
+        total_return = 0
+    else:
+        total_pnl = trades_df['盈亏金额'].sum()
+        final_capital = initial_capital + total_pnl
+        total_return = (final_capital - initial_capital) / initial_capital * 100
+    
+    total_trades = len(trades_df)
+    
+    winning_trades = trades_df[trades_df['盈亏金额'] > 0]
+    losing_trades = trades_df[trades_df['盈亏金额'] < 0]
+    
+    win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
+    total_profit = winning_trades['盈亏金额'].sum() if len(winning_trades) > 0 else 0
+    total_loss = abs(losing_trades['盈亏金额'].sum()) if len(losing_trades) > 0 else 0
+    profit_factor = total_profit / total_loss if total_loss > 0 else 0
+    
+    # T+1调整统计
+    t1_adjusted = trades_df[trades_df['T+1调整'] == '是(T0→T1)']
+    t1_count = len(t1_adjusted)
+    t1_pnl = t1_adjusted['盈亏金额'].sum() if len(t1_adjusted) > 0 else 0
+    
+    # 构建HTML报告
+    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+    
+    # 实时信号部分
+    realtime_html = ""
+    if realtime_signal:
+        signal_color = "green" if realtime_signal['triggered'] else "orange" if realtime_signal['score'] >= 3 else "gray"
+        signal_text = "🟢 触发买入" if realtime_signal['triggered'] else "🟡 接近触发" if realtime_signal['score'] >= 3 else "⚪ 未触发"
+        
+        realtime_html = f"""
+        <h2>🔔 实时信号检测 (收盘前)</h2>
+        <table>
+            <tr><th>指标</th><th>数值</th></tr>
+            <tr><td>检测时间</td><td>{now_str}</td></tr>
+            <tr><td>实时价格</td><td>{realtime_signal['current_price']:.2f}</td></tr>
+            <tr><td>较上一K线</td><td>{realtime_signal['price_change_pct']*100:+.2f}%</td></tr>
+            <tr><td>RSI(估算)</td><td>{realtime_signal['estimated_rsi']:.2f}</td></tr>
+            <tr><td>KDJ J(估算)</td><td>{realtime_signal['estimated_j']:.2f}</td></tr>
+            <tr><td>布林带下轨</td><td>{realtime_signal['bb_lower']:.2f}</td></tr>
+            <tr><td>信号评分</td><td style="color: {signal_color}; font-weight: bold;">{realtime_signal['score']}/4</td></tr>
+            <tr><td>触发信号</td><td>{', '.join(realtime_signal['signals']) if realtime_signal['signals'] else '无'}</td></tr>
+            <tr><td>最终判断</td><td style="color: {signal_color}; font-size: 16px; font-weight: bold;">{signal_text}</td></tr>
+        </table>
+        """
+    
+    html = f"""
+    <html><head><style>
+    body {{ font-family: Arial, sans-serif; margin: 20px; }}
+    h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
+    h2 {{ color: #555; margin-top: 30px; }}
+    table {{ border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 14px; }}
+    th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
+    th {{ background-color: #007bff; color: white; }}
+    tr:nth-child(even) {{ background-color: #f2f2f2; }}
+    .positive {{ color: green; font-weight: bold; }}
+    .negative {{ color: red; font-weight: bold; }}
+    .highlight {{ background-color: #fff3cd; }}
+    </style></head><body>
+    <h1>🚀 创业板50交易报告 (T+1)</h1>
+    <p>生成时间: {now_str}</p>
+    <p>数据区间: 近3个月</p>
+    {realtime_html}
+    
+    <h2>📊 总体绩效</h2>
+    <table>
+        <tr><th>指标</th><th>数值</th></tr>
+        <tr><td>初始资金</td><td>{initial_capital:,.0f}元</td></tr>
+        <tr><td>最终资金</td><td>{final_capital:,.0f}元</td></tr>
+        <tr><td>总收益率</td><td class="{'positive' if total_return >= 0 else 'negative'}">{total_return:+.2f}%</td></tr>
+        <tr><td>总交易次数</td><td>{total_trades}笔</td></tr>
+        <tr><td>胜率</td><td>{win_rate:.1f}%</td></tr>
+        <tr><td>盈亏比</td><td>{profit_factor:.2f}</td></tr>
+    </table>
+    
+    <h2>📝 最近交易明细</h2>
+    <table>
+        <tr><th>开仓时间</th><th>平仓时间</th><th>开仓价</th><th>平仓价</th>
+            <th>盈亏</th><th>退出原因</th><th>T+1调整</th></tr>
+    """
+    
+    for _, trade in trades_df.tail(10).iterrows():
+        pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
+        t1_flag = "✓" if trade['T+1调整'] == '是(T0→T1)' else ""
+        html += f"""
+        <tr>
+            <td>{trade['开仓时间'].strftime('%m-%d %H:%M')}</td>
+            <td>{trade['平仓时间'].strftime('%m-%d %H:%M')}</td>
+            <td>{trade['开仓价格']:.2f}</td>
+            <td>{trade['平仓价格']:.2f}</td>
+            <td class="{pnl_class}">{trade['盈亏金额']:+.0f}</td>
+            <td>{trade['退出原因']}</td>
+            <td>{t1_flag}</td>
+        </tr>
+        """
+    
+    if len(trades_df) == 0:
+        html += "<tr><td colspan='7'>近2个月无交易信号触发</td></tr>"
+    
+    html += "</table></body></html>"
+    
+    # 纯文本报告
+    text = f"""
+创业板50交易报告 (T+1)
+生成时间: {now_str}
+
+{f"【实时信号检测】\n时间: {now_str}\n实时价格: {realtime_signal['current_price']:.2f}\n信号评分: {realtime_signal['score']}/4\n触发信号: {', '.join(realtime_signal['signals']) if realtime_signal['signals'] else '无'}\n判断: {'触发买入' if realtime_signal['triggered'] else '未触发'}\n" if realtime_signal else ""}
+【总体绩效】
+初始资金: {initial_capital:,.0f}元
+最终资金: {final_capital:,.0f}元
+总收益率: {total_return:+.2f}%
+总交易次数: {total_trades}笔
+胜率: {win_rate:.1f}%
+"""
+    
+    return html, text, final_capital
+
+
+def main():
+    """主程序 - 实时信号增强版"""
+    print("="*80)
+    print("🚀 创业板50只做多T+1自动交易报告系统 (实时信号增强版)")
+    print("="*80)
+    print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+    
+    initial_capital = 1000000
+    realtime_signal = None
+    
+    # 判断是否是收盘前时间
+    pre_close = is_pre_close_time()
+    if pre_close:
+        print("\n🔔 检测到收盘前时间(14:50+),将获取实时行情计算信号...")
+    
+    # 1. 获取数据
+    print("\n📊 步骤1: 获取近3个月数据...")
+    try:
+        config_manager = ConfigManager('config.json')
+        fetcher = IntradayDataFetcher(config_manager)
+        end_date = datetime.now()
+        start_date = end_date - timedelta(days=90)
+        raw_data = fetcher.fetch_30min_data(start_date, end_date)
+        
+        if raw_data is None or len(raw_data) == 0:
+            print("❌ 数据获取失败")
+            return
+        
+        print(f"✅ 数据获取成功: {len(raw_data)}条K线")
+        print(f"   数据区间: {raw_data.index[0]} ~ {raw_data.index[-1]}")
+        
+    except Exception as e:
+        print(f"❌ 数据获取失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return
+    
+    # 2. 如果是收盘前时间,获取实时信号
+    if pre_close:
+        print("\n📈 步骤1.5: 获取实时行情并计算信号...")
+        try:
+            realtime = get_realtime_price()
+            if realtime:
+                print(f"   实时价格: {realtime['price']:.2f} (时间: {realtime['time']})")
+                
+                # 计算技术指标
+                data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
+                last_kline = data_with_indicators.iloc[-1]
+                
+                # 计算实时信号
+                realtime_signal = calculate_realtime_signal(realtime['price'], last_kline)
+                print(f"   实时信号评分: {realtime_signal['score']}/4")
+                print(f"   触发信号: {', '.join(realtime_signal['signals']) if realtime_signal['signals'] else '无'}")
+                print(f"   是否触发: {'是' if realtime_signal['triggered'] else '否'}")
+            else:
+                print("   ⚠️ 获取实时行情失败,跳过实时信号计算")
+        except Exception as e:
+            print(f"   ⚠️ 实时信号计算失败: {e}")
+    
+    # 3. 生成多空双向信号并执行
+    print("\n📈 步骤2: 运行多空双向策略获取做多交易...")
+    try:
+        data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
+        signal_generator = DualDirectionSignalGenerator()
+        signals_df = signal_generator.generate_dual_direction_signals(data_with_indicators)
+        
+        executor = DualDirectionExecutor(initial_capital=initial_capital)
+        results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
+        
+        long_trades = trades_df[trades_df['交易方向'] == '做多'].copy()
+        print(f"✅ 多空策略完成: 共{len(trades_df)}笔交易, 做多{len(long_trades)}笔")
+        
+    except Exception as e:
+        print(f"❌ 多空策略执行失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return
+    
+    # 4. 应用T+1规则转换
+    print("\n🔄 步骤3: 应用T+1规则转换...")
+    try:
+        t1_trades = simulate_t1_trades_v2(data_with_indicators, long_trades, initial_capital)
+        print(f"✅ T+1转换完成: {len(t1_trades)}笔交易")
+    except Exception as e:
+        print(f"❌ T+1转换失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return
+    
+    # 5. 检查当天交易并决定是否发送邮件
+    print("\n📊 步骤4: 检查当天交易情况...")
+    has_today_trade, today_trades = check_today_trades(t1_trades)
+    
+    # 判断是否应该发送邮件
+    should_send = False
+    send_reason = ""
+    
+    if has_today_trade:
+        should_send = True
+        send_reason = f"当天有{len(today_trades)}笔交易"
+    elif realtime_signal and realtime_signal['triggered']:
+        should_send = True
+        send_reason = "实时信号触发买入"
+    elif pre_close:
+        should_send = True
+        send_reason = "收盘前例行报告(含实时信号)"
+    elif is_post_close_time():
+        should_send = True
+        send_reason = "盘后时间,当天无交易,发送例行报告"
+    else:
+        should_send = False
+        send_reason = "当天无交易,非关键时间点,跳过发送"
+    
+    print(f"\n📧 邮件发送决策: {send_reason}")
+    
+    if should_send:
+        # 6. 生成报告(含实时信号)
+        print("\n📝 步骤5: 生成报告...")
+        html_report, text_report, final_capital = generate_report(t1_trades, initial_capital, realtime_signal)
+        
+        # 7. 发送邮件
+        print("\n📧 步骤6: 发送邮件...")
+        total_trades = len(t1_trades)
+        total_return = (final_capital/initial_capital-1)*100
+        
+        # 邮件主题包含实时信号信息
+        if realtime_signal:
+            sig_emoji = "🟢" if realtime_signal['triggered'] else "⚪"
+            subject = f"{sig_emoji} CYB50-T1报告 {datetime.now().strftime('%m-%d %H:%M')} | 实时{realtime_signal['score']}分 | 收益{total_return:+.2f}%"
+        else:
+            subject = f"🚀 CYB50-T1报告 {datetime.now().strftime('%m-%d %H:%M')} | 收益{total_return:+.2f}% | {total_trades}笔交易"
+        
+        send_email(subject, html_report, text_report)
+    else:
+        print("⏭️  跳过邮件发送")
+    
+    print("\n✅ 全部完成!")
+    print("="*80)
+
+
+if __name__ == "__main__":
+    main()

+ 62 - 0
cat-fly/cron_jobs_realtime.json

@@ -0,0 +1,62 @@
+[
+  {
+    "id": "c5adbeeb-65a4-45e2-81ef-433ce8020b4f",
+    "name": "catfly-morning-930",
+    "schedule": {"expr": "30 9 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(9:30)"}
+  },
+  {
+    "id": "ff236e01-f316-4369-9ca5-55503dae9abe",
+    "name": "catfly-morning-1000",
+    "schedule": {"expr": "0 10 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(10:00)"}
+  },
+  {
+    "id": "a8fb0378-6667-4f61-aab4-214617517be7",
+    "name": "catfly-morning-1030",
+    "schedule": {"expr": "30 10 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(10:30)"}
+  },
+  {
+    "id": "227acd1b-4924-429d-b6bf-8b4ecb2e60c0",
+    "name": "catfly-morning-1100",
+    "schedule": {"expr": "0 11 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(11:00)"}
+  },
+  {
+    "id": "af4963cf-fd52-4e81-83e2-fa49f3b4b69e",
+    "name": "catfly-morning-1130",
+    "schedule": {"expr": "30 11 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(11:30)"}
+  },
+  {
+    "id": "f6346777-2be0-4960-98b3-2cb924241a93",
+    "name": "catfly-afternoon-1300",
+    "schedule": {"expr": "0 13 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(13:00)"}
+  },
+  {
+    "id": "4b119356-fb84-436d-9371-9050b88ebeee",
+    "name": "catfly-afternoon-1330",
+    "schedule": {"expr": "30 13 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(13:30)"}
+  },
+  {
+    "id": "fecfb54b-62b7-489d-bfbf-5c074a0b3278",
+    "name": "catfly-afternoon-1400",
+    "schedule": {"expr": "0 14 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(14:00)"}
+  },
+  {
+    "id": "de969866-e2c5-4110-ad43-6f95b577f150",
+    "name": "catfly-afternoon-1430",
+    "schedule": {"expr": "30 14 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(14:30)"}
+  },
+  {
+    "id": "f7925c43-23ae-459a-849c-898787a6e0a7",
+    "name": "catfly-afternoon-1450-realtime",
+    "schedule": {"expr": "50 14 * * 1-5", "tz": "Asia/Shanghai"},
+    "payload": {"text": "执行创业板50自动交易报告系统(14:50收盘-实时信号版)", "script": "auto_report_realtime.py"}
+  }
+]

+ 179 - 0
cat-fly/realtime_signal.py

@@ -0,0 +1,179 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+创业板50实时信号计算器
+在任意时刻获取实时行情,计算当前信号状态
+"""
+
+import pandas as pd
+import numpy as np
+import akshare as ak
+import sys
+import warnings
+warnings.filterwarnings('ignore')
+
+sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
+from cyb50_30min_dual_direction import ConfigManager, IntradayDataFetcher, DualDirectionSignalGenerator
+from datetime import datetime, timedelta
+
+
+def get_realtime_price():
+    """获取创业板50实时价格"""
+    try:
+        # 使用新浪接口获取实时行情(更稳定)
+        df = ak.stock_zh_index_spot_sina()
+        cyb50 = df[df['代码'] == 'sz399673']
+        if len(cyb50) > 0:
+            return {
+                'price': float(cyb50.iloc[0]['最新价']),
+                'open': float(cyb50.iloc[0]['今开']),
+                'high': float(cyb50.iloc[0]['最高']),
+                'low': float(cyb50.iloc[0]['最低']),
+                'volume': float(cyb50.iloc[0]['成交量']),
+                'time': datetime.now().strftime('%H:%M:%S')
+            }
+    except Exception as e:
+        print(f"获取实时行情失败: {e}")
+    return None
+
+
+def calculate_signal_at_time(current_price, last_kline_data):
+    """
+    基于最新价格和最后一根K线数据,估算当前信号状态
+    
+    参数:
+        current_price: 当前实时价格
+        last_kline_data: 最后一根完整K线的技术指标数据 (Series)
+    """
+    score = 0
+    signals = []
+    
+    # 1. RSI估算 - 假设价格变化与RSI变化大致线性
+    last_close = last_kline_data['Close']
+    price_change_pct = (current_price - last_close) / last_close
+    
+    # 简化估算:价格每跌1%,RSI约跌2-3个点
+    estimated_rsi = last_kline_data['RSI'] + price_change_pct * 250
+    
+    if estimated_rsi < 30:
+        score += 2
+        signals.append(f"RSI超卖(估{estimated_rsi:.1f})")
+    elif estimated_rsi < 35:
+        score += 1
+        signals.append(f"RSI偏弱(估{estimated_rsi:.1f})")
+    
+    # 2. KDJ估算 - 价格影响J值较大
+    estimated_j = last_kline_data['J'] + price_change_pct * 300
+    
+    if estimated_j < 0:
+        score += 1
+        signals.append(f"KDJ极端超卖(估J={estimated_j:.1f})")
+    
+    # 3. 布林带位置判断
+    bb_lower = last_kline_data['BB_lower']
+    bb_upper = last_kline_data['BB_upper']
+    
+    if current_price <= bb_lower * 1.005:
+        score += 2
+        signals.append("触及下轨")
+    elif current_price <= bb_lower * 1.02:
+        score += 1
+        signals.append("接近下轨")
+    
+    # 4. 价格动量 - 使用实时价格与开盘/昨日收盘比较
+    if price_change_pct < -0.015:
+        score += 1
+        signals.append(f"动量超卖({price_change_pct*100:.2f}%)")
+    
+    # 5. MA趋势判断(基于已有数据)
+    if last_kline_data['MA6'] > last_kline_data['MA12']:
+        score += 1
+        signals.append("MA短期上行")
+    else:
+        score -= 1
+        signals.append("MA下降趋势惩罚")
+    
+    return {
+        'score': score,
+        'signals': signals,
+        'estimated_rsi': estimated_rsi,
+        'estimated_j': estimated_j,
+        'price_change_pct': price_change_pct,
+        'bb_lower': bb_lower,
+        'bb_upper': bb_upper,
+        'current_price': current_price,
+        'last_close': last_close
+    }
+
+
+def generate_realtime_report():
+    """生成实时信号报告"""
+    print("="*80)
+    print("🚀 创业板50实时信号检测")
+    print(f"检测时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+    print("="*80)
+    
+    # 1. 获取历史30分钟K线数据(用于计算技术指标)
+    print("\n📊 获取历史数据...")
+    config_manager = ConfigManager('config.json')
+    fetcher = IntradayDataFetcher(config_manager)
+    
+    end_date = datetime.now()
+    start_date = end_date - timedelta(days=30)  # 取30天足够计算指标
+    raw_data = fetcher.fetch_30min_data(start_date, end_date)
+    
+    if raw_data is None or len(raw_data) == 0:
+        print("❌ 数据获取失败")
+        return None
+    
+    # 2. 计算技术指标
+    data_with_indicators = fetcher.calculate_intraday_indicators(raw_data)
+    last_kline = data_with_indicators.iloc[-1]
+    
+    print(f"   最后一根K线: {data_with_indicators.index[-1]}")
+    print(f"   收盘价: {last_kline['Close']:.2f}")
+    
+    # 3. 获取实时价格
+    print("\n📈 获取实时行情...")
+    realtime = get_realtime_price()
+    
+    if realtime is None:
+        print("❌ 获取实时价格失败")
+        return None
+    
+    print(f"   当前时间: {realtime['time']}")
+    print(f"   当前价格: {realtime['price']:.2f}")
+    print(f"   今日最高: {realtime['high']:.2f}")
+    print(f"   今日最低: {realtime['low']:.2f}")
+    
+    # 4. 计算当前信号
+    print("\n🎯 计算实时信号...")
+    signal_result = calculate_signal_at_time(realtime['price'], last_kline)
+    
+    print(f"\n{'='*80}")
+    print("实时信号评估结果")
+    print(f"{'='*80}")
+    print(f"当前价格: {signal_result['current_price']:.2f}")
+    print(f"最后一根K线收盘: {signal_result['last_close']:.2f}")
+    print(f"价格变化: {signal_result['price_change_pct']*100:+.2f}%")
+    print(f"\n技术指标估算:")
+    print(f"  RSI(估算): {signal_result['estimated_rsi']:.2f}")
+    print(f"  KDJ J(估算): {signal_result['estimated_j']:.2f}")
+    print(f"  布林带: [{signal_result['bb_lower']:.2f}, {signal_result['bb_upper']:.2f}]")
+    print(f"\n信号评分: {signal_result['score']}/4 (阈值: 4分触发买入)")
+    print(f"触发信号: {', '.join(signal_result['signals']) if signal_result['signals'] else '无'}")
+    
+    if signal_result['score'] >= 4:
+        print(f"\n🟢 建议: 触发买入信号!")
+    elif signal_result['score'] >= 3:
+        print(f"\n🟡 建议: 接近触发,建议关注")
+    else:
+        print(f"\n⚪ 建议: 未触发买入信号")
+    
+    print(f"{'='*80}")
+    
+    return signal_result
+
+
+if __name__ == "__main__":
+    generate_realtime_report()