问题描述
解决方案
这是一个基于 QMT (Quantitative Trading Platform) API 实现的 SuperTrend (超级趋势) 指标交易策略。
策略逻辑说明
SuperTrend 是一种趋势跟踪指标,类似于移动止损线。它结合了价格和波动率(ATR)来确定趋势方向。
- 计算 ATR (平均真实波幅):用于衡量市场波动率。
- 计算上下轨:
- 上轨 (Upper Band) = (最高价 + 最低价) / 2 + (倍数 * ATR)
- 下轨 (Lower Band) = (最高价 + 最低价) / 2 - (倍数 * ATR)
- SuperTrend 计算逻辑:
- 当收盘价高于 SuperTrend 线时,趋势为多头(显示为绿色/下轨支撑)。
- 当收盘价低于 SuperTrend 线时,趋势为空头(显示为红色/上轨压力)。
- 交易信号:
- 买入:当价格突破 SuperTrend 线转为多头趋势时。
- 卖出/平仓:当价格跌破 SuperTrend 线转为空头趋势时。
Python 策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# --- 策略参数设置 ---
ContextInfo.stock_code = '600000.SH' # 交易标的:浦发银行
ContextInfo.account_id = '600000248' # 请替换为您的资金账号
ContextInfo.account_type = 'STOCK' # 账号类型:STOCK-股票, FUTURE-期货
ContextInfo.period = '1d' # 运行周期:1d 代表日线
ContextInfo.atr_period = 10 # ATR 计算周期
ContextInfo.factor = 3.0 # SuperTrend 倍数因子 (通常为 3.0)
ContextInfo.trade_vol = 100 # 每次交易数量 (股)
# 设置股票池 (必须设置,否则无法获取历史数据)
ContextInfo.set_universe([ContextInfo.stock_code])
# 绑定账号 (实盘/模拟盘必须)
ContextInfo.set_account(ContextInfo.account_id)
def calculate_supertrend(df, period, factor):
"""
计算 SuperTrend 指标
"""
# 1. 计算 TR (真实波幅)
df['hl'] = df['high'] - df['low']
df['hc'] = abs(df['high'] - df['close'].shift(1))
df['lc'] = abs(df['low'] - df['close'].shift(1))
df['tr'] = df[['hl', 'hc', 'lc']].max(axis=1)
# 2. 计算 ATR
df['atr'] = df['tr'].rolling(window=period).mean()
# 3. 计算基础上下轨
df['basic_upper'] = (df['high'] + df['low']) / 2 + factor * df['atr']
df['basic_lower'] = (df['high'] + df['low']) / 2 - factor * df['atr']
# 4. 初始化最终上下轨和趋势
df['final_upper'] = df['basic_upper']
df['final_lower'] = df['basic_lower']
df['supertrend'] = 0.0
df['trend'] = True # True 为多头, False 为空头
# 5. 迭代计算最终轨道 (因为当前值依赖于前一个值,难以完全向量化)
# 注意:为了效率,实盘中通常只计算最近的一段数据
for i in range(period, len(df)):
# 计算 Final Upper
if (df['basic_upper'].iloc[i] < df['final_upper'].iloc[i-1]) or \
(df['close'].iloc[i-1] > df['final_upper'].iloc[i-1]):
df.at[df.index[i], 'final_upper'] = df['basic_upper'].iloc[i]
else:
df.at[df.index[i], 'final_upper'] = df['final_upper'].iloc[i-1]
# 计算 Final Lower
if (df['basic_lower'].iloc[i] > df['final_lower'].iloc[i-1]) or \
(df['close'].iloc[i-1] < df['final_lower'].iloc[i-1]):
df.at[df.index[i], 'final_lower'] = df['basic_lower'].iloc[i]
else:
df.at[df.index[i], 'final_lower'] = df['final_lower'].iloc[i-1]
# 计算 SuperTrend 和 Trend 方向
prev_trend = df['trend'].iloc[i-1]
curr_close = df['close'].iloc[i]
if prev_trend: # 之前是多头
if curr_close <= df['final_lower'].iloc[i]:
df.at[df.index[i], 'trend'] = False # 转空
df.at[df.index[i], 'supertrend'] = df['final_upper'].iloc[i]
else:
df.at[df.index[i], 'trend'] = True # 保持多
df.at[df.index[i], 'supertrend'] = df['final_lower'].iloc[i]
else: # 之前是空头
if curr_close >= df['final_upper'].iloc[i]:
df.at[df.index[i], 'trend'] = True # 转多
df.at[df.index[i], 'supertrend'] = df['final_lower'].iloc[i]
else:
df.at[df.index[i], 'trend'] = False # 保持空
df.at[df.index[i], 'supertrend'] = df['final_upper'].iloc[i]
return df
def get_position(ContextInfo, stock_code):
"""
获取当前持仓数量
"""
# 获取持仓对象列表
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
for pos in positions:
if pos.m_strInstrumentID == stock_code:
return pos.m_nVolume
return 0
def handlebar(ContextInfo):
"""
K线周期回调函数
"""
# 过滤掉未完成的K线 (如果是日线策略,通常在收盘或次日开盘交易)
# 如果是回测模式,ContextInfo.is_last_bar() 为 True 时才代表当前K线走完
# 如果是实盘,通常建议使用定时器或在尾盘触发,这里演示标准逻辑
index = ContextInfo.barpos
realtime = ContextInfo.get_bar_timetag(index)
# 1. 获取历史行情数据
# 获取足够长的数据以计算 ATR 和 趋势迭代,这里取 100 根
count = 100
data_dict = ContextInfo.get_market_data_ex(
['open', 'high', 'low', 'close'],
[ContextInfo.stock_code],
period=ContextInfo.period,
count=count,
dividend_type='front' # 前复权
)
if ContextInfo.stock_code not in data_dict:
return
df = data_dict[ContextInfo.stock_code]
# 数据长度不足无法计算
if len(df) < ContextInfo.atr_period + 2:
return
# 2. 计算 SuperTrend
df = calculate_supertrend(df, ContextInfo.atr_period, ContextInfo.factor)
# 获取最新一根K线的数据(当前 bar)
current_bar = df.iloc[-1]
# 获取前一根K线的数据(用于判断交叉)
prev_bar = df.iloc[-2]
# 3. 获取当前持仓
curr_pos = get_position(ContextInfo, ContextInfo.stock_code)
# 4. 生成交易信号
# 信号逻辑:
# 趋势由 False 变为 True (价格上穿 SuperTrend) -> 买入
# 趋势由 True 变为 False (价格下穿 SuperTrend) -> 卖出
signal_buy = (prev_bar['trend'] == False) and (current_bar['trend'] == True)
signal_sell = (prev_bar['trend'] == True) and (current_bar['trend'] == False)
# 5. 执行交易
# 买入逻辑
if signal_buy and curr_pos == 0:
print(f"[{timetag_to_datetime(realtime, '%Y-%m-%d %H:%M:%S')}] 触发买入信号: 价格 {current_bar['close']}")
passorder(23, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
# 卖出逻辑
elif signal_sell and curr_pos > 0:
print(f"[{timetag_to_datetime(realtime, '%Y-%m-%d %H:%M:%S')}] 触发卖出信号: 价格 {current_bar['close']}")
passorder(24, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, curr_pos, ContextInfo)
# 6. (可选) 在图表上画线,方便回测观察
ContextInfo.paint('SuperTrend', current_bar['supertrend'], -1, 0)
代码关键点解析
-
calculate_supertrend函数:- 这是策略的核心。由于 QMT 内置库(如
talib)通常没有直接的 SuperTrend 函数,我们需要手动实现。 - 迭代计算:SuperTrend 的一个特性是当前的轨道值依赖于前一个时间点的轨道值(为了保持轨道平滑且不反向移动),因此代码中使用了
for循环从period开始遍历数据。 - 趋势判断:通过比较
close和final_upper/final_lower来动态更新trend状态。
- 这是策略的核心。由于 QMT 内置库(如
-
数据获取 (
get_market_data_ex):- 我们获取了
count=100的数据。这是为了确保 ATR 计算稳定,并且有足够的历史数据来让 SuperTrend 的迭代逻辑收敛。 - 使用了
'front'(前复权),这在股票回测中非常重要,避免因分红派息导致的价格跳空触发错误信号。
- 我们获取了
-
交易执行 (
passorder):23代表买入,24代表卖出。1101代表单股、单账号、普通交易。5代表使用最新价下单(市价成交的近似模拟),价格填-1。
-
持仓检查 (
get_position):- 在开仓前检查
curr_pos == 0防止重复开仓。 - 在平仓前检查
curr_pos > 0确保有仓可平。
- 在开仓前检查
如何在 QMT 中使用
- 打开 QMT 客户端,进入【模型研究】或【策略编辑器】。
- 新建一个 Python 策略。
- 将上述代码完整复制粘贴进去。
- 修改参数:
- 将
ContextInfo.account_id修改为您实际的资金账号。 - 根据需要修改
stock_code(标的)和period(周期)。
- 将
- 回测:点击“回测”按钮,设置时间区间,观察策略表现。
- 实盘:在实盘运行前,请务必先在模拟盘验证逻辑,并确保账号已登录。
Q&A
Q: 为什么 SuperTrend 计算部分要用循环,不能直接用 Pandas 向量化吗?
A: SuperTrend 的定义中包含递归逻辑(即:当前的 Final Upper Band 依赖于前一个时间点的 Final Upper Band)。虽然可以通过复杂的 NumPy 逻辑部分向量化,但在 Python 策略中,使用显式的循环逻辑更清晰、更不容易出错,且对于 100 行左右的数据量,性能影响可以忽略不计。
Q: 这个策略可以用于期货吗?
A: 可以。需要做以下修改:
ContextInfo.account_type改为'FUTURE'。passorder的参数需要调整为期货的开平仓指令(如0开多,1平昨多 等)。- 标的代码需要改为期货合约代码(如
'IF2306.IF')。
Q: 如何调整策略的灵敏度?
A: 修改 init 函数中的:
ContextInfo.atr_period:数值越小,ATR 变化越快,指标越灵敏。ContextInfo.factor:数值越小(如 2.0),轨道离价格越近,交易信号越频繁;数值越大(如 4.0),轨道越远,过滤震荡效果越好,但信号会滞后。