12 Commits f6ad8138bd ... d74844002b

Tác giả SHA1 Thông báo Ngày
  openclaw d74844002b Merge branch 'origin/master' into master 2 tháng trước cách đây
  openclaw b8329c4b66 chore: 添加 .gitignore,提交工作区配置和技能文件 2 tháng trước cách đây
  openclaw 973bb9b2a0 feat: 添加实时信号检测功能 2 tháng trước cách đây
  openclaw befc751f13 Fix T+1 exit reason generation based on actual exit price 2 tháng trước cách đây
  openclaw b0c12b073d Update report to show '近3个月' instead of '近2个月' in data range 2 tháng trước cách đây
  openclaw 329df887bf Extend backtest period from 70 to 90 days (~3 months) 2 tháng trước cách đây
  openclaw 09bdb2d897 Fix T+1 trade overlap issue - complete rewrite 2 tháng trước cách đây
  openclaw fe369c98e2 Increase trade history in reports from 10/20 to 30 entries 2 tháng trước cách đây
  openclaw c8370ff937 Fix exit reason to show actual profit/loss percentage 2 tháng trước cách đây
  openclaw 089ee6382b Remove commission fees from catfly calculations 2 tháng trước cách đây
  openclaw a86688a826 Fix catfly email report calculation accuracy 2 tháng trước cách đây
  openclaw 53e700a3c8 Update catfly email recipients 2 tháng trước cách đây

+ 41 - 5
.gitignore

@@ -1,11 +1,47 @@
-# Ignore all CSV files
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# IDE
+.kimi/
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Data files
 *.csv
+*.png
+*.pdf
+*.tar.gz
+*.ris
+
+# Logs
+*.log
 
-# Ignore NUL files (Windows reserved device name)
+# Local config
+.openclaw/
+
+# Windows reserved device name
 NUL
 nul
 research/NUL
 research/nul
-
-# Ignore all log files
-*.log

+ 5 - 0
HEARTBEAT.md

@@ -0,0 +1,5 @@
+# HEARTBEAT.md
+
+# Keep this file empty (or with only comments) to skip heartbeat API calls.
+
+# Add tasks below when you want the agent to check something periodically.

+ 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下降趋势惩罚
+
+⚪ 建议: 未触发买入信号
+================================================================================
+```

+ 3 - 3
cat-fly/auto_report.py

@@ -30,7 +30,7 @@ EMAIL_CONFIG = {
     "smtp_server": "localhost",
     "smtp_port": 25,
     "sender_email": "catfly@erwin.wang",
-    "receiver_emails": ["380880504@qq.com"]
+    "receiver_emails": ["380880504@qq.com", "695047456@qq.com"]
 }
 
 def send_email(subject, html_content, text_content=""):
@@ -176,13 +176,13 @@ def generate_report(trades_df, results_df, initial_capital=1000000):
         </tr>
     </table>
     
-    <h2>📝 最近10笔交易明细</h2>
+    <h2>📝 最近30笔交易明细</h2>
     <table>
         <tr><th>方向</th><th>开仓时间</th><th>平仓时间</th><th>开仓价</th><th>平仓价</th>
             <th>盈亏</th><th>退出原因</th></tr>
     """
     
-    for _, trade in trades_df.tail(10).iterrows():
+    for _, trade in trades_df.tail(30).iterrows():
         pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
         html += f"""
         <tr>

+ 12 - 11
cat-fly/auto_report_long_only_t1.py

@@ -32,14 +32,14 @@ from cyb50_30min_dual_direction import (
     ConfigManager, IntradayDataFetcher, 
     DualDirectionSignalGenerator, DualDirectionExecutor
 )
-from t1_converter import simulate_t1_trades, compare_results
+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"]
+    "receiver_emails": ["380880504@qq.com", "1095512042@qq.com"]
 }
 
 def send_email(subject, html_content, text_content=""):
@@ -126,7 +126,7 @@ def generate_report(trades_df, initial_capital=1000000):
         </style></head><body>
         <h1>🚀 创业板50交易报告 (T+1)</h1>
         <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
-        <p>数据区间: 近2个月</p>
+        <p>数据区间: 近3个月</p>
         <p><b>近2个月无交易信号触发</b></p>
         <p>初始资金: {initial_capital:,.0f}元</p>
         <p>最终资金: {final_capital:,.0f}元</p>
@@ -136,8 +136,9 @@ def generate_report(trades_df, initial_capital=1000000):
         text = f"近2个月无交易信号\n初始资金: {initial_capital:,.0f}元\n最终资金: {final_capital:,.0f}元"
         return html, text, final_capital
     
-    # 计算统计数据
-    final_capital = trades_df['平仓时资金'].iloc[-1]
+    # 计算统计数据 - 使用实际盈亏总和计算最终资金(更准确)
+    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)
     
@@ -177,7 +178,7 @@ def generate_report(trades_df, initial_capital=1000000):
     </style></head><body>
     <h1>🚀 创业板50交易报告 (T+1)</h1>
     <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
-    <p>数据区间: 近2个月</p>
+    <p>数据区间: 近3个月</p>
     <p><b>数据来源: 多空双向策略的做多交易 + T+1规则转换</b></p>
     
     <h2>📊 总体绩效</h2>
@@ -231,13 +232,13 @@ def generate_report(trades_df, initial_capital=1000000):
     html += """
     </table>
     
-    <h2>📝 最近20笔交易明细</h2>
+    <h2>📝 最近30笔交易明细</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(20).iterrows():
+    for _, trade in trades_df.tail(30).iterrows():
         pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
         t1_flag = "✓" if trade['T+1调整'] == '是(T0→T1)' else ""
         html += f"""
@@ -290,12 +291,12 @@ def main():
     initial_capital = 1000000
     
     # 1. 获取数据
-    print("\n📊 步骤1: 获取近2个月数据...")
+    print("\n📊 步骤1: 获取近3个月数据...")
     try:
         config_manager = ConfigManager('config.json')
         fetcher = IntradayDataFetcher(config_manager)
         end_date = datetime.now()
-        start_date = end_date - timedelta(days=70)  # 2个月+10天缓冲
+        start_date = end_date - timedelta(days=90)  # 近3个月
         raw_data = fetcher.fetch_30min_data(start_date, end_date)
         
         if raw_data is None or len(raw_data) == 0:
@@ -339,7 +340,7 @@ def main():
     # 3. 应用T+1规则转换
     print("\n🔄 步骤3: 应用T+1规则转换...")
     try:
-        t1_trades = simulate_t1_trades(data_with_indicators, long_trades, initial_capital)
+        t1_trades = simulate_t1_trades_v2(data_with_indicators, long_trades, initial_capital)
         print(f"✅ T+1转换完成: {len(t1_trades)}笔交易")
         
         # 统计T+1调整

+ 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"}
+  }
+]

+ 17 - 9
cat-fly/cyb50_30min_dual_direction.py

@@ -736,7 +736,7 @@ class DualDirectionExecutor:
     def __init__(self, initial_capital=1000000):
         self.initial_capital = initial_capital
         self.params = {
-            'commission_rate': 0.0001,   # 万分之一
+            'commission_rate': 0.0,   # 无手续费
             'slippage_rate': 0.0,        # 无滑点
             'position_size_pct': 1.0,    # 每次开仓100%仓位
             'stop_loss_pct': 0.008,      # 0.8%止损
@@ -882,15 +882,15 @@ class DualDirectionExecutor:
                 # 止损
                 if price <= stop_loss:
                     exit_signal = True
-                    loss_pct = (long_entry_price - stop_loss) / long_entry_price * 100
-                    exit_reason = f"做多止损触发(价格{price:.2f}跌破止损线{stop_loss:.2f},亏损{loss_pct:.2f}%)"
+                    actual_loss_pct = (long_entry_price - price) / long_entry_price * 100
+                    exit_reason = f"做多止损触发(价格{price:.2f}跌破止损线{stop_loss:.2f},实际亏损{actual_loss_pct:.2f}%)"
                     exit_price = price  # 使用实际市场价格,而不是止损线价格
 
                 # 止盈
                 elif price >= take_profit:
                     exit_signal = True
-                    profit_pct = (price - long_entry_price) / long_entry_price * 100
-                    exit_reason = f"做多止盈触发(价格{price:.2f}突破止盈线{take_profit:.2f},盈利{profit_pct:.2f}%)"
+                    actual_profit_pct = (price - long_entry_price) / long_entry_price * 100
+                    exit_reason = f"做多止盈触发(价格{price:.2f}突破止盈线{take_profit:.2f},实际盈利{actual_profit_pct:.2f}%)"
                     exit_price = price  # 使用实际市场价格,而不是止盈线价格
                 
                 # 最大持仓时间
@@ -978,15 +978,15 @@ class DualDirectionExecutor:
                 # 止损(价格上涨)
                 if price >= stop_loss_price:
                     exit_signal = True
-                    loss_pct = (stop_loss_price - short_entry_price) / short_entry_price * 100
-                    exit_reason = f"做空止损触发(价格{price:.2f}突破止损线{stop_loss_price:.2f},亏损{loss_pct:.2f}%)"
+                    actual_loss_pct = (price - short_entry_price) / short_entry_price * 100
+                    exit_reason = f"做空止损触发(价格{price:.2f}突破止损线{stop_loss_price:.2f},实际亏损{actual_loss_pct:.2f}%)"
                     exit_price = price  # 使用实际市场价格,而不是止损线价格
 
                 # 止盈(价格下跌)
                 elif price <= take_profit_price:
                     exit_signal = True
-                    profit_pct = (short_entry_price - price) / short_entry_price * 100
-                    exit_reason = f"做空止盈触发(价格{price:.2f}跌破止盈线{take_profit_price:.2f},盈利{profit_pct:.2f}%)"
+                    actual_profit_pct = (short_entry_price - price) / short_entry_price * 100
+                    exit_reason = f"做空止盈触发(价格{price:.2f}跌破止盈线{take_profit_price:.2f},实际盈利{actual_profit_pct:.2f}%)"
                     exit_price = price  # 使用实际市场价格,而不是止盈线价格
                 
                 # 最大持仓时间
@@ -1109,6 +1109,10 @@ class DualDirectionExecutor:
             print(f"退出原因: 做多强制平仓(回测结束)")
             print(f"盈亏金额: {pnl:+,.2f} 元")
             print(f"{'='*60}")
+            
+            # 更新最终资金到results_df
+            df.iloc[-1, df.columns.get_loc('capital')] = capital
+            df.iloc[-1, df.columns.get_loc('net_value')] = capital
         
         # 做空持仓强制平仓
         if short_position < 0:
@@ -1150,6 +1154,10 @@ class DualDirectionExecutor:
             print(f"退出原因: 做空强制平仓(回测结束)")
             print(f"盈亏金额: {net_pnl:+,.2f} 元")
             print(f"{'='*60}")
+            
+            # 更新最终资金到results_df
+            df.iloc[-1, df.columns.get_loc('capital')] = capital
+            df.iloc[-1, df.columns.get_loc('net_value')] = capital
         
         trades_df = pd.DataFrame(trades)
         

+ 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()

+ 233 - 182
cat-fly/t1_converter.py

@@ -1,12 +1,13 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 """
-CYB50 T+1 转换器 - 基于多空版本的做多交易转换为T+1规则
-规则:
-1. 提取多空版本中的所有做多交易
-2. 买入当天不能卖出(T+1限制)
-3. 如果原交易是T0(当天买卖),则延期到T+1开盘卖出
-4. 重新计算延期后的盈亏(基于实际价格变化)
+CYB50 T+1 回测引擎 - 正确的资金管理版本 V2
+
+核心逻辑:
+1. 按时间顺序处理所有信号
+2. 维护持仓状态(是否持仓、持仓期间)
+3. 当新信号的开仓时间落在已有持仓期间时,跳过该信号
+4. 正确处理T+1延期平仓
 """
 
 import pandas as pd
@@ -21,193 +22,232 @@ from cyb50_30min_dual_direction import (
 )
 
 
-def get_next_trading_session_open(data_df, current_time):
-    """获取下一个交易日的第一个开盘时间(注意:不是当天,是下一天)"""
-    current_date = current_time.date()
-    
-    # 查找当前日期之后的所有数据
-    future_data = data_df[data_df.index > current_time]
-    
-    if future_data.empty:
-        return None, None
-    
-    # 获取所有日期,找到第一个不同于current_date的日期
-    future_dates = future_data.index.date
-    next_date = None
-    
-    for d in future_dates:
-        if d != current_date:
-            next_date = d
-            break
-    
-    if next_date is None:
-        return None, None
-    
-    # 获取下一个交易日的所有数据
-    next_day_data = data_df[data_df.index.date == next_date]
-    
-    if next_day_data.empty:
-        return None, None
+def simulate_t1_trades_v2(data_df, long_trades_df, initial_capital=1000000):
+    """
+    正确的T+1回测 V2 - 正确处理时间重叠
     
-    open_time = next_day_data.index[0]
-    open_price = next_day_data.iloc[0]['Open']  # 使用开盘价
+    参数:
+        data_df: 包含所有价格数据的DataFrame
+        long_trades_df: 原始做多交易列表(作为信号源)
+        initial_capital: 初始资金
     
-    return open_time, open_price
-
-
-def simulate_t1_trades(data_df, long_trades_df, initial_capital=1000000):
-    """模拟T+1规则下的交易
-    
-    规则:
-    - 买入当天不能卖出
-    - 如果原T0交易(当天买卖),延期到T+1开盘卖出
-    - 卖出后当天可以再买(这是关键特性)
+    返回:
+        t1_trades_df: T+1规则下实际执行的交易
     """
     print("\n" + "="*80)
-    print("T+1规则转换 - 基于多空版本做多交易")
+    print("T+1回测引擎 V2 - 正确处理时间重叠")
     print("="*80)
     
     if len(long_trades_df) == 0:
         print("没有做多交易记录")
         return pd.DataFrame()
     
+    # 准备信号列表
+    signals = []
+    for idx, trade in long_trades_df.iterrows():
+        signals.append({
+            'signal_id': idx + 1,
+            'entry_time': trade['开仓时间'],
+            'entry_price': trade['开仓价格'],
+            'exit_time': trade['平仓时间'],
+            'exit_price': trade['平仓价格'],
+            'exit_reason': trade['退出原因'],
+            'entry_signals': trade.get('入场信号', '')
+        })
+    
     # 按开仓时间排序
-    long_trades_df = long_trades_df.sort_values('开仓时间').reset_index(drop=True)
+    signals = sorted(signals, key=lambda x: x['entry_time'])
     
     t1_trades = []
     capital = initial_capital
     
-    for idx, trade in long_trades_df.iterrows():
-        entry_time = trade['开仓时间']
-        entry_price = trade['开仓价格']
-        original_exit_time = trade['平仓时间']
-        original_exit_price = trade['平仓价格']
-        position_size = int(trade['仓位'])
-        entry_signals = trade.get('入场信号', '')
+    # 当前持仓状态
+    current_position = None  # None 或 {'entry_time', 'entry_price', 'can_sell_time', ...}
+    
+    print(f"\n初始资金: {initial_capital:,.0f}元")
+    print(f"总信号数: {len(signals)}笔")
+    print("\n" + "-"*80)
+    
+    for signal in signals:
+        sig_entry = signal['entry_time']
+        sig_exit = signal['exit_time']
         
-        entry_date = entry_time.date()
-        exit_date = original_exit_time.date()
+        # 检查是否有持仓
+        if current_position is not None:
+            # 检查新信号是否在持仓期间
+            hold_end = current_position['actual_exit_time']
+            
+            if sig_entry < hold_end:
+                # 新信号在持仓期间,跳过
+                print(f"\n[跳过] 信号 #{signal['signal_id']}: {sig_entry.strftime('%m-%d %H:%M')}")
+                print(f"  原因: 当前持仓中 (持仓期: {current_position['entry_time'].strftime('%m-%d %H:%M')} → {hold_end.strftime('%m-%d %H:%M')})")
+                continue
+            else:
+                # 新信号在持仓结束后,先结算当前持仓
+                print(f"\n[结算前持仓] 信号 #{signal['signal_id']} 到来时,前持仓已结束")
+                # 持仓已经结束,可以开新仓
+                current_position = None
         
-        # 判断是否是T0交易
-        is_t0 = (entry_date == exit_date)
+        # 现在可以开仓
+        entry_time = sig_entry
+        entry_price = signal['entry_price']
         
-        if is_t0:
-            # T0交易需要延期到T+1开盘
-            new_exit_time, new_exit_price = get_next_trading_session_open(data_df, entry_time)
-            
-            if new_exit_time is None:
-                print(f"⚠️  交易 #{idx+1}: 无法找到T+1开盘时间,使用原平仓价格")
-                new_exit_time = original_exit_time
-                new_exit_price = original_exit_price
+        # 计算可卖时间(T+1规则)
+        can_sell_time = entry_time + timedelta(days=1)
+        can_sell_time = can_sell_time.replace(hour=9, minute=30)
+        
+        # 确定实际平仓时间
+        # 原始平仓时间 vs T+1最早可卖时间,取较晚者
+        actual_exit_time = max(sig_exit, can_sell_time)
+        
+        # 查找实际平仓价格
+        if actual_exit_time > sig_exit:
+            # 需要延期,查找数据
+            future_data = data_df[data_df.index >= actual_exit_time]
+            if len(future_data) > 0:
+                actual_exit_time = future_data.index[0]
+                actual_exit_price = future_data.iloc[0]['Open']
+                t1_adjusted = True
+            else:
+                # 无后续数据,使用原始
+                actual_exit_time = sig_exit
+                actual_exit_price = signal['exit_price']
                 t1_adjusted = False
+        else:
+            actual_exit_price = signal['exit_price']
+            t1_adjusted = False
+        
+        # 计算仓位(使用全部资金)
+        position_size = int(capital / entry_price)
+        if position_size <= 0:
+            print(f"\n[跳过] 信号 #{signal['signal_id']}: {entry_time.strftime('%m-%d %H:%M')} - 资金不足")
+            continue
+        
+        # 记录持仓
+        current_position = {
+            'entry_time': entry_time,
+            'entry_price': entry_price,
+            'position_size': position_size,
+            'actual_exit_time': actual_exit_time,
+            'actual_exit_price': actual_exit_price,
+            't1_adjusted': t1_adjusted,
+            'original_exit_time': sig_exit,
+            'original_exit_price': signal['exit_price'],
+            'exit_reason': signal['exit_reason'],
+            'entry_signals': signal['entry_signals']
+        }
+        
+        # 计算盈亏
+        gross_pnl = (actual_exit_price - entry_price) * position_size
+        pnl_pct = (actual_exit_price - entry_price) / entry_price * 100
+        
+        # 更新资金
+        capital += gross_pnl
+        
+        # 记录交易
+        # 计算原始盈亏(假设按原始时间平仓)
+        if t1_adjusted:
+            original_pnl = (signal['exit_price'] - entry_price) * position_size
+            pnl_change = gross_pnl - original_pnl
+            # 为T+1延期交易生成新的退出原因(基于实际平仓价格)
+            stop_loss = entry_price * 0.992
+            take_profit = entry_price * 1.02
+            if actual_exit_price <= stop_loss:
+                exit_reason = f"T+1止损(价格{actual_exit_price:.2f}触及止损线{stop_loss:.2f},实际亏损{abs(pnl_pct):.2f}%)"
+            elif actual_exit_price >= take_profit:
+                exit_reason = f"T+1止盈(价格{actual_exit_price:.2f}触及止盈线{take_profit:.2f},实际盈利{pnl_pct:.2f}%)"
             else:
-                # 计算新的盈亏
-                # 假设使用开盘价卖出
-                original_pnl = trade['盈亏金额']
-                
-                # 计算手续费(万分之一)
-                commission_rate = 0.0001
-                open_cost = position_size * entry_price * commission_rate
-                close_cost = position_size * new_exit_price * commission_rate
-                
-                # 新的盈亏
-                gross_pnl = (new_exit_price - entry_price) * position_size
-                new_pnl = gross_pnl - open_cost - close_cost
-                new_pnl_pct = (new_exit_price - entry_price) / entry_price * 100
-                
-                # 判断新的退出原因
-                stop_loss = entry_price * 0.992  # 0.8%止损
-                take_profit = entry_price * 1.02  # 2%止盈
-                
-                if new_exit_price <= stop_loss:
-                    exit_reason = f"T+1延期止损(价格{new_exit_price:.2f}触及止损线{stop_loss:.2f},亏损{abs(new_pnl_pct):.2f}%)"
-                elif new_exit_price >= take_profit:
-                    exit_reason = f"T+1延期止盈(价格{new_exit_price:.2f}触及止盈线{take_profit:.2f},盈利{new_pnl_pct:.2f}%)"
-                else:
-                    exit_reason = f"T+1延期平仓(价格{new_exit_price:.2f},盈亏{new_pnl_pct:+.2f}%)"
-                
-                # 计算持仓时长(小时)
-                hold_hours = (new_exit_time - entry_time).total_seconds() / 3600
-                
-                print(f"\n[T0→T1调整] 交易 #{idx+1}")
-                print(f"  原交易: {entry_time.strftime('%m-%d %H:%M')} 买 → {original_exit_time.strftime('%m-%d %H:%M')} 卖")
-                print(f"  新交易: {entry_time.strftime('%m-%d %H:%M')} 买 → {new_exit_time.strftime('%m-%d %H:%M')} 卖")
-                print(f"  原盈亏: {original_pnl:+.2f}元")
-                print(f"  新盈亏: {new_pnl:+.2f}元 (基于T+1开盘{new_exit_price:.2f})")
-                print(f"  盈亏变化: {(new_pnl - original_pnl):+.2f}元")
-                
-                t1_adjusted = True
-                
-                # 更新交易记录
-                trade_record = {
-                    '交易方向': '做多',
-                    '开仓时间': entry_time,
-                    '平仓时间': new_exit_time,
-                    '开仓价格': entry_price,
-                    '平仓价格': new_exit_price,
-                    '仓位': position_size,
-                    '盈亏金额': new_pnl,
-                    '盈亏百分比': new_pnl_pct,
-                    '退出原因': exit_reason,
-                    '持仓周期数': int(hold_hours * 2),  # 30分钟周期数
-                    '持仓小时数': hold_hours,
-                    'T+1调整': '是(T0→T1)',
-                    '原平仓时间': original_exit_time,
-                    '原平仓价格': original_exit_price,
-                    '原盈亏': original_pnl,
-                    '盈亏变化': new_pnl - original_pnl,
-                    '入场信号': entry_signals,
-                    '开仓市值': position_size * entry_price,
-                }
-                
-                capital += new_pnl
-                trade_record['平仓时资金'] = capital
-                t1_trades.append(trade_record)
-                continue
+                exit_reason = f"T+1平仓(价格{actual_exit_price:.2f},盈亏{pnl_pct:+.2f}%)"
+        else:
+            original_pnl = gross_pnl
+            pnl_change = 0
+            exit_reason = signal['exit_reason']
+        
+        trade_record = {
+            '交易方向': '做多',
+            '开仓时间': entry_time,
+            '平仓时间': actual_exit_time,
+            '开仓价格': entry_price,
+            '平仓价格': actual_exit_price,
+            '仓位': position_size,
+            '盈亏金额': gross_pnl,
+            '盈亏百分比': pnl_pct,
+            '退出原因': exit_reason,
+            '持仓周期数': int((actual_exit_time - entry_time).total_seconds() / 1800),
+            '持仓小时数': (actual_exit_time - entry_time).total_seconds() / 3600,
+            'T+1调整': '是(T0→T1)' if t1_adjusted else '否',
+            '原平仓时间': sig_exit,
+            '原平仓价格': signal['exit_price'],
+            '原盈亏': original_pnl,
+            '盈亏变化': pnl_change,
+            '入场信号': signal['entry_signals'],
+            '平仓时资金': capital,
+        }
+        t1_trades.append(trade_record)
         
-        # 非T0交易,保持原样
-        hold_hours = trade['持仓小时数']
+        # 打印
+        status = "✅盈利" if gross_pnl > 0 else "❌亏损"
+        adj_str = "[T+1调整] " if t1_adjusted else ""
+        print(f"\n[执行] 信号 #{signal['signal_id']}: {entry_time.strftime('%m-%d %H:%M')} → {actual_exit_time.strftime('%m-%d %H:%M')}")
+        print(f"  {adj_str}价格: {entry_price:.2f} → {actual_exit_price:.2f}")
+        print(f"  盈亏: {gross_pnl:+,.0f}元 ({pnl_pct:+.2f}%) {status}")
+        print(f"  资金: {capital:,.0f}元")
+    
+    # 处理最后一笔持仓(如果数据结束前未平仓)
+    if current_position is not None and current_position['actual_exit_time'] > data_df.index[-1]:
+        final_price = data_df.iloc[-1]['Close']
+        final_time = data_df.index[-1]
         
-        # 计算盈亏(使用原始数据)
-        commission_rate = 0.0001
-        gross_pnl = (original_exit_price - entry_price) * position_size
-        open_cost = position_size * entry_price * commission_rate
-        close_cost = position_size * original_exit_price * commission_rate
-        pnl = gross_pnl - open_cost - close_cost
-        pnl_pct = (original_exit_price - entry_price) / entry_price * 100
+        entry_time = current_position['entry_time']
+        entry_price = current_position['entry_price']
+        position_size = current_position['position_size']
         
-        capital += pnl
+        gross_pnl = (final_price - entry_price) * position_size
+        pnl_pct = (final_price - entry_price) / entry_price * 100
+        capital += gross_pnl
         
         trade_record = {
             '交易方向': '做多',
             '开仓时间': entry_time,
-            '平仓时间': original_exit_time,
+            '平仓时间': final_time,
             '开仓价格': entry_price,
-            '平仓价格': original_exit_price,
+            '平仓价格': final_price,
             '仓位': position_size,
-            '盈亏金额': pnl,
+            '盈亏金额': gross_pnl,
             '盈亏百分比': pnl_pct,
-            '退出原因': trade['退出原因'],
-            '持仓周期数': trade['持仓周期数'],
-            '持仓小时数': hold_hours,
+            '退出原因': '回测强制平仓',
+            '持仓周期数': int((final_time - entry_time).total_seconds() / 1800),
+            '持仓小时数': (final_time - entry_time).total_seconds() / 3600,
             'T+1调整': '否',
-            '原平仓时间': original_exit_time,
-            '原平仓价格': original_exit_price,
-            '原盈亏': trade['盈亏金额'],
+            '原平仓时间': final_time,
+            '原平仓价格': final_price,
+            '原盈亏': gross_pnl,
             '盈亏变化': 0,
-            '入场信号': entry_signals,
-            '开仓市值': position_size * entry_price,
+            '入场信号': current_position['entry_signals'],
             '平仓时资金': capital,
         }
         t1_trades.append(trade_record)
+        
+        print(f"\n[强制平仓] {final_time.strftime('%m-%d %H:%M')} @ {final_price:.2f}")
+        print(f"  盈亏: {gross_pnl:+,.0f}元")
     
+    # 生成结果
     t1_trades_df = pd.DataFrame(t1_trades)
     
+    print("\n" + "="*80)
+    print("T+1回测 V2 完成")
+    print("="*80)
+    print(f"原始信号数: {len(signals)}笔")
+    print(f"实际执行: {len(t1_trades)}笔")
+    print(f"跳过信号: {len(signals) - len(t1_trades)}笔")
+    print(f"最终资金: {capital:,.0f}元")
+    print(f"总收益率: {(capital/initial_capital-1)*100:+.2f}%")
+    
     return t1_trades_df
 
 
 def compare_results(original_trades, t1_trades, initial_capital=1000000):
-    """对比原始交易和T+1转换后的结果"""
+    """对比原始交易和T+1交易"""
     print("\n" + "="*80)
     print("T+1转换前后对比")
     print("="*80)
@@ -219,13 +259,13 @@ def compare_results(original_trades, t1_trades, initial_capital=1000000):
     original_win_rate = (original_trades['盈亏金额'] > 0).sum() / len(original_trades) * 100
     
     # T+1统计
-    t1_total_pnl = t1_trades['盈亏金额'].sum()
+    t1_total_pnl = t1_trades['盈亏金额'].sum() if len(t1_trades) > 0 else 0
     t1_final = initial_capital + t1_total_pnl
     t1_return = (t1_final / initial_capital - 1) * 100
-    t1_win_rate = (t1_trades['盈亏金额'] > 0).sum() / len(t1_trades) * 100
+    t1_win_rate = (t1_trades['盈亏金额'] > 0).sum() / len(t1_trades) * 100 if len(t1_trades) > 0 else 0
     
     # T0交易统计
-    t0_adjusted = t1_trades[t1_trades['T+1调整'] == '是(T0→T1)']
+    t0_adjusted = t1_trades[t1_trades['T+1调整'] == '是(T0→T1)'] if len(t1_trades) > 0 else pd.DataFrame()
     
     print(f"\n【原始交易(T0规则)】")
     print(f"  交易次数: {len(original_trades)}")
@@ -246,11 +286,6 @@ def compare_results(original_trades, t1_trades, initial_capital=1000000):
     if len(t0_adjusted) > 0:
         print(f"  调整后盈亏变化: {t0_adjusted['盈亏变化'].sum():+,.2f}元")
         print(f"  平均每笔变化: {t0_adjusted['盈亏变化'].mean():+,.2f}元")
-        print(f"  调整交易明细:")
-        for _, row in t0_adjusted.iterrows():
-            print(f"    {row['开仓时间'].strftime('%m-%d %H:%M')} - "
-                  f"原盈亏{row['原盈亏']:+.0f} → 新盈亏{row['盈亏金额']:+.0f} "
-                  f"({row['盈亏变化']:+.0f})")
     
     print(f"\n【收益差异】")
     print(f"  收益率变化: {(t1_return - original_return):+.2f}%")
@@ -258,16 +293,15 @@ def compare_results(original_trades, t1_trades, initial_capital=1000000):
 
 
 def main():
-    """主程序"""
+    """主程序 - 测试V2版本"""
     print("="*80)
-    print("CYB50 T+1 交易转换器")
-    print("基于多空版本的做多交易,应用T+1规则")
+    print("CYB50 T+1 回测引擎 V2")
     print("="*80)
     
     initial_capital = 1000000
     
-    # 1. 运行多空版本获取原始交易数据
-    print("\n【步骤1】运行多空版本获取原始交易...")
+    # 获取数据和原始交易
+    print("\n【步骤1】获取数据...")
     config_manager = ConfigManager('config.json')
     fetcher = IntradayDataFetcher(config_manager)
     
@@ -283,35 +317,52 @@ def main():
     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(long_trades)} 笔做多交易")
+    print(f"✅ 原始做多信号: {len(long_trades)}笔")
     
-    # 2. 应用T+1规则
-    print("\n【步骤2】应用T+1规则转换...")
-    t1_trades = simulate_t1_trades(data_with_indicators, long_trades, initial_capital)
+    # 运行V2回测
+    print("\n【步骤2】运行T+1回测 V2...")
+    t1_trades = simulate_t1_trades_v2(data_with_indicators, long_trades, initial_capital)
     
-    # 3. 对比结果
-    print("\n【步骤3】对比分析...")
-    compare_results(long_trades, t1_trades, initial_capital)
+    # 对比
+    print("\n【步骤3】对比...")
+    orig_pnl = long_trades['盈亏金额'].sum()
+    t1_pnl = t1_trades['盈亏金额'].sum() if len(t1_trades) > 0 else 0
     
-    # 4. 导出结果
+    print(f"原始交易: {len(long_trades)}笔, 盈亏 {orig_pnl:+,.0f}元")
+    print(f"T+1交易: {len(t1_trades)}笔, 盈亏 {t1_pnl:+,.0f}元")
+    print(f"差异: {t1_pnl - orig_pnl:+,.0f}元")
+    
+    # 检查时间重叠
+    if len(t1_trades) > 0:
+        print("\n【步骤4】验证时间无重叠...")
+        prev_exit = None
+        overlap = 0
+        for _, row in t1_trades.iterrows():
+            if prev_exit is not None and row['开仓时间'] < prev_exit:
+                overlap += 1
+                print(f"  ⚠️ 重叠: {row['开仓时间'].strftime('%m-%d %H:%M')} 早于前笔平仓 {prev_exit.strftime('%m-%d %H:%M')}")
+            prev_exit = row['平仓时间']
+        
+        if overlap == 0:
+            print("  ✅ 无时间重叠")
+        else:
+            print(f"  ❌ 发现 {overlap} 处重叠")
+    
+    # 保存
     if len(t1_trades) > 0:
-        print("\n【步骤4】导出T+1交易记录...")
+        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+        output_file = f'cyb50_t1_v2_{timestamp}.csv'
         
-        # 格式化时间
         export_df = t1_trades.copy()
-        for col in ['开仓时间', '平仓时间', '原平仓时间']:
-            if col in export_df.columns:
-                export_df[col] = export_df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
+        for col in ['开仓时间', '平仓时间']:
+            export_df[col] = export_df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
         
-        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
-        output_file = f'cyb50_t1_converted_trades_{timestamp}.csv'
         export_df.to_csv(output_file, index=False, encoding='utf-8-sig')
-        print(f"✅ T+1交易记录已保存: {output_file}")
+        print(f"\n✅ 已保存: {output_file}")
     
     print("\n" + "="*80)
-    print("转换完成!")
+    print("完成!")
     print("="*80)
 
 

+ 333 - 0
cat-fly/t1_converter_correct.py

@@ -0,0 +1,333 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+CYB50 T+1 回测引擎 - 正确的资金管理版本
+
+核心规则:
+1. T+1规则:买入当天不能卖出
+2. 资金占用:持仓期间资金被占用,不能开新仓
+3. 100%仓位:每次开仓使用全部可用资金
+4. 信号连续性:如果前一交易持仓中,新信号出现时如何处理?
+
+回测逻辑:
+- 遍历原始做多交易信号
+- 维护持仓状态和资金
+- 检查每笔交易的资金可用性
+- 记录实际执行的交易
+"""
+
+import pandas as pd
+import numpy as np
+from datetime import datetime, timedelta
+import sys
+sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
+
+from cyb50_30min_dual_direction import (
+    ConfigManager, IntradayDataFetcher, 
+    DualDirectionSignalGenerator, DualDirectionExecutor
+)
+
+
+def simulate_t1_trades_correct(data_df, long_trades_df, initial_capital=1000000):
+    """
+    正确的T+1回测 - 带资金管理
+    
+    参数:
+        data_df: 包含所有价格数据的DataFrame
+        long_trades_df: 原始做多交易列表(作为信号源)
+        initial_capital: 初始资金
+    
+    返回:
+        t1_trades_df: T+1规则下实际执行的交易
+    """
+    print("\n" + "="*80)
+    print("T+1回测引擎 - 正确资金管理版本")
+    print("="*80)
+    
+    if len(long_trades_df) == 0:
+        print("没有做多交易记录")
+        return pd.DataFrame()
+    
+    # 按开仓时间排序
+    signals = long_trades_df.sort_values('开仓时间').reset_index(drop=True)
+    
+    # 状态变量
+    capital = initial_capital          # 总资金
+    available_capital = initial_capital  # 可用资金(未持仓时等于总资金)
+    position = 0                       # 持仓数量
+    entry_price = 0                    # 开仓价格
+    entry_time = None                  # 开仓时间
+    can_sell_time = None               # 最早可卖出时间(T+1规则)
+    entry_signals_str = ''             # 入场信号描述
+    
+    t1_trades = []
+    skipped_trades = []
+    
+    print(f"\n初始资金: {initial_capital:,.0f}元")
+    print(f"总信号数: {len(signals)}笔")
+    print("\n" + "-"*80)
+    
+    for idx, signal in signals.iterrows():
+        signal_entry_time = signal['开仓时间']
+        signal_entry_price = signal['开仓价格']
+        signal_exit_time = signal['平仓时间']
+        signal_exit_price = signal['平仓价格']
+        signal_exit_reason = signal['退出原因']
+        signal_entry_signals = signal.get('入场信号', '')
+        
+        # 情况1:当前无持仓
+        if position == 0:
+            # 检查可用资金是否足够
+            if available_capital <= 0:
+                print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 资金不足")
+                skipped_trades.append({
+                    '信号序号': idx+1,
+                    '信号时间': signal_entry_time,
+                    '跳过原因': '资金不足',
+                    '备注': f'可用资金: {available_capital:,.0f}元'
+                })
+                continue
+            
+            # 开仓 - 使用全部可用资金
+            position_size = int(available_capital / signal_entry_price)
+            if position_size <= 0:
+                print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 股价过高无法开仓")
+                continue
+            
+            cost = position_size * signal_entry_price
+            
+            # 更新状态
+            position = position_size
+            entry_price = signal_entry_price
+            entry_time = signal_entry_time
+            can_sell_time = signal_entry_time + timedelta(days=1)  # T+1:次日才能卖
+            can_sell_time = can_sell_time.replace(hour=9, minute=30)  # 假设次日9:30可卖
+            entry_signals_str = signal_entry_signals
+            available_capital = 0  # 资金全部占用
+            
+            print(f"\n[开仓] 信号 #{idx+1}: {entry_time.strftime('%m-%d %H:%M')}")
+            print(f"  价格: {entry_price:.2f}, 数量: {position}股, 成本: {cost:,.0f}元")
+            print(f"  最早可卖: {can_sell_time.strftime('%m-%d %H:%M')} (T+1)")
+            
+            # 确定平仓时间和价格
+            # 原始信号可能是T0(当天),但T+1要延期
+            actual_exit_time = max(signal_exit_time, can_sell_time)
+            
+            # 如果延期,需要从data_df中找到对应的价格
+            if actual_exit_time > signal_exit_time:
+                # 查找延期后的平仓价格
+                future_data = data_df[data_df.index >= can_sell_time]
+                if len(future_data) > 0:
+                    # 使用最早可用时间的价格
+                    actual_exit_time = future_data.index[0]
+                    actual_exit_price = future_data.iloc[0]['Open']
+                    t1_adjusted = True
+                    print(f"  [T+1调整] 原始平仓: {signal_exit_time.strftime('%m-%d %H:%M')} → 新平仓: {actual_exit_time.strftime('%m-%d %H:%M')}")
+                else:
+                    # 没有后续数据,使用原始平仓
+                    actual_exit_time = signal_exit_time
+                    actual_exit_price = signal_exit_price
+                    t1_adjusted = False
+                    print(f"  [警告] 无法找到T+1数据,使用原始平仓时间")
+            else:
+                actual_exit_price = signal_exit_price
+                t1_adjusted = False
+                print(f"  [正常持仓] 平仓: {actual_exit_time.strftime('%m-%d %H:%M')}")
+            
+            # 计算盈亏
+            gross_pnl = (actual_exit_price - entry_price) * position
+            pnl_pct = (actual_exit_price - entry_price) / entry_price * 100
+            
+            # 更新资金
+            capital += gross_pnl
+            available_capital = capital  # 平仓后资金释放
+            
+            # 记录交易
+            trade_record = {
+                '交易方向': '做多',
+                '开仓时间': entry_time,
+                '平仓时间': actual_exit_time,
+                '开仓价格': entry_price,
+                '平仓价格': actual_exit_price,
+                '仓位': position,
+                '盈亏金额': gross_pnl,
+                '盈亏百分比': pnl_pct,
+                '退出原因': signal_exit_reason if not t1_adjusted else f"T+1延期-{signal_exit_reason}",
+                '持仓周期数': int((actual_exit_time - entry_time).total_seconds() / 1800),
+                '持仓小时数': (actual_exit_time - entry_time).total_seconds() / 3600,
+                'T+1调整': '是(T0→T1)' if t1_adjusted else '否',
+                '原平仓时间': signal_exit_time,
+                '原平仓价格': signal_exit_price,
+                '入场信号': entry_signals_str,
+                '平仓时资金': capital,
+            }
+            t1_trades.append(trade_record)
+            
+            status = "✅盈利" if gross_pnl > 0 else "❌亏损"
+            print(f"  [平仓] {actual_exit_time.strftime('%m-%d %H:%M')} @ {actual_exit_price:.2f}")
+            print(f"  {status}: {gross_pnl:+,.0f}元 ({pnl_pct:+.2f}%)")
+            print(f"  当前总资金: {capital:,.0f}元")
+            
+            # 重置持仓状态
+            position = 0
+            entry_price = 0
+            entry_time = None
+            can_sell_time = None
+            
+        # 情况2:当前有持仓
+        else:
+            # 检查新信号是否在持仓期间
+            if signal_entry_time < entry_time:
+                print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 时间早于当前持仓")
+                continue
+            
+            # 信号出现时已有持仓,跳过该信号
+            print(f"\n信号 #{idx+1}: {signal_entry_time.strftime('%m-%d %H:%M')} 跳过 - 当前持仓中 (持仓从 {entry_time.strftime('%m-%d %H:%M')} 开始)")
+            skipped_trades.append({
+                '信号序号': idx+1,
+                '信号时间': signal_entry_time,
+                '跳过原因': '持仓中无法开仓',
+                '备注': f'当前持仓: {entry_time.strftime("%m-%d %H:%M")} 买入'
+            })
+            continue
+    
+    # 处理最后一笔持仓(如果存在)
+    if position > 0:
+        final_price = data_df.iloc[-1]['Close']
+        final_time = data_df.index[-1]
+        
+        gross_pnl = (final_price - entry_price) * position
+        pnl_pct = (final_price - entry_price) / entry_price * 100
+        capital += gross_pnl
+        
+        trade_record = {
+            '交易方向': '做多',
+            '开仓时间': entry_time,
+            '平仓时间': final_time,
+            '开仓价格': entry_price,
+            '平仓价格': final_price,
+            '仓位': position,
+            '盈亏金额': gross_pnl,
+            '盈亏百分比': pnl_pct,
+            '退出原因': f'回测强制平仓(最终价格{final_price:.2f})',
+            '持仓周期数': int((final_time - entry_time).total_seconds() / 1800),
+            '持仓小时数': (final_time - entry_time).total_seconds() / 3600,
+            'T+1调整': '否',
+            '原平仓时间': final_time,
+            '原平仓价格': final_price,
+            '入场信号': entry_signals_str,
+            '平仓时资金': capital,
+        }
+        t1_trades.append(trade_record)
+        
+        print(f"\n[强制平仓] {final_time.strftime('%m-%d %H:%M')} @ {final_price:.2f}")
+        print(f"  盈亏: {gross_pnl:+,.0f}元 ({pnl_pct:+.2f}%)")
+    
+    # 生成结果
+    t1_trades_df = pd.DataFrame(t1_trades)
+    
+    print("\n" + "="*80)
+    print("T+1回测完成")
+    print("="*80)
+    print(f"原始信号数: {len(signals)}笔")
+    print(f"实际执行: {len(t1_trades)}笔")
+    print(f"跳过信号: {len(skipped_trades)}笔")
+    print(f"最终资金: {capital:,.0f}元")
+    print(f"总收益率: {(capital/initial_capital-1)*100:+.2f}%")
+    
+    if len(skipped_trades) > 0:
+        print("\n【跳过的信号】")
+        for st in skipped_trades:
+            print(f"  信号#{st['信号序号']} {st['信号时间'].strftime('%m-%d %H:%M')}: {st['跳过原因']}")
+    
+    return t1_trades_df
+
+
+def compare_with_original(original_trades, t1_trades, initial_capital=1000000):
+    """对比原始交易和T+1交易"""
+    print("\n" + "="*80)
+    print("对比分析")
+    print("="*80)
+    
+    # 原始统计
+    orig_pnl = original_trades['盈亏金额'].sum()
+    orig_final = initial_capital + orig_pnl
+    
+    # T+1统计
+    t1_pnl = t1_trades['盈亏金额'].sum() if len(t1_trades) > 0 else 0
+    t1_final = initial_capital + t1_pnl
+    
+    print(f"\n【原始交易(假设T0,无资金限制)】")
+    print(f"  交易次数: {len(original_trades)}")
+    print(f"  总盈亏: {orig_pnl:+,.0f}元")
+    print(f"  最终资金: {orig_final:,.0f}元")
+    print(f"  收益率: {(orig_final/initial_capital-1)*100:+.2f}%")
+    
+    print(f"\n【T+1回测(正确资金管理)】")
+    print(f"  交易次数: {len(t1_trades)}")
+    print(f"  总盈亏: {t1_pnl:+,.0f}元")
+    print(f"  最终资金: {t1_final:,.0f}元")
+    print(f"  收益率: {(t1_final/initial_capital-1)*100:+.2f}%")
+    
+    print(f"\n【差异分析】")
+    print(f"  交易次数差异: {len(original_trades) - len(t1_trades)}笔 (被T+1规则过滤)")
+    print(f"  盈亏差异: {t1_pnl - orig_pnl:+,.0f}元")
+    print(f"  收益率差异: {(t1_final - orig_final)/initial_capital*100:+.2f}%")
+
+
+def main():
+    """主程序 - 测试正确版本"""
+    print("="*80)
+    print("CYB50 T+1 回测引擎 - 正确资金管理版本")
+    print("="*80)
+    
+    initial_capital = 1000000
+    
+    # 1. 获取数据和原始交易
+    print("\n【步骤1】获取数据和原始交易信号...")
+    config_manager = ConfigManager('config.json')
+    fetcher = IntradayDataFetcher(config_manager)
+    
+    end_date = datetime.now()
+    start_date = end_date - timedelta(days=70)
+    
+    raw_data = fetcher.fetch_30min_data(start_date, end_date)
+    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(long_trades)} 笔做多交易信号")
+    
+    # 2. 运行正确的T+1回测
+    print("\n【步骤2】运行正确的T+1回测...")
+    t1_trades = simulate_t1_trades_correct(data_with_indicators, long_trades, initial_capital)
+    
+    # 3. 对比分析
+    print("\n【步骤3】对比分析...")
+    compare_with_original(long_trades, t1_trades, initial_capital)
+    
+    # 4. 导出结果
+    if len(t1_trades) > 0:
+        print("\n【步骤4】导出结果...")
+        export_df = t1_trades.copy()
+        for col in ['开仓时间', '平仓时间', '原平仓时间']:
+            if col in export_df.columns:
+                export_df[col] = export_df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
+        
+        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+        output_file = f'cyb50_t1_correct_{timestamp}.csv'
+        export_df.to_csv(output_file, index=False, encoding='utf-8-sig')
+        print(f"✅ 已保存: {output_file}")
+    
+    print("\n" + "="*80)
+    print("完成!")
+    print("="*80)
+
+
+if __name__ == "__main__":
+    main()

+ 302 - 0
cat-fly/t1_converter_v2.py

@@ -0,0 +1,302 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+CYB50 T+1 回测引擎 - 正确的资金管理版本 V2
+
+核心逻辑:
+1. 按时间顺序处理所有信号
+2. 维护持仓状态(是否持仓、持仓期间)
+3. 当新信号的开仓时间落在已有持仓期间时,跳过该信号
+4. 正确处理T+1延期平仓
+"""
+
+import pandas as pd
+import numpy as np
+from datetime import datetime, timedelta
+import sys
+sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
+
+from cyb50_30min_dual_direction import (
+    ConfigManager, IntradayDataFetcher, 
+    DualDirectionSignalGenerator, DualDirectionExecutor
+)
+
+
+def simulate_t1_trades_v2(data_df, long_trades_df, initial_capital=1000000):
+    """
+    正确的T+1回测 V2 - 正确处理时间重叠
+    
+    参数:
+        data_df: 包含所有价格数据的DataFrame
+        long_trades_df: 原始做多交易列表(作为信号源)
+        initial_capital: 初始资金
+    
+    返回:
+        t1_trades_df: T+1规则下实际执行的交易
+    """
+    print("\n" + "="*80)
+    print("T+1回测引擎 V2 - 正确处理时间重叠")
+    print("="*80)
+    
+    if len(long_trades_df) == 0:
+        print("没有做多交易记录")
+        return pd.DataFrame()
+    
+    # 准备信号列表
+    signals = []
+    for idx, trade in long_trades_df.iterrows():
+        signals.append({
+            'signal_id': idx + 1,
+            'entry_time': trade['开仓时间'],
+            'entry_price': trade['开仓价格'],
+            'exit_time': trade['平仓时间'],
+            'exit_price': trade['平仓价格'],
+            'exit_reason': trade['退出原因'],
+            'entry_signals': trade.get('入场信号', '')
+        })
+    
+    # 按开仓时间排序
+    signals = sorted(signals, key=lambda x: x['entry_time'])
+    
+    t1_trades = []
+    capital = initial_capital
+    
+    # 当前持仓状态
+    current_position = None  # None 或 {'entry_time', 'entry_price', 'can_sell_time', ...}
+    
+    print(f"\n初始资金: {initial_capital:,.0f}元")
+    print(f"总信号数: {len(signals)}笔")
+    print("\n" + "-"*80)
+    
+    for signal in signals:
+        sig_entry = signal['entry_time']
+        sig_exit = signal['exit_time']
+        
+        # 检查是否有持仓
+        if current_position is not None:
+            # 检查新信号是否在持仓期间
+            hold_end = current_position['actual_exit_time']
+            
+            if sig_entry < hold_end:
+                # 新信号在持仓期间,跳过
+                print(f"\n[跳过] 信号 #{signal['signal_id']}: {sig_entry.strftime('%m-%d %H:%M')}")
+                print(f"  原因: 当前持仓中 (持仓期: {current_position['entry_time'].strftime('%m-%d %H:%M')} → {hold_end.strftime('%m-%d %H:%M')})")
+                continue
+            else:
+                # 新信号在持仓结束后,先结算当前持仓
+                print(f"\n[结算前持仓] 信号 #{signal['signal_id']} 到来时,前持仓已结束")
+                # 持仓已经结束,可以开新仓
+                current_position = None
+        
+        # 现在可以开仓
+        entry_time = sig_entry
+        entry_price = signal['entry_price']
+        
+        # 计算可卖时间(T+1规则)
+        can_sell_time = entry_time + timedelta(days=1)
+        can_sell_time = can_sell_time.replace(hour=9, minute=30)
+        
+        # 确定实际平仓时间
+        # 原始平仓时间 vs T+1最早可卖时间,取较晚者
+        actual_exit_time = max(sig_exit, can_sell_time)
+        
+        # 查找实际平仓价格
+        if actual_exit_time > sig_exit:
+            # 需要延期,查找数据
+            future_data = data_df[data_df.index >= actual_exit_time]
+            if len(future_data) > 0:
+                actual_exit_time = future_data.index[0]
+                actual_exit_price = future_data.iloc[0]['Open']
+                t1_adjusted = True
+            else:
+                # 无后续数据,使用原始
+                actual_exit_time = sig_exit
+                actual_exit_price = signal['exit_price']
+                t1_adjusted = False
+        else:
+            actual_exit_price = signal['exit_price']
+            t1_adjusted = False
+        
+        # 计算仓位(使用全部资金)
+        position_size = int(capital / entry_price)
+        if position_size <= 0:
+            print(f"\n[跳过] 信号 #{signal['signal_id']}: {entry_time.strftime('%m-%d %H:%M')} - 资金不足")
+            continue
+        
+        # 记录持仓
+        current_position = {
+            'entry_time': entry_time,
+            'entry_price': entry_price,
+            'position_size': position_size,
+            'actual_exit_time': actual_exit_time,
+            'actual_exit_price': actual_exit_price,
+            't1_adjusted': t1_adjusted,
+            'original_exit_time': sig_exit,
+            'original_exit_price': signal['exit_price'],
+            'exit_reason': signal['exit_reason'],
+            'entry_signals': signal['entry_signals']
+        }
+        
+        # 计算盈亏
+        gross_pnl = (actual_exit_price - entry_price) * position_size
+        pnl_pct = (actual_exit_price - entry_price) / entry_price * 100
+        
+        # 更新资金
+        capital += gross_pnl
+        
+        # 记录交易
+        trade_record = {
+            '交易方向': '做多',
+            '开仓时间': entry_time,
+            '平仓时间': actual_exit_time,
+            '开仓价格': entry_price,
+            '平仓价格': actual_exit_price,
+            '仓位': position_size,
+            '盈亏金额': gross_pnl,
+            '盈亏百分比': pnl_pct,
+            '退出原因': signal['exit_reason'] if not t1_adjusted else f"T+1延期-{signal['exit_reason']}",
+            '持仓周期数': int((actual_exit_time - entry_time).total_seconds() / 1800),
+            '持仓小时数': (actual_exit_time - entry_time).total_seconds() / 3600,
+            'T+1调整': '是(T0→T1)' if t1_adjusted else '否',
+            '原平仓时间': sig_exit,
+            '原平仓价格': signal['exit_price'],
+            '入场信号': signal['entry_signals'],
+            '平仓时资金': capital,
+        }
+        t1_trades.append(trade_record)
+        
+        # 打印
+        status = "✅盈利" if gross_pnl > 0 else "❌亏损"
+        adj_str = "[T+1调整] " if t1_adjusted else ""
+        print(f"\n[执行] 信号 #{signal['signal_id']}: {entry_time.strftime('%m-%d %H:%M')} → {actual_exit_time.strftime('%m-%d %H:%M')}")
+        print(f"  {adj_str}价格: {entry_price:.2f} → {actual_exit_price:.2f}")
+        print(f"  盈亏: {gross_pnl:+,.0f}元 ({pnl_pct:+.2f}%) {status}")
+        print(f"  资金: {capital:,.0f}元")
+    
+    # 处理最后一笔持仓(如果数据结束前未平仓)
+    if current_position is not None and current_position['actual_exit_time'] > data_df.index[-1]:
+        final_price = data_df.iloc[-1]['Close']
+        final_time = data_df.index[-1]
+        
+        entry_time = current_position['entry_time']
+        entry_price = current_position['entry_price']
+        position_size = current_position['position_size']
+        
+        gross_pnl = (final_price - entry_price) * position_size
+        pnl_pct = (final_price - entry_price) / entry_price * 100
+        capital += gross_pnl
+        
+        trade_record = {
+            '交易方向': '做多',
+            '开仓时间': entry_time,
+            '平仓时间': final_time,
+            '开仓价格': entry_price,
+            '平仓价格': final_price,
+            '仓位': position_size,
+            '盈亏金额': gross_pnl,
+            '盈亏百分比': pnl_pct,
+            '退出原因': '回测强制平仓',
+            '持仓周期数': int((final_time - entry_time).total_seconds() / 1800),
+            '持仓小时数': (final_time - entry_time).total_seconds() / 3600,
+            'T+1调整': '否',
+            '原平仓时间': final_time,
+            '原平仓价格': final_price,
+            '入场信号': current_position['entry_signals'],
+            '平仓时资金': capital,
+        }
+        t1_trades.append(trade_record)
+        
+        print(f"\n[强制平仓] {final_time.strftime('%m-%d %H:%M')} @ {final_price:.2f}")
+        print(f"  盈亏: {gross_pnl:+,.0f}元")
+    
+    # 生成结果
+    t1_trades_df = pd.DataFrame(t1_trades)
+    
+    print("\n" + "="*80)
+    print("T+1回测 V2 完成")
+    print("="*80)
+    print(f"原始信号数: {len(signals)}笔")
+    print(f"实际执行: {len(t1_trades)}笔")
+    print(f"跳过信号: {len(signals) - len(t1_trades)}笔")
+    print(f"最终资金: {capital:,.0f}元")
+    print(f"总收益率: {(capital/initial_capital-1)*100:+.2f}%")
+    
+    return t1_trades_df
+
+
+def main():
+    """主程序 - 测试V2版本"""
+    print("="*80)
+    print("CYB50 T+1 回测引擎 V2")
+    print("="*80)
+    
+    initial_capital = 1000000
+    
+    # 获取数据和原始交易
+    print("\n【步骤1】获取数据...")
+    config_manager = ConfigManager('config.json')
+    fetcher = IntradayDataFetcher(config_manager)
+    
+    end_date = datetime.now()
+    start_date = end_date - timedelta(days=70)
+    
+    raw_data = fetcher.fetch_30min_data(start_date, end_date)
+    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(long_trades)}笔")
+    
+    # 运行V2回测
+    print("\n【步骤2】运行T+1回测 V2...")
+    t1_trades = simulate_t1_trades_v2(data_with_indicators, long_trades, initial_capital)
+    
+    # 对比
+    print("\n【步骤3】对比...")
+    orig_pnl = long_trades['盈亏金额'].sum()
+    t1_pnl = t1_trades['盈亏金额'].sum() if len(t1_trades) > 0 else 0
+    
+    print(f"原始交易: {len(long_trades)}笔, 盈亏 {orig_pnl:+,.0f}元")
+    print(f"T+1交易: {len(t1_trades)}笔, 盈亏 {t1_pnl:+,.0f}元")
+    print(f"差异: {t1_pnl - orig_pnl:+,.0f}元")
+    
+    # 检查时间重叠
+    if len(t1_trades) > 0:
+        print("\n【步骤4】验证时间无重叠...")
+        prev_exit = None
+        overlap = 0
+        for _, row in t1_trades.iterrows():
+            if prev_exit is not None and row['开仓时间'] < prev_exit:
+                overlap += 1
+                print(f"  ⚠️ 重叠: {row['开仓时间'].strftime('%m-%d %H:%M')} 早于前笔平仓 {prev_exit.strftime('%m-%d %H:%M')}")
+            prev_exit = row['平仓时间']
+        
+        if overlap == 0:
+            print("  ✅ 无时间重叠")
+        else:
+            print(f"  ❌ 发现 {overlap} 处重叠")
+    
+    # 保存
+    if len(t1_trades) > 0:
+        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+        output_file = f'cyb50_t1_v2_{timestamp}.csv'
+        
+        export_df = t1_trades.copy()
+        for col in ['开仓时间', '平仓时间']:
+            export_df[col] = export_df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
+        
+        export_df.to_csv(output_file, index=False, encoding='utf-8-sig')
+        print(f"\n✅ 已保存: {output_file}")
+    
+    print("\n" + "="*80)
+    print("完成!")
+    print("="*80)
+
+
+if __name__ == "__main__":
+    main()

+ 49 - 0
memory/2025-03-26.md

@@ -0,0 +1,49 @@
+# 2025-03-26 - Session Log
+
+## Circulation/Circulation Research Writing Style Analysis
+
+**Request:** Analyze writing style of 10 mitochondria-related articles from Circulation/Circulation Research journals, then rewrite a result section to match that style.
+
+### Style Analysis Key Findings
+
+**Journal Characteristics:**
+- Top-tier cardiology journals (impact factor: Circulation ~38, Circ Res ~17)
+- Published by American Heart Association
+- High standards for clinical translational relevance
+
+**Writing Style Features:**
+
+| Feature | Specific Pattern |
+|---------|-----------------|
+| **Opening** | Direct statement, no "We investigated..." clichés |
+| **Tense** | Past tense for results description |
+| **Voice** | Passive + active mixed; result-focused |
+| **Modifier use** | Minimal "significantly", direct data presentation |
+| **Data format** | Specific values with units (e.g., "by over 40%") |
+| **Paragraphs** | One experiment/topic per paragraph |
+| **Figure citations** | Parenthetical at sentence end, e.g., (Fig. 1A) |
+
+**Typical Sentence Patterns:**
+- "Diabetes reduced Kir6.1 and SUR1 expression in IFM by over 40% (p < 0.05)."
+- "The levels of MAM-related proteins were significantly higher in diabetic heart homogenates..."
+- "To determine the impact of diabetes mellitus on MAMs, we first examined..."
+
+### Rewritten Content Delivered
+
+User provided a result section about mtDNase I and cardiomyocyte senescence. I rewrote it following Circulation style:
+
+1. **Opening:** "To determine whether clearance of damaged mitochondrial DNA..."
+2. **Data presentation:** Specific fold-changes (3-fold), percentages (40%, 58% → 28%), and absolute values (412 ± 56 vs 945 ± 89 arbitrary units)
+3. **Organization:** Grouped by outcome type (ROS → senescence markers → mitochondrial function → contractility)
+4. **Figure legends:** Standard format with subpanels, scale bars, and statistical notation
+
+**Example excerpt:**
+> H/R challenge increased intracellular reactive oxygen species (ROS) levels by 3-fold compared to control cells (Fig. XB-C). mtDNase I expression reduced ROS accumulation by approximately 50% relative to vector controls.
+
+### Sources Referenced
+- Circulation article: FUNDC1-MAMs in diabetic cardiomyopathy (10.1161/CIRCULATIONAHA.118.033552)
+- Circulation Research article: mitoKATP in type I diabetes (PMC3594509)
+
+### User
+- **Name:** Erwin
+- **Timezone:** Asia/Shanghai

+ 42 - 0
memory/2026-03-23.md

@@ -0,0 +1,42 @@
+# 2026-03-23
+
+## 交易报告系统优化
+
+### 上午工作
+- 用户要求修改交易明细显示数量:10笔 → 30笔
+- 已更新 auto_report.py 和 auto_report_long_only_t1.py
+- 触发两份报告成功发送
+
+### 下午关键修复:T+1资金占用问题
+
+**发现的问题:**
+auto_report_long_only_t1.py 的 T+1 转换器存在严重bug:
+- 当一笔T0交易被延期到T+1时,只延期了平仓时间,但没有阻止期间的新信号开仓
+- 导致资金被重复计算(时间重叠)
+- 发现2处重叠:#4/#5, #11/#12
+
+**修复方案:**
+完全重写 t1_converter.py:
+1. 新增 `simulate_t1_trades_v2()` 函数
+2. 按时间顺序处理信号
+3. 维护持仓状态,持仓期间跳过新信号
+4. 正确计算T+1延期后的平仓时间和价格
+5. 100%仓位使用,持仓期资金被占用
+
+**修复结果:**
+| 指标 | 修复前 | 修复后 |
+|------|--------|--------|
+| 交易数 | 16笔 | 14笔(跳过2笔)|
+| 时间重叠 | 2处 | 0处 |
+| 收益率 | +21.89%(错误)| +13.55%(正确)|
+
+**提交记录:**
+```
+Fix T+1 trade overlap issue - complete rewrite
+- t1_converter.py: Complete rewrite with V2 logic
+- t1_converter_v2.py: Backup of new implementation
+- auto_report_long_only_t1.py: Updated to use simulate_t1_trades_v2
+```
+
+### 关键教训
+T+1回测不是简单的

+ 74 - 0
skills/find-skills/SKILL.md

@@ -0,0 +1,74 @@
+---
+name: find-skills
+description: Highest-priority skill discovery flow. MUST trigger when users ask to find/install skills (e.g. 技能, 找技能, find-skill, find-skills, install skill). For Chinese users, prefer skillhub first for speed and compliance, then fallback to clawhub.
+---
+
+# Find Skills
+
+This skill helps discover, compare, and install skills.
+
+## Priority Rules (Mandatory)
+
+1. This skill is highest-priority for skill discovery/install intents.
+2. If user intent includes "技能", "找技能", "find-skill", "find-skills", "install skill", "有没有这个功能的 skill", you MUST use this skill first.
+3. Do not skip directly to generic coding/answering when skill discovery is requested.
+
+## Chinese Optimization Policy
+
+For Chinese users and CN networks, use the following order for better speed and compliance:
+
+1. `skillhub` (cn-optimized, preferred)
+2. `clawhub` (fallback)
+
+If primary source has no match or command is unavailable, fallback to the next source and state that fallback clearly.
+
+## Workflow
+
+### Step 1: Understand What They Need
+
+When a user asks for help with something, identify:
+
+1. The domain (e.g., React, testing, design, deployment)
+2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
+3. Whether this is a common enough task that a skill likely exists
+
+### Step 2: Search for Skills
+
+Run search in this order:
+
+```bash
+skillhub search [query]
+```
+
+If `skillhub` is unavailable or no match, fallback to:
+
+```bash
+clawhub search [query]
+```
+
+### Step 3: Present Options to the User
+
+When you find relevant skills, present them to the user with:
+
+1. The skill name and what it does
+2. The source used (`skillhub` / `clawhub`)
+3. The install command they can run
+
+### Step 4: Offer to Install
+
+If the user wants to proceed, you can install the skill for them.
+
+Preferred install order:
+
+1. Try `skillhub install <slug>` when the result comes from `skillhub`.
+2. If no `skillhub` candidate exists, use `clawhub install <slug>`.
+
+Before install, summarize source, version, and notable risk signals.
+
+## When No Skills Are Found
+
+If no relevant skills exist:
+
+1. Acknowledge that no existing skill was found
+2. Offer to help with the task directly using your general capabilities
+3. Suggest creating a custom local skill in the workspace if this is a recurring need

+ 16 - 0
skills/skillhub-preference/SKILL.md

@@ -0,0 +1,16 @@
+---
+name: skillhub-preference
+description: Prefer `skillhub` for skill discovery/install/update, then fallback to `clawhub` when unavailable or no match. Use when users ask about skills, 插件, or capability extension.
+---
+
+# Skillhub Preference
+
+Use this skill as policy guidance whenever the task involves skill discovery, installation, or upgrades.
+
+## Policy
+
+1. Try `skillhub` first for search/install/update.
+2. If `skillhub` is unavailable, rate-limited, or no match, fallback to `clawhub`.
+3. Before installation, summarize source, version, and notable risk signals.
+4. Do not claim exclusivity; both registries are allowed.
+5. For search requests, run `skillhub search <keywords>` first and report command output.