问题描述
解决方案
在 QMT 中,要导出回测的每一笔交易明细,最标准的方法是利用策略生命周期中的 stop(ContextInfo) 函数。
stop() 函数会在回测结束时自动运行一次。此时,我们可以调用 get_trade_detail_data 获取整个回测期间产生的所有成交记录,将其转换为 DataFrame 并导出为 CSV 文件。
以下是具体的实现步骤和代码示例。
核心思路
- 策略运行:在
handlebar中正常进行买卖操作。 - 回测结束:在
stop函数中,使用get_trade_detail_data(..., 'DEAL')获取所有成交单对象。 - 数据解析:遍历成交对象列表,提取日期、时间、代码、方向、价格、数量、手续费等关键字段。
- 数据导出:使用
pandas库将数据保存为 CSV 文件,方便后续在 Excel 或 Python 中计算盈亏比、最大连亏等指标。
完整代码示例
你可以将以下代码复制到 QMT 的 Python 编辑器中进行回测。代码包含了一个简单的均线策略用于生成交易数据,以及核心的导出逻辑。
# -*- coding: gbk -*-
import pandas as pd
import os
def init(ContextInfo):
# 1. 设置账号信息 (回测模式下账号ID通常可以随意填,或者使用默认)
ContextInfo.accID = '6000000000'
ContextInfo.accountType = 'STOCK'
ContextInfo.set_account(ContextInfo.accID)
# 2. 策略参数设置 (示例:简单的均线策略)
ContextInfo.stock = '600000.SH'
ContextInfo.set_universe([ContextInfo.stock])
ContextInfo.period = 5 # 5日均线
# 3. 设置导出路径 (请确保D盘存在,或者修改为你电脑上存在的路径)
ContextInfo.export_path = 'D:/qmt_backtest_records.csv'
def handlebar(ContextInfo):
# --- 示例策略逻辑:为了生成交易记录 ---
# 获取历史收盘价
closes = ContextInfo.get_history_data(ContextInfo.period + 1, '1d', 'close')
if ContextInfo.stock not in closes:
return
close_list = closes[ContextInfo.stock]
if len(close_list) < ContextInfo.period:
return
ma5 = sum(close_list[-5:]) / 5
current_price = close_list[-1]
# 获取当前持仓
positions = get_trade_detail_data(ContextInfo.accID, ContextInfo.accountType, 'POSITION')
curr_vol = 0
for pos in positions:
if pos.m_strInstrumentID == ContextInfo.stock:
curr_vol = pos.m_nVolume
break
# 简单的交易逻辑
if current_price > ma5 and curr_vol == 0:
# 均线上方买入
order_shares(ContextInfo.stock, 1000, 'fix', current_price, ContextInfo, ContextInfo.accID)
elif current_price < ma5 and curr_vol > 0:
# 均线下方卖出
order_shares(ContextInfo.stock, -curr_vol, 'fix', current_price, ContextInfo, ContextInfo.accID)
def stop(ContextInfo):
"""
回测结束时触发,用于导出交易记录
"""
print("回测结束,开始导出交易记录...")
# 1. 获取所有成交记录 ('DEAL')
# 注意:这里获取的是回测期间产生的所有成交明细
deal_list = get_trade_detail_data(ContextInfo.accID, ContextInfo.accountType, 'DEAL')
if not deal_list:
print("本次回测无成交记录。")
return
# 2. 解析成交对象属性
data = []
for deal in deal_list:
# 解析买卖方向:48代表买入,49代表卖出 (参考API文档附录)
direction = "买入" if deal.m_nDirection == 48 else "卖出"
record = {
"日期": deal.m_strTradeDate,
"时间": deal.m_strTradeTime,
"代码": deal.m_strInstrumentID,
"名称": deal.m_strInstrumentName,
"方向": direction,
"成交均价": deal.m_dPrice,
"成交数量": deal.m_nVolume,
"成交金额": deal.m_dTradeAmount,
"手续费": deal.m_dComssion,
"平仓盈亏": deal.m_dCloseProfit, # 注意:股票回测中此字段可能不准确,通常需自己计算
"备注": deal.m_strRemark
}
data.append(record)
# 3. 转换为DataFrame并导出
df = pd.DataFrame(data)
# 按日期和时间排序
df = df.sort_values(by=['日期', '时间'])
try:
# 导出为CSV,使用gbk编码防止中文乱码
df.to_csv(ContextInfo.export_path, index=False, encoding='gbk')
print(f"交易记录已成功导出至: {ContextInfo.export_path}")
print(f"共导出 {len(df)} 条成交记录")
except Exception as e:
print(f"导出失败: {e}")
print("请检查路径是否存在或文件是否被占用。")
关键点说明
-
get_trade_detail_data(..., 'DEAL'):- 这是核心函数。参数
'DEAL'表示获取成交明细。 - 返回的是一个对象列表,每个对象包含该笔成交的详细信息。
- 这是核心函数。参数
-
对象属性解析:
m_strTradeDate/m_strTradeTime: 交易时间。m_nDirection: 买卖方向。QMT底层使用枚举值,48 代表买入 (ENTRUST_BUY),49 代表卖出 (ENTRUST_SELL)。m_dPrice: 成交价格。m_nVolume: 成交数量。m_dComssion: 手续费。
-
关于盈亏计算:
- 虽然成交对象中有
m_dCloseProfit(平仓盈亏) 属性,但在股票回测模式下,该字段有时可能不会自动计算或计算逻辑与预期不同(它在期货回测中更准确)。 - 建议:导出上述 CSV 后,在 Excel 或 Python 中通过“先进先出”或“平均成本法”自行匹配买单和卖单来计算每一笔完整交易(Round-trip)的盈亏。
- 虽然成交对象中有
如何在外部计算指标(思路)
导出 CSV 后,你可以使用 Python (Pandas) 进行如下分析:
- 计算单笔盈亏:将“卖出金额 - 对应买入成本 - 手续费”得到净盈亏。
- 盈亏比:
总盈利金额 / abs(总亏损金额)或平均盈利金额 / abs(平均亏损金额)。 - 最大连亏次数:
- 按时间排序交易。
- 标记每笔交易是盈是亏(1为盈,-1为亏)。
- 计算连续出现的 -1 的最大长度。
Q&A
Q: 为什么我导出的 CSV 是乱码?
A: 代码中使用了 encoding='gbk',这是为了适配 Excel 打开中文 CSV 的默认编码。如果你使用其他软件(如 Mac 上的 Numbers 或某些文本编辑器),可能需要将其改为 encoding='utf-8-sig'。
Q: stop 函数没有执行怎么办?
A: 确保你的回测正常跑完了全程。如果中途手动停止或报错中断,stop 函数可能不会被触发。
Q: 能否导出委托记录(包括未成交的)?
A: 可以。将 get_trade_detail_data 的第三个参数改为 'ORDER' 即可获取所有委托单,包括已撤单和废单。