问题描述
解决方案
这是一个非常经典的量化交易需求。日内均值反转策略(Intraday Mean Reversion)的核心逻辑是:价格围绕均值波动,当价格偏离均值过大(超买或超卖)时,大概率会回归均值。
通常我们使用 布林带(Bollinger Bands) 来实现这一逻辑:
- 中轨:移动平均线(MA)。
- 上轨:MA + K倍标准差(压力位)。
- 下轨:MA - K倍标准差(支撑位)。
策略逻辑:
- 开空仓:价格突破上轨(超买)。
- 开多仓:价格跌破下轨(超卖)。
- 平仓:价格回归中轨(均值回归)。
- 尾盘风控:收盘前强制平仓,不持仓过夜。
以下是基于 QMT Python API 编写的完整策略代码。
QMT 日内均值反转策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import time
def init(ContextInfo):
"""
策略初始化函数
"""
# 1. 设置交易标的(此处以沪深300股指期货主力合约为例,因为期货支持T+0,适合日内策略)
# 如果是股票,需要有底仓才能做日内T+0,或者只能做T+1
ContextInfo.stock_code = 'IF00.IF'
ContextInfo.set_universe([ContextInfo.stock_code])
# 2. 设置资金账号 (请替换为您真实的资金账号)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
ContextInfo.account_type = 'FUTURE' # FUTURE:期货, STOCK:股票
ContextInfo.set_account(ContextInfo.account_id)
# 3. 策略参数设置
ContextInfo.period = '1m' # 运行周期:1分钟
ContextInfo.lookback = 20 # 均线周期 (N)
ContextInfo.k_std = 2.0 # 标准差倍数 (K)
ContextInfo.trade_vol = 1 # 每次交易手数
# 4. 变量初始化
ContextInfo.position = 0 # 当前持仓状态:0空仓,1多头,-1空头
def get_current_position(ContextInfo):
"""
获取当前持仓状态的辅助函数
返回: 0-无持仓, 1-多头, -1-空头
"""
# 获取持仓明细
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
net_vol = 0
for pos in positions:
if pos.m_strInstrumentID == ContextInfo.stock_code.split('.')[0]: # 简单的代码匹配
# 期货持仓方向:48-多头(ENTRUST_BUY), 49-空头(ENTRUST_SELL)
# 注意:不同柜台返回的Direction可能不同,需根据实际情况调试
# 这里简化处理:多单量 - 空单量
if pos.m_nDirection == 48:
net_vol += pos.m_nVolume
elif pos.m_nDirection == 49:
net_vol -= pos.m_nVolume
if net_vol > 0:
return 1
elif net_vol < 0:
return -1
else:
return 0
def handlebar(ContextInfo):
"""
K线周期回调函数
"""
# 跳过历史K线,只在最后根K线(实时行情)交易
if not ContextInfo.is_last_bar():
return
# 1. 获取当前时间,处理尾盘清仓逻辑
# get_bar_timetag 返回的是毫秒时间戳
timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
current_time_str = timetag_to_datetime(timetag, '%H:%M:%S')
# 假设 14:55 之后不再开新仓,并强制平仓
if current_time_str >= '14:55:00':
curr_pos = get_current_position(ContextInfo)
if curr_pos == 1:
print(f"尾盘强平:卖出平仓 {ContextInfo.stock_code}")
# 24: 卖出, 6: 平多(优先平今) - 期货常用
passorder(6, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
elif curr_pos == -1:
print(f"尾盘强平:买入平仓 {ContextInfo.stock_code}")
# 23: 买入, 8: 平空(优先平今) - 期货常用
passorder(8, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
return
# 2. 获取行情数据
# 多取一些数据以保证计算均线时数据足够
count = ContextInfo.lookback + 5
data_map = ContextInfo.get_market_data_ex(
['close'],
[ContextInfo.stock_code],
period=ContextInfo.period,
count=count,
dividend_type='none'
)
if ContextInfo.stock_code not in data_map:
return
df = data_map[ContextInfo.stock_code]
# 数据长度不足,无法计算指标
if len(df) < ContextInfo.lookback:
return
# 3. 计算布林带指标
# 计算滚动均值 (中轨)
df['mean'] = df['close'].rolling(window=ContextInfo.lookback).mean()
# 计算滚动标准差
df['std'] = df['close'].rolling(window=ContextInfo.lookback).std()
# 计算上轨
df['upper'] = df['mean'] + ContextInfo.k_std * df['std']
# 计算下轨
df['lower'] = df['mean'] - ContextInfo.k_std * df['std']
# 获取最新一根K线的数据
current_close = df['close'].iloc[-1]
current_upper = df['upper'].iloc[-1]
current_lower = df['lower'].iloc[-1]
current_mean = df['mean'].iloc[-1]
# 4. 获取当前持仓状态
# 注意:实盘中建议使用 get_trade_detail_data 获取真实持仓,回测可用 ContextInfo.position 模拟
# 这里为了演示逻辑,使用 ContextInfo.position 变量进行简化控制
# 实盘请替换为 get_current_position(ContextInfo)
# 5. 交易逻辑
# --- 开仓逻辑 ---
if ContextInfo.position == 0:
# 价格突破上轨,认为超买,开空单
if current_close > current_upper:
print(f"信号触发:价格({current_close}) > 上轨({current_upper}) -> 开空")
# opType=3: 开空 (期货)
passorder(3, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
ContextInfo.position = -1
# 价格跌破下轨,认为超卖,开多单
elif current_close < current_lower:
print(f"信号触发:价格({current_close}) < 下轨({current_lower}) -> 开多")
# opType=0: 开多 (期货)
passorder(0, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
ContextInfo.position = 1
# --- 平仓逻辑 (均值回归) ---
elif ContextInfo.position == 1: # 持有多单
# 价格回归到中轨上方,平多
if current_close >= current_mean:
print(f"信号触发:价格({current_close}) 回归中轨({current_mean}) -> 平多")
# opType=6: 平多(优先平今)
passorder(6, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
ContextInfo.position = 0
elif ContextInfo.position == -1: # 持有空单
# 价格回归到中轨下方,平空
if current_close <= current_mean:
print(f"信号触发:价格({current_close}) 回归中轨({current_mean}) -> 平空")
# opType=8: 平空(优先平今)
passorder(8, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
ContextInfo.position = 0
# (可选) 在界面画图方便观察
ContextInfo.paint('Upper', current_upper, -1, 0, 'red')
ContextInfo.paint('Middle', current_mean, -1, 0, 'white')
ContextInfo.paint('Lower', current_lower, -1, 0, 'green')
代码关键点解析
-
编码格式:
- 第一行
# -*- coding: gbk -*-是必须的,因为 QMT 的 Python 环境默认使用 GBK 编码,否则中文字符会乱码或报错。
- 第一行
-
数据获取 (
get_market_data_ex):- 我们使用
get_market_data_ex获取历史数据。 count设置为lookback + 5是为了确保 Pandas 在计算rolling(滚动窗口)时有足够的数据,避免计算结果前几行为NaN。
- 我们使用
-
指标计算:
- 利用
pandas的rolling(window=20).mean()和std()快速计算布林带。这是 Python 量化最常用的方式,比循环计算效率高得多。
- 利用
-
交易函数 (
passorder):- 本策略演示的是期货交易逻辑(因为日内反转通常需要 T+0)。
opType参数说明:0: 开多3: 开空6: 平多(优先平今,适合日内)8: 平空(优先平今,适合日内)
orderType参数1101代表:单股、单账号、普通、股/手方式下单。prType参数5代表:最新价下单。
-
日内风控:
- 代码中包含了一个时间判断
if current_time_str >= '14:55:00':。 - 这是日内策略的关键,确保在收盘前强制平掉所有仓位,避免隔夜风险。
- 代码中包含了一个时间判断
如何在 QMT 中运行
- 打开 QMT 客户端,进入【模型研究】或【策略交易】界面。
- 新建一个 Python 策略。
- 将上述代码复制粘贴进去。
- 修改账号:将
YOUR_ACCOUNT_ID替换为你实际的资金账号。 - 修改标的:代码中默认是
IF00.IF(股指期货主力),如果你想跑股票(需有底仓)或 ETF,请修改ContextInfo.stock_code。 - 点击【编译】,确保无误后,点击【运行】或【回测】。
常见问题与优化建议
- 股票 T+1 问题:如果你将此策略用于 A 股股票,由于 T+1 限制,当天买入的无法当天卖出。股票日内策略通常需要“底仓做 T”,即先持有股票,日内高抛低吸。如果是这种情况,需要修改
passorder的逻辑,并检查可用持仓。 - 滑点控制:代码中使用的是
最新价下单,实盘中为了保证成交,通常会使用对手价(prType=14) 或者在最新价基础上增加一定的滑点(例如price参数传-1,然后在算法交易参数里设置超价)。 - 信号闪烁:本策略在
handlebar中运行,如果是实时行情,handlebar会在每个 Tick 更新时触发。为了避免同一根 K 线反复开平仓,可以使用ContextInfo.is_new_bar()判断,或者记录上一次交易的 Bar 索引。
Q&A
Q: 如何将此策略改为股票交易?
A: 股票是 T+1 制度。若要实现日内反转,必须持有底仓。逻辑需改为:信号触发做多时,买入;信号触发做空时,卖出持有的底仓。同时 passorder 的 opType 需改为 23 (买入) 和 24 (卖出)。
Q: 为什么使用 get_market_data_ex 而不是 get_market_data?
A: get_market_data_ex 是 QMT 较新的接口,返回的数据结构更清晰(字典包含 DataFrame),且处理多标的数据时性能更好,官方推荐使用。
Q: 如何调整策略的灵敏度?
A: 修改 ContextInfo.lookback (均线周期) 和 ContextInfo.k_std (标准差倍数)。lookback 越小越灵敏但噪点多;k_std 越小交易越频繁,越大交易越少但信号质量可能更高。