问题描述
解决方案
这是一个非常实用的量化交易需求。要实现“根据市场波动大小自动调整止损幅度”,最经典且有效的方法是使用 ATR(平均真实波幅,Average True Range) 指标。
策略核心逻辑
- 衡量波动率 (ATR):
- ATR 指标可以量化当前市场的波动程度。
- 当市场波动剧烈时,ATR 值变大;市场平稳时,ATR 值变小。
- 动态计算止损价:
- 止损距离 = ATR × 倍数(例如 2.0)。
- 止损价 = 当前价格 - 止损距离。
- 这样,在波动大时,止损线会自动放宽,避免被正常的市场噪音震出局;在波动小时,止损线会自动收紧,保护利润。
- 移动止损 (Trailing Stop):
- 为了锁定利润,我们通常会让止损线“只升不降”。如果价格上涨,止损线跟随上移;如果价格下跌,止损线保持不变,直到价格触及止损线触发卖出。
QMT 策略代码实现
以下是一个完整的 Python 策略代码。该策略使用 双均线(金叉) 作为入场信号,使用 ATR 动态移动止损 作为出场信号。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import talib
def init(ContextInfo):
# 1. 设置策略参数
ContextInfo.stock_list = ['600000.SH', '000001.SZ'] # 示例股票池
ContextInfo.set_universe(ContextInfo.stock_list)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID' # 请替换为您的资金账号
ContextInfo.account_type = 'STOCK' # 账号类型:STOCK股票,FUTURE期货
# 策略参数
ContextInfo.ma_short_period = 5 # 短期均线周期
ContextInfo.ma_long_period = 20 # 长期均线周期
ContextInfo.atr_period = 14 # ATR计算周期
ContextInfo.atr_multiplier = 2.0 # ATR倍数,用于控制止损宽度
# 全局变量:用于存储每只股票当前的动态止损价
# 格式:{ 'stock_code': stop_price }
ContextInfo.dynamic_stop_prices = {}
def handlebar(ContextInfo):
# 获取当前K线位置
index = ContextInfo.barpos
# 获取当前时间
realtime = ContextInfo.get_bar_timetag(index)
# 遍历股票池
for stock in ContextInfo.stock_list:
# 2. 获取历史行情数据 (多取一些数据以保证指标计算准确)
# 我们需要 High, Low, Close 来计算 ATR
data_len = 100
market_data = ContextInfo.get_market_data_ex(
['high', 'low', 'close', 'open'],
[stock],
period='1d',
start_time='',
end_time='',
count=data_len,
dividend_type='front',
fill_data=True,
subscribe=True
)
if stock not in market_data or len(market_data[stock]) < ContextInfo.atr_period + 5:
continue
df = market_data[stock]
# 3. 计算技术指标
# 转换数据类型为 float,talib 需要
high_prices = df['high'].values.astype(float)
low_prices = df['low'].values.astype(float)
close_prices = df['close'].values.astype(float)
# 计算 ATR
atr = talib.ATR(high_prices, low_prices, close_prices, timeperiod=ContextInfo.atr_period)
current_atr = atr[-1] # 获取最新的ATR值
# 计算均线
ma_short = talib.SMA(close_prices, timeperiod=ContextInfo.ma_short_period)
ma_long = talib.SMA(close_prices, timeperiod=ContextInfo.ma_long_period)
# 获取最新价格
current_price = close_prices[-1]
prev_price = close_prices[-2]
# 4. 获取当前持仓信息
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
holding_vol = 0
for pos in positions:
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == stock:
holding_vol = pos.m_nVolume
break
# ================= 交易逻辑 =================
# --- 场景 A: 持有仓位,检查是否触发动态止损 ---
if holding_vol > 0:
# 获取该股票记录的止损价
stop_price = ContextInfo.dynamic_stop_prices.get(stock, 0)
# 1. 检查是否触发止损
if current_price < stop_price:
print(f"【止损卖出】{stock} 当前价: {current_price} < 止损价: {stop_price:.2f} (波动率ATR: {current_atr:.3f})")
passorder(24, 1101, ContextInfo.account_id, stock, 5, -1, holding_vol, ContextInfo)
# 清除止损价记录
del ContextInfo.dynamic_stop_prices[stock]
else:
# 2. 动态调整止损价 (Trailing Stop)
# 逻辑:如果价格上涨,止损价跟随上涨;如果价格下跌,止损价保持不变
# 新的潜在止损价 = 当前价格 - (ATR * 倍数)
new_potential_stop = current_price - (current_atr * ContextInfo.atr_multiplier)
# 如果新的止损价高于旧的止损价,则上移止损线(锁定利润)
if new_potential_stop > stop_price:
ContextInfo.dynamic_stop_prices[stock] = new_potential_stop
# print(f"【止损上移】{stock} 新止损价: {new_potential_stop:.2f}")
# --- 场景 B: 无仓位,检查开仓信号 ---
elif holding_vol == 0:
# 简单的均线金叉策略:短期上穿长期
if ma_short[-1] > ma_long[-1] and ma_short[-2] <= ma_long[-2]:
print(f"【买入开仓】{stock} 均线金叉,当前价: {current_price}")
passorder(23, 1101, ContextInfo.account_id, stock, 5, -1, 1000, ContextInfo) # 买入1000股
# *** 关键步骤:开仓时初始化止损价 ***
# 初始止损价 = 开仓价 - (当前ATR * 倍数)
initial_stop_price = current_price - (current_atr * ContextInfo.atr_multiplier)
ContextInfo.dynamic_stop_prices[stock] = initial_stop_price
print(f"--> 初始动态止损价已设定: {initial_stop_price:.2f} (ATR: {current_atr:.3f})")
# 绘图 (可选,用于回测观察)
# ContextInfo.paint('ATR', current_atr, -1, 0)
代码详解
-
引入
talib库:- QMT 内置了
talib库,这是计算 ATR 最快、最标准的方法。 talib.ATR(high, low, close, timeperiod)函数直接返回 ATR 序列。
- QMT 内置了
-
全局变量
ContextInfo.dynamic_stop_prices:- 这是一个字典,用于在内存中记录每只持仓股票当前的止损线。
- 因为止损线是动态变化的,我们需要在每次
handlebar运行时读取并更新它。
-
开仓时的初始化:
- 当触发买入信号时,立即计算初始止损价:
当前价 - (ATR * 倍数)。 - 如果当前市场波动大(ATR大),止损线会离价格比较远,防止被震荡洗盘。
- 当触发买入信号时,立即计算初始止损价:
-
持仓时的动态调整 (Trailing Logic):
- 这是策略的灵魂所在。代码中有一行逻辑:
if new_potential_stop > stop_price: ContextInfo.dynamic_stop_prices[stock] = new_potential_stop - 这意味着:止损线只能上移,不能下移。
- 当股价上涨,
current_price变大,算出来的new_potential_stop也会变大,于是止损线跟着往上走,保护浮盈。 - 当股价下跌,
new_potential_stop会变小,但因为我们只取最大值,所以止损线保持在原位不动,等待价格跌破。
- 这是策略的灵魂所在。代码中有一行逻辑:
如何使用
- 打开 QMT 客户端,进入“模型研究” -> “策略编辑器”。
- 新建一个 Python 策略。
- 将上述代码复制进去。
- 修改
ContextInfo.account_id为你的实际账号。 - 点击“回测”查看效果,或者在实盘模式下运行。
常见问题 Q&A
Q: 为什么使用 ATR 而不是百分比(比如固定 5% 止损)?
A: 固定百分比止损忽略了市场环境。在牛市高波动期,5% 可能一天就跌到了,容易被误杀;在低波动期,5% 可能太宽了,导致亏损过大。ATR 能根据当前市场的“脾气”来决定安全距离。
Q: ContextInfo.atr_multiplier 设置多少合适?
A: 通常设置为 1.5 到 3.0 之间。
- 如果是短线激进策略,可以设为 1.5 或 2.0。
- 如果是中长线趋势策略,建议设为 2.5 或 3.0,以容忍更大的回撤。
Q: 这个策略在实盘中重启后,止损价会丢失吗?
A: 是的,上述代码中的 ContextInfo.dynamic_stop_prices 存储在内存中。如果 QMT 软件关闭重启,这个变量会清空。
- 解决方法:在实盘中,通常建议将止损价写入本地文件(如 txt 或 json)或使用 QMT 的
ContextInfo.set_dict_data(如果支持持久化) / 数据库。简单的做法是在init函数里加一段逻辑:如果持有仓位但字典里没有止损价,则按当前价 - ATR重新初始化一个保护性止损。