erwin 1 miesiąc temu
rodzic
commit
c9c242ba47

+ 7 - 1
.claude/settings.local.json

@@ -47,7 +47,13 @@
       "Bash(git commit:*)",
       "Bash(git push:*)",
       "Bash(../.venv/Scripts/python.exe auto_report_long_only_t1.py)",
-      "Bash(../../.venv/Scripts/python.exe auto_report_long_only_t1.py)"
+      "Bash(../../.venv/Scripts/python.exe auto_report_long_only_t1.py)",
+      "Bash(../.venv/Scripts/python.exe fetch_cyb50_data_2023.py)",
+      "Bash(../.venv/Scripts/python.exe fetch_mairui_cyb50.py)",
+      "Bash(.venv/Scripts/python.exe cat-fly/fetch_mairui_cyb50.py)",
+      "Bash(which python*)",
+      "Bash(/c/Users/erwin/AppData/Local/Programs/Python/Python311/python.exe fetch_mairui_cyb50.py)",
+      "Read(//c/Users/erwin/AppData/Local/Programs/Python/**)"
     ]
   }
 }

Plik diff jest za duży
+ 5801 - 0
cat-fly/data/cyb50_30min_2023_to_20260325.csv


+ 276 - 0
cat-fly/fetch_mairui_cyb50.py

@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+麦儒数据 (mairui) API - 获取创业板50指数30分钟K线数据
+文档: https://www.mairui.club/
+Token: AE17EE23-AAE4-492F-A959-EC883DFA5A76
+"""
+
+import pandas as pd
+import requests
+import json
+import os
+from datetime import datetime, timedelta
+import time
+import warnings
+warnings.filterwarnings('ignore')
+
+
+class MairuiDataFetcher:
+    """麦儒数据获取器"""
+
+    def __init__(self):
+        self.token = "AE17EE23-AAE4-492F-A959-EC883DFA5A76"
+        self.base_url = "https://api.mairuiapi.com"
+        self.data_dir = "data"
+        os.makedirs(self.data_dir, exist_ok=True)
+
+    def get_index_minute_data(self, index_code="399673.SZ", period="30", start_date=None, end_date=None):
+        """
+        获取指数分钟历史数据
+        :param index_code: 指数代码,创业板50 = 399673.SZ
+        :param period: 周期,支持 1,5,15,30,60,d(日线)
+        :param start_date: 开始时间 (YYYYMMDD)
+        :param end_date: 结束时间 (YYYYMMDD)
+        :return: DataFrame
+        """
+        try:
+            # 麦儒API接口:获取指数历史K线数据
+            # 格式: /hsindex/history/指数代码/周期/Token?st=开始时间&et=结束时间
+            url = f"{self.base_url}/hsindex/history/{index_code}/{period}/{self.token}"
+
+            # 添加时间参数
+            params = {}
+            if start_date:
+                params['st'] = start_date
+            if end_date:
+                params['et'] = end_date
+
+            if params:
+                url += "?" + "&".join([f"{k}={v}" for k, v in params.items()])
+
+            print(f"[麦儒数据] 请求URL: {url}")
+
+            headers = {
+                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+            }
+
+            response = requests.get(url, headers=headers, timeout=30)
+            response.encoding = 'utf-8'
+
+            if response.status_code != 200:
+                print(f"[错误] HTTP状态码: {response.status_code}")
+                return None
+
+            data = response.json()
+
+            if isinstance(data, dict) and 'error' in data:
+                print(f"[错误] API返回错误: {data['error']}")
+                return None
+
+            if not data or not isinstance(data, list):
+                print(f"[错误] 返回数据格式不正确")
+                return None
+
+            # 转换为DataFrame
+            df = pd.DataFrame(data)
+
+            # 标准化列名
+            column_mapping = {
+                'd': 'DateTime',  # 日期时间
+                'o': 'Open',      # 开盘
+                'h': 'High',      # 最高
+                'l': 'Low',       # 最低
+                'c': 'Close',     # 收盘
+                'v': 'Volume',    # 成交量
+                'e': 'Amount',    # 成交额
+                't': 'DateTime'   # 有些接口用t表示时间
+            }
+
+            df.rename(columns=column_mapping, inplace=True)
+
+            # 转换时间格式
+            if 'DateTime' in df.columns:
+                df['DateTime'] = pd.to_datetime(df['DateTime'])
+                df.set_index('DateTime', inplace=True)
+                df.sort_index(inplace=True)
+
+            # 转换数值类型
+            numeric_cols = ['Open', 'High', 'Low', 'Close', 'Volume', 'Amount']
+            for col in numeric_cols:
+                if col in df.columns:
+                    df[col] = pd.to_numeric(df[col], errors='coerce')
+
+            print(f"[麦儒数据] 成功获取 {len(df)} 条数据")
+            print(f"[麦儒数据] 时间范围: {df.index[0]} 至 {df.index[-1]}")
+
+            return df
+
+        except Exception as e:
+            print(f"[错误] 获取数据失败: {e}")
+            return None
+
+    def get_index_history(self, index_code="399673"):
+        """
+        获取指数历史数据(日线)
+        :param index_code: 指数代码
+        :return: DataFrame
+        """
+        try:
+            # 麦儒API接口:获取指数历史数据
+            url = f"{self.base_url}/zslsh/{index_code}/{self.token}"
+
+            print(f"[麦儒数据] 获取历史数据: {url}")
+
+            headers = {
+                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+            }
+
+            response = requests.get(url, headers=headers, timeout=30)
+            response.encoding = 'utf-8'
+
+            if response.status_code != 200:
+                print(f"[错误] HTTP状态码: {response.status_code}")
+                return None
+
+            data = response.json()
+
+            if not data or not isinstance(data, list):
+                print(f"[错误] 返回数据格式不正确")
+                return None
+
+            df = pd.DataFrame(data)
+
+            # 标准化列名
+            column_mapping = {
+                'd': 'DateTime',
+                'o': 'Open',
+                'h': 'High',
+                'l': 'Low',
+                'c': 'Close',
+                'v': 'Volume',
+                'e': 'Amount',
+                'zf': 'Change_Pct',  # 涨跌幅
+                'zde': 'Change_Amount'  # 涨跌额
+            }
+
+            df.rename(columns=column_mapping, inplace=True)
+
+            if 'DateTime' in df.columns:
+                df['DateTime'] = pd.to_datetime(df['DateTime'])
+                df.set_index('DateTime', inplace=True)
+                df.sort_index(inplace=True)
+
+            return df
+
+        except Exception as e:
+            print(f"[错误] 获取历史数据失败: {e}")
+            return None
+
+    def get_cyb50_30min_2023_to_now(self):
+        """
+        获取创业板50指数2023年至今的30分钟数据
+        """
+        print("=" * 70)
+        print("麦儒数据 - 创业板50指数30分钟数据获取")
+        print("=" * 70)
+
+        # 设置时间范围
+        start_date = "20230101"
+        end_date = datetime.now().strftime("%Y%m%d")
+
+        print(f"时间范围: {start_date} 至 {end_date}")
+
+        # 获取30分钟数据
+        data = self.get_index_minute_data(
+            index_code="399673.SZ",
+            period="30",
+            start_date=start_date,
+            end_date=end_date
+        )
+
+        if data is not None and not data.empty:
+            # 筛选2023年至今的数据
+            start_date = pd.to_datetime("2023-01-01")
+            data = data[data.index >= start_date]
+
+            print(f"\n筛选后数据: {len(data)} 条")
+            print(f"时间范围: {data.index[0]} 至 {data.index[-1]}")
+
+            return data
+
+        return None
+
+    def save_data(self, data, filename=None):
+        """保存数据到CSV和TXT格式"""
+        if data is None or data.empty:
+            print("[错误] 没有数据可保存")
+            return None
+
+        if filename is None:
+            timestamp = datetime.now().strftime("%Y%m%d")
+            filename = f"cyb50_30min_2023_to_{timestamp}.csv"
+
+        filepath = os.path.join(self.data_dir, filename)
+
+        # 保存CSV
+        data.to_csv(filepath, encoding='utf-8-sig')
+
+        # 保存TXT(兼容原有格式)
+        txt_filename = filename.replace('.csv', '.txt')
+        txt_filepath = os.path.join(self.data_dir, txt_filename)
+
+        with open(txt_filepath, 'w', encoding='utf-8') as f:
+            f.write("创业板50指数 30分钟数据 (来源:麦儒数据)\n")
+            f.write("Date Time Open High Low Close Volume Amount\n")
+            for idx, row in data.iterrows():
+                date_str = idx.strftime('%Y/%m/%d')
+                time_str = idx.strftime('%H%M')
+                f.write(f"{date_str} {time_str} "
+                        f"{row['Open']:.2f} {row['High']:.2f} {row['Low']:.2f} {row['Close']:.2f} "
+                        f"{row['Volume']:.0f} {row.get('Amount', 0):.2f}\n")
+
+        print(f"\n[保存成功]")
+        print(f"  CSV: {filepath}")
+        print(f"  TXT: {txt_filepath}")
+        print(f"  记录数: {len(data)}")
+        print(f"  文件大小: {os.path.getsize(filepath) / 1024:.2f} KB")
+
+        return filepath
+
+
+def main():
+    """主函数"""
+    print("\n" + "=" * 70)
+    print("  麦儒数据 - 创业板50指数 30分钟K线数据获取")
+    print("  时间范围: 2023年至今")
+    print("=" * 70 + "\n")
+
+    fetcher = MairuiDataFetcher()
+
+    # 获取数据
+    data = fetcher.get_cyb50_30min_2023_to_now()
+
+    if data is not None and not data.empty:
+        # 保存数据
+        fetcher.save_data(data)
+
+        # 数据统计
+        print("\n" + "=" * 70)
+        print("数据统计")
+        print("=" * 70)
+        print(f"总记录数: {len(data)}")
+        print(f"交易日数: {len(set(data.index.date))}")
+        print(f"时间范围: {data.index[0]} 至 {data.index[-1]}")
+        print(f"\n价格统计:")
+        print(f"  最高价: {data['High'].max():.2f}")
+        print(f"  最低价: {data['Low'].min():.2f}")
+        print(f"  最新价: {data['Close'].iloc[-1]:.2f}")
+        print(f"  涨跌幅: {(data['Close'].iloc[-1] / data['Close'].iloc[0] - 1) * 100:.2f}%")
+        print("=" * 70 + "\n")
+    else:
+        print("\n[错误] 数据获取失败\n")
+
+
+if __name__ == "__main__":
+    main()

+ 398 - 0
cat-fly/t1/auto_report_long_only_t1.py

@@ -0,0 +1,398 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+创业板50指数 - 只做多T+1自动化交易报告系统
+基于 cyb50_30min_long_only_t1.py 策略
+"""
+
+import sys
+import os
+
+# 设置 stdout 编码为 utf-8
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+# 调试版本:所有依赖文件在同一目录下,无需额外路径设置
+# 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, compare_results
+
+# ==================== 邮件配置 ====================
+EMAIL_CONFIG = {
+    "smtp_server": "localhost",
+    "smtp_port": 25,
+    "sender_email": "cyb50-t1@erwin.wang",
+    "receiver_emails": ["380880504@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 check_today_trades(trades_df):
+    """检查当天是否有交易
+    
+    返回:
+        has_today_trade: bool, 当天是否有交易
+        today_trades: DataFrame, 当天的交易记录
+    """
+    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):
+    """生成只做多T+1交易报告(基于多空版本转换后的数据)"""
+    
+    if len(trades_df) == 0:
+        final_capital = initial_capital
+        total_return = 0
+        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; }}
+        </style></head><body>
+        <h1>🚀 创业板50交易报告 (T+1)</h1>
+        <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
+        <p>数据区间: 近2个月</p>
+        <p><b>近2个月无交易信号触发</b></p>
+        <p>初始资金: {initial_capital:,.0f}元</p>
+        <p>最终资金: {final_capital:,.0f}元</p>
+        <p>收益率: {total_return:+.2f}%</p>
+        </body></html>
+        """
+        text = f"近2个月无交易信号\n初始资金: {initial_capital:,.0f}元\n最终资金: {final_capital:,.0f}元"
+        return html, text, final_capital
+    
+    # 计算统计数据
+    final_capital = trades_df['平仓时资金'].iloc[-1]
+    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_win = len(t1_adjusted[t1_adjusted['盈亏金额'] > 0]) if len(t1_adjusted) > 0 else 0
+    t1_pnl = t1_adjusted['盈亏金额'].sum() if len(t1_adjusted) > 0 else 0
+    
+    # 非T+1调整交易
+    t1_normal = trades_df[trades_df['T+1调整'] != '是(T0→T1)']
+    normal_count = len(t1_normal)
+    normal_win = len(t1_normal[t1_normal['盈亏金额'] > 0]) if len(t1_normal) > 0 else 0
+    normal_pnl = t1_normal['盈亏金额'].sum() if len(t1_normal) > 0 else 0
+    
+    # HTML报告 - 使用原版cat-fly蓝色主题
+    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>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
+    <p>数据区间: 近2个月</p>
+    <p><b>数据来源: 多空双向策略的做多交易 + T+1规则转换</b></p>
+    
+    <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>🔄 交易统计 (T+1调整影响)</h2>
+    <table>
+        <tr><th>类型</th><th>交易次数</th><th>胜率</th><th>总盈亏</th></tr>
+        <tr>
+            <td>正常交易</td><td>{normal_count}笔</td>
+            <td>{(normal_win/normal_count*100 if normal_count>0 else 0):.1f}%</td>
+            <td class="{'positive' if normal_pnl >= 0 else 'negative'}">{normal_pnl:+,.0f}元</td>
+        </tr>
+        <tr class="{'highlight' if t1_count > 0 else ''}">
+            <td>T+1调整(T0→T1)</td><td>{t1_count}笔</td>
+            <td>{(t1_win/t1_count*100 if t1_count>0 else 0):.1f}%</td>
+            <td class="{'positive' if t1_pnl >= 0 else 'negative'}">{t1_pnl:+,.0f}元</td>
+        </tr>
+    </table>
+    
+    <h2>📈 T+1调整明细</h2>
+    <table>
+        <tr><th>开仓时间</th><th>原平仓</th><th>新平仓</th><th>原盈亏</th><th>新盈亏</th><th>变化</th></tr>
+    """
+    
+    for _, trade in t1_adjusted.iterrows():
+        change = trade['盈亏变化']
+        change_class = "positive" if change >= 0 else "negative"
+        html += f"""
+        <tr>
+            <td>{trade['开仓时间'].strftime('%m-%d %H:%M')}</td>
+            <td>{trade['原平仓时间'].strftime('%m-%d %H:%M')}</td>
+            <td>{trade['平仓时间'].strftime('%m-%d %H:%M')}</td>
+            <td>{trade['原盈亏']:+.0f}</td>
+            <td class="{'positive' if trade['盈亏金额'] >= 0 else 'negative'}">{trade['盈亏金额']:+.0f}</td>
+            <td class="{change_class}">{change:+.0f}</td>
+        </tr>
+        """
+    
+    if len(t1_adjusted) == 0:
+        html += "<tr><td colspan='6'>无T+1调整交易</td></tr>"
+    
+    html += """
+    </table>
+    
+    <h2>📝 最近20笔交易明细</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():
+        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>
+        """
+    
+    html += "</table></body></html>"
+    
+    # 纯文本报告
+    text = f"""
+创业板50交易报告 (T+1)
+生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+数据来源: 多空双向策略的做多交易 + T+1规则转换
+
+【总体绩效】
+初始资金: {initial_capital:,.0f}元
+最终资金: {final_capital:,.0f}元
+总收益率: {total_return:+.2f}%
+总交易次数: {total_trades}笔
+胜率: {win_rate:.1f}%
+盈亏比: {profit_factor:.2f}
+
+【T+1调整统计】
+正常交易: {normal_count}笔, 盈亏{normal_pnl:+,.0f}元
+T+1调整(T0→T1): {t1_count}笔, 盈亏{t1_pnl:+,.0f}元
+
+【最近20笔交易】
+{trades_df.tail(20)[['开仓时间', '平仓时间', '盈亏金额', '退出原因']].to_string(index=False)}
+"""
+    
+    return html, text, final_capital
+
+
+def main():
+    """主程序 - 使用多空版本做多交易 + T+1规则转换"""
+    print("="*80)
+    print("🚀 创业板50只做多T+1自动交易报告系统")
+    print("(基于多空双向策略的做多交易 + T+1规则转换)")
+    print("="*80)
+    print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+    
+    initial_capital = 1000000
+    
+    # 1. 获取数据
+    print("\n📊 步骤1: 获取近2个月数据...")
+    try:
+        config_manager = ConfigManager('config.json')
+        fetcher = IntradayDataFetcher(config_manager)
+        end_date = datetime.now()
+        start_date = end_date - timedelta(days=70)  # 2个月+10天缓冲
+        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. 生成多空双向信号并执行
+    print("\n📈 步骤2: 运行多空双向策略获取做多交易...")
+    try:
+        # 计算技术指标
+        print("   计算技术指标...")
+        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
+    
+    # 3. 应用T+1规则转换
+    print("\n🔄 步骤3: 应用T+1规则转换...")
+    try:
+        t1_trades = simulate_t1_trades(data_with_indicators, long_trades, initial_capital)
+        print(f"✅ T+1转换完成: {len(t1_trades)}笔交易")
+        
+        # 统计T+1调整
+        t1_adjusted = t1_trades[t1_trades['T+1调整'] == '是(T0→T1)']
+        print(f"   T+1调整交易: {len(t1_adjusted)}笔")
+        
+    except Exception as e:
+        print(f"❌ T+1转换失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return
+    
+    # 4. 检查当天交易并决定是否发送邮件
+    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 is_post_close_time():
+        # 没有当天交易,但在盘后时间(15:00-15:30),发送一次
+        should_send = True
+        send_reason = "盘后时间,当天无交易,发送例行报告"
+    else:
+        # 没有当天交易,也不在盘后时间,跳过发送
+        should_send = False
+        send_reason = "当天无交易,非盘后时间,跳过发送"
+    
+    print(f"\n📧 邮件发送决策: {send_reason}")
+    
+    if should_send:
+        # 5. 生成报告
+        print("\n📝 步骤5: 生成报告...")
+        html_report, text_report, final_capital = generate_report(t1_trades, initial_capital)
+        
+        # 6. 发送邮件
+        print("\n📧 步骤6: 发送邮件...")
+        total_trades = len(t1_trades)
+        total_return = (final_capital/initial_capital-1)*100
+        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()

+ 16 - 0
cat-fly/t1/config.json

@@ -0,0 +1,16 @@
+{
+  "data_source": {
+    "use_local_file": false,
+    "local_file_path": "D:\\work\\project\\catfly\\SZ#399673.txt"
+  },
+  "strategy": {
+    "initial_capital": 1000000,
+    "backtest_start_date": "2026-01-01",
+    "backtest_end_date": "2026-03-05",
+    "prewamp_days": 30,
+    "position_size_pct": 1.0,
+    "stop_loss_pct": 0.008,
+    "take_profit_pct": 0.02,
+    "max_hold_bars": 16
+  }
+}

Plik diff jest za duży
+ 5801 - 0
cat-fly/t1/cyb50_30min_2023_to_20260325.csv


Plik diff jest za duży
+ 1385 - 0
cat-fly/t1/cyb50_30min_dual_direction.py


+ 320 - 0
cat-fly/t1/t1_converter.py

@@ -0,0 +1,320 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+CYB50 T+1 转换器 - 基于多空版本的做多交易转换为T+1规则
+规则:
+1. 提取多空版本中的所有做多交易
+2. 买入当天不能卖出(T+1限制)
+3. 如果原交易是T0(当天买卖),则延期到T+1开盘卖出
+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 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
+    
+    open_time = next_day_data.index[0]
+    open_price = next_day_data.iloc[0]['Open']  # 使用开盘价
+    
+    return open_time, open_price
+
+
+def simulate_t1_trades(data_df, long_trades_df, initial_capital=1000000):
+    """模拟T+1规则下的交易
+    
+    规则:
+    - 买入当天不能卖出
+    - 如果原T0交易(当天买卖),延期到T+1开盘卖出
+    - 卖出后当天可以再买(这是关键特性)
+    """
+    print("\n" + "="*80)
+    print("T+1规则转换 - 基于多空版本做多交易")
+    print("="*80)
+    
+    if len(long_trades_df) == 0:
+        print("没有做多交易记录")
+        return pd.DataFrame()
+    
+    # 按开仓时间排序
+    long_trades_df = long_trades_df.sort_values('开仓时间').reset_index(drop=True)
+    
+    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('入场信号', '')
+        
+        entry_date = entry_time.date()
+        exit_date = original_exit_time.date()
+        
+        # 判断是否是T0交易
+        is_t0 = (entry_date == exit_date)
+        
+        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
+                t1_adjusted = False
+            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
+        
+        # 非T0交易,保持原样
+        hold_hours = trade['持仓小时数']
+        
+        # 计算盈亏(使用原始数据)
+        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
+        
+        capital += pnl
+        
+        trade_record = {
+            '交易方向': '做多',
+            '开仓时间': entry_time,
+            '平仓时间': original_exit_time,
+            '开仓价格': entry_price,
+            '平仓价格': original_exit_price,
+            '仓位': position_size,
+            '盈亏金额': pnl,
+            '盈亏百分比': pnl_pct,
+            '退出原因': trade['退出原因'],
+            '持仓周期数': trade['持仓周期数'],
+            '持仓小时数': hold_hours,
+            'T+1调整': '否',
+            '原平仓时间': original_exit_time,
+            '原平仓价格': original_exit_price,
+            '原盈亏': trade['盈亏金额'],
+            '盈亏变化': 0,
+            '入场信号': entry_signals,
+            '开仓市值': position_size * entry_price,
+            '平仓时资金': capital,
+        }
+        t1_trades.append(trade_record)
+    
+    t1_trades_df = pd.DataFrame(t1_trades)
+    
+    return t1_trades_df
+
+
+def compare_results(original_trades, t1_trades, initial_capital=1000000):
+    """对比原始交易和T+1转换后的结果"""
+    print("\n" + "="*80)
+    print("T+1转换前后对比")
+    print("="*80)
+    
+    # 原始统计
+    original_total_pnl = original_trades['盈亏金额'].sum()
+    original_final = initial_capital + original_total_pnl
+    original_return = (original_final / initial_capital - 1) * 100
+    original_win_rate = (original_trades['盈亏金额'] > 0).sum() / len(original_trades) * 100
+    
+    # T+1统计
+    t1_total_pnl = t1_trades['盈亏金额'].sum()
+    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
+    
+    # T0交易统计
+    t0_adjusted = t1_trades[t1_trades['T+1调整'] == '是(T0→T1)']
+    
+    print(f"\n【原始交易(T0规则)】")
+    print(f"  交易次数: {len(original_trades)}")
+    print(f"  总盈亏: {original_total_pnl:+,.2f}元")
+    print(f"  最终资金: {original_final:,.2f}元")
+    print(f"  收益率: {original_return:+.2f}%")
+    print(f"  胜率: {original_win_rate:.1f}%")
+    
+    print(f"\n【T+1转换后】")
+    print(f"  交易次数: {len(t1_trades)}")
+    print(f"  总盈亏: {t1_total_pnl:+,.2f}元")
+    print(f"  最终资金: {t1_final:,.2f}元")
+    print(f"  收益率: {t1_return:+.2f}%")
+    print(f"  胜率: {t1_win_rate:.1f}%")
+    
+    print(f"\n【T+1调整统计】")
+    print(f"  T0→T1调整交易数: {len(t0_adjusted)}笔")
+    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}%")
+    print(f"  绝对盈亏差: {(t1_total_pnl - original_total_pnl):+,.2f}元")
+
+
+def main():
+    """主程序"""
+    print("="*80)
+    print("CYB50 T+1 交易转换器")
+    print("基于多空版本的做多交易,应用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(data_with_indicators, long_trades, initial_capital)
+    
+    # 3. 对比结果
+    print("\n【步骤3】对比分析...")
+    compare_results(long_trades, t1_trades, initial_capital)
+    
+    # 4. 导出结果
+    if len(t1_trades) > 0:
+        print("\n【步骤4】导出T+1交易记录...")
+        
+        # 格式化时间
+        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_converted_trades_{timestamp}.csv'
+        export_df.to_csv(output_file, index=False, encoding='utf-8-sig')
+        print(f"✅ T+1交易记录已保存: {output_file}")
+    
+    print("\n" + "="*80)
+    print("转换完成!")
+    print("="*80)
+
+
+if __name__ == "__main__":
+    main()