问题描述
解决方案
这是一个基于 QMT 平台 Python API 编写的 CR 指标(能量指标) 交易策略。
策略逻辑说明
CR 指标(Energy of Market)用于判断买卖气势的强弱。本策略采用经典的 CR 与其均线交叉 的逻辑:
- CR 计算公式:
- 中间价 (MID) = (昨日最高价 + 昨日最低价) / 2
- 上升值 = 今日最高价 - MID(若为负则记为0)
- 下跌值 = MID - 今日最低价(若为负则记为0)
- CR(N) = (N日内上升值之和 / N日内下跌值之和) * 100
- 交易信号:
- 买入信号(金叉):当 CR 线上穿 CR 均线(MA_CR),且 CR 值处于较低位置(例如小于 150,防止高位追涨),全仓买入。
- 卖出信号(死叉):当 CR 线其下穿 CR 均线(MA_CR),或者 CR 进入极度超买区(例如大于 300),清仓卖出。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 设置策略运行的股票列表(示例:浦发银行)
ContextInfo.stock_code = '600000.SH'
ContextInfo.set_universe([ContextInfo.stock_code])
# 设置资金账号(请替换为您的真实资金账号)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
ContextInfo.account_type = 'STOCK' # 股票账号
ContextInfo.set_account(ContextInfo.account_id)
# 策略参数设置
ContextInfo.N = 26 # CR指标的计算周期
ContextInfo.M = 10 # CR均线的计算周期
ContextInfo.period = '1d' # 运行周期:日线
# 买卖阈值设置
ContextInfo.buy_threshold = 150 # 买入时CR不应过高
ContextInfo.sell_threshold = 300 # 止盈/超买阈值
def get_cr_indicator(ContextInfo, stock_code):
"""
计算CR指标及其均线
"""
# 获取足够的历史数据,长度需要覆盖 N + M + 缓冲
count = ContextInfo.N + ContextInfo.M + 20
# 使用 get_market_data_ex 获取数据
# 注意:CR计算需要 High, Low
data_dict = ContextInfo.get_market_data_ex(
['high', 'low', 'close'],
[stock_code],
period=ContextInfo.period,
count=count,
dividend_type='front' # 前复权
)
if stock_code not in data_dict:
return None, None
df = data_dict[stock_code]
if len(df) < ContextInfo.N + 1:
return None, None
# 1. 计算中间价 MID = (昨日最高 + 昨日最低) / 2
# shift(1) 表示取前一行的数据
df['ref_high'] = df['high'].shift(1)
df['ref_low'] = df['low'].shift(1)
df['mid_price'] = (df['ref_high'] + df['ref_low']) / 2
# 2. 计算上升值和下跌值
# 上升值 = max(0, 今日最高 - 昨日中间价)
df['up_strength'] = (df['high'] - df['mid_price']).clip(lower=0)
# 下跌值 = max(0, 昨日中间价 - 今日最低)
df['down_strength'] = (df['mid_price'] - df['low']).clip(lower=0)
# 3. 计算 CR = N日上升和 / N日下跌和 * 100
# rolling(N).sum() 计算滚动求和
df['p1_sum'] = df['up_strength'].rolling(window=ContextInfo.N).sum()
df['p2_sum'] = df['down_strength'].rolling(window=ContextInfo.N).sum()
# 处理分母为0的情况,避免报错
df['cr'] = np.where(df['p2_sum'] == 0, 0, (df['p1_sum'] / df['p2_sum']) * 100)
# 4. 计算 CR 的均线
df['cr_ma'] = df['cr'].rolling(window=ContextInfo.M).mean()
# 返回最后两行数据用于判断交叉
return df.iloc[-2], df.iloc[-1]
def handlebar(ContextInfo):
"""
K线周期回调函数
"""
# 获取当前正在处理的K线索引
index = ContextInfo.barpos
# 获取当前时间
realtime = ContextInfo.get_bar_timetag(index)
stock = ContextInfo.stock_code
# 计算指标
prev_bar, curr_bar = get_cr_indicator(ContextInfo, stock)
if prev_bar is None or curr_bar is None:
return # 数据不足,跳过
# 提取当前和上一周期的 CR 及 MA 值
cr_curr = curr_bar['cr']
cr_prev = prev_bar['cr']
ma_curr = curr_bar['cr_ma']
ma_prev = prev_bar['cr_ma']
# 检查是否包含无效值(NaN)
if np.isnan(cr_curr) or np.isnan(ma_curr) or np.isnan(cr_prev):
return
# 获取当前持仓
positions = ContextInfo.get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
holding_vol = 0
for pos in positions:
if pos.m_strInstrumentID == stock:
holding_vol = pos.m_nVolume
break
# --- 交易逻辑 ---
# 1. 买入条件:
# (1) CR 上穿 MA (金叉)
# (2) CR 当前值小于设定的安全阈值 (避免高位接盘)
is_golden_cross = (cr_prev <= ma_prev) and (cr_curr > ma_curr)
is_safe_zone = cr_curr < ContextInfo.buy_threshold
if is_golden_cross and is_safe_zone:
if holding_vol == 0:
# 全仓买入 (目标仓位调整为 100%)
print(f"[{ContextInfo.barpos}] 金叉买入信号: CR={cr_curr:.2f}, MA={ma_curr:.2f}")
order_target_percent(stock, 1.0, ContextInfo, ContextInfo.account_id)
# 2. 卖出条件:
# (1) CR 下穿 MA (死叉)
# (2) 或者 CR 超过超买阈值 (止盈风控)
is_death_cross = (cr_prev >= ma_prev) and (cr_curr < ma_curr)
is_overbought = cr_curr > ContextInfo.sell_threshold
if (is_death_cross or is_overbought) and holding_vol > 0:
# 清仓卖出 (目标仓位调整为 0%)
reason = "死叉" if is_death_cross else "超买"
print(f"[{ContextInfo.barpos}] {reason}卖出信号: CR={cr_curr:.2f}, MA={ma_curr:.2f}")
order_target_percent(stock, 0.0, ContextInfo, ContextInfo.account_id)
# 可视化输出 (仅在回测模式下有效,画在副图)
ContextInfo.paint('CR', cr_curr, -1, 0, 'red')
ContextInfo.paint('CR_MA', ma_curr, -1, 0, 'yellow')
代码关键点解析
-
数据获取 (
get_market_data_ex):- 我们使用了
get_market_data_ex接口,这是 QMT 推荐的高效数据接口。 - 获取了
high,low,close字段。虽然 CR 计算主要用 High 和 Low,但 Close 通常用于复权计算和后续可能的扩展。 count设置为N + M + 20,确保有足够的历史数据来计算移动平均线,避免计算初期出现 NaN(空值)。
- 我们使用了
-
CR 指标计算 (
get_cr_indicator):- 利用
pandas的矢量化运算,避免了低效的循环。 shift(1)用于获取昨日数据。clip(lower=0)替代了max(x, 0)的逻辑,处理负值情况。rolling(window=N).sum()用于计算 N 日内的累加和。
- 利用
-
交易执行 (
order_target_percent):- 使用了
order_target_percent函数。这是一个高级交易函数,它会自动计算需要买入或卖出的数量,将该股票的持仓调整到账户总资产的指定比例(这里是 1.0 即 100% 买入,0.0 即清仓)。 - 相比
passorder,这个函数在编写策略逻辑时更简洁,不需要手动计算手数。
- 使用了
-
绘图 (
paint):- 在代码末尾加入了
ContextInfo.paint,这样在回测界面的副图中可以看到 CR 和 CR_MA 的走势,方便验证策略信号是否正确。
- 在代码末尾加入了
如何使用
- 打开 QMT 客户端,进入“模型研究”或“策略编辑器”。
- 新建一个 Python 策略。
- 将上述代码完整复制进去。
- 修改
init函数中的ContextInfo.account_id为您的实际资金账号(如果是回测,系统会自动分配虚拟账号,可不改)。 - 点击“编译”检查无误后,点击“回测”即可看到策略在历史行情下的表现。
Q&A
Q: 为什么 CR 指标要用昨日的中间价而不是今日的开盘价?
A: CR 指标的设计初衷是认为昨日的中间价((最高+最低)/2)是一个比较客观的均衡点。用今日最高/最低价与昨日均衡点比较,更能反映出今日多空双方相对于昨日均衡点的强弱程度。
Q: 代码中的 ContextInfo.buy_threshold = 150 是什么意思?
A: 这是一个风控条件。CR 指标如果数值过高(例如超过 200 或 300),说明市场已经过热,此时即使出现金叉也可能是诱多。限制买入阈值可以过滤掉高位风险。
Q: 如何将此策略应用到多只股票?
A: 在 init 函数中,将 ContextInfo.stock_code 改为一个列表,例如 ['600000.SH', '000001.SZ']。然后将 handlebar 函数中的逻辑放入一个 for stock in ContextInfo.get_universe(): 的循环中即可。