问题描述
解决方案
这是一个基于 QMT 平台的量化交易策略,实现了均线金叉买入,并结合了固定止损和**移动止损(动态止盈)**的功能。
策略逻辑说明
- 入场信号(买入):
- 使用简单的双均线策略作为示例:当短期均线(MA5)上穿长期均线(MA10)时买入。
- 固定止损(Stop Loss):
- 当当前价格低于持仓成本的一定比例(例如 -3%)时,强制止损。
- 移动止损/动态止盈(Trailing Stop):
- 触发条件:当股价从持仓成本上涨超过一定比例(例如 +5%)后,激活移动止损逻辑。
- 回撤卖出:记录持仓期间的最高价。如果当前价格从最高价回撤超过设定比例(例如 -2%),则触发止盈卖出。
- 效果:让利润奔跑,同时在趋势反转时锁定利润。
QMT 策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 1. 设置策略参数
ContextInfo.account_id = '6000000000' # 请修改为您的资金账号
ContextInfo.account_type = 'STOCK' # 账号类型:STOCK-股票,FUTURE-期货
ContextInfo.is_backtest = True # 是否为回测模式,实盘请改为 False
# 均线参数
ContextInfo.ma_short_period = 5
ContextInfo.ma_long_period = 10
# 交易参数
ContextInfo.buy_amount = 1000 # 每次买入股数
# --- 核心风控参数 ---
ContextInfo.stop_loss_pct = 0.05 # 固定止损比例:亏损 5% 止损
ContextInfo.trailing_start_pct = 0.05 # 移动止损激活阈值:盈利 5% 后开启移动止损
ContextInfo.trailing_gap_pct = 0.02 # 移动止损回撤比例:从最高点回撤 2% 止盈
# 2. 全局变量
# 用于记录持仓股票在持仓期间的最高价,格式:{'stock_code': high_price}
ContextInfo.holding_high_prices = {}
# 3. 设置股票池 (示例:平安银行,贵州茅台)
ContextInfo.target_list = ['000001.SZ', '600519.SH']
ContextInfo.set_universe(ContextInfo.target_list)
# 4. 绑定账号 (实盘必须)
if not ContextInfo.is_backtest:
ContextInfo.set_account(ContextInfo.account_id)
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前K线索引
index = ContextInfo.barpos
# 获取当前时间
realtime = ContextInfo.get_bar_timetag(index)
# 获取股票池
stock_list = ContextInfo.get_universe()
# 获取持仓信息
positions = get_current_positions(ContextInfo)
# --- 1. 准备数据 ---
# 获取历史行情数据 (多取一些以计算均线)
data_len = ContextInfo.ma_long_period + 2
# 使用 get_market_data_ex 获取数据,效率更高
market_data = ContextInfo.get_market_data_ex(
['open', 'high', 'low', 'close'],
stock_list,
period=ContextInfo.period,
count=data_len,
dividend_type='front' # 前复权
)
# --- 2. 遍历股票进行逻辑判断 ---
for stock in stock_list:
if stock not in market_data:
continue
df = market_data[stock]
if len(df) < ContextInfo.ma_long_period:
continue
# 获取最新价格
current_price = df['close'].iloc[-1]
# 获取当前K线的最高价(用于更新移动止损的高点)
current_high = df['high'].iloc[-1]
# --- 3. 卖出逻辑 (止损与移动止盈) ---
if stock in positions:
pos_info = positions[stock]
cost_price = pos_info['cost_price'] # 持仓成本
volume = pos_info['volume'] # 持仓数量
# A. 更新持仓期间的最高价
# 如果是新买入的股票,初始化最高价为成本价或当前高价
if stock not in ContextInfo.holding_high_prices:
ContextInfo.holding_high_prices[stock] = max(cost_price, current_high)
else:
# 如果当前价格创新高,更新最高价
if current_high > ContextInfo.holding_high_prices[stock]:
ContextInfo.holding_high_prices[stock] = current_high
record_high = ContextInfo.holding_high_prices[stock]
# B. 计算收益率情况
profit_pct = (current_price - cost_price) / cost_price
drawdown_pct = (record_high - current_price) / record_high
sell_signal = False
sell_reason = ""
# C. 检查固定止损
if profit_pct <= -ContextInfo.stop_loss_pct:
sell_signal = True
sell_reason = f"固定止损触发: 当前亏损 {profit_pct*100:.2f}%"
# D. 检查移动止损 (动态止盈)
# 条件1: 当前盈利超过激活阈值 (trailing_start_pct)
# 条件2: 从最高点回撤超过设定比例 (trailing_gap_pct)
elif (current_price > cost_price * (1 + ContextInfo.trailing_start_pct)) and \
(drawdown_pct >= ContextInfo.trailing_gap_pct):
sell_signal = True
sell_reason = f"移动止盈触发: 最高价 {record_high}, 回撤 {drawdown_pct*100:.2f}%"
# 执行卖出
if sell_signal:
print(f"[{stock}] {sell_reason}, 执行卖出")
order_target_volume(ContextInfo, stock, 0) # 清仓
# 卖出后清除最高价记录
if stock in ContextInfo.holding_high_prices:
del ContextInfo.holding_high_prices[stock]
continue # 卖出后跳过买入逻辑
# --- 4. 买入逻辑 (均线金叉) ---
else: # 没有持仓才考虑买入
# 计算均线
close_prices = df['close']
ma_short = close_prices.rolling(ContextInfo.ma_short_period).mean()
ma_long = close_prices.rolling(ContextInfo.ma_long_period).mean()
# 获取最后两个点判断金叉
ma_s_curr = ma_short.iloc[-1]
ma_s_prev = ma_short.iloc[-2]
ma_l_curr = ma_long.iloc[-1]
ma_l_prev = ma_long.iloc[-2]
# 金叉判断: 短线上穿长线
if ma_s_prev <= ma_l_prev and ma_s_curr > ma_l_curr:
print(f"[{stock}] 金叉触发 (MA{ContextInfo.ma_short_period}={ma_s_curr:.2f}, MA{ContextInfo.ma_long_period}={ma_l_curr:.2f}), 执行买入")
order_target_volume(ContextInfo, stock, ContextInfo.buy_amount)
# 买入时初始化最高价记录
ContextInfo.holding_high_prices[stock] = current_price
def get_current_positions(ContextInfo):
"""
获取当前持仓信息的辅助函数
返回字典: {'stock_code': {'volume': 100, 'cost_price': 10.5}}
"""
pos_dict = {}
# 回测模式下使用 ContextInfo.get_trade_detail_data 可能不准确,通常回测引擎会自动维护
# 但为了兼容实盘逻辑,这里演示如何获取
if ContextInfo.is_backtest:
# QMT回测模式下,通常不需要手动查询持仓对象来做逻辑判断,
# 但为了演示移动止损需要成本价,我们这里模拟一下或使用内置函数
# 注意:QMT回测中 get_trade_detail_data 可能返回空,需依赖 ContextInfo.get_position 等(视版本而定)
# 这里使用通用的 get_trade_detail_data,但在回测中可能需要特定处理
# 为简化代码,假设回测系统能返回正确对象,或者用户在实盘使用
pass
# 获取持仓对象列表
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
for pos in positions:
stock_code = pos.m_strInstrumentID + "." + pos.m_strExchangeID
vol = pos.m_nVolume
price = pos.m_dOpenPrice # 开仓均价
# 过滤掉空仓
if vol > 0:
pos_dict[stock_code] = {
'volume': vol,
'cost_price': price
}
return pos_dict
def order_target_volume(ContextInfo, stock_code, target_volume):
"""
下单辅助函数:调整持仓到目标数量
"""
# 23: 买入, 24: 卖出
# 1101: 单股单账号
# 获取当前持仓
positions = get_current_positions(ContextInfo)
curr_vol = 0
if stock_code in positions:
curr_vol = positions[stock_code]['volume']
diff = target_volume - curr_vol
if diff > 0:
# 买入
passorder(23, 1101, ContextInfo.account_id, stock_code, 5, -1, diff, ContextInfo)
elif diff < 0:
# 卖出
passorder(24, 1101, ContextInfo.account_id, stock_code, 5, -1, abs(diff), ContextInfo)
关键功能详解
-
ContextInfo.holding_high_prices(核心):- 这是一个字典,用于“记忆”我们持仓期间股票达到过的最高价格。
- 逻辑:每次
handlebar运行时,如果持有某只股票,就检查当前的high价格是否高于字典里记录的价格。如果是,就更新它。这是计算“回撤”的基础。 - 清理:当股票卖出后,需要从字典中删除该股票的记录,以免影响下次买入。
-
移动止损逻辑 (Trailing Stop):
- 代码段:
elif (current_price > cost_price * (1 + ContextInfo.trailing_start_pct)) and \ (drawdown_pct >= ContextInfo.trailing_gap_pct): - 解释:
current_price > cost_price * (1 + 0.05): 首先,股价必须比成本价高出 5%(激活门槛),防止刚买入波动就触发止盈。drawdown_pct >= 0.02: 其次,当前价格相比于持仓期间的最高价 (record_high),回撤了 2% 以上。- 只有同时满足这两个条件,才会触发卖出。
- 代码段:
-
数据获取:
- 使用了
get_market_data_ex,这是 QMT 推荐的高效数据接口,返回的是 pandas DataFrame 格式,方便进行均线计算 (rolling().mean())。
- 使用了
使用前注意事项
- 账号设置:请务必将
ContextInfo.account_id修改为您实际的资金账号。 - 回测与实盘:
- 代码中设置了
ContextInfo.is_backtest。如果是实盘,请将其改为False。 - 实盘中,
get_trade_detail_data需要确保客户端已登录交易账号。
- 代码中设置了
- 编码格式:QMT 的 Python 编辑器通常需要文件头包含
# -*- coding: gbk -*-以正确处理中文字符。
Q&A
Q: 为什么移动止损不直接用最高价回撤,而要设置一个激活阈值(trailing_start_pct)?
A: 如果不设置激活阈值(例如盈利5%才开始),股票刚买入时如果微涨后马上微跌(例如涨1%跌回0.5%),就会立即触发回撤卖出,导致频繁止损磨损本金。设置阈值是为了确保“有利润”了再开始保护利润。
Q: passorder 函数中的参数 5 和 -1 是什么意思?
A: 5 代表报价类型为“最新价”(PRTP_LATEST),-1 是价格参数,当使用最新价下单时,价格参数通常填 -1 或 0,系统会自动取当前行情的最新价。
Q: 如何修改为“目标止盈”(例如涨10%直接卖出)?
A: 您可以在卖出逻辑的 if/elif 结构中增加一个判断:
elif profit_pct >= 0.10: # 盈利超过10%
sell_signal = True
sell_reason = "目标止盈触发"