RSI 与 MACD 背离检测:Pine Script 实作为何比肉眼难
你在图上看到明显的背离,往回翻历史,五个里有四个对。同样的逻辑一写成 Pine Script——也就是 TradingView 内建的策略脚本语言——贴上去跑,报警要嘛不响、要嘛在 K 线还没收就跳、要嘛比你眼睛慢五根 K 线。
问题不在你,也不在 Pine 语法。是「背离」这件事在**事后看**跟**每根 K 线实时判断**两个世界,本来就对不起来——人眼可以往回扫一整张图挑转折点,程序在当下这根 K 线只知道「到目前为止」。这篇不重讲背离的含义(RSI 与 MACD 两篇教程里有),也不写「稳定的背离策略」——那需要回测与参数搜索,不是一篇文章能给的。只讲三件事:人眼与程序之间差在哪、Pine 上最小可行的实作是什么、以及 alert 发出去之后最容易踩到的陷阱。
落差一:你回头看的「前一个高点」,程序当下不知道
人眼判定背离的方式,是往图表左边扫、挑一个看得出来的高点,然后看今天的高点有没有超过、RSI 有没有跟上。这个动作对程序来说有一个很不友善的假设:你已经知道那个高点是高点了。
程序只能用 ta.pivothigh(source, leftBars, rightBars) 找转折点,而这个函数的语义是「左边 leftBars 根都比它低、且右边 rightBars 根也都比它低才算」。这意味着它一定要等到 rightBars 根 K 线之后才有办法确认,也就是你在当下这根拿到的 pivot 位置,其实是 rightBars 根之前发生的事——这不是 bug,是设计。
也就是说:你想比较「这一个 pivot high 有没有低于前一个 pivot high」,最快的时机是这一个 pivot 刚刚被确认的那一根 K 线——比信号真正的高点晚 rightBars 根。left 与 right 通常设一样(3~5 是常见值),越大延迟越久、噪声也越少。
落差二:barstate.isconfirmed 之前的信号不算数
RSI、MACD 这些指标在当下这根 K 线的值,会随着 close 一路变到收盘。K 线还没收之前你看到的「RSI 68」下一秒可能变 71 或 65——同一根 K 线上,你的背离判定会来回触发好几次,而每一次都会发 alert 出去。这叫 repainting,是 Pine 教程里反复讲、实作上仍反复踩的陷阱。
处理方式只有一种:把「这一次条件是否成立」的判定绑在 barstate.isconfirmed 之后(表示这根 K 线已收盘、值不会再变)。或者更保守,用上一根 K 线的值 close[1]、rsi[1] 来判断——写进 Pine 就是 and barstate.isconfirmed 或 and not barstate.isrealtime。
这件事跟 Pine Script 的 Alert 没有记忆 那篇讲的是同一层问题:Pine 的每根 K 线都是一次独立的计算,你如果不明确告诉它「等这根 K 线稳定下来再说」,它会用中间值发出信号,而中间值几秒后就不在了。
落差三:「上一个 pivot」不会自动记在变量里
ta.pivothigh() 每次调用都告诉你「当下这根有没有一个新确认的 pivot」——有就返回 pivot 值,没有就返回 na。它不会替你存「上一个 pivot 是多少、在哪一根」。
要判断背离,你至少需要两个 pivot high 拿来比较:上一个与这一个。Pine 提供两种写法:
var变量自己存:声明var float lastHigh = na,每次拿到新的 pivot 就把旧值搬走、把新值塞进来ta.valuewhen(cond, source, occurrence):直接问「上上次 cond 成立时 source 是多少」
两种都可以,ta.valuewhen 写起来短但比较慢,var 版本直观而且好调试。下一节就用 var 版本示范,跟 tvsbot 既有的 RSI 指标完整教学 与 MACD 指标完整教学 两篇附的代码一致,方便你直接搬过去接。
最小可行实作:RSI 多头背离检测
先看要检测的形状:价格创新低,但 RSI 没有创新低——这是多头背离(bullish divergence),暗示下跌动能在减弱。
先把输入与 RSI 值准备好:
//@version=5
indicator("RSI Bullish Divergence (minimal)", overlay=true)
rsiLen = input.int(14, "RSI length")
left = input.int(5, "Pivot left bars")
right = input.int(5, "Pivot right bars")
rsi = ta.rsi(close, rsiLen)
left 与 right 通常取同一个值——这里都是 5,代表 pivot 需要左右各 5 根 K 线都比它高/低才算,换句话说会延迟 5 根 K 线。接着找 pivot 并把上一个记起来:
// pivot low 需要 right 根 K 线后才确认;这里拿到的是 right 根之前的低点
pl = ta.pivotlow(low, left, right)
pl_rsi = ta.pivotlow(rsi, left, right)
// 把上一个确认的 pivot 存起来,准备跟这一个对比
var float prevPriceLow = na
var float prevRsiLow = na
var 是 Pine 的持久变量声明,只在第一根 K 线初始化一次,之后每根 K 线的赋值都会覆写上去。最后判断背离并画上去:
bullishDiv = false
if not na(pl) and not na(pl_rsi) and not na(prevPriceLow) and not na(prevRsiLow)
// 价格更低、RSI 反而更高 → 多头背离
bullishDiv := pl < prevPriceLow and pl_rsi > prevRsiLow
// 这一根有新的确认 pivot,就把「上一个」的位置换成这一个
if not na(pl)
prevPriceLow := pl
if not na(pl_rsi)
prevRsiLow := pl_rsi
// 只在 K 线收盘后画,避免中间值画上去又消失
plotshape(bullishDiv and barstate.isconfirmed, "Bull Div",
shape.triangleup, location.belowbar,
color.new(color.green, 0), size = size.small)
这段代码是**最小可行**,不是「可以直接拿去实盘」的版本。它会漏检测、也会误报——这是背离检测本质上就有的问题,不是实作缺陷。放进账户之前要做的事,见下一节与最后的 FAQ。
三个实作陷阱:范例对照
写背离脚本会踩的问题,八成落在这三格里。左栏是天真做法,右栏是应该的做法。
| 情境 | 常见的写法(会坏) | 应该的写法 | |
|---|---|---|---|
| 找上一个高点 | 用 high[10] 这种固定回看几根 K 线去猜 | ta.pivothigh 找确认 pivot,再用 var 记住上一个 | |
| K 线未收就发信号 | 条件成立就发,没有 barstate.isconfirmed | 条件外面套 and barstate.isconfirmed,同一根只算一次 | |
| alert 触发次数 | 用 alertcondition 默认会每次满足都触发 | alert() 搭配 alert.freq_once_per_bar_close,一根 K 线只发一次 |
这张表是给你对比用的,不用背——关键是每一格都在解决同一个问题:**当下这根 K 线的值会变,所以任何「跟过去比」的逻辑都必须等这根 K 线稳定下来再判定**。这三个习惯任缺一个,你的策略回测看起来很漂亮,实盘 alert 就开始乱发。
alert 要发出去,就别用 alertcondition
alertcondition() 这个老 API,也就是 Pine 早期给你「触发报警」的老函数,要你在 TradingView 图表上手动开一个对应的报警、选 condition、绑 webhook。webhook 就是把报警 payload 自动发到你自己服务器的那条通道,消息内容也是在报警设定里填。
用它做背离检测有两个限制:消息不能带动态值(例如「这一次 RSI 比前一次高多少」);触发频率不是写在 Pine 里,而是要到 Create Alert 对话框选 Frequency 下拉——TradingView Help Center「Differences between alert frequencies」页列出的四项字面是 Once only/Once per bar/Once per bar close/Once per minute or every time。
新一点的做法是 alert():消息可以在 Pine 里实时组成字符串,频率也直接用 Pine 常量指定(例如 alert.freq_once_per_bar_close)。要接自动下单建议直接用它:
if bullishDiv and barstate.isconfirmed
msg = "RSI bullish divergence | price=" + str.tostring(pl) +
" prev=" + str.tostring(prevPriceLow) +
" rsi=" + str.tostring(pl_rsi, "#.##")
alert(msg, alert.freq_once_per_bar_close)
`alert.freq_once_per_bar_close` 意思是「同一根 K 线最多发一次,而且只在收盘那一刻发」。这是背离信号能做到的最保守设定——实盘要接执行端([TradingView Webhook 完整教学](/blog/zh-Hans/tradingview-webhook-tutorial) 那篇有讲怎么设)之前,就先跑这个设定观察两、三天。
诚实的一段:背离不是必然反转,程序检测还多一层滞后
背离的统计意义是动能衰竭的一种样式,不是价格反转的必然前兆。连续的背离可能维持很久——RSI 一直背离、价格继续往目标方向走——这种情况叫「hidden divergence」还是「failed divergence」,看你翻的是哪本书。
程序检测到「一个确认的背离 pivot」通常比人眼晚 3~8 根 K 线,而背离作为信号本来就是滞后的。这两个滞后叠起来:
- 人眼看到 pivot 到程序确认:延迟
right根 K 线(5 根很常见) - Pine 发出 alert 到执行端收到 webhook:再多几秒到十几秒
- 执行端市价下单到成交:再一次到数次滑价
你收到信号的时候,价格可能已经开始反转、也可能继续走。任何自动化背离策略都要有另一个确认条件(例如均线交叉、成交量放大)来过滤,这一步不加,背离会变成「风险最大的噪声来源」而不是信号。
这篇没有给你回测结果、也没有给你参数建议,因为我们没有跑过各交易对、各时间框架的穷举——写出来也是拍脑袋的数字。你要自己拿上面的最小可行实作在 TradingView Strategy Tester 上跑,并且比较「加背离过滤 vs 不加」在同一个标的的长期期望值差多少。有差再往下走,没差就退回 RSI/MACD 交叉那些更粗但更稳的信号。
收到背离信号之后,要不要进场
单一个背离信号本身不足以做决策,这是决策树的第一问;下面每一支都能整段拿掉不影响流程,但少任何一支胜率都会恶化。
barstate.isconfirmed)?常见问题
参数与时间框架
left 与 right 要设多少?
可以在 1 分钟框架上检测背离吗?
信号与执行
MACD 背离跟 RSI 背离要选一个吗?
ta.pivotlow(rsi, left, right) 换成用 ta.macd() 出来的 MACD line 再取 pivot。MACD 通常比 RSI 早出一点,但假信号也多。有人两个同时背离才进场。alert 发出去之后,执行端要注意什么?
alert.freq_once_per_bar_close 叠起来刚好双重保险。ta.valuewhen 版本会比较好吗?
ta.valuewhen(not na(pl), pl, 1) 意思是「上一次 pl 不是 na 的时候的值」。要「上一个」而非「这一个」,off-by-one 错很难查。调试阶段用 var 版本比较好追。Get started
把 RSI/MACD 背离的 alert 接上自动下单——用你自己的 API key,先 dry-run 观察两天再开实盘,风控自己配置,收到的每一笔信号在 dashboard 的 signals 页逐笔可查。
免费注册 TVSBot