🚀 AI 一键生成 ptrade 策略代码

如何使用 get_trades API 在 PTrade 中进行深度的交易复盘与绩效分析?

ptrade | 发布时间: 2025/12/17
以下内容由 EasyQuant 生成。

问题描述

如何获取我策略的所有成交记录,并自己计算胜率和盈亏比等指标?

解决方案

在PTrade中,想要获取策略运行以来的所有成交记录并进行绩效分析(如胜率、盈亏比),不能使用仅返回当日成交的 get_trades() 函数。

你需要使用 get_trades_file() 接口。该接口会在回测的 after_trading_end 阶段生成一个包含所有历史成交记录的CSV文件路径。

实现思路

  1. 获取数据:在 after_trading_end 中调用 get_trades_file() 获取CSV文件路径,并使用 pandas 读取。
  2. 数据清洗:区分买入和卖出操作。
  3. 配对计算 (FIFO):为了计算胜率和盈亏比,必须将“卖出”与对应的“买入”进行配对(通常采用先进先出 FIFO 原则),从而计算每一笔完整交易(Round-trip)的盈亏。
  4. 统计指标:基于配对后的盈亏数据计算胜率、盈亏比等。

完整策略代码示例

以下代码包含了一个简单的均线策略用于生成交易数据,以及核心的 绩效统计逻辑。请将此代码复制到PTrade回测环境中运行。

import pandas as pd
import numpy as np

def initialize(context):
    # 设置股票池,这里以恒生电子为例
    g.security = '600570.SS'
    set_universe(g.security)
    
    # 设置一些回测参数
    set_commission(commission_ratio=0.0003, min_commission=5.0)
    set_slippage(slippage=0.0)

def handle_data(context, data):
    # --- 这是一个简单的双均线策略,目的是为了产生交易记录以便后续计算 ---
    security = g.security
    
    # 获取历史收盘价
    hist = get_history(20, '1d', 'close', security, fq=None, include=False)
    if len(hist) < 20:
        return
        
    close_arr = hist['close'].values
    ma5 = close_arr[-5:].mean()
    ma20 = close_arr[-20:].mean()
    
    curr_price = data[security]['close']
    position = get_position(security).amount
    cash = context.portfolio.cash
    
    # 金叉买入
    if ma5 > ma20 and position == 0:
        order_value(security, cash)
        # log.info("买入 %s" % security)
        
    # 死叉卖出
    elif ma5 < ma20 and position > 0:
        order_target(security, 0)
        # log.info("卖出 %s" % security)

def after_trading_end(context, data):
    # 获取当前回测日期
    current_date = context.blotter.current_dt.strftime("%Y-%m-%d")
    
    # 注意:为了节省计算资源,通常建议只在回测的最后一天进行全量统计
    # 这里为了演示,每次盘后都计算一次。实际使用建议判断日期。
    
    # 1. 获取成交记录文件路径
    trade_path = get_trades_file()
    
    if not trade_path:
        return

    try:
        # 2. 读取CSV文件
        # PTrade导出的CSV通常包含中文表头,pandas读取时能自动处理
        df = pd.read_csv(trade_path, encoding='gbk') # 或者是 utf-8,视环境而定,通常gbk兼容性好
        
        # 如果没有交易记录,直接返回
        if len(df) == 0:
            return

        # 3. 调用自定义函数计算指标
        stats = calculate_performance_metrics(df)
        
        # 4. 打印结果
        log.info("======== 策略绩效统计 (%s) ========" % current_date)
        log.info("总交易次数(平仓): %d" % stats['total_trades'])
        log.info("胜率: %.2f%%" % (stats['win_rate'] * 100))
        log.info("盈亏比: %.2f" % stats['pl_ratio'])
        log.info("总盈利: %.2f" % stats['total_profit'])
        log.info("最大单笔盈利: %.2f" % stats['max_win'])
        log.info("最大单笔亏损: %.2f" % stats['max_loss'])
        log.info("========================================")
        
    except Exception as e:
        log.error("计算绩效指标时出错: %s" % str(e))

def calculate_performance_metrics(df):
    """
    核心计算逻辑:使用FIFO(先进先出)原则匹配买卖交易,计算盈亏
    """
    # 映射表头,确保兼容性 (根据PTrade文档)
    # 文档表头: 订单编号,成交编号,委托编号,标的代码,交易类型,成交数量,成交价,成交金额,交易费用,交易时间
    # 对应的英文列名通常是: order_id, trading_id, entrust_id, security_code, order_type, volume, price, total_money, trading_fee, trade_time
    
    # 简单的列名清洗,去除空格
    df.columns = [c.strip() for c in df.columns]
    
    # 按照交易时间排序
    if 'trade_time' in df.columns:
        df = df.sort_values('trade_time')
    
    # 存储每笔平仓交易的盈亏
    pnl_list = []
    
    # 按股票代码分组处理
    grouped = df.groupby('security_code')
    
    for code, group in grouped:
        # 持仓队列:存储 [价格, 数量]
        long_positions = [] 
        
        for index, row in group.iterrows():
            order_type = str(row['order_type']) # 交易类型:买入/卖出
            price = float(row['price'])
            volume = abs(int(row['volume']))
            commission = float(row['trading_fee'])
            
            # 判断方向 (PTrade中通常 "买" 或 "Buy" 代表买入)
            if "买" in order_type or "Buy" in order_type or order_type == '1':
                # 买入:加入持仓队列
                # 记录成本时,将手续费分摊到单价中,或者单独计算。这里简化处理,手续费在平仓时统一扣除或忽略
                # 为了精确计算,我们将手续费视为成本的一部分
                cost_price = price # 暂不含佣金的纯价格
                long_positions.append({'price': cost_price, 'vol': volume, 'fee': commission})
                
            elif "卖" in order_type or "Sell" in order_type or order_type == '2':
                # 卖出:进行FIFO配对
                sell_vol_left = volume
                trade_profit = 0.0
                trade_cost = 0.0 # 对应的买入成本
                
                # 扣除卖出的手续费
                trade_profit -= commission
                
                while sell_vol_left > 0 and len(long_positions) > 0:
                    buy_pos = long_positions[0] # 取出最早的一笔买入
                    
                    matched_vol = min(sell_vol_left, buy_pos['vol'])
                    
                    # 计算这部分数量的盈亏: (卖价 - 买价) * 数量
                    profit = (price - buy_pos['price']) * matched_vol
                    
                    # 扣除对应比例的买入手续费
                    buy_fee_part = buy_pos['fee'] * (matched_vol / (buy_pos['vol'] + matched_vol - sell_vol_left if buy_pos['vol'] > matched_vol else buy_pos['vol'])) 
                    # 上面的手续费计算比较复杂,简化为:按比例扣除买入时的手续费
                    # 简单处理:如果完全消耗掉这笔买入,扣除全部买入费;否则按比例
                    if matched_vol == buy_pos['vol']:
                         trade_profit -= buy_pos['fee']
                    else:
                         trade_profit -= buy_pos['fee'] * (matched_vol / buy_pos['vol'])
                         # 更新剩余买入单的手续费
                         long_positions[0]['fee'] -= buy_pos['fee'] * (matched_vol / buy_pos['vol'])

                    trade_profit += profit
                    
                    sell_vol_left -= matched_vol
                    long_positions[0]['vol'] -= matched_vol
                    
                    # 如果该笔买入单全部匹配完,从队列移除
                    if long_positions[0]['vol'] <= 0:
                        long_positions.pop(0)
                
                # 记录这一笔卖出操作产生的总盈亏
                # 注意:如果sell_vol_left > 0 说明有卖空行为或者数据缺失,这里暂忽略卖空逻辑
                pnl_list.append(trade_profit)

    # --- 统计指标计算 ---
    if len(pnl_list) == 0:
        return {
            'total_trades': 0, 'win_rate': 0, 'pl_ratio': 0, 
            'total_profit': 0, 'max_win': 0, 'max_loss': 0
        }
    
    pnl_array = np.array(pnl_list)
    
    # 1. 盈利交易
    winning_trades = pnl_array[pnl_array > 0]
    # 2. 亏损交易
    losing_trades = pnl_array[pnl_array <= 0]
    
    # 总交易次数
    total_trades = len(pnl_list)
    
    # 胜率
    win_rate = len(winning_trades) / total_trades if total_trades > 0 else 0
    
    # 平均盈利
    avg_win = winning_trades.mean() if len(winning_trades) > 0 else 0
    # 平均亏损 (取绝对值)
    avg_loss = abs(losing_trades.mean()) if len(losing_trades) > 0 else 0
    
    # 盈亏比
    pl_ratio = avg_win / avg_loss if avg_loss > 0 else float('inf')
    
    return {
        'total_trades': total_trades,
        'win_rate': win_rate,
        'pl_ratio': pl_ratio,
        'total_profit': pnl_array.sum(),
        'max_win': pnl_array.max() if len(pnl_array) > 0 else 0,
        'max_loss': pnl_array.min() if len(pnl_array) > 0 else 0
    }

代码关键点解析

  1. get_trades_file():

    • 这是获取全量数据的关键。它返回一个CSV文件的绝对路径。
    • 该文件包含了回测开始至今的所有委托和成交详情。
  2. FIFO (先进先出) 匹配逻辑:

    • PTrade的原始数据是流水账(买入一笔、卖出一笔)。为了计算“胜率”,我们需要定义什么是“一次交易”。
    • 代码中 calculate_performance_metrics 函数使用了一个队列 long_positions
    • 当遇到买入时,将价格和数量存入队列。
    • 当遇到卖出时,从队列头部取出最早的买入单进行匹配,计算差价。这能准确反映每一轮(Round-trip)操作的实际盈亏。
  3. 指标计算:

    • 胜率 (Win Rate) = 盈利次数 / 总平仓次数。
    • 盈亏比 (P/L Ratio) = 平均单笔盈利金额 / 平均单笔亏损金额(绝对值)。
  4. 注意事项:

    • 编码问题: pd.read_csv 时,如果遇到乱码,请尝试 encoding='gbk'encoding='utf-8-sig'
    • 运行频率: after_trading_end 每天都会运行。如果在回测时间跨度很长、交易非常频繁的情况下,每天都读取并解析大文件会拖慢回测速度。建议在代码中加入日期判断,仅在回测结束日期执行,或者仅在研究环境中调用。

Q&A

Q: 为什么不直接用 context.portfolio.pnl?
A: context.portfolio.pnl 给出的是账户总的浮动盈亏,它无法告诉你这笔钱是由多少次盈利交易和多少次亏损交易组成的,因此无法计算胜率。

Q: 这段代码支持做空(融券)吗?
A: 上述代码的FIFO逻辑主要针对做多(先买后卖)。如果是期货或融券(先卖后买),需要增加一个 short_positions 队列,逻辑反过来写(卖出入队,买入出队匹配)。

Q: 可以在盘中实时计算吗?
A: 不建议。get_trades_file() 涉及文件IO操作,盘中频繁调用会影响策略执行效率,且盘中数据可能未完全写入文件。建议仅在盘后处理。