问题描述
解决方案
在 QMT (Quantitative Trading Platform) 中,要获取回测的详细交易数据并生成自定义报告,最有效的方法是在策略的 stop(ContextInfo) 函数中(即回测结束时)调用 get_trade_detail_data 接口。
该接口可以获取委托(ORDER)、成交(DEAL)、持仓(POSITION)等详细信息。结合 Python 的 pandas 库,我们可以将这些数据整理成 DataFrame 并导出为 Excel 或 CSV 格式的报告。
核心步骤
- 策略初始化:在
init中设置账号。 - 交易逻辑:在
handlebar中执行买卖操作(生成交易记录)。 - 数据获取与导出:
- 在
stop函数中,使用get_trade_detail_data(account_id, account_type, 'DEAL')获取所有成交记录。 - 遍历返回的对象列表,提取属性(如代码、价格、数量、方向等)。
- 使用
pandas计算简单的统计指标(如总盈亏、交易次数)。 - 将结果保存到本地文件。
- 在
完整代码示例
以下是一个完整的策略代码。它包含了一个简单的均线策略用于生成交易数据,并在回测结束时生成一份包含“交易明细”和“账户资金”的 Excel 报告。
# -*- coding: gbk -*-
import pandas as pd
import os
import datetime
def init(ContextInfo):
# 1. 设置账号 (回测模式下通常使用模拟账号或任意字符串作为ID)
ContextInfo.account_id = 'test_account'
ContextInfo.account_type = 'STOCK'
ContextInfo.set_account(ContextInfo.account_id)
# 2. 策略参数设置
ContextInfo.stock = '600000.SH' # 浦发银行
ContextInfo.set_universe([ContextInfo.stock])
ContextInfo.period = '1d'
ContextInfo.ma_short = 5
ContextInfo.ma_long = 10
# 3. 报告保存路径 (默认保存在QMT安装目录的 userdata 文件夹下,也可指定绝对路径)
ContextInfo.report_path = 'C:/QMT_Reports/'
if not os.path.exists(ContextInfo.report_path):
try:
os.makedirs(ContextInfo.report_path)
except:
ContextInfo.report_path = './' # 如果创建失败则保存在当前目录
def handlebar(ContextInfo):
# 获取当前K线位置
index = ContextInfo.barpos
realtime = ContextInfo.get_bar_timetag(index)
# 获取历史收盘价
close_data = ContextInfo.get_market_data_ex(
['close'],
[ContextInfo.stock],
period=ContextInfo.period,
count=ContextInfo.ma_long + 2,
dividend_type='front'
)
if ContextInfo.stock not in close_data or len(close_data[ContextInfo.stock]) < ContextInfo.ma_long:
return
close_series = close_data[ContextInfo.stock]['close']
# 计算均线
ma_short_val = close_series.rolling(ContextInfo.ma_short).mean().iloc[-1]
ma_long_val = close_series.rolling(ContextInfo.ma_long).mean().iloc[-1]
prev_ma_short = close_series.rolling(ContextInfo.ma_short).mean().iloc[-2]
prev_ma_long = close_series.rolling(ContextInfo.ma_long).mean().iloc[-2]
# 获取当前持仓
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
current_vol = 0
for pos in positions:
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == ContextInfo.stock:
current_vol = pos.m_nVolume
break
# 简单的金叉死叉策略
# 金叉买入
if prev_ma_short < prev_ma_long and ma_short_val > ma_long_val:
if current_vol == 0:
order_shares(ContextInfo.stock, 1000, 'fix', close_series.iloc[-1], ContextInfo, ContextInfo.account_id)
print(f"[{timetag_to_datetime(realtime, '%Y-%m-%d')}] 买入 1000 股")
# 死叉卖出
elif prev_ma_short > prev_ma_long and ma_short_val < ma_long_val:
if current_vol > 0:
order_shares(ContextInfo.stock, -current_vol, 'fix', close_series.iloc[-1], ContextInfo, ContextInfo.account_id)
print(f"[{timetag_to_datetime(realtime, '%Y-%m-%d')}] 卖出 {current_vol} 股")
def stop(ContextInfo):
print("回测结束,开始生成自定义报告...")
# --- 1. 获取成交记录 (DEAL) ---
# get_trade_detail_data 返回的是对象列表,需要解析属性
deals = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'DEAL')
deal_list = []
for d in deals:
# 解析买卖方向: 48=买入, 49=卖出 (参考API文档附录)
direction_str = "买入" if d.m_nDirection == 48 else "卖出"
deal_list.append({
'成交日期': d.m_strTradeDate,
'成交时间': d.m_strTradeTime,
'证券代码': d.m_strInstrumentID,
'证券名称': d.m_strInstrumentName,
'操作方向': direction_str,
'成交价格': d.m_dPrice,
'成交数量': d.m_nVolume,
'成交金额': d.m_dTradeAmount,
'手续费': d.m_dComssion,
'委托号': d.m_strOrderSysID,
'成交编号': d.m_strTradeID
})
df_deals = pd.DataFrame(deal_list)
# --- 2. 获取账户最终状态 (ACCOUNT) ---
accounts = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'ACCOUNT')
account_list = []
for acc in accounts:
account_list.append({
'账号ID': acc.m_strAccountID,
'总资产': acc.m_dBalance,
'可用资金': acc.m_dAvailable,
'持仓市值': acc.m_dStockValue,
'总盈亏': acc.m_dPositionProfit + acc.m_dCloseProfit # 简单估算
})
df_account = pd.DataFrame(account_list)
# --- 3. 生成 Excel 报告 ---
if not df_deals.empty:
# 按日期排序
df_deals = df_deals.sort_values(by=['成交日期', '成交时间'])
# 生成文件名
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
file_name = f"Backtest_Report_{timestamp}.xlsx"
full_path = os.path.join(ContextInfo.report_path, file_name)
try:
# 使用 Pandas ExcelWriter 写入多个 Sheet
with pd.ExcelWriter(full_path) as writer:
df_deals.to_excel(writer, sheet_name='成交明细', index=False)
df_account.to_excel(writer, sheet_name='账户资金', index=False)
# 可以在这里添加更多统计,例如按标的统计盈亏
if '成交金额' in df_deals.columns:
summary = df_deals.groupby('证券代码')[['成交数量', '成交金额', '手续费']].sum()
summary.to_excel(writer, sheet_name='标的汇总')
print(f"自定义交易报告已生成: {full_path}")
except Exception as e:
print(f"生成报告失败: {e}")
print("请检查路径权限或是否安装了 openpyxl/xlsxwriter 库")
else:
print("回测期间无成交记录,未生成报告。")
代码解析
-
stop(ContextInfo)函数:- 这是 QMT 策略生命周期的最后一个环节,仅在回测结束或停止运行时调用一次。这是汇总数据和生成报告的最佳位置。
-
get_trade_detail_data接口:- 这是获取交易数据的核心。
- 参数
'DEAL':获取成交记录。返回的对象包含m_strInstrumentID(代码),m_dPrice(价格),m_nVolume(量),m_nDirection(方向) 等属性。 - 参数
'ACCOUNT':获取资金账号状态。返回的对象包含m_dBalance(总资产),m_dAvailable(可用资金) 等。 - 注意:返回的是 C++ 对象,不能直接打印看内容,必须通过
obj.属性名的方式访问。我在代码中演示了如何将这些属性提取到字典中。
-
数据处理与导出:
- 使用
pandas.DataFrame将提取的列表转换为表格数据。 - 使用
pd.ExcelWriter将不同的数据(成交明细、账户资金、汇总统计)写入同一个 Excel 文件的不同 Sheet 中,方便查看。
- 使用
-
方向代码映射:
- QMT 内部使用整数表示买卖方向(如 48 代表买入,49 代表卖出)。代码中通过
d.m_nDirection == 48进行了转换,使报告更易读。
- QMT 内部使用整数表示买卖方向(如 48 代表买入,49 代表卖出)。代码中通过
常见问题
- 报告保存在哪里?
- 代码中设置了
ContextInfo.report_path。如果设置为'./',通常会保存在 QMT 安装目录下的bin.x64或策略运行的临时目录中。建议指定一个明确的绝对路径(如C:/QMT_Reports/)以确保能找到文件。
- 代码中设置了
- 缺少库报错:
- 导出 Excel 需要 Python 环境安装
openpyxl或xlsxwriter。QMT 内置的 Python 环境通常包含这些库。如果报错,可以尝试改用df.to_csv(..., encoding='gbk')导出为 CSV 文件。
- 导出 Excel 需要 Python 环境安装
Q&A
Q: 如何在报告中包含未成交的委托单?
A: 在 stop 函数中,调用 get_trade_detail_data(..., 'ORDER')。这将返回所有委托记录。你可以检查对象的 m_nOrderStatus 属性(例如 56 表示已成,54 表示已撤)来筛选未成交或已撤单的记录。
Q: 为什么 get_trade_detail_data 返回的数据是空的?
A: 请确保:1. 在 init 中正确调用了 set_account 绑定账号。2. 策略确实触发了下单逻辑(passorder 等函数被调用)。3. 回测的撮合机制成功成交了订单(检查价格是否在当日最高最低价范围内)。
Q: 这个方法适用于实盘吗?
A: 适用。get_trade_detail_data 在实盘模式下同样工作,会返回当日的实际交易数据。如果你想在每天收盘后自动保存当天的交割单,可以在 stop 函数或者设定一个定时任务(run_time)在 15:00 后执行此逻辑。