🚀 AI 一键生成 qmt 策略代码

如何在 QMT 回测结束后生成自定义的绩效分析报告?

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

问题描述

如何获取回测的详细交易数据并生成自定义报告?

解决方案

在 QMT (Quantitative Trading Platform) 中,要获取回测的详细交易数据并生成自定义报告,最有效的方法是在策略的 stop(ContextInfo) 函数中(即回测结束时)调用 get_trade_detail_data 接口。

该接口可以获取委托(ORDER)、成交(DEAL)、持仓(POSITION)等详细信息。结合 Python 的 pandas 库,我们可以将这些数据整理成 DataFrame 并导出为 Excel 或 CSV 格式的报告。

核心步骤

  1. 策略初始化:在 init 中设置账号。
  2. 交易逻辑:在 handlebar 中执行买卖操作(生成交易记录)。
  3. 数据获取与导出
    • 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("回测期间无成交记录,未生成报告。")

代码解析

  1. stop(ContextInfo) 函数

    • 这是 QMT 策略生命周期的最后一个环节,仅在回测结束或停止运行时调用一次。这是汇总数据和生成报告的最佳位置。
  2. get_trade_detail_data 接口

    • 这是获取交易数据的核心。
    • 参数 'DEAL':获取成交记录。返回的对象包含 m_strInstrumentID (代码), m_dPrice (价格), m_nVolume (量), m_nDirection (方向) 等属性。
    • 参数 'ACCOUNT':获取资金账号状态。返回的对象包含 m_dBalance (总资产), m_dAvailable (可用资金) 等。
    • 注意:返回的是 C++ 对象,不能直接打印看内容,必须通过 obj.属性名 的方式访问。我在代码中演示了如何将这些属性提取到字典中。
  3. 数据处理与导出

    • 使用 pandas.DataFrame 将提取的列表转换为表格数据。
    • 使用 pd.ExcelWriter 将不同的数据(成交明细、账户资金、汇总统计)写入同一个 Excel 文件的不同 Sheet 中,方便查看。
  4. 方向代码映射

    • QMT 内部使用整数表示买卖方向(如 48 代表买入,49 代表卖出)。代码中通过 d.m_nDirection == 48 进行了转换,使报告更易读。

常见问题

  • 报告保存在哪里?
    • 代码中设置了 ContextInfo.report_path。如果设置为 './',通常会保存在 QMT 安装目录下的 bin.x64 或策略运行的临时目录中。建议指定一个明确的绝对路径(如 C:/QMT_Reports/)以确保能找到文件。
  • 缺少库报错
    • 导出 Excel 需要 Python 环境安装 openpyxlxlsxwriter。QMT 内置的 Python 环境通常包含这些库。如果报错,可以尝试改用 df.to_csv(..., encoding='gbk') 导出为 CSV 文件。

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 后执行此逻辑。