SuperTrend+ADX双指标趋势跟随策略原理与Python实现详解
做趋势交易的人一定绕不开那两个经典问题趋势到底走没走出来现在进场是顺势还是接飞刀我做过一段时间CTA实盘也把市面上的趋势指标挨个试了个遍最后长期留在策略里的组合就是SuperTrend配ADX。这篇文章想把这对“黄金搭档”的原理、Python实现和实战调参一次讲透。SuperTrend负责把行情压缩成一条直观的跟踪线告诉你“当前该站在多头还是空头这一边”ADX负责告诉你“这股势头到底有多猛值不值得信任”。单独用SuperTrend震荡市里会被反复打脸单独用ADX能识别强度却给不了具体的进出场位置。两个一组合就是一个把“方向”和“强度”都管住的完整框架。这篇文章适合正在写趋势策略、被震荡行情折磨过、或者想把手上的SuperTrend信号加一层过滤器的量化研究者。我会把ATR的计算细节、DMI的推导过程、信号叠加的逻辑、回测时容易踩的坑全部交代清楚代码直接可跑策略框架可以直接改成你自己的品种和周期。1. SuperTrend指标原理解析1.1 从ATR说起SuperTrend的地基SuperTrend这个名字听着高级看穿之后非常简单它就是一条围绕价格波动的自适应跟踪线。它跟的是平均真实波幅ATRAverage True RangeATR本身就是衡量波动率的工具通俗地讲就是“最近一根K线平均能晃荡多少个点位”。真正的趋势跟踪难的不是“涨了买跌了卖”而是震荡和回调时还能不能拿住仓位。ATR能告诉你当前市场的“噪音水平”SuperTrend再用这个噪音水平去构建一条上下轨。行情向上突破上轨就认为上升趋势成立跟踪线翻到多头方向行情向下跌破下轨就认为下降趋势成立跟踪线翻到空头方向。1.2 真实波幅TR的计算真实波幅TRTrue 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, period14): 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() ], axis1).max(axis1) # 首根K线没有前收盘直接用高低价差 atr tr.ewm(alpha1/period, min_periodsperiod).mean() return atr这里用的ewm是变种的Wilder平滑和直接用rolling(14).mean()的区别在于记忆连续性。Wilder平滑对近期的波动更敏感同时不会让很久以前的极端波动一次性掉出窗口这一点在趋势跟踪里很关键。1.3 SuperTrend上下轨与方向判定有了ATRSuperTrend就顺理成章了。需要两个参数周期length倍数multiplier经典默认参数是10和3也就是用10周期ATR把ATR乘以3作为上下轨的缓冲宽度。基本逻辑def calc_supertrend(df, period10, multiplier3): 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, indexdf.index) direction pd.Series(1, indexdf.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之前先要计算方向变动值DMDirectional Movement。DM的定义当前最高价减去前一根最高价且这个差值要大于前一根最低价减去当前最低价的差值才有效如果差值小于等于0DM直接取0。-DM同理当前最低价减去前一根最低价且这个差值要大于前一根最高价减去当前最高价的差值才有效如果差值小于等于0-DM取0。用Python实现def calc_di(df, period14): 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(alpha1/period, min_periodsperiod).mean() minus_dm_smooth pd.Series(minus_dm).ewm(alpha1/period, min_periodsperiod).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_diDI和-DI都是百分比形式。DI大于-DI说明多头力量占优反之空头力量占优。DI和-DI的差构成了方向性指数DXDirectional IndexDX再经过平滑就成了ADX。2.3 DX与ADXDX的计算dx (plus_di - minus_di).abs() / (plus_di minus_di) * 100ADX就是对DX做一次Wilder平滑参数通常取14adx dx.ewm(alpha1/period, min_periodsperiod).mean()为什么ADX会比DX有用因为单日DX波动极大一根大阳线可能让DX从20飙到60下一根小十字星又跌回15。ADX平滑之后显示的是“平均”的方向强度过滤掉了单日噪音。把整套逻辑合到一起的完整实现def calc_adx(df, period14): plus_di, minus_di calc_di(df, period) dx (plus_di - minus_di).abs() / (plus_di minus_di) * 100 adx dx.ewm(alpha1/period, min_periodsperiod).mean() return adx, plus_di, minus_di2.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 策略逻辑设计策略整体分为三大部分计算指标SuperTrend、ADX、DI和-DI生成信号SuperTrend翻多且ADX满足阈值买入SuperTrend翻空且ADX满足阈值卖出或做空ADX跌破阈值平仓回测评估计算收益曲线、最大回撤、胜率等基础绩效指标这里不引入复杂的第三方回测框架就用向量化思想写一套轻量回测逻辑透明出了问题一眼就能看到数据哪一步不对。对于想快速验证指标想法的人来说这套代码比背框架好用得多。4.2 完整可运行代码先把指标计算和回测逻辑整合成一个完整的脚本以下代码基于经典的OHLCV数据格式import pandas as pd import numpy as np def calc_atr(df, period14): prev_close df[close].shift(1) tr pd.concat([ df[high] - df[low], (df[high] - prev_close).abs(), (df[low] - prev_close).abs() ], axis1).max(axis1) return tr.ewm(alpha1/period, min_periodsperiod).mean() def calc_supertrend(df, period10, multiplier3): 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, indexdf.index) direction pd.Series(1, indexdf.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, period14): 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(alpha1/period, min_periodsperiod).mean() minus_dm_smooth pd.Series(minus_dm).ewm(alpha1/period, min_periodsperiod).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(alpha1/period, min_periodsperiod).mean() df[adx] adx df[plus_di] plus_di df[minus_di] minus_di return df def backtest(df, adx_threshold25, initial_capital100000): 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, period10, multiplier3) df calc_adx(df, period14) result_df, metrics backtest(df, adx_threshold25) 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 period101220SuperTrend multiplier2.53.04.0ADX period141418ADX阈值202530没有万能参数。波动率高的品种比如主流数字货币和某些商品期货multiplier要加大否则一根大影线就打出止损波动率低的品种比如债券期货和部分股指multiplier可以适当放小。ADX阈值也是一样。我看过一个统计某商品期货日线级别的ADX中位数在22左右但到了周线级别中位数能上到28。所以换周期之后阈值一定要重新校准不能拿着日线的25直接套到周线上。5.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平滑需要预热期手动丢弃初期数据不要强制填0ADX很高但SuperTrend方向频繁变可能用了超短线周期切换更高周期或加大multiplier6.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从低到高再走低的过程。见过它在真实行情里的表现你才能对参数和过滤器有真正的信任感。这种信任不是回测曲线能给你的。