🚀 AI 一键生成 qmt 策略代码

不仅看深度:如何计算策略的最大回撤恢复时间?

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

问题描述

怎么计算策略回撤的最长恢复时间?

解决方案

在 QMT(Quant Trader)量化交易平台中,计算策略回撤的最长恢复时间(Maximum Drawdown Recovery Time),通常需要在回测结束时,利用记录下来的净值曲线进行分析。

最长恢复时间指的是:策略从一个权益高点跌落后,直到重新创出新高所经历的最长时间段。

以下是具体的实现逻辑和代码示例。

核心逻辑

  1. 数据收集:在 handlebar 运行过程中,记录每个时间点(K线时间)的策略净值。
  2. 计算累计最大值:利用 Pandas 计算净值的累计最大值(High Water Mark)。
  3. 标记创新高时间:记录每一次净值创新高的时间点。
  4. 计算恢复期:对于每一个交易日,计算其距离“上一次创新高时间”的时间差。
  5. 取最大值:所有时间差中的最大值即为最长恢复时间。

QMT 策略代码实现

你可以将以下代码直接复制到 QMT 的策略编辑器中进行回测。代码会在回测结束(最后一根 K 线)时输出计算结果。

# -*- coding: gbk -*-
import pandas as pd
import numpy as np

def init(ContextInfo):
    # 1. 设置回测参数(示例)
    ContextInfo.set_account('account_id')
    ContextInfo.set_universe(['000300.SH'])
    ContextInfo.set_commission(0.0003)
    ContextInfo.set_slippage(1, 0.0)
    
    # 2. 初始化用于存储净值数据的列表
    # 格式: [{'date': '2023-01-01', 'nav': 1.02}, ...]
    ContextInfo.nav_records = []

def handlebar(ContextInfo):
    # 简单示例策略:始终持有
    # 实际使用时,这里是你的交易逻辑
    if ContextInfo.is_new_bar():
        passorder(23, 1101, ContextInfo.accid, '000300.SH', 5, -1, 1, ContextInfo)

    # -------------------------------------------------------
    # 数据记录逻辑
    # -------------------------------------------------------
    # 获取当前K线的时间戳并转换为日期字符串
    timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
    date_str = timetag_to_datetime(timetag, '%Y-%m-%d')
    
    # 获取当前策略净值
    # 注意:get_net_value 在回测模式下有效
    nav = ContextInfo.get_net_value(ContextInfo.barpos)
    
    # 记录数据
    ContextInfo.nav_records.append({
        'date': date_str,
        'nav': nav
    })

    # -------------------------------------------------------
    # 在回测结束时进行计算
    # -------------------------------------------------------
    if ContextInfo.is_last_bar():
        calculate_max_recovery_time(ContextInfo)

def calculate_max_recovery_time(ContextInfo):
    """
    计算最长回撤恢复时间
    """
    if not ContextInfo.nav_records:
        print("没有净值记录,无法计算。")
        return

    # 1. 将记录转换为 Pandas DataFrame
    df = pd.DataFrame(ContextInfo.nav_records)
    df['date'] = pd.to_datetime(df['date'])
    df.set_index('date', inplace=True)

    # 2. 计算累计最大净值 (High Water Mark)
    df['high_water_mark'] = df['nav'].cummax()

    # 3. 标记是否处于回撤状态 (当前净值 < 历史最高)
    # 如果 nav < high_water_mark,说明处于回撤期
    # 如果 nav == high_water_mark,说明创新高(恢复完成)
    
    # 4. 计算恢复天数逻辑
    # 我们需要找到每个时间点,距离“上一次创新高时间”的天数
    
    # 创建一个序列记录“最近一次创新高的日期”
    df['last_peak_date'] = df.index
    # 只有在创新高的时候保留日期,否则设为NaN
    df.loc[df['nav'] < df['high_water_mark'], 'last_peak_date'] = np.nan
    # 前向填充,这样每一行都知道最近一次创新高是哪天
    df['last_peak_date'] = df['last_peak_date'].fillna(method='ffill')
    
    # 计算当前日期距离最近一次创新高日期的天数差
    df['recovery_days'] = (df.index - df['last_peak_date']).dt.days

    # 5. 获取最大值
    max_recovery_days = df['recovery_days'].max()
    
    # 找到发生最长恢复期的结束日期(便于排查)
    if max_recovery_days > 0:
        end_date_of_longest = df['recovery_days'].idxmax()
        start_date_of_longest = df.loc[end_date_of_longest, 'last_peak_date']
        
        print("=" * 50)
        print(f"【回撤恢复分析结果】")
        print(f"最长回撤恢复时间: {max_recovery_days} 天")
        print(f"该期间范围: {start_date_of_longest.date()} 至 {end_date_of_longest.date()}")
        print("=" * 50)
    else:
        print("策略表现完美,无回撤或始终创新高。")

代码详解

  1. ContextInfo.nav_records: 我们在 init 中创建一个列表,用来在内存中暂存每一根 K 线的日期和净值。
  2. timetag_to_datetime: QMT 的时间戳是毫秒级的,使用此函数将其转换为 Pandas 可识别的日期格式(YYYY-MM-DD)。
  3. df['nav'].cummax(): 这是 Pandas 的核心函数,用于计算序列的累计最大值。如果当前净值小于这个值,说明策略处于回撤中。
  4. fillna(method='ffill'): 这是计算恢复时间的关键技巧。
    • 我们首先标记出所有创新高的日期。
    • 对于处于回撤期的日子,我们将它的“最近创新高日期”设为 NaN。
    • 使用 ffill(前向填充),将最近的一个有效日期(即峰值日期)填充给后续的回撤日子。
    • 最后用 当前日期 - 最近峰值日期 即可得到当前的恢复时长。
  5. is_last_bar(): 确保计算逻辑只在回测结束时运行一次,避免在每一根 K 线都进行繁重的 DataFrame 运算,提高回测速度。

注意事项

  • 周期选择:上述代码计算的是自然日(Calendar Days)。如果你使用的是日线回测,结果就是天数。如果你使用的是分钟线回测,dt.days 依然计算的是日期差。如果需要计算“交易 K 线数量”,可以将索引改为整数索引(0, 1, 2...)直接相减。
  • 未恢复的回撤:如果策略在回测结束时正处于一个巨大的回撤中(尚未恢复),上述逻辑依然会计算这段时间,并将其视为潜在的最长恢复时间之一。这是符合风险控制逻辑的。

Q&A

Q: 为什么使用 ContextInfo.get_net_value 而不是自己计算资产?
A: ContextInfo.get_net_value 是 QMT 官方提供的接口,它自动处理了分红、送股、出入金等对净值的影响,比手动用 ContextInfo.capital + 持仓市值 计算更准确且方便。

Q: 这段代码可以在实盘中使用吗?
A: 理论上可以,但实盘中 is_last_bar() 的含义是“最新的一根 K 线”。在实盘模式下,通常不需要实时计算历史最长恢复时间,建议在盘后分析或回测阶段使用此逻辑。

Q: 如何计算最长恢复的 K 线根数而不是天数?
A: 在代码中,不要使用 df.index (日期) 相减。可以创建一个从 0 开始的整数列 df['bar_count'] = range(len(df)),然后用同样的逻辑计算 bar_count 的差值。