问题描述
解决方案
这是一个基于 相对活力指数 (Relative Vigor Index, RVI) 的 QMT 量化交易策略源码。
策略原理
RVI 是一种振荡指标,用于衡量价格趋势的信念。其核心思想是:在上升趋势中,收盘价通常高于开盘价;在下降趋势中,收盘价通常低于开盘价。
交易逻辑:
- 计算 RVI 线:基于收盘价与开盘价的差值,对比最高价与最低价的差值,经过平滑处理得出。
- 计算 Signal 线:RVI 线的加权移动平均线。
- 买入信号:当 RVI 线 上穿 Signal 线(金叉)时,买入。
- 卖出信号:当 RVI 线 下穿 Signal 线(死叉)时,卖出(清仓)。
策略源码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 设置策略运行的标的,这里以浦发银行为例
ContextInfo.stock = '600000.SH'
# 设置策略运行周期,这里使用日线
ContextInfo.period = '1d'
# RVI 指标参数 N
ContextInfo.N = 10
# 设置资金账号 (请替换为您自己的资金账号)
ContextInfo.account_id = '您的资金账号'
ContextInfo.account_type = 'STOCK' # 股票账户
# 设定股票池
ContextInfo.set_universe([ContextInfo.stock])
# 每次交易的金额(回测模式下使用)
ContextInfo.trade_amount = 100000
def get_rvi(high, low, close, open_price, n):
"""
计算 RVI 指标
公式参考 Ehlers 的定义
"""
# 1. 计算分子 (Close - Open) 的平滑值
# Value1 = ((Close - Open) + 2 * (Close[1] - Open[1]) + 2 * (Close[2] - Open[2]) + (Close[3] - Open[3])) / 6
co = close - open_price
value1 = (co + 2 * co.shift(1) + 2 * co.shift(2) + co.shift(3)) / 6
# 2. 计算分母 (High - Low) 的平滑值
# Value2 = ((High - Low) + 2 * (High[1] - Low[1]) + 2 * (High[2] - Low[2]) + (High[3] - Low[3])) / 6
hl = high - low
value2 = (hl + 2 * hl.shift(1) + 2 * hl.shift(2) + hl.shift(3)) / 6
# 3. 计算 N 周期的累加
num = value1.rolling(window=n).sum()
denom = value2.rolling(window=n).sum()
# 4. 计算 RVI
# 处理分母为0的情况
rvi = np.where(denom != 0, num / denom, 0)
rvi_series = pd.Series(rvi, index=close.index)
# 5. 计算 Signal 线 (RVI 的加权移动平均)
# Signal = (RVI + 2 * RVI[1] + 2 * RVI[2] + RVI[3]) / 6
signal = (rvi_series + 2 * rvi_series.shift(1) + 2 * rvi_series.shift(2) + rvi_series.shift(3)) / 6
return rvi_series, signal
def handlebar(ContextInfo):
"""
K线逐根运行函数
"""
# 获取当前 Bar 的索引
index = ContextInfo.barpos
# 获取当前时间
realtime = ContextInfo.get_bar_timetag(index)
# 获取标的代码
stock = ContextInfo.stock
# 获取历史行情数据
# 为了计算 RVI 和 Signal,我们需要 N + 前置平滑(3) + 信号平滑(3) 的数据量,取 50 根比较稳妥
count = ContextInfo.N + 20
# 使用 get_market_data_ex 获取数据 (推荐使用 ex 接口)
# 注意:QMT 返回的数据是按列排列的 DataFrame
data_map = ContextInfo.get_market_data_ex(
['open', 'high', 'low', 'close'],
[stock],
period=ContextInfo.period,
count=count,
dividend_type='front' # 前复权
)
if stock not in data_map:
return
df = data_map[stock]
# 数据长度不足以计算指标时直接返回
if len(df) < ContextInfo.N + 6:
return
# 计算 RVI 和 Signal
rvi, signal = get_rvi(df['high'], df['low'], df['close'], df['open'], ContextInfo.N)
# 获取最新和上一个周期的指标值
# iloc[-1] 是当前根 K 线(如果是盘中,则是未走完的 K 线)
# iloc[-2] 是上一根 K 线
curr_rvi = rvi.iloc[-1]
prev_rvi = rvi.iloc[-2]
curr_signal = signal.iloc[-1]
prev_signal = signal.iloc[-2]
# 打印调试信息 (可选)
# print(f"Time: {timetag_to_datetime(realtime, '%Y-%m-%d %H:%M:%S')} RVI: {curr_rvi:.4f}, Signal: {curr_signal:.4f}")
# 获取当前持仓
positions = ContextInfo.get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
current_vol = 0
for pos in positions:
if pos.m_strInstrumentID == stock:
current_vol = pos.m_nVolume
break
# --- 交易逻辑 ---
# 1. 买入信号:RVI 上穿 Signal (金叉)
if prev_rvi < prev_signal and curr_rvi > curr_signal:
if current_vol == 0:
print(f"产生买入信号: {stock}, RVI金叉")
# 使用 order_target_value 调整持仓市值到指定金额
# 注意:实盘中请确保 account_id 设置正确,并根据需要替换为 passorder
order_target_value(stock, ContextInfo.trade_amount, ContextInfo, ContextInfo.account_id)
# 2. 卖出信号:RVI 下穿 Signal (死叉)
elif prev_rvi > prev_signal and curr_rvi < curr_signal:
if current_vol > 0:
print(f"产生卖出信号: {stock}, RVI死叉")
# 清仓
order_target_value(stock, 0, ContextInfo, ContextInfo.account_id)
# 3. 绘图 (可选,用于回测界面展示)
ContextInfo.paint('RVI', curr_rvi, -1, 0, 'red')
ContextInfo.paint('Signal', curr_signal, -1, 0, 'green')
代码关键点说明
-
数据获取 (
get_market_data_ex):- 使用了
get_market_data_ex接口,这是 QMT 推荐的高效数据接口。 - 设置了
dividend_type='front'(前复权),这对于技术指标计算非常重要,可以避免分红派息导致的价格跳空影响指标准确性。
- 使用了
-
RVI 计算逻辑 (
get_rvi):- 代码中严格遵循了 RVI 的经典算法(Ehlers 算法)。
- 使用了
pandas的shift和rolling函数来处理平滑和移动求和,避免了复杂的循环,提高了计算效率。 - 平滑处理:RVI 的核心在于分子(收盘-开盘)和分母(最高-最低)都经过了
[1, 2, 2, 1]权重的平滑处理,这比简单的移动平均更能过滤噪声。
-
交易执行 (
order_target_value):- 使用了
order_target_value函数。这个函数非常适合策略编写,因为它会自动计算需要买入或卖出的数量。 - 买入时:将持仓调整到
ContextInfo.trade_amount(例如 10 万元)。 - 卖出时:将持仓调整到 0(清仓)。
- 使用了
-
绘图 (
paint):- 在回测界面的副图中绘制了 RVI(红线)和 Signal(绿线),方便直观验证策略信号。
如何使用
- 打开 QMT 客户端,进入“模型研究”或“策略编辑器”。
- 新建一个 Python 策略。
- 将上述代码复制粘贴进去。
- 修改账号:将
ContextInfo.account_id = '您的资金账号'修改为您实际的资金账号。 - 点击“编译”检查无误后,可以进行“回测”或在实盘/模拟盘中“运行”。
Q&A
Q: 为什么 RVI 指标计算要用 shift(1), shift(2) 等?
A: RVI 的经典公式中包含了一个类似于 FIR 滤波器的平滑过程(权重为 1/6, 2/6, 2/6, 1/6),这需要用到当前周期及前三个周期的数据。shift(n) 用于获取前 n 个周期的数据。
Q: 这个策略适合什么周期?
A: RVI 是一种短线振荡指标,通常在日线或 60 分钟线上效果较好。如果用于分钟级(如 1 分钟、5 分钟),噪音可能会比较大,建议适当增大参数 N。
Q: 如何在实盘中避免信号闪烁?
A: 当前代码使用的是 curr_rvi (当前 K 线) 和 prev_rvi (上一根 K 线)。在实盘中,当前 K 线未走完时,curr_rvi 会随价格跳动。如果希望信号完全确认后再下单,可以在 handlebar 开头添加 if not ContextInfo.is_last_bar(): return,但这会导致回测无法进行。更通用的做法是只在 K 线收盘时(下一根 K 线开始瞬间)或者使用 ContextInfo.is_new_bar() 逻辑来触发下单。上述代码为了回测演示方便,采用了实时计算逻辑。