🚀 AI 一键生成 qmt 策略代码

QMT 量化实战:钱德动量摆动指标 (CMO) 策略 Python 源码

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

问题描述

如何实现 CMO 指标的量化策略?

解决方案

这是一个关于如何在 QMT (Quantitative Trading Platform) 平台上实现 钱德动量摆动指标 (Chande Momentum Oscillator, CMO) 量化策略的详细指南。

1. 策略原理:CMO 指标

CMO 是由 Tushar Chande 发明的动量震荡指标。它通过计算特定周期内价格上涨总和与下跌总和的差值,除以总的价格变动量,来衡量市场的强弱。

公式:
$$CMO = \frac{Su - Sd}{Su + Sd} \times 100$$

  • $Su$:N 日内收盘价上涨幅度的总和。
  • $Sd$:N 日内收盘价下跌幅度的总和(取绝对值)。
  • 取值范围:-100 到 +100。

策略逻辑(均值回归/超买超卖):
本策略采用经典的超买超卖逻辑:

  1. 买入信号:当 CMO 从下方穿过 超卖线(例如 -50),视为市场超卖反弹,执行买入。
  2. 卖出信号:当 CMO 从上方穿过 超买线(例如 +50),视为市场超买回调,执行卖出(或平仓)。

2. QMT 策略代码实现

以下代码是一个完整的 Python 策略,可以直接复制到 QMT 的策略编辑器中使用。代码包含了数据获取、指标计算和交易执行。

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

def init(ContextInfo):
    """
    初始化函数,设定策略参数和变量
    """
    # 1. 设置策略参数
    ContextInfo.stock_code = '600000.SH'  # 标的股票:浦发银行
    ContextInfo.account_id = '6000000248' # 请替换为您真实的资金账号
    ContextInfo.period = '1d'             # 周期:日线
    ContextInfo.cmo_n = 14                # CMO 计算周期
    ContextInfo.overbought = 50           # 超买阈值
    ContextInfo.oversold = -50            # 超卖阈值
    
    # 2. 设置股票池(用于回测)
    ContextInfo.set_universe([ContextInfo.stock_code])
    
    # 3. 设置交易账号
    ContextInfo.set_account(ContextInfo.account_id)

def calculate_cmo(close_prices, n):
    """
    计算 CMO 指标的辅助函数
    :param close_prices: 收盘价序列 (pandas Series)
    :param n: 周期
    :return: CMO 序列
    """
    # 计算每日价格变动
    delta = close_prices.diff()
    
    # 分离上涨和下跌的幅度
    # where(条件, 满足时的值, 不满足时的值)
    up = np.where(delta > 0, delta, 0)
    down = np.where(delta < 0, -delta, 0)
    
    # 转换为 Series 以使用 rolling 函数
    up_series = pd.Series(up)
    down_series = pd.Series(down)
    
    # 计算 N 周期内的上涨总和 (Su) 和下跌总和 (Sd)
    sum_up = up_series.rolling(window=n).sum()
    sum_down = down_series.rolling(window=n).sum()
    
    # 计算 CMO: (Su - Sd) / (Su + Sd) * 100
    # 注意处理分母为0的情况(虽然极少见)
    total_move = sum_up + sum_down
    cmo = 100 * (sum_up - sum_down) / total_move
    
    # 填充 NaN 值
    return cmo.fillna(0).values

def handlebar(ContextInfo):
    """
    K线处理函数,每根K线执行一次
    """
    # 获取当前 K 线索引
    index = ContextInfo.barpos
    
    # 获取当前时间
    realtime = ContextInfo.get_bar_timetag(index)
    
    # 1. 获取历史行情数据
    # 获取足够长度的数据以计算指标,N + 缓冲
    count = ContextInfo.cmo_n + 20 
    
    # 使用 get_market_data_ex 获取数据 (推荐使用 ex 接口)
    # 返回格式: {code: dataframe}
    market_data = ContextInfo.get_market_data_ex(
        ['close'], 
        [ContextInfo.stock_code], 
        period=ContextInfo.period, 
        count=count,
        dividend_type='front', # 前复权
        subscribe=True
    )
    
    if ContextInfo.stock_code not in market_data:
        return
        
    df = market_data[ContextInfo.stock_code]
    
    # 确保数据量足够计算
    if len(df) < ContextInfo.cmo_n + 1:
        return

    # 2. 计算 CMO 指标
    close_prices = df['close']
    cmo_values = calculate_cmo(close_prices, ContextInfo.cmo_n)
    
    # 获取最近两个 CMO 值用于判断交叉
    # cmo_curr: 当前 K 线的 CMO
    # cmo_prev: 上一根 K 线的 CMO
    cmo_curr = cmo_values[-1]
    cmo_prev = cmo_values[-2]
    
    # 打印日志方便调试 (仅在回测或实盘的最后一根K线打印)
    if ContextInfo.is_last_bar():
        print(f"时间: {timetag_to_datetime(realtime, '%Y-%m-%d %H:%M:%S')}, CMO: {cmo_curr:.2f}")

    # 3. 交易逻辑
    # 获取当前持仓
    positions = ContextInfo.get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
    current_holding = 0
    for pos in positions:
        if pos.m_strInstrumentID == ContextInfo.stock_code:
            current_holding = pos.m_nVolume
            break

    # 信号 1: 买入 (CMO 上穿超卖线 -50)
    # 逻辑: 上一刻 <= -50 且 当前 > -50
    if cmo_prev <= ContextInfo.oversold and cmo_curr > ContextInfo.oversold:
        if current_holding == 0:
            print(f"买入信号触发: CMO上穿 {ContextInfo.oversold}")
            # 全仓买入 (目标仓位 100%)
            order_target_percent(ContextInfo.stock_code, 1.0, ContextInfo, ContextInfo.account_id)

    # 信号 2: 卖出 (CMO 下穿超买线 +50)
    # 逻辑: 上一刻 >= 50 且 当前 < 50
    elif cmo_prev >= ContextInfo.overbought and cmo_curr < ContextInfo.overbought:
        if current_holding > 0:
            print(f"卖出信号触发: CMO下穿 {ContextInfo.overbought}")
            # 清仓卖出 (目标仓位 0%)
            order_target_percent(ContextInfo.stock_code, 0.0, ContextInfo, ContextInfo.account_id)

3. 代码关键点解析

  1. 数据获取 (get_market_data_ex):

    • 我们使用了 get_market_data_ex 接口,这是 QMT 推荐的获取行情数据的方式。
    • count 参数设置为 ContextInfo.cmo_n + 20,是为了确保有足够的数据进行滚动窗口计算(Rolling Window)。如果数据太少,计算出的 CMO 前面部分会是 NaN。
  2. 指标计算 (calculate_cmo):

    • 虽然 QMT 内置了 talib 库(可以直接调用 talib.CMO),但为了保证代码在不同环境下的兼容性以及逻辑的透明度,我在代码中使用了 pandasnumpy 手动实现了 CMO 算法。
    • 逻辑:先计算 diff,然后分离出 up (上涨部分) 和 down (下跌部分),最后用 rolling(n).sum() 求和并套用公式。
  3. 交易信号:

    • 上穿 (Cross Over): cmo_prev <= -50cmo_curr > -50。这代表动量从极度悲观开始转强。
    • 下穿 (Cross Under): cmo_prev >= 50cmo_curr < 50。这代表动量从极度乐观开始转弱。
  4. 下单执行 (order_target_percent):

    • 使用 order_target_percent 是最简单的仓位管理方式。
    • 1.0 代表调整仓位至总资产的 100%(全仓买入)。
    • 0.0 代表调整仓位至 0%(清仓卖出)。

4. 如何在 QMT 中运行

  1. 打开 QMT 客户端,进入 “模型研究” -> “策略编辑器”
  2. 新建一个 Python 策略。
  3. 将上述代码完整复制粘贴到编辑器中。
  4. 修改参数
    • ContextInfo.account_id 修改为您自己的模拟或实盘资金账号。
    • 如果需要,修改 stock_code 为您想交易的股票。
  5. 点击 “编译” 检查是否有语法错误。
  6. 点击 “回测” 查看历史表现,或在行情图上运行查看信号。

Q&A

Q: 如何调整 CMO 的灵敏度?
A: 修改 init 函数中的 ContextInfo.cmo_n。数值越小(如 9),指标越敏感,信号越多但假信号也多;数值越大(如 20),指标越平滑,信号越少但滞后性增加。

Q: 为什么使用 get_market_data_ex 而不是 get_market_data
A: get_market_data_ex 是 QMT 的新版接口,返回的数据结构更清晰(字典包含 DataFrame),且支持更多参数(如复权方式、订阅开关),是官方推荐的用法。

Q: 这个策略可以用于期货吗?
A: 可以。只需将 stock_code 改为期货合约代码(如 'IF2306.IF'),并将 order_target_percent 替换为期货专用的下单函数(如 buy_open, sell_close 等),因为期货涉及开平仓和保证金机制,不能简单使用目标价值下单。