ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SuperTrend+ADX双指标趋势跟随策略原理与Python实现详解

SuperTrend+ADX双指标趋势跟随策略原理与Python实现详解

做趋势交易的人一定绕不开那两个经典问题:趋势到底走没走出来?现在进场是顺势还是接飞刀?我做过一段时间CTA实盘,也把市面上的趋势指标挨个试了个遍,最后长期留在策略里的组合,就是SuperTrend配ADX。这篇文章想把这对“黄金搭档”的原理、Python实现和实战调参一次讲透。

SuperTrend负责把行情压缩成一条直观的跟踪线,告诉你“当前该站在多头还是空头这一边”;ADX负责告诉你“这股势头到底有多猛,值不值得信任”。单独用SuperTrend,震荡市里会被反复打脸;单独用ADX,能识别强度却给不了具体的进出场位置。两个一组合,就是一个把“方向”和“强度”都管住的完整框架。

这篇文章适合正在写趋势策略、被震荡行情折磨过、或者想把手上的SuperTrend信号加一层过滤器的量化研究者。我会把ATR的计算细节、DMI的推导过程、信号叠加的逻辑、回测时容易踩的坑全部交代清楚,代码直接可跑,策略框架可以直接改成你自己的品种和周期。

1. SuperTrend指标原理解析

1.1 从ATR说起:SuperTrend的地基

SuperTrend这个名字听着高级,看穿之后非常简单,它就是一条围绕价格波动的自适应跟踪线。它跟的是平均真实波幅(ATR,Average True Range),ATR本身就是衡量波动率的工具,通俗地讲就是“最近一根K线平均能晃荡多少个点位”。

真正的趋势跟踪,难的不是“涨了买跌了卖”,而是震荡和回调时还能不能拿住仓位。ATR能告诉你当前市场的“噪音水平”,SuperTrend再用这个噪音水平去构建一条上下轨。行情向上突破上轨,就认为上升趋势成立,跟踪线翻到多头方向;行情向下跌破下轨,就认为下降趋势成立,跟踪线翻到空头方向。

1.2 真实波幅TR的计算

真实波幅TR(True Range)的定义要记住三个值:

  • 当前K线最高价减去当前K线最低价,即|High - Low|
  • 当前K线最高价减去前一根K线收盘价的绝对值,即|High - PrevClose|
  • 当前K线最低价减去前一根K线收盘价的绝对值,即|Low - PrevClose|

取三者最大值就是TR。

为什么是这三项?因为有时候K线直接跳空高开或低开,用当根高低价差会漏掉跳空带来的波动。TR把前收盘加进来,等于把跳空缺口也算进了“真实波幅”。这也是为什么ATR比普通标准差指标更能反映行情真正的波动烈度。

ATR就是TR在N个周期内的均值,常用14。用Python写一段最朴素的计算逻辑:

import pandas as pd import numpy as np def calc_atr(df, period=14): high = df['high'] low = df['low'] close = df['close'] prev_close = close.shift(1) tr = pd.concat([ high - low, (high - prev_close).abs(), (low - prev_close).abs() ], axis=1).max(axis=1) # 首根K线没有前收盘,直接用高低价差 atr = tr.ewm(alpha=1/period, min_periods=period).mean() return atr

这里用的ewm是变种的Wilder平滑,和直接用rolling(14).mean()的区别在于记忆连续性。Wilder平滑对近期的波动更敏感,同时不会让很久以前的极端波动一次性掉出窗口,这一点在趋势跟踪里很关键。

1.3 SuperTrend上下轨与方向判定

有了ATR,SuperTrend就顺理成章了。需要两个参数:

  • 周期length
  • 倍数multiplier

经典默认参数是10和3,也就是用10周期ATR,把ATR乘以3作为上下轨的缓冲宽度。

基本逻辑:

def calc_supertrend(df, period=10, multiplier=3): atr = calc_atr(df, period) hl2 = (df['high'] + df['low']) / 2 upper_band = hl2 + multiplier * atr lower_band = hl2 - multiplier * atr supertrend = pd.Series(np.nan, index=df.index) direction = pd.Series(1, index=df.index) for i in range(1, len(df)): # 上轨不能突降,下轨不能突升,保证轨道的连续性 upper_band.iloc[i] = min(upper_band.iloc[i], upper_band.iloc[i-1]) if direction.iloc[i-1] == 1 else upper_band.iloc[i] lower_band.iloc[i] = max(lower_band.iloc[i], lower_band.iloc[i-1]) if direction.iloc[i-1] == -1 else lower_band.iloc[i] if direction.iloc[i-1] == 1: direction.iloc[i] = 1 if df['close'].iloc[i] > upper_band.iloc[i] else -1 else: direction.iloc[i] = -1 if df['close'].iloc[i] < lower_band.iloc[i] else 1 supertrend.iloc[i] = lower_band.iloc[i] if direction.iloc[i] == 1 else upper_band.iloc[i] return supertrend, direction

这里有一处非常容易写错的地方:轨道的“连续性”处理。上升趋势中,如果价格回调导致计算出来的上轨突然下移,不能让上轨直接跳下去,否则行情稍微一抖就会沾到上轨触发反转信号。所以要取当前计算值和前值较小者,而不是直接用最新值。同理,下降趋势中的下轨,要取当前计算值和前值较大者。

很多初学实现的人省略了这一步,直接用原始上下轨判方向,结果就是信号在震荡行情里频繁翻转。这个细节直接决定SuperTrend在实盘里好不好用。

1.4 SuperTrend信号怎么看

direction为1时,表示趋势偏多,SuperTrend线画在K线下方,扮演支撑位角色;direction为-1时,表示趋势偏空,SuperTrend线画在K线上方,扮演压力位角色。信号翻转时,就是方向切换的时刻。

我个人经验是,SuperTrend在日线和小时级别表现最稳定,在分钟级别上噪音偏大,如果一定要做分钟级别,multiplier要加大到4以上。具体参数后面专门讲调参时再展开。

2. ADX指标原理解析

2.1 ADX不是用来判断方向的指标

ADX全称是Average Directional Index,平均方向性指数。很多人第一次看到ADX会误会它和MACD一样判断涨跌,其实它只衡量趋势的强度,不给出多空方向。真正的多空方向,要看它的两个辅助线:+DI和-DI。

打个比方:ADX就像汽车仪表盘上的时速表,告诉你车开得有多快,但不告诉你车是往南开还是往北开。方向要看+DI和-DI的相对高低。

这个区分极其关键,也解释了为什么ADX最适合给SuperTrend做过滤器。SuperTrend管方向,ADX管力度,两者各司其职,不重叠不冲突。

2.2 +DI与-DI的计算

计算ADX之前,先要计算方向变动值DM(Directional Movement)。

+DM的定义:当前最高价减去前一根最高价,且这个差值要大于前一根最低价减去当前最低价的差值,才有效;如果差值小于等于0,+DM直接取0。

-DM同理,当前最低价减去前一根最低价,且这个差值要大于前一根最高价减去当前最高价的差值,才有效;如果差值小于等于0,-DM取0。

用Python实现:

def calc_di(df, period=14): high = df['high'] low = df['low'] prev_high = high.shift(1) prev_low = low.shift(1) up_move = high - prev_high down_move = prev_low - low plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0) minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0) # 用Wilder平滑 plus_dm_smooth = pd.Series(plus_dm).ewm(alpha=1/period, min_periods=period).mean() minus_dm_smooth = pd.Series(minus_dm).ewm(alpha=1/period, min_periods=period).mean() atr_smooth = calc_atr(df, period) plus_di = 100 * plus_dm_smooth / atr_smooth minus_di = 100 * minus_dm_smooth / atr_smooth return plus_di, minus_di

+DI和-DI都是百分比形式。+DI大于-DI说明多头力量占优,反之空头力量占优。+DI和-DI的差,构成了方向性指数DX(Directional Index),DX再经过平滑就成了ADX。

2.3 DX与ADX

DX的计算:

dx = (plus_di - minus_di).abs() / (plus_di + minus_di) * 100

ADX就是对DX做一次Wilder平滑,参数通常取14:

adx = dx.ewm(alpha=1/period, min_periods=period).mean()

为什么ADX会比DX有用?因为单日DX波动极大,一根大阳线可能让DX从20飙到60,下一根小十字星又跌回15。ADX平滑之后,显示的是“平均”的方向强度,过滤掉了单日噪音。

把整套逻辑合到一起的完整实现:

def calc_adx(df, period=14): plus_di, minus_di = calc_di(df, period) dx = (plus_di - minus_di).abs() / (plus_di + minus_di) * 100 adx = dx.ewm(alpha=1/period, min_periods=period).mean() return adx, plus_di, minus_di

2.4 ADX数值怎么读

ADX取值范围在0到100之间。常见经验值:

  • ADX低于20:趋势很弱或没有趋势,典型的震荡市
  • ADX在20到40之间:趋势正在形成或发展中,适合趋势跟踪
  • ADX高于40:趋势非常强劲,但要小心过度延展

这个阈值不是死规矩。不同周期、不同品种的“强趋势”标准不一样。有人用25做阈值,有人用30。我感觉最佳阈值要在具体品种上回测统计,先看ADX分布,再定阈值。

3. 为什么SuperTrend与ADX是“黄金搭档”

3.1 一个早晚会单独失效的SuperTrend

单独用SuperTrend的问题,我最开始做回测的时候就撞上了:在单边行情里,它确实能吃到一整段利润;但行情一旦进入横盘整理,价格反复穿越上下轨,信号就会高频翻转。

用一个词形容就是“磨损”。每次翻转产生一次手续费和滑点,账户资金就在震荡中被慢慢磨掉。更麻烦的是,SuperTrend的信号频率自己控制不了,它不像布林带那样可以通过带宽判断“现在适不适合交易”。

3.2 ADX能精准堵住震荡期的亏损

ADX的强项恰好弥补了这个缺陷。ADX低的时候,说明当前没有像样的方向性推动力,价格只是在原地打转。这时SuperTrend给的信号,大概率是假突破。

把ADX作为过滤器叠加到SuperTrend上,逻辑就变成:只有当ADX高于某个阈值,确认趋势强度足够时,才允许SuperTrend信号进场。震荡期ADX低于阈值,即使SuperTrend翻了信号,策略也只是空仓等待,而不是反复止损。

我把这对组合称为“黄金搭档”,核心原因就是它们一个管“怎么做”,一个管“做不做”。SuperTrend解决不了的是“这个信号值不值得做”,ADX恰好能回答。

3.3 ADX本身不适合单独做信号

反过来说,ADX单独用也成不了策略。ADX很高的时候,只代表当前趋势很强,但价格已经走了很远,追进去大概率买在趋势尾段。ADX刚抬头的时候,又没法给出具体进场点位。

所以实战中最佳配合方式是:用SuperTrend确定方向和进出场位置,用ADX过滤掉低质量信号。ADX用来做闸门,SuperTrend用来做方向盘,闸门不开,方向盘打到哪都没有意义。

3.4 组合后的信号明确且可执行

组合之后的交易规则非常清晰:

  • 当前方向为多头,且ADX大于阈值,做多
  • 当前方向为空头,且ADX大于阈值,做空
  • ADX低于阈值,空仓观望,不参与震荡

另外还有一个进阶用法:ADX的高点拐头经常警示趋势即将反转或加速,配合SuperTrend翻信号一起看,可以减少右侧进场的滞后感。

4. 完整策略实现:Python代码与回测

4.1 策略逻辑设计

策略整体分为三大部分:

  1. 计算指标:SuperTrend、ADX、+DI和-DI
  2. 生成信号:SuperTrend翻多且ADX满足阈值,买入;SuperTrend翻空且ADX满足阈值,卖出或做空;ADX跌破阈值,平仓
  3. 回测评估:计算收益曲线、最大回撤、胜率等基础绩效指标

这里不引入复杂的第三方回测框架,就用向量化思想写一套轻量回测,逻辑透明,出了问题一眼就能看到数据哪一步不对。对于想快速验证指标想法的人来说,这套代码比背框架好用得多。

4.2 完整可运行代码

先把指标计算和回测逻辑整合成一个完整的脚本,以下代码基于经典的OHLCV数据格式:

import pandas as pd import numpy as np def calc_atr(df, period=14): prev_close = df['close'].shift(1) tr = pd.concat([ df['high'] - df['low'], (df['high'] - prev_close).abs(), (df['low'] - prev_close).abs() ], axis=1).max(axis=1) return tr.ewm(alpha=1/period, min_periods=period).mean() def calc_supertrend(df, period=10, multiplier=3): atr = calc_atr(df, period) hl2 = (df['high'] + df['low']) / 2 upper_band = hl2 + multiplier * atr lower_band = hl2 - multiplier * atr supertrend = pd.Series(np.nan, index=df.index) direction = pd.Series(1, index=df.index) for i in range(1, len(df)): if direction.iloc[i-1] == 1: upper_band.iloc[i] = min(upper_band.iloc[i], upper_band.iloc[i-1]) else: lower_band.iloc[i] = max(lower_band.iloc[i], lower_band.iloc[i-1]) if direction.iloc[i-1] == 1: direction.iloc[i] = 1 if df['close'].iloc[i] > upper_band.iloc[i] else -1 else: direction.iloc[i] = -1 if df['close'].iloc[i] < lower_band.iloc[i] else 1 supertrend.iloc[i] = lower_band.iloc[i] if direction.iloc[i] == 1 else upper_band.iloc[i] df['supertrend'] = supertrend df['direction'] = direction return df def calc_adx(df, period=14): prev_high = df['high'].shift(1) prev_low = df['low'].shift(1) up_move = df['high'] - prev_high down_move = prev_low - df['low'] plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0) minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0) plus_dm_smooth = pd.Series(plus_dm).ewm(alpha=1/period, min_periods=period).mean() minus_dm_smooth = pd.Series(minus_dm).ewm(alpha=1/period, min_periods=period).mean() atr_smooth = calc_atr(df, period) plus_di = 100 * plus_dm_smooth / atr_smooth minus_di = 100 * minus_dm_smooth / atr_smooth dx = (plus_di - minus_di).abs() / (plus_di + minus_di) * 100 adx = dx.ewm(alpha=1/period, min_periods=period).mean() df['adx'] = adx df['plus_di'] = plus_di df['minus_di'] = minus_di return df def backtest(df, adx_threshold=25, initial_capital=100000): df['position'] = 0 # 进场条件:SuperTrend方向为多/空,且ADX过阈值 long_condition = (df['direction'] == 1) & (df['adx'] > adx_threshold) short_condition = (df['direction'] == -1) & (df['adx'] > adx_threshold) # 离场条件:ADX跌破阈值则退出持仓 exit_long = (df['direction'] == 1) & (df['adx'] <= adx_threshold) exit_short = (df['direction'] == -1) & (df['adx'] <= adx_threshold) df.loc[long_condition, 'position'] = 1 df.loc[short_condition, 'position'] = -1 df.loc[exit_long, 'position'] = 0 df.loc[exit_short, 'position'] = 0 # 持仓状态保持:只做多头+空头的简单切换 df['position'] = df['position'].replace(0, np.nan).ffill().fillna(0) # 收益率计算 df['returns'] = df['close'].pct_change() df['strategy_returns'] = df['position'].shift(1) * df['returns'] # 资金曲线 df['equity'] = initial_capital * (1 + df['strategy_returns']).cumprod() # 绩效指标 total_return = df['equity'].iloc[-1] / initial_capital - 1 daily_returns = df['strategy_returns'].dropna() sharpe = np.sqrt(len(daily_returns)) * daily_returns.mean() / daily_returns.std() max_drawdown = (df['equity'] / df['equity'].cummax() - 1).min() return df, { 'total_return': total_return, 'sharpe': sharpe, 'max_drawdown': max_drawdown, 'positive_rate': (daily_returns > 0).mean() }

跑一个简单示例:

# 假设df已经加载好,包含open, high, low, close, volume df = calc_supertrend(df, period=10, multiplier=3) df = calc_adx(df, period=14) result_df, metrics = backtest(df, adx_threshold=25) print(f"总收益率: {metrics['total_return']:.2%}") print(f"夏普比率: {metrics['sharpe']:.2f}") print(f"最大回撤: {metrics['max_drawdown']:.2%}") print(f"胜率: {metrics['positive_rate']:.2%}")

4.3 回测时最容易犯的错

上面这段代码看着简单,里面有几个埋点必须说明白,这些都是我实际写回测时踩过的坑。

第一,position的shift(1)。策略信号基于当根K线收盘价产生,但你最早只能在下一根K线开盘或收盘时成交。直接拿当根返回值做收益计算,会造成前视偏差(look-ahead bias),回测曲线会异常好看,实盘却完全做不到。所以在策略收益那一行,一定要对position做shift(1)。

第二,ADX过滤条件不能和SuperTrend翻信号逻辑写反。正确逻辑是“方向已经定了,ADX决定做不做”。如果你写的是“ADX刚突破阈值才进场”,那会在ADX高位回落时错过离场,在底部攀升时错过进场。过滤器和触发器的职责要分清。

第三,空仓期间的position用ffill填充,这里有一个隐性设定:一旦进入空仓状态,需要SuperTrend和ADX重新同时满足条件才能再次进场。如果你没有这段逻辑,程序会把空仓状态直接跳回上一次的多头或空头方向,这绝对是错误信号。

4.4 改用日线数据和真实品种前的验证步骤

在实盘或更精细的回测平台上跑之前,建议先做三轮验证:

第一轮:肉眼验证指标画线。把SuperTrend线、ADX、+DI、-DI全部画到图上,随机抽查10个信号点,手动确认信号逻辑没有错位。

第二轮:与买卖点一一对照。挑一段大涨和一段横盘,看策略有没有在ADX低于阈值时频繁交易,是否在趋势最强时持有仓位。

第三轮:参数敏感性分析。把period和multiplier、adx_threshold各取几个相邻值,观察绩效是否剧烈波动。如果参数微调导致结果天翻地覆,说明策略过拟合风险极大。

5. 参数优化与实战调参心得

5.1 三个核心参数的物理意义

SuperTrend有两个参数:ATR周期period和倍数multiplier。ADX有一个参数:周期period,外加一个过滤器阈值adx_threshold。

period决定了指标对行情的响应速度。数值越小,指标越敏感,信号出得越早,但假信号越多;数值越大,指标越迟钝,信号滞后严重,但穿过震荡区的韧性更强。

multiplier决定了轨道离价格的距离。multiplier越小,轨道越贴近价格,信号早出,但容易被小回调打掉;multiplier越大,轨道越宽,能滤掉更多噪音,但进场明显偏晚。

ATR倍数和ADX阈值本质上都在做同一件事:过滤噪音。但两者过滤维度不同,一个从波动幅度上过滤,一个从趋势强度上过滤。联合使用比单靠某一个参数更能适应不同市场状态。

5.2 一个相对稳妥的起始参数组

参数快速追踪型稳健型大周期型
SuperTrend period101220
SuperTrend multiplier2.53.04.0
ADX period141418
ADX阈值202530

没有万能参数。波动率高的品种,比如主流数字货币和某些商品期货,multiplier要加大,否则一根大影线就打出止损;波动率低的品种,比如债券期货和部分股指,multiplier可以适当放小。

ADX阈值也是一样。我看过一个统计,某商品期货日线级别的ADX中位数在22左右,但到了周线级别,中位数能上到28。所以换周期之后,阈值一定要重新校准,不能拿着日线的25直接套到周线上。

5.3 调参时如何避免过拟合

如果你是拿历史数据一个个试参数,试出个漂亮曲线就急着上实盘,那我劝你先冷静。一个坑我反复踩过:回测曲线越是完美,参数对历史数据的“记忆”越强,实盘越容易翻车。

健康的调参顺序应该是:

  1. 把数据切成样本内和样本外两部分,样本内确定参数范围
  2. 在样本外验证参数表现,不追求最优,追求稳定
  3. 观察相邻参数组的收益分布是否平滑,如果只有孤零零一组参数暴利,其他全部亏损,剔除掉

还有一点:不要只盯总收益率和夏普比率。最大回撤和回撤持续周期,决定了你能不能拿住这个策略。一个年化30%但回撤50%的策略,和年化20%但回撤15%的策略放在一起,我会选后者,因为我见过太多人在50%回撤时心理崩溃。

5.4 过滤器阈值是不是越灵活越好

严格来说,ADX阈值本身也可以动态化。比如用ATR的百分位滚动来确定“当前波动下什么算高趋势强度”,这种做法在震荡转单边的时候反应更快。

但我个人建议,新手阶段先用固定阈值。动态阈值的优势需要足够的样本量才能体现,而且会引入更多自由度,过拟合风险成倍增加。固定阈值至少让人一眼能看懂信号怎么来的,出了问题也容易排查。策略先做减法,做简单,稳定了再谈复杂化。

6. 常见问题与排查技巧实录

6.1 高频踩坑点速查表

现象原因解决方案
回测收益极高但实盘连续亏损前视偏差,position未shift检查策略收益计算时是否用了当根信号
震荡期交易频繁,磨损严重ADX阈值设太低或未加ADX过滤提高阈值,观察ADX分布曲线
大涨行情中途被甩下车multiplier太小或ATR周期太短适当加大multiplier,延长period
信号翻转但ADX还没跟上指标同步性错位先确认SuperTrend和ADX用的是同一组OHLC数据
数据前几根指标为NaNWilder平滑需要预热期手动丢弃初期数据,不要强制填0
ADX很高但SuperTrend方向频繁变可能用了超短线周期切换更高周期或加大multiplier

6.2 SuperTrend连续上轨被突破的真相

我最开始写SuperTrend实现的时候,跑出一段连续信号翻转的行情,一度怀疑是算法写错了。后来把行情图调出来才发现,那段行情是典型的“一字横盘”,多空双方反复拉锯,每根K线都同时触上轨又触下轨。

这种情况靠加ADX过滤还不够,因为ADX在横盘末期可能已经开始抬头。更稳妥的方式是叠加一个“K线实体过滤”:只有收盘价明显收在轨道外侧才算有效突破,例如要求突破幅度超过0.2个ATR。这个过滤条件能多挡掉一大部分毛刺信号。

6.3 ADX数据与SuperTrend信号不同步怎么处理

你会发现有时SuperTrend已经翻空,但ADX还在40以上高位运行。这是因为ADX衡量的是过去一段时间的平均趋势强度,天然滞后。此时不要强行要求ADX先降下去再进空单,正确做法是:SuperTrend失去多头方向,ADX强度虽高但方向已不匹配,先离场观望,等新的方向信号确认后再进场。

用ADX不是用来“预测趋势何时结束”,而是用来“避免在无趋势时进场”,记住这一点,就不会总纠结它为什么不提前拐头。

6.4 极端行情下的应对经验

单根巨大的影线会在一瞬间打穿SuperTrend的上下轨,又迅速拉回。这种情况下信号会闪断,策略可能先止损一次,又马上反向进场,然后再次止损。

我的应对方法是在策略层加一条“最短持仓K线数”规则,比如信号翻转后至少持仓2根K线,期间无论指标怎么说都不动。这个规则会增加单笔回撤,但能大幅减少极端行情下的反复止损次数,整体收益反而提升。

6.5 代码实现的性能注意点

这里有个实际经验:如果数据量巨大,比如几年逐笔或几分钟级别的K线,循环实现SuperTrend会比较慢。可以先用Numba的jit加速循环,或者用PyTorch把序列计算并行化。但如果你只是试验策略思路,没必要为性能优化耗费精力,先把逻辑正确放在第一位。

很多人的误区是先折腾性能优化,最后发现指标算错了,白瞎了半天。先跑顺最基本逻辑,验证策略有效,再回头做性能优化,顺序千万别搞反。

写在最后

我个人在实际操作中的体会是,SuperTrend和ADX这套组合最讨喜的地方,不是它胜率有多高,而是它让交易决策变得特别清晰。每次看到信号的时候,我都知道为什么进场、为什么空仓,不再凭感觉猜行情。信号质量不达标就不做,这种纪律性,比任何花哨的指标都值钱。

最后再分享一个小技巧:实盘部署之前,一定要让策略先跑一段模拟盘或最小仓位实盘,时间跨度至少覆盖一次完整的ADX从低到高再走低的过程。见过它在真实行情里的表现,你才能对参数和过滤器有真正的信任感。这种信任,不是回测曲线能给你的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进