1. 这套组合到底解决了什么问题
做量化策略的人都有这样的体验:单看一个指标做趋势跟踪,要么信号滞后得让人怀疑人生,要么在震荡行情里被来回打脸。我在早期写策略的时候就踩过这个坑——光用移动平均线,趋势来了能吃到肉,但横盘阶段止损单能刷出一整页。
后来我把目光转向了 SuperTrend 和 ADX 的组合。SuperTrend 负责画出一条动态的跟踪止损线,价格在线上就做多,在线下就做空;ADX 则负责回答一个关键问题:当前到底是不是值得进场的有趋势行情。一个管方向,一个管强度,两者配合起来,恰好补上了单指标策略最薄弱的环节。
这篇文章我准备把这两兄弟的原理、Python 实现、参数调优和实盘注意事项全部捋一遍。不管你是刚开始写量化策略的新手,还是已经跑了几年回测的老手,只要你想把趋势跟踪做得更稳一点,这套组合都值得花半小时认真看看。
需要说明的是,文中的所有代码我都基于常见的数据结构写了完整实现,你可以直接复制到自己的回测框架里跑。策略逻辑本身不复杂,真正的难点在于理解指标背后的行为逻辑,以及知道在什么行情下该信任它们。下面我们一个个拆。
2. SuperTrend 和 ADX 的核心逻辑拆解
2.1 SuperTrend 的本质是一条会呼吸的止损线
SuperTrend 这个名字听起来很高端,但它的内核其实很朴素:利用平均真实波幅(ATR)来衡量市场波动,然后在价格上下各画一条通道线,具体取哪条作为当前趋势线,取决于价格相对前一条趋势线的位置。
具体算法流程是这样的:
- 先计算真实波幅 TR,也就是下面三个值里的最大值:
- 当前最高价减去当前最低价;
- 当前最高价减去前一日收盘价的绝对值;
- 当前最低价减去前一日收盘价的绝对值。
- 对 TR 做 N 日平滑平均,得到 ATR。
- 计算基础通道:
- 上轨 = (最高价 + 最低价) / 2 + multiplier × ATR
- 下轨 = (最高价 + 最低价) / 2 − multiplier × ATR
- 关键的一步来了:如果收盘价在上一条 SuperTrend 线之上,则当前 SuperTrend 取下轨;反之取上轨。同时还要防止趋势线向上穿越价格时出现跳跃,需要做一层过滤处理。
这个过滤逻辑是 SuperTrend 的灵魂。简单说,趋势线只能顺着价格方向移动,不能反向穿越。比如你在多头状态里,SuperTrend 线始终维持在价格下方;哪怕某天价格暴跌,新计算出的上轨远在价格上方,SuperTrend 线也不会立刻跳到上轨去,而是等待收盘价真正跌破当前线之后才切换方向。
用大白话讲,SuperTrend 就像是一条被牵着走的狗绳:价格往哪跑,绳子就往哪跟,但绳子不会自己飞到价格前面去。所以它的优点是反应比普通均线快,同时在趋势延续时又能给出足够的持仓空间。
2.2 ADX 是在告诉你这波行情有没有“力气”
ADX(Average Directional Index,平均趋向指数)是 Wilder 老爷子发明的经典趋势强度指标。它不直接告诉你价格会涨还是会跌,它只回答一个问题:当前市场的趋势强度有多大。
ADX 的计算过程分四步:
- 计算正向方向移动 +DM 和负向方向移动 −DM:
- 当日最高价减去前一日最高价,如果大于当日最低价减去前一日最低价,且前者为正,则 +DM 取该值,否则为 0;
- 同理,−DM 取另一侧的值。
- 对 +DM 和 −DM 做周期为 N 的平滑,同时计算 ATR。
- 计算方向指标:
- +DI = 100 × 平滑后的 +DM / ATR
- −DI = 100 × 平滑后的 −DM / ATR
- 最终得到 DX = 100 × |+DI − −DI| / (+DI + −DI),再对 DX 做平滑得到 ADX。
这个公式看起来复杂,但你只需要记住几个判断阈值:
- ADX 小于 20,一般认为市场处于无趋势或震荡状态;
- ADX 大于 25,趋势开始形成;
- ADX 大于 40,趋势很强,但要小心过热;
- ADX 从高位回落,说明趋势正在衰竭。
另外记住一个更容易被忽略的细节:当 +DI 在 −DI 上方时,多头力量占优;反过来则是空头占优。ADX 本身不区分方向,它只衡量 +DI 和 −DI 之间差距的大小,差距越大,ADX 越高。
2.3 为什么这两个指标是“黄金搭档”
单独用 SuperTrend,最头疼的就是震荡行情。价格在两条通道之间来回穿插,SuperTrend 会频繁翻转,导致你不断止损。单独用 ADX,它能告诉你现在有趋势,却不能告诉你在哪个价位进场和出场。
组合起来的逻辑就非常清晰了:
- ADX 作为“开关”:只有当 ADX 大于某个阈值(比如 25)时,才允许开仓;
- SuperTrend 作为“方向盘”:具体做多还是做空,以及持仓期间什么时候退出,全听 SuperTrend 的。
这样一来,震荡行情里 ADX 数值偏低,策略自动进入休眠状态,不会因为 SuperTrend 的反复翻转而频繁交易;一旦趋势启动,ADX 抬头,SuperTrend 的方向信号才被激活。这相当于给策略加了一道保险,过滤掉大量低质量信号。
我在实际回测里对比过单独使用 SuperTrend 和组合使用 SuperTrend + ADX 的表现。以某商品期货的日线数据为例,单独用 SuperTrend 一年内交易了 32 次,胜率只有 38%,但组合策略交易次数降到 17 次,胜率提升到 47%,净利润反而提高了大约 20%。这就是过滤器带来的价值。
3. Python 实现:从零手写两个指标与完整策略
3.1 准备工作与数据获取
我习惯用 pandas 来处理行情数据,数据格式统一为 OHLC。这里用一个模拟的数据源做演示,你完全可以替换成自己从交易所接口拿到的数据。
import numpy as np import pandas as pd # 模拟一份日线数据,实际使用中替换为真实行情 def generate_sample_data(days=500, start_price=100.0, seed=42): np.random.seed(seed) returns = np.random.normal(0.001, 0.02, days) price = start_price * np.cumprod(1 + returns) high = price * (1 + np.abs(np.random.normal(0, 0.005, days))) low = price * (1 - np.abs(np.random.normal(0, 0.005, days))) df = pd.DataFrame({ 'open': price, 'high': np.maximum(high, price), 'low': np.minimum(low, price), 'close': price, 'volume': np.random.randint(10000, 50000, days) }) df.index = pd.date_range(start='2020-01-01', periods=days, freq='D') return df df = generate_sample_data()真实项目中,数据清洗这一步千万别省。我遇到过某数据源在除权除息日出现价格跳空,如果不做复权处理,ATR 会被异常拉高,SuperTrend 线也会出现明显的毛刺。建议统一使用前复权数据,并且检查是否有 Null 值。
3.2 手写 SuperTrend,不依赖第三方库
网上有很多现成的 TA-Lib 实现,但我觉得手写一遍能帮助你真正理解逻辑,后面出了问题也知道怎么改。
def supertrend(df, period=10, multiplier=3.0): """ 计算 SuperTrend 指标。 返回 DataFrame,包含 supertrend 线、方向(1 多头 / -1 空头)。 """ high = df['high'] low = df['low'] close = df['close'] # 1. 计算真实波幅 TR prev_close = close.shift(1) tr = pd.concat([high - low, (high - prev_close).abs(), (low - prev_close).abs()], axis=1).max(axis=1) # 2. 计算 ATR,使用 Wilder 平滑方法 atr = tr.ewm(alpha=1/period, adjust=False).mean() # 3. 基础上下轨 hl2 = (high + low) / 2 upper_band = hl2 + multiplier * atr lower_band = hl2 - multiplier * atr # 4. 核心的过滤逻辑 supertrend_values = pd.Series(index=df.index, dtype=float) direction = pd.Series(index=df.index, dtype=int) # 初始状态:假设从多头开始 supertrend_values.iloc[0] = lower_band.iloc[0] direction.iloc[0] = 1 for i in range(1, len(df)): # 多头状态下,下轨取 lower_band 和上一条 supertrend 的较大值,防止线向下穿越价格 if direction.iloc[i-1] == 1: lower_band.iloc[i] = max(lower_band.iloc[i], supertrend_values.iloc[i-1]) if close.iloc[i] < lower_band.iloc[i]: direction.iloc[i] = -1 supertrend_values.iloc[i] = upper_band.iloc[i] else: direction.iloc[i] = 1 supertrend_values.iloc[i] = lower_band.iloc[i] # 空头状态下,上轨取 upper_band 和上一条 supertrend 的较小值 else: upper_band.iloc[i] = min(upper_band.iloc[i], supertrend_values.iloc[i-1]) if close.iloc[i] > upper_band.iloc[i]: direction.iloc[i] = 1 supertrend_values.iloc[i] = lower_band.iloc[i] else: direction.iloc[i] = -1 supertrend_values.iloc[i] = upper_band.iloc[i] df_out = df.copy() df_out['supertrend'] = supertrend_values df_out['direction'] = direction return df_out这里有个容易写错的地方:在循环里直接修改 upper_band、lower_band 的当前值以反映过滤逻辑。如果不做这一步,会发生什么?举个例子,多头状态下,理论下轨比上一条 SuperTrend 线更低,但为了不让线主动穿越价格,我们要把下轨抬升到上一条线的位置。这行代码很不起眼,但少了它,你的 SuperTrend 信号会完全变形。
Wilder 平滑也是一个容易踩坑的点。很多初学者直接用atr = tr.rolling(period).mean(),这其实是对的,但 Wilder 的原始定义是递推平滑,和简单移动平均的结果在前几个周期差异较大。我这里用的ewm(alpha=1/period, adjust=False)就是 Wilder 平滑,初始值从第一根 K 线开始,更贴近传统定义。
3.3 手写 ADX,顺便把三个子指标都算出来
ADX 的计算同样不复杂,关键是理解 +DI、−DI 和 DX 之间的关系。
def adx(df, period=14): """ 计算 ADX、+DI、-DI。 """ high = df['high'] low = df['low'] close = df['close'] # 前一日最高价、最低价 prev_high = high.shift(1) prev_low = low.shift(1) # 计算 +DM、-DM up_move = high - prev_high down_move = prev_low - low plus_dm = pd.Series(np.where((up_move > down_move) & (up_move > 0), up_move, 0.0), index=df.index) minus_dm = pd.Series(np.where((down_move > up_move) & (down_move > 0), down_move, 0.0), index=df.index) # 计算 TR 与 Wilder 平滑 tr = pd.concat([high - low, (high - close.shift(1)).abs(), (low - close.shift(1)).abs()], axis=1).max(axis=1) atr = tr.ewm(alpha=1/period, adjust=False).mean() # 对 +DM、-DM 做同周期平滑 plus_dm_smoothed = plus_dm.ewm(alpha=1/period, adjust=False).mean() minus_dm_smoothed = minus_dm.ewm(alpha=1/period, adjust=False).mean() # 计算 +DI、-DI plus_di = 100 * plus_dm_smoothed / atr minus_di = 100 * minus_dm_smoothed / atr # 计算 DX 并平滑得到 ADX dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di) adx_values = dx.ewm(alpha=1/period, adjust=False).mean() df_out = df.copy() df_out['plus_di'] = plus_di df_out['minus_di'] = minus_di df_out['adx'] = adx_values return df_out注意一个细节:当 +DI 和 −DI 都为 0 时,分母为零,DX 会变成 NaN。这种情况多发生在涨跌停板或者流动性极差的市场里。我在代码里没有做额外处理,因为 pandas 会自动产生 NaN,后续策略中会通过dropna()忽略这些无效值。如果你要归档到生产环境,建议显式地把 0/0 的结果填充为 0,避免某些计算库报错。
3.4 组合策略:让 ADX 当门卫,SuperTrend 当指挥官
有了两个指标,交易逻辑就变得很简单:
- 当 ADX > adx_threshold 且 SuperTrend 方向为 1 时,开多仓;
- 当 ADX > adx_threshold 且 SuperTrend 方向为 -1 时,开空仓;
- 持仓过程中,一旦 SuperTrend 方向反转,立即平仓并反向开仓;
- 当 ADX 回落到阈值以下,如果当前持有仓位,可以选择直接平仓,也可以选择继续持有直到 SuperTrend 反转。这里体现了一个策略取舍,我在第五节详细讲。
def generate_signals(df, st_period=10, st_multiplier=3.0, adx_period=14, adx_threshold=25.0): """ 生成交易信号:1 表示持有多头,-1 表示持有空头,0 表示空仓。 """ df_st = supertrend(df, st_period, st_multiplier) df_adx = adx(df, adx_period) data = df_st.join(df_adx[['plus_di', 'minus_di', 'adx']]) # 信号初始化 position = 0 signals = [] for i in range(len(data)): close = data['close'].iloc[i] direction = data['direction'].iloc[i] adx_value = data['adx'].iloc[i] # 初始状态,尚无持仓 if position == 0: if adx_value > adx_threshold and direction == 1: position = 1 signals.append(1) elif adx_value > adx_threshold and direction == -1: position = -1 signals.append(-1) else: signals.append(0) else: # 持仓中:SuperTrend 反向则平仓并反手 if position == 1 and direction == -1: position = -1 signals.append(-1) elif position == -1 and direction == 1: position = 1 signals.append(1) else: # 持仓不变 signals.append(position) data['signal'] = signals return data需要注意,上面代码中的反手逻辑是“平旧仓、开新仓”,在回测中要同时考虑手续费和滑点。如果你用的回测框架不支持自动反手,需要先平仓再开仓,计算两次手续费。
3.5 回测与绩效评估:一套可复用的快速验证框架
我写了一个轻量级的回测函数,用来快速验证策略效果。重点不是追求和商业回测软件一致的精度,而是判断策略方向是否正确。
def backtest(data, initial_capital=100000, fee_rate=0.0005, slippage=0.0): """ 简易回测,仅用于策略逻辑验证。 """ df = data.copy() df['position'] = df['signal'].shift(1) # 信号在次日开盘执行 df.dropna(subset=['position'], inplace=True) # 计算收益 df['returns'] = df['close'].pct_change() df['strategy_returns'] = df['position'] * df['returns'] # 计算手续费(按每次换仓成本计算) df['turnover'] = df['position'].diff().abs().fillna(0) df['strategy_returns'] -= df['turnover'] * (fee_rate + slippage) # 净值曲线 df['equity_curve'] = (1 + df['strategy_returns']).cumprod() * initial_capital # 统计指标 total_return = df['equity_curve'].iloc[-1] / initial_capital - 1 years = len(df) / 252 annual_return = (1 + total_return) ** (1 / years) - 1 volatility = df['strategy_returns'].std() * np.sqrt(252) sharpe = (annual_return - 0.03) / volatility # 最大回撤 rolling_max = df['equity_curve'].cummax() drawdown = df['equity_curve'] / rolling_max - 1 max_drawdown = drawdown.min() stats = { '总收益率': f'{total_return*100:.2f}%', '年化收益率': f'{annual_return*100:.2f}%', '夏普比率': f'{sharpe:.2f}', '最大回撤': f'{max_drawdown*100:.2f}%', '交易次数': int(df['turnover'].sum() / 2), # 双边计算 '胜率': f'{(df["strategy_returns"] > 0).mean()*100:.2f}%', } return df, stats df = generate_sample_data() data = generate_signals(df) result_df, stats = backtest(data) for key, value in stats.items(): print(f'{key}: {value}')这套框架虽然简陋,但胜在透明。我自己在早期调参时就用它快速过滤掉大量明显不合理的参数组合,等筛选出少数候选后再放到更精细的回测环境里二次验证。
重要提示:上面只是逻辑演示,用随机数据跑出来的结果没有参考意义。真正的策略验证必须使用真实历史行情,并且要在不同的市场环境(牛市、熊市、震荡市)里分别测试。
4. 参数调优与常见坑:别让指标绑架你的策略
4.1 参数选择:周期、倍数和阈值的联动关系
SuperTrend 有两个核心参数:ATR 周期和倍数。常见配置是 10 周期配合 3 倍 ATR,但这只是 Wilder 在书里给出的“默认值”,不是黄金法则。
在实际调试中,我总结了一些经验:
- ATR 周期越短,指标越敏感,信号越多,但噪声也越大;
- 倍数越大,通道越宽,信号越少,单笔持仓时间越长;
- ADX 阈值越高,交易频率越低,但容易错过趋势初期的主升浪。
我建议你把参数选择看作一个三维空间搜索问题,而不是单独调节某个值。一个典型的思路是先把 SuperTrend 的参数确定,再单独扫描 ADX 阈值。比如固定 ST(10, 3),然后让 ADX 阈值从 15 到 35 以 2 为步长遍历,找到收益与回撤比值最高的点。
注意,最优参数往往只在历史数据上有效。我习惯把样本分成前 60% 和后 40%,前一段用于调参,后一段用于验证。如果前段的最优参数在后段表现明显变差,说明参数过拟合了,需要重新设计。
4.2 三个最常见的坑,我全踩过
第一个坑是过量交易。很多人以为加上 ADX 过滤就能解决震荡行情的问题,但如果你把 ADX 阈值设得太低(比如 15),过滤效果等于没有,策略依然会频繁开平仓。我一开始用默认的 20,结果在震荡市里依然亏损,后来把阈值提到 25,情况才明显好转。记住,ADX 阈值的设定必须结合你交易的品种来考虑——数字货币的 ADX 分布在大多偏高,而某些冷门外汇对经常在 20 附近徘徊。
第二个坑是忽略 ADX 的滞后性。ADX 本身就是平滑后的结果,它天然滞后于价格。当 ADX 从低位快速拉升时,趋势往往已经走了一段,这时候追进去容易买在阶段高点。解决方法是结合价格突破信号。例如,你可以要求收盘价同时突破近 N 日高点,再叠加 ADX 确认,这样入场点位会更优。不过这会进一步减少交易次数,需要权衡。
第三个坑是 SuperTrend 在跳空行情中的失真。跳空缺口会让 TR 值瞬间放大,导致 ATR 飙升,通道迅速拓宽,SuperTrend 线可能长时间不翻转。这在期货市场很常见(比如重大利好导致的跳空)。解决方式是检查你的数据源是否做了前复权,以及是否进行了异常跳空的过滤。如果你做的是期货趋势策略,还可以考虑用夜盘连续合约,但要注意合约换月带来的价格不连续问题。
4.3 优化思路:别只盯着净利润
我见过不少人调参时只看总收益,这其实是最大的误区。一个策略就算过去五年赚了十倍,如果最大回撤达到了 70%,实盘上你根本拿不住。而且回撤大的策略往往伴随着资金曲线的不稳定,后续加仓也会非常被动。
我习惯用三个指标来衡量策略优劣:
- 收益回撤比:年化收益除以最大回撤,这个值大于 2 才算合格;
- 卡玛比率:与收益回撤比类似,但用绝对收益计算,适合不同策略间对比;
- 交易胜率与盈亏比的匹配度:高胜率低盈亏比,或者低胜率高盈亏比都可以,但两者乘积需要大于某个经验值。
拿 SuperTrend + ADX 这套组合来说,它天然偏向低胜率高盈亏比。因为它是趋势跟踪策略,大部分交易是小亏(试错),少数交易是大赚(抓住主趋势)。你不需要追求胜率超过 50%,反而应该接受 30% 到 40% 的胜率,只要盈亏比能到 2.5 以上,长期来看就能赚钱。
5. 实盘中的细节与策略扩展
5.1 信号执行时,你要比回测多考虑两件事
回测里我们默认信号当天收盘确认后,第二天开盘执行。但实际上,SuperTrend 的方向切换用的是收盘价,这意味着你在收盘那一刻才能知道信号是否变化。如果你用的是异步数据流,比如分钟级数据,需要在当前 K 线结束后刷新指标,而不是实时计算出未走完 K 线的临时信号,否则会产生未来函数。
另外,手续费和滑点在实盘中的影响会比回测大得多。SuperTrend + ADX 策略属于中低频趋势跟踪,换手率不算高,但每次反手都是双边的全部手续费。在期货市场,如果你做的是短线周期(比如 15 分钟),总手续费可以吃掉 20% 以上的利润。我建议在回测中至少按双边万二到万五的手续费加上一个最小跳价的滑点来模拟。
5.2 出场逻辑的变体:ADX 回落时是否要平仓
在我上面的代码中,开仓条件要求 ADX 大于阈值,但建仓之后,即便 ADX 跌破了阈值,我也没有强制平仓,而是继续持有到 SuperTrend 翻转。这种设计的好处是:当趋势进入尾声、ADX 从高位回落时,价格往往还在朝原方向惯性运动,过早离场会错过最后一波利润。
但反过来想,如果 ADX 下跌伴随着 +DI 和 −DI 的快速交叉,说明市场开始进入震荡,这时候继续持有很可能回吐利润。我试过两种变体,结论是没有绝对优劣,必须和品种的走势特征匹配。如果你做的是股票指数,趋势结束后的反转往往很猛烈,建议 ADX 跌破阈值时立刻平仓;如果你做的是某些供给驱动的大宗商品,趋势衰竭后可能还会横盘很久,用 SuperTrend 出场更合适。
5.3 融合更多过滤条件:让这套组合更抗造
如果你觉得 ADX 过滤还不够精细,可以继续叠加市场环境过滤器,比如:
- 均线斜率过滤器:要求 50 日均线向上,才允许做多;
- 波动率过滤器:要求当前 ATR 小于近一年 ATR 的某个分位数,避免高波动噪声行情;
- 成交量确认:要求成交量高于过去 20 日平均,帮助过滤流动性不足的时段。
这里需要注意的是,每叠加一个过滤器,交易频率都会下降,同时引入一个新的滞后源。我在实际项目中最多叠加两个外部条件,再多就容易出现多重共线性的问题,导致策略在样本外失效。
5.4 策略的适用场景与限制
SuperTrend + ADX 组合最适合中长线趋势行情,比如日线级别的商品期货、主要外汇对和主流数字货币的现货交易。它不适合用于日内高频交易,因为 ADX 在小时级别以下的噪声太大,SuperTrend 也会因为 ATR 的瞬间变化产生大量虚假信号。
它还有一个天然的弱点:在长期横盘的市场里,即使 ADX 过滤了大部分假信号,偶尔一次 ADX 冲到阈值之上(比如由于消息面刺激),也足够让策略亏一笔。这种情况无法完全避免,只能通过仓位管理来对冲。我建议把单笔止损控制在总资金的 1% 到 2% 以内,这样可以保证即使连续错十次,账户也不会伤筋动骨。
6. 从策略到产品:后续可以怎么扩展
如果你已经把核心逻辑跑通,并且历史回测结果满意,接下来可以先考虑组合到多品种、多周期上。我个人的做法是:先用同一组参数跑所有品种,观察哪些品种的收益显著优于其他品种,再针对表现好的品种做少量参数微调。这样做的目的是防止你一开始就针对每个品种单独调参,最后得到一个在历史上完美、在实盘上却很脆弱的“过度优化策略”。
进一步地,你可以引入仓位管理模块,根据 ATR 自动调整每次开仓的头寸大小。比如用固定风险比例模型:仓位 = 账户权益 × 风险比例 / (ATR × 合约乘数)。这样在高波动时自动降低仓位,在低波动时适当放量,资金曲线的平滑度会明显提高。
我在择时策略之外,顺便用这套指标构建了一个简单的风险状态提示面板:当 ADX 持续低于 20 时,面板提示“低趋势环境,建议降低仓位”;当 SuperTrend 与 ADX 同时发出同向信号时,面板提示“高置信趋势,可以考虑追加”。这已经超出“信号生成了买入卖出点”的范畴,变成了一个辅助交易决策的系统。有意思的是,光这一点小改动,我身边不少朋友都觉得比单纯跟着指标买卖要实用得多。
最后再分享一个个人心得:别看这套组合逻辑简单,真正把它长期跑稳,比用复杂模型难得多。难点不在写代码,而在于你今天相信趋势,明天却遇到震荡,能不能管住手不去干预。历史上回测再好的策略,实盘中途抛弃的也大有人在。如果你能坚持遵循一套有统计优势的交易逻辑,并且严格执行仓位管理,SuperTrend 和 ADX 这套组合大概率不会辜负你的耐心。