问题描述
解决方案
在PTrade中,想要获取策略运行以来的所有成交记录并进行绩效分析(如胜率、盈亏比),不能使用仅返回当日成交的 get_trades() 函数。
你需要使用 get_trades_file() 接口。该接口会在回测的 after_trading_end 阶段生成一个包含所有历史成交记录的CSV文件路径。
实现思路
- 获取数据:在
after_trading_end中调用get_trades_file()获取CSV文件路径,并使用pandas读取。 - 数据清洗:区分买入和卖出操作。
- 配对计算 (FIFO):为了计算胜率和盈亏比,必须将“卖出”与对应的“买入”进行配对(通常采用先进先出 FIFO 原则),从而计算每一笔完整交易(Round-trip)的盈亏。
- 统计指标:基于配对后的盈亏数据计算胜率、盈亏比等。
完整策略代码示例
以下代码包含了一个简单的均线策略用于生成交易数据,以及核心的 绩效统计逻辑。请将此代码复制到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
}
代码关键点解析
-
get_trades_file():- 这是获取全量数据的关键。它返回一个CSV文件的绝对路径。
- 该文件包含了回测开始至今的所有委托和成交详情。
-
FIFO (先进先出) 匹配逻辑:
- PTrade的原始数据是流水账(买入一笔、卖出一笔)。为了计算“胜率”,我们需要定义什么是“一次交易”。
- 代码中
calculate_performance_metrics函数使用了一个队列long_positions。 - 当遇到买入时,将价格和数量存入队列。
- 当遇到卖出时,从队列头部取出最早的买入单进行匹配,计算差价。这能准确反映每一轮(Round-trip)操作的实际盈亏。
-
指标计算:
- 胜率 (Win Rate) = 盈利次数 / 总平仓次数。
- 盈亏比 (P/L Ratio) = 平均单笔盈利金额 / 平均单笔亏损金额(绝对值)。
-
注意事项:
- 编码问题:
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操作,盘中频繁调用会影响策略执行效率,且盘中数据可能未完全写入文件。建议仅在盘后处理。