问题描述
解决方案
判断当前市场处于“震荡市”(盘整)还是“趋势市”(单边上涨或下跌)是量化策略中至关重要的一环。没有一种绝对完美的方法,但通常使用以下几种技术指标或统计方法来进行量化判定:
- ADX (平均趋向指标):这是最常用的判断趋势强度的指标。通常 ADX > 25 视为趋势市,ADX < 20 视为震荡市。
- 均线排列与斜率:短期均线与长期均线的相对位置(多头/空头排列)以及均线的斜率(平坦则为震荡)。
- 布林带带宽 (Bandwidth):布林带收口通常意味着波动率降低,进入震荡;开口扩大通常意味着趋势开始。
- 考夫曼效率系数 (Efficiency Ratio):衡量价格变动的效率,效率高为趋势,效率低为震荡。
下面我将提供一个基于 QMT 平台 的完整 Python 策略代码示例。该代码主要使用 ADX 指标 结合 均线系统 来判断当前的市场状态。
QMT 策略代码示例
此代码可以直接在 QMT 的策略编辑器中运行。它会计算 ADX 和均线,并在日志中输出当前的判断结果。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import talib
def init(ContextInfo):
# 设置要操作的股票,这里以沪深300ETF为例
ContextInfo.stock = '510300.SH'
ContextInfo.set_universe([ContextInfo.stock])
# 策略参数设置
ContextInfo.adx_period = 14 # ADX计算周期
ContextInfo.adx_threshold = 25 # ADX趋势阈值,大于此值认为有趋势
ContextInfo.ma_short_period = 20 # 短期均线
ContextInfo.ma_long_period = 60 # 长期均线
# 设置账号(仅作示例,实盘需替换真实账号)
ContextInfo.account_id = 'test_account'
ContextInfo.account_type = 'STOCK'
def handlebar(ContextInfo):
# 获取当前K线位置
index = ContextInfo.barpos
# 获取当前主图代码(或者使用init中设置的代码)
stock_code = ContextInfo.stock
# 获取历史行情数据,长度需要满足计算指标的需求
# 我们需要计算 MA60,且需要一定的缓冲,所以取 100 根 K 线
data_len = 100
# 使用 get_market_data_ex 获取数据 (推荐使用 ex 接口)
# 注意:这里获取的是日线数据,如果在分钟线运行,period需改为 '1m', '5m' 等
market_data = ContextInfo.get_market_data_ex(
['high', 'low', 'close'],
[stock_code],
period=ContextInfo.period,
count=data_len,
dividend_type='front', # 前复权
subscribe=True
)
if stock_code not in market_data:
return
df = market_data[stock_code]
# 确保数据长度足够计算指标
if len(df) < ContextInfo.ma_long_period:
return
# 提取 numpy 数组用于 talib 计算
high_prices = df['high'].values
low_prices = df['low'].values
close_prices = df['close'].values
# --- 方法 1:使用 ADX 指标判断趋势强度 ---
# ADX 反映的是趋势的“强度”,而不是方向
adx = talib.ADX(high_prices, low_prices, close_prices, timeperiod=ContextInfo.adx_period)
current_adx = adx[-1]
# --- 方法 2:使用均线排列判断趋势方向 ---
ma_short = talib.SMA(close_prices, timeperiod=ContextInfo.ma_short_period)
ma_long = talib.SMA(close_prices, timeperiod=ContextInfo.ma_long_period)
current_ma_short = ma_short[-1]
current_ma_long = ma_long[-1]
current_close = close_prices[-1]
# --- 综合判定逻辑 ---
market_status = "未知"
trend_direction = "无"
# 判定 1: 震荡市判定
# 如果 ADX 低于阈值,或者 均线纠缠(这里简化为收盘价在两根均线之间),视为震荡
if current_adx < ContextInfo.adx_threshold:
market_status = "震荡市 (ADX弱)"
elif (current_close > min(current_ma_short, current_ma_long)) and (current_close < max(current_ma_short, current_ma_long)):
market_status = "震荡市 (均线夹层)"
else:
market_status = "趋势市"
# 如果是趋势市,进一步判断方向
if current_close > current_ma_short and current_ma_short > current_ma_long:
trend_direction = "上涨趋势 (多头排列)"
elif current_close < current_ma_short and current_ma_short < current_ma_long:
trend_direction = "下跌趋势 (空头排列)"
else:
trend_direction = "趋势不明 (均线未完全发散)"
# --- 输出结果 ---
# 仅在最后一根K线打印,避免历史回测刷屏
if ContextInfo.is_last_bar():
timetag = ContextInfo.get_bar_timetag(index)
date_str = timetag_to_datetime(timetag, '%Y-%m-%d %H:%M:%S')
print(f"时间: {date_str} | 标的: {stock_code}")
print(f"ADX值: {current_adx:.2f} (阈值: {ContextInfo.adx_threshold})")
print(f"收盘价: {current_close:.2f} | MA{ContextInfo.ma_short_period}: {current_ma_short:.2f} | MA{ContextInfo.ma_long_period}: {current_ma_long:.2f}")
print(f"判定结果: 【{market_status}】 {trend_direction if market_status == '趋势市' else ''}")
print("-" * 30)
# --- 可视化绘图 (可选) ---
# 在图表上绘制 ADX 指标,方便观察
# 注意:ADX 值通常在 0-100 之间,与股价坐标轴不同,建议在副图查看或使用 limit='noaxis'
# ContextInfo.paint('ADX', current_adx, -1, 0)
代码逻辑详解
-
数据获取 (
get_market_data_ex):- 我们获取了
high(最高价),low(最低价),close(收盘价)。 count=100确保有足够的数据来计算 60 日均线和 ADX。dividend_type='front'使用前复权数据,这对于技术指标计算非常重要,可以消除分红配股造成的缺口。
- 我们获取了
-
ADX 指标计算:
- 使用
talib.ADX函数。 - 原理: ADX (Average Directional Index) 衡量趋势的强度。
- 判定:
- 当
ADX < 25(或 20) 时,通常意味着市场处于无趋势或弱趋势状态(震荡)。 - 当
ADX > 25且 ADX 正在上升时,意味着趋势正在增强(无论是上涨还是下跌)。
- 当
- 使用
-
均线系统 (MA):
- 计算短期 (MA20) 和长期 (MA60) 均线。
- 辅助判定: 即使 ADX 很高,我们也可以结合均线来确认方向。
- 多头排列: 价格 > 短期均线 > 长期均线 (强上涨趋势)。
- 空头排列: 价格 < 短期均线 < 长期均线 (强下跌趋势)。
- 夹层: 价格位于两条均线之间,通常是震荡整理的表现。
-
综合判定:
- 代码优先判断 ADX 是否过低。如果过低,直接定性为震荡。
- 如果 ADX 较高,再看均线形态来确定是上涨趋势还是下跌趋势。
其他可选的判定方法(思路)
如果你觉得 ADX 指标滞后,可以尝试修改代码实现以下逻辑:
-
布林带带宽法:
upper, middle, lower = talib.BBANDS(close_prices, timeperiod=20) bandwidth = (upper[-1] - lower[-1]) / middle[-1] # 如果 bandwidth 处于历史低位(例如小于 0.05 或 5%),通常是极度缩量的震荡市 -
近期波动率法 (ATR):
atr = talib.ATR(high_prices, low_prices, close_prices, timeperiod=14) # 如果 ATR 持续走低,代表波动率下降,市场趋于震荡 -
价格区间法 (Donchian Channel):
计算过去 N 天的最高价和最低价。如果(最高价 - 最低价) / 收盘价非常小,说明价格被压缩在一个很窄的箱体内,即为震荡。
常见问题 (Q&A)
Q: 为什么我的 ADX 计算结果一直是 nan?
A: 通常是因为获取的数据长度不够。ADX 需要较长的预热数据。如果你设置周期为 14,建议至少获取 30-50 根 K 线数据。在上面的代码中,我设置了 count=100 来避免这个问题。
Q: 这个策略适合什么周期?
A: 震荡和趋势是相对于周期而言的。日线级别的震荡在 5 分钟级别可能是一个明显的趋势。代码中使用了 ContextInfo.period,这意味着它会自动适应你在 QMT 界面上选择的周期。
Q: 如何在震荡市中避免频繁开仓止损?
A: 在代码的 handlebar 中,你可以加入过滤逻辑:if market_status == "震荡市": return。这样策略在震荡市就会停止交易,或者你可以切换到专门做高抛低吸的网格策略逻辑。