问题描述
解决方案
这是一个非常实用的量化交易需求。在 QMT 中实现“跌破20日均线卖出”的动态止损策略,核心逻辑分为三步:
- 获取数据:获取标的股票过去20个交易日的收盘价。
- 计算指标:计算20日移动平均线(MA20)和当前最新价格。
- 交易判断:如果当前持仓且最新价小于MA20,则触发卖出信号。
为了让策略完整可运行,我在代码中加入了一个简单的买入逻辑(站上20日均线买入),以便您在回测时能看到开仓和平仓的完整过程。
QMT 动态止损策略代码
# -*- coding: gbk -*-
import pandas as pd
def init(ContextInfo):
# 1. 设置策略基本参数
ContextInfo.account_id = '6000000000' # 请替换为您真实的资金账号
ContextInfo.account_type = 'STOCK' # 账号类型:STOCK-股票,FUTURE-期货
ContextInfo.ma_period = 20 # 均线周期:20日
# 2. 设置股票池(示例:平安银行)
ContextInfo.stock_code = '000001.SZ'
ContextInfo.set_universe([ContextInfo.stock_code])
# 3. 设置交易费率等(回测用)
ContextInfo.set_commission(0.0003) # 设置手续费万三
print("策略初始化完成,监控标的: {}, 止损均线: MA{}".format(ContextInfo.stock_code, ContextInfo.ma_period))
def handlebar(ContextInfo):
# 获取当前K线对应的股票代码
# 如果是回测模式,handlebar会逐根K线运行;如果是实盘,会在最新K线运行
stock_code = ContextInfo.stock_code
# 1. 获取历史行情数据
# 我们需要获取 ma_period 数量的数据来计算均线
# 为了计算准确,通常多取一点数据,这里取 ma_period + 2 条
data = ContextInfo.get_market_data_ex(
['close'],
[stock_code],
period='1d',
count=ContextInfo.ma_period + 2,
dividend_type='front' # 使用前复权数据
)
# 检查数据是否获取成功
if stock_code not in data or data[stock_code].empty:
return
df = data[stock_code]
# 如果数据长度不足以计算MA20,则跳过
if len(df) < ContextInfo.ma_period:
return
# 2. 计算指标
# 计算收盘价的 rolling mean (移动平均)
# 获取最近一根K线的MA20值
ma_values = df['close'].rolling(window=ContextInfo.ma_period).mean()
current_ma20 = ma_values.iloc[-1] # 最新MA20
current_price = df['close'].iloc[-1] # 最新收盘价
# 3. 获取当前持仓信息
position = get_position(ContextInfo, stock_code)
# 4. 交易逻辑判断
# --- 止损/卖出逻辑:跌破20日均线 ---
# 条件:持有仓位 且 当前价格 < 20日均线
if position > 0 and current_price < current_ma20:
print(f"时间:{ContextInfo.get_bar_timetag(ContextInfo.barpos)} | 触发止损: 现价{current_price:.2f} < MA20 {current_ma20:.2f}")
# 执行卖出:opType=24(卖出), orderType=1101(单股单账号), priceType=5(最新价)
passorder(24, 1101, ContextInfo.account_id, stock_code, 5, -1, position, ContextInfo)
# --- 买入逻辑(为了演示完整性添加):站上20日均线 ---
# 条件:无持仓 且 当前价格 > 20日均线
elif position == 0 and current_price > current_ma20:
print(f"时间:{ContextInfo.get_bar_timetag(ContextInfo.barpos)} | 触发买入: 现价{current_price:.2f} > MA20 {current_ma20:.2f}")
# 执行买入:opType=23(买入), 买入1000股
passorder(23, 1101, ContextInfo.account_id, stock_code, 5, -1, 1000, ContextInfo)
# 辅助函数:获取指定股票的持仓数量
def get_position(ContextInfo, stock_code):
# 获取交易明细中的持仓信息
# 注意:回测模式下,get_trade_detail_data 返回的是回测系统的虚拟持仓
# 实盘模式下,返回的是真实账号的持仓
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
for pos in positions:
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == stock_code:
return pos.m_nVolume
return 0
代码核心逻辑解析
-
数据获取 (
get_market_data_ex):- 我们使用了
get_market_data_ex接口,这是 QMT 推荐的新版数据接口。 count=ContextInfo.ma_period + 2:为了计算20日均线,我们至少需要20根K线。多取2根是为了防止数据边界问题。dividend_type='front':使用前复权数据。在涉及价格比较(如均线策略)时,复权非常重要,否则分红除权会导致股价“假摔”跌破均线,触发错误止损。
- 我们使用了
-
指标计算:
- 利用
pandas的rolling(window=20).mean()快速计算移动平均线。 iloc[-1]获取序列中最新的一个数据点。
- 利用
-
持仓查询 (
get_position):- 我封装了一个
get_position辅助函数,通过get_trade_detail_data遍历账户持仓,精确查找当前标的的持仓数量。这在实盘和回测中都通用。
- 我封装了一个
-
下单函数 (
passorder):- 卖出:
opType=24。 - 买入:
opType=23。 - 下单量: 卖出时使用了
position变量,意味着全仓卖出(清仓止损)。
- 卖出:
如何使用
- 打开 QMT 软件,进入【模型研究】或【策略编辑器】。
- 新建一个 Python 策略。
- 将上述代码完整复制进去。
- 修改账号: 将
ContextInfo.account_id修改为您自己的资金账号(回测模式下可忽略或使用默认)。 - 运行回测: 在右侧设置回测区间(例如最近一年),点击【运行】即可看到买卖点和资金曲线。
Q&A
Q: 为什么使用 get_market_data_ex 而不是 get_market_data?
A: get_market_data_ex 是 QMT 较新的接口,返回的数据结构更清晰(字典包含 DataFrame),且在处理多股、多周期数据时性能更好,参数定义也更符合 Python 习惯。
Q: 如何将止损条件改为“收盘价跌破均线 3%”?
A: 您可以修改判断逻辑:
将 if current_price < current_ma20:
改为 if current_price < current_ma20 * 0.97:
这样只有当价格低于均线的 97%(即跌破 3%)时才会触发卖出,可以过滤掉一些假突破。
Q: 实盘运行时,这个策略会在盘中触发还是收盘触发?
A: 这取决于您运行策略的频率。
- 如果您在界面上设置“K线生成后运行”,它会在每天收盘(日线走完)后检测,第二天开盘下单。
- 如果您设置了“固定时间间隔”(如每3秒),它会在盘中实时检测。如果盘中价格瞬间跌破均线,就会立即触发卖出。对于均线策略,通常建议以收盘价为准,以避免盘中震荡洗盘。