问题描述
解决方案
这是一个基于 Vortex Indicator (VI, 旋涡指标) 的趋势跟踪策略。
策略原理
Vortex 指标由两条线组成:$VI^+$(正向趋势)和 $VI^-$(负向趋势)。
- $VI^+$ (Vortex Positive):衡量上升趋势的强度。
- $VI^-$ (Vortex Negative):衡量下降趋势的强度。
交易逻辑:
- 买入信号(金叉):当 $VI^+$ 上穿 $VI^-$ 时,表明上升趋势确立,执行买入。
- 卖出信号(死叉):当 $VI^+$ 下穿 $VI^-$ 时,表明下降趋势确立,执行卖出(平仓)。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 设置策略参数:Vortex指标的周期,通常为14
ContextInfo.vi_period = 14
# 设置股票池,这里示例使用 沪深300ETF
ContextInfo.stock_code = '510300.SH'
ContextInfo.set_universe([ContextInfo.stock_code])
# 设置资金账号(请替换为您的真实账号)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
ContextInfo.account_type = 'STOCK'
ContextInfo.set_account(ContextInfo.account_id)
# 设置回测参数(仅回测有效,实盘需在界面设置)
ContextInfo.set_commission(0, [0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 5]) # 设置费率
print("Vortex 策略初始化完成")
def handlebar(ContextInfo):
"""
K线逐笔运行函数
"""
# 获取当前正在处理的股票代码
stock_code = ContextInfo.stock_code
# 获取当前K线索引
index = ContextInfo.barpos
# 获取历史行情数据
# 我们需要足够的历史数据来计算 rolling sum,取 period * 2 确保数据充足
data_count = ContextInfo.vi_period * 2 + 5
# 使用 get_market_data_ex 获取数据 (开高低收)
# 注意:period='1d' 表示日线,可根据需要修改为 '15m', '30m' 等
market_data = ContextInfo.get_market_data_ex(
['high', 'low', 'close', 'open'],
[stock_code],
period='1d',
count=data_count,
dividend_type='front' # 前复权
)
if stock_code not in market_data:
return
df = market_data[stock_code]
# 如果数据量不足以计算指标,直接返回
if len(df) < ContextInfo.vi_period + 2:
return
# --- Vortex 指标计算逻辑 ---
# 1. 计算 True Range (TR)
# TR = Max(High - Low, |High - PrevClose|, |Low - PrevClose|)
high = df['high']
low = df['low']
close = df['close']
prev_close = close.shift(1)
tr1 = high - low
tr2 = abs(high - prev_close)
tr3 = abs(low - prev_close)
# 逐元素取最大值
true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
# 2. 计算 VM+ 和 VM- (Vortex Movement)
# VM+ = |Current High - Previous Low|
# VM- = |Current Low - Previous High|
prev_low = low.shift(1)
prev_high = high.shift(1)
vm_plus = abs(high - prev_low)
vm_minus = abs(low - prev_high)
# 3. 对 TR, VM+, VM- 进行周期求和 (Sum over period)
tr_sum = true_range.rolling(window=ContextInfo.vi_period).sum()
vm_plus_sum = vm_plus.rolling(window=ContextInfo.vi_period).sum()
vm_minus_sum = vm_minus.rolling(window=ContextInfo.vi_period).sum()
# 4. 计算最终的 VI+ 和 VI-
vi_plus = vm_plus_sum / tr_sum
vi_minus = vm_minus_sum / tr_sum
# 获取当前K线和上一根K线的指标值(用于判断交叉)
# iloc[-1] 是当前bar,iloc[-2] 是上一根bar
current_vi_plus = vi_plus.iloc[-1]
current_vi_minus = vi_minus.iloc[-1]
prev_vi_plus = vi_plus.iloc[-2]
prev_vi_minus = vi_minus.iloc[-2]
# 检查是否计算出有效值 (排除NaN)
if np.isnan(current_vi_plus) or np.isnan(current_vi_minus):
return
# --- 交易信号判断 ---
# 获取当前持仓
positions = ContextInfo.get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
current_holding = 0
for pos in positions:
if pos.m_strInstrumentID == stock_code:
current_holding = pos.m_nVolume
break
# 信号 1: 金叉 (VI+ 上穿 VI-) -> 买入
buy_signal = (prev_vi_plus < prev_vi_minus) and (current_vi_plus > current_vi_minus)
# 信号 2: 死叉 (VI+ 下穿 VI-) -> 卖出
sell_signal = (prev_vi_plus > prev_vi_minus) and (current_vi_plus < current_vi_minus)
# --- 执行交易 ---
# 如果是最后一根K线(实时行情或回测的当前步)
if ContextInfo.is_last_bar():
timetag = ContextInfo.get_bar_timetag(index)
date_str = timetag_to_datetime(timetag, '%Y-%m-%d %H:%M:%S')
if buy_signal:
# 如果没有持仓,则全仓买入
if current_holding == 0:
print(f"[{date_str}] 金叉出现 (VI+={current_vi_plus:.4f}, VI-={current_vi_minus:.4f}),执行买入: {stock_code}")
# 目标市值下单:调整仓位到 100% (即全仓)
# 注意:实盘中请谨慎使用 order_target_percent,建议根据资金管理逻辑调整
order_target_percent(stock_code, 1.0, "FIX", df['close'].iloc[-1], ContextInfo, ContextInfo.account_id)
elif sell_signal:
# 如果有持仓,则清仓
if current_holding > 0:
print(f"[{date_str}] 死叉出现 (VI+={current_vi_plus:.4f}, VI-={current_vi_minus:.4f}),执行卖出: {stock_code}")
# 目标市值下单:调整仓位到 0% (即清仓)
order_target_percent(stock_code, 0.0, "FIX", df['close'].iloc[-1], ContextInfo, ContextInfo.account_id)
# --- 绘图 (可选,用于回测界面观察) ---
# 绘制 VI+ 和 VI- 曲线
ContextInfo.paint('VI_Plus', current_vi_plus, -1, 0, 'red') # 红色代表多头趋势
ContextInfo.paint('VI_Minus', current_vi_minus, -1, 0, 'green') # 绿色代表空头趋势
代码详细解析
-
初始化 (
init):- 设定了
vi_period = 14,这是 Vortex 指标的标准参数。 - 设定了交易标的(如
510300.SH)和账户信息。 - 注意:请务必将
YOUR_ACCOUNT_ID替换为您实际的 QMT 资金账号。
- 设定了
-
数据获取 (
handlebar):- 使用
get_market_data_ex获取历史数据。 count设置为vi_period * 2 + 5。这是因为计算rolling(14).sum()需要前14天的数据,而计算shift(1)又需要多一天,为了保证计算稳定,多取一些数据是安全的做法。
- 使用
-
指标计算:
- TR (真实波幅):取
High-Low,|High-PrevClose|,|Low-PrevClose|三者中的最大值。 - VM (旋涡移动量):
VM+是当前最高价与前一最低价之差的绝对值。VM-是当前最低价与前一最高价之差的绝对值。
- 归一化:将
VM和TR进行 14 周期求和,然后相除得到最终的VI+和VI-。
- TR (真实波幅):取
-
信号生成:
- 利用
pandas的iloc[-1](当前) 和iloc[-2](前一周期) 来检测交叉。 - 金叉:前一刻 $VI^+ < VI^-$ 且 当前 $VI^+ > VI^-$。
- 死叉:前一刻 $VI^+ > VI^-$ 且 当前 $VI^+ < VI^-$。
- 利用
-
交易执行:
- 使用
order_target_percent函数。 - 买入时设置目标仓位为
1.0(100% 持仓)。 - 卖出时设置目标仓位为
0.0(0% 持仓,即清仓)。 - 这种方式比直接用
passorder更容易管理仓位,避免重复下单导致超仓。
- 使用
Q&A
Q: 如何修改策略的运行周期(例如从日线改为15分钟线)?
A: 在 get_market_data_ex 函数中,将 period='1d' 修改为 period='15m' 即可。同时,请确保在 QMT 界面运行策略时,主图周期也调整为对应的周期,或者确保下载了对应的分钟级历史数据。
Q: 为什么使用 get_market_data_ex 而不是 get_market_data?
A: get_market_data_ex 是 QMT 较新的接口,返回的数据结构(字典包含 DataFrame)更规范,处理多只股票时更方便,且在处理复权和停牌填充方面表现更好。
Q: 可以在实盘中直接使用这个代码吗?
A: 代码逻辑是完整的,但在实盘前建议:
- 填写真实的
account_id。 - 在 QMT 的“模型研究”中进行回测,验证参数(如周期14)是否适合当前市场。
- 实盘建议增加风控逻辑(如止损、单笔最大下单金额限制),目前的
order_target_percent(..., 1.0)是全仓买入,风险较高。