问题描述
解决方案
这是一个基于 ATR(平均真实波幅)指标的通道突破策略。
策略逻辑
- 计算 ATR:衡量市场的波动率。
- 构建通道:
- 中轨:过去 N 天的收盘价移动平均线 (MA)。
- 上轨:中轨 + (ATR * 乘数)。
- 下轨:中轨 - (ATR * 乘数)。
- 交易信号:
- 买入开仓:当价格突破上轨时,认为趋势向上,买入。
- 卖出平仓:当价格跌破下轨(或中轨,视激进程度而定,本策略采用跌破下轨反转)时,卖出。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# --- 策略参数设置 ---
ContextInfo.stock_code = '600000.SH' # 标的股票:浦发银行
ContextInfo.period = 20 # ATR 和 均线的计算周期
ContextInfo.multiplier = 2.0 # 通道宽度乘数 (上轨 = MA + multiplier * ATR)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID' # 请替换为您的资金账号
ContextInfo.trade_qty = 100 # 每次交易数量
# --- 设置股票池 ---
ContextInfo.set_universe([ContextInfo.stock_code])
# --- 设置交易账号 ---
ContextInfo.set_account(ContextInfo.account_id)
def get_position(ContextInfo, stock_code):
"""
获取当前持仓数量的辅助函数
"""
try:
# 获取持仓对象列表
positions = get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
for pos in positions:
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == stock_code:
return pos.m_nVolume
return 0
except Exception as e:
print("获取持仓失败: ", e)
return 0
def calculate_atr_bands(df, period, multiplier):
"""
计算 ATR 和 通道上下轨
"""
# 1. 计算 TR (True Range)
# TR = Max(High-Low, Abs(High-PreClose), Abs(Low-PreClose))
df['high_low'] = df['high'] - df['low']
df['pre_close'] = df['close'].shift(1)
df['high_pre_close'] = abs(df['high'] - df['pre_close'])
df['low_pre_close'] = abs(df['low'] - df['pre_close'])
df['tr'] = df[['high_low', 'high_pre_close', 'low_pre_close']].max(axis=1)
# 2. 计算 ATR (简单移动平均)
df['atr'] = df['tr'].rolling(window=period).mean()
# 3. 计算中轨 (MA)
df['mid'] = df['close'].rolling(window=period).mean()
# 4. 计算上下轨
df['upper'] = df['mid'] + (multiplier * df['atr'])
df['lower'] = df['mid'] - (multiplier * df['atr'])
return df
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前 Bar 的索引
index = ContextInfo.barpos
stock_code = ContextInfo.stock_code
# 获取历史行情数据 (取足够多的数据以计算指标,例如 period + 10)
# 注意:get_market_data_ex 返回的是字典 {code: dataframe}
data_count = ContextInfo.period + 10
market_data = ContextInfo.get_market_data_ex(
['open', 'high', 'low', 'close'],
[stock_code],
period='1d',
count=data_count,
dividend_type='front' # 前复权
)
if stock_code not in market_data:
return
df = market_data[stock_code]
# 数据量不足时不计算
if len(df) < ContextInfo.period + 1:
return
# 计算指标
df = calculate_atr_bands(df, ContextInfo.period, ContextInfo.multiplier)
# 获取最新完成的一根K线的数据(用于生成信号)
# 注意:实盘中通常使用上一根K线的收盘价来决定是否在当前时刻交易,避免未来函数
last_bar = df.iloc[-2] # 上一根K线(已走完)
curr_bar = df.iloc[-1] # 当前K线(正在进行)
# 获取当前持仓
current_pos = get_position(ContextInfo, stock_code)
# --- 交易逻辑 ---
# 1. 买入信号:上一根K线收盘价突破上轨
if last_bar['close'] > last_bar['upper']:
if current_pos == 0:
print(f"时间: {curr_bar.name}, 价格突破上轨,买入开仓。收盘价: {last_bar['close']}, 上轨: {last_bar['upper']}")
# 23: 买入, 1101: 单股单账号普通下单, 5: 最新价
passorder(23, 1101, ContextInfo.account_id, stock_code, 5, -1, ContextInfo.trade_qty, ContextInfo)
# 2. 卖出信号:上一根K线收盘价跌破下轨
elif last_bar['close'] < last_bar['lower']:
if current_pos > 0:
print(f"时间: {curr_bar.name}, 价格跌破下轨,卖出平仓。收盘价: {last_bar['close']}, 下轨: {last_bar['lower']}")
# 24: 卖出
passorder(24, 1101, ContextInfo.account_id, stock_code, 5, -1, current_pos, ContextInfo)
# --- (可选) 绘图 ---
# 在回测界面绘制指标
ContextInfo.paint('Upper', last_bar['upper'], -1, 0, 'red')
ContextInfo.paint('Lower', last_bar['lower'], -1, 0, 'green')
ContextInfo.paint('Mid', last_bar['mid'], -1, 0, 'white')
代码关键点解析
-
# -*- coding: gbk -*-:- QMT 的 Python 编辑器默认使用 GBK 编码,这行代码对于防止中文注释乱码至关重要。
-
get_market_data_ex:- 这是 QMT 获取行情的推荐接口。它返回的是一个 Pandas DataFrame,非常适合进行向量化的指标计算。
- 我们获取了
period + 10条数据,确保有足够的数据来计算移动平均和 ATR。
-
ATR 计算逻辑:
- 首先计算 TR(真实波幅),它是当日最高价与最低价之差、当日最高价与昨日收盘价之差的绝对值、当日最低价与昨日收盘价之差的绝对值中的最大值。
- 然后对 TR 进行移动平均得到 ATR。
-
信号生成 (
iloc[-2]):- 在
handlebar中,df.iloc[-1]通常代表当前正在跳动的 K 线(未走完)。 - 为了避免“未来函数”(即利用收盘后才确定的价格在盘中交易),策略逻辑通常基于上一根已完成的 K 线 (
df.iloc[-2]) 的收盘价来判断是否突破。
- 在
-
交易函数
passorder:23代表买入,24代表卖出。1101代表单股、单账号、按股数下单。5代表使用最新价下单(市价单的一种模拟,实盘中可能需要根据流动性调整为对手价等)。
如何使用
- 打开 QMT 客户端,进入“模型研究”或“策略编辑器”。
- 新建一个 Python 策略。
- 将上述代码复制粘贴进去。
- 修改
ContextInfo.account_id为你实际的资金账号。 - 点击“编译”检查是否有语法错误。
- 在回测模式下选择标的(如 600000.SH)和周期(如日线)进行回测。
Q&A
Q: 如何调整通道的灵敏度?
A: 修改 init 函数中的 ContextInfo.period (周期) 和 ContextInfo.multiplier (乘数)。
- 增大
period或multiplier会使通道变宽,交易信号变少,但可能过滤掉震荡。 - 减小它们会使通道变窄,信号变多,但可能面临更多假突破。
Q: 为什么使用 get_market_data_ex 而不是 get_market_data?
A: get_market_data_ex 是 QMT 的新版接口,返回的数据结构更规范(Pandas DataFrame),处理多只股票和多字段时更方便,且性能更好。
Q: 这个策略可以用于期货吗?
A: 逻辑上可以,但需要修改下单函数 passorder 的参数。期货涉及开仓(0/3)和平仓(1/2/4/5),且需要区分多空方向,而股票通常只有买入(23)和卖出(24)。