问题描述
解决方案
这是一个基于 VHF (Vertical Horizontal Filter,垂直水平过滤指标) 的趋势过滤策略。
策略原理
VHF 指标主要用于判断市场是处于趋势状态还是震荡状态,它本身不指示方向,只指示趋势的强度。
- VHF 计算公式:
分子= N周期内最高收盘价 - N周期内最低收盘价分母= N周期内每日收盘价变动绝对值的总和VHF= 分子 / 分母
- 策略逻辑:
- 趋势判断:当 VHF 值较高时,表明处于趋势行情;当 VHF 值较低时,表明处于震荡行情。
- 方向判断:由于 VHF 不具备方向性,我们结合 双均线 (MA) 来判断买卖方向。
- 买入条件:短期均线 > 长期均线(多头排列) 且 VHF > 设定阈值(确认趋势强度足够)。
- 卖出条件:短期均线 < 长期均线(空头排列) 或 VHF < 设定阈值(趋势减弱进入震荡)。
策略代码
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
def initialize(context):
"""
初始化函数,设定基准、手续费、滑点及策略参数
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定手续费:股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# --- 策略参数设置 ---
# 操作标的:这里以沪深300ETF为例,也可以换成具体股票
g.security = '510300.XSHG'
# VHF指标周期 N
g.vhf_n = 28
# VHF过滤阈值 (通常在0.3到0.4之间,需根据标的波动特性调整)
# 大于此值认为有趋势,小于此值认为震荡
g.vhf_threshold = 0.30
# 均线周期,用于判断方向
g.ma_short_window = 10
g.ma_long_window = 30
# 每天开盘时运行
run_daily(market_open, time='09:30')
def calculate_vhf(security, n):
"""
计算VHF指标
VHF = (HCP - LCP) / Sum of absolute close price changes
"""
# 获取过去 N+1 天的数据(因为计算变动需要多一天前值)
# 注意:attribute_history 获取的是不包含当天的历史数据
data = attribute_history(security, n + 1, '1d', ['close'])
closes = data['close'].values
if len(closes) < n + 1:
return 0
# 取最近 N 天的收盘价用于计算最高最低
recent_n_closes = closes[-n:]
# 分子:N周期内最高价 - 最低价
hcp = np.max(recent_n_closes)
lcp = np.min(recent_n_closes)
numerator = hcp - lcp
# 分母:N周期内收盘价变动绝对值之和
# np.diff 计算相邻元素差值
diffs = np.abs(np.diff(closes))
denominator = np.sum(diffs[-n:])
if denominator == 0:
return 0
vhf = numerator / denominator
return vhf
def market_open(context):
"""
每日交易逻辑
"""
security = g.security
# 1. 获取均线数据
# 获取足够长度的历史数据以计算长期均线
hist_data = attribute_history(security, g.ma_long_window, '1d', ['close'])
if len(hist_data) < g.ma_long_window:
return
closes = hist_data['close'].values
# 计算短期和长期均线
ma_short = np.mean(closes[-g.ma_short_window:])
ma_long = np.mean(closes)
# 2. 计算 VHF 指标
vhf_value = calculate_vhf(security, g.vhf_n)
# 3. 获取当前持仓和资金
position = context.portfolio.positions[security]
cash = context.portfolio.available_cash
# 记录指标值方便回测查看
record(VHF=vhf_value, MA_Short=ma_short, MA_Long=ma_long)
# --- 交易信号判断 ---
# 信号1:趋势强度足够 (VHF > 阈值)
is_trending = vhf_value > g.vhf_threshold
# 信号2:方向判断 (短均线 > 长均线)
is_bullish = ma_short > ma_long
# 买入逻辑:
# 1. 处于上升趋势 (均线金叉)
# 2. 且 VHF 指示当前处于趋势行情中 (过滤掉均线纠缠的震荡期)
if is_bullish and is_trending:
if position.closeable_amount == 0:
log.info("趋势确立且向上,VHF: %.3f, 买入 %s" % (vhf_value, security))
order_value(security, cash)
# 卖出逻辑:
# 1. 趋势反转 (均线死叉)
# 2. 或者 VHF 指示进入震荡期 (趋势消失,获利了结或止损)
elif (not is_bullish) or (not is_trending):
if position.closeable_amount > 0:
reason = "均线死叉" if not is_bullish else "进入震荡(VHF过低)"
log.info("卖出信号触发 [%s],VHF: %.3f, 卖出 %s" % (reason, vhf_value, security))
order_target(security, 0)
策略要点说明
- VHF 的计算细节:
- 分母计算需要
N个变动值,因此在attribute_history中我们需要获取N+1个收盘价数据,利用np.diff计算每日的差值。
- 分母计算需要
- 阈值选择 (
g.vhf_threshold):- 代码中默认设置为
0.30。 - 如果标的波动剧烈(如创业板),VHF 普遍较高,可能需要调高阈值。
- 如果标的走势平稳(如银行股),可能需要调低阈值。
- 代码中默认设置为
- 结合均线:
- VHF 最大的缺点是无法区分上涨趋势还是下跌趋势。因此,代码中引入了
MA10和MA30。只有当均线呈多头排列且 VHF 确认趋势存在时才买入。
- VHF 最大的缺点是无法区分上涨趋势还是下跌趋势。因此,代码中引入了
- 止盈止损:
- 本策略通过 VHF 下穿阈值(趋势结束)或均线死叉(趋势反转)作为自然的离场信号。
Q&A
Q: 如何优化 VHF 阈值?
A: 固定阈值(如0.3)可能无法适应所有市场阶段。一种优化方法是使用动态阈值,例如计算过去 100 天 VHF 的移动平均值或分位数,当当前 VHF 大于其历史平均值时视为趋势强烈。
Q: 这个策略适合什么标的?
A: 该策略适合具有明显趋势性的标的,如宽基指数 ETF(沪深300、中证500)或强周期性行业股票。对于长期横盘织布的股票,VHF 会长期低于阈值,导致策略一直空仓,这本身也是一种保护机制。
Q: 为什么使用 attribute_history 而不是 get_price?
A: 在 JoinQuant 的回测引擎(run_daily 或 handle_data)中,attribute_history 是获取回测当前时间点之前历史数据的标准且高效的方法,它会自动处理复权和停牌填充,且不会引入未来数据。