Pine Script

RSI 与 MACD 背离检测:Pine Script 实作为何比肉眼难

2026-09-28·8 分钟阅读

你在图上看到明显的背离,往回翻历史,五个里有四个对。同样的逻辑一写成 Pine Script——也就是 TradingView 内建的策略脚本语言——贴上去跑,报警要嘛不响、要嘛在 K 线还没收就跳、要嘛比你眼睛慢五根 K 线。

问题不在你,也不在 Pine 语法。是「背离」这件事在**事后看**跟**每根 K 线实时判断**两个世界,本来就对不起来——人眼可以往回扫一整张图挑转折点,程序在当下这根 K 线只知道「到目前为止」。这篇不重讲背离的含义(RSI 与 MACD 两篇教程里有),也不写「稳定的背离策略」——那需要回测与参数搜索,不是一篇文章能给的。只讲三件事:人眼与程序之间差在哪、Pine 上最小可行的实作是什么、以及 alert 发出去之后最容易踩到的陷阱。

先说结论
一:`ta.pivothigh(source, left, right)` 需要「右边 right 根 K 线」都比 pivot 低才会确认,所以你在当下这根 K 线看到的 pivot 永远是 right 根前的事——right=5 就是延迟 5 根 K 线。二:实时的 RSI/MACD 读数在 K 线收之前会持续变,把信号绑在 `barstate.isconfirmed` 之后再判定,不然报警会在同一根 K 线响好几次还互相矛盾。三:「上一个 pivot」不会自动被 Pine 记住,你要自己用 `var` 存起来,不然每根 K 线都在跟自己比。

落差一:你回头看的「前一个高点」,程序当下不知道

人眼判定背离的方式,是往图表左边扫、挑一个看得出来的高点,然后看今天的高点有没有超过、RSI 有没有跟上。这个动作对程序来说有一个很不友善的假设:你已经知道那个高点是高点了。

程序只能用 ta.pivothigh(source, leftBars, rightBars) 找转折点,而这个函数的语义是「左边 leftBars 根都比它低、且右边 rightBars 根也都比它低才算」。这意味着它一定要等到 rightBars 根 K 线之后才有办法确认,也就是你在当下这根拿到的 pivot 位置,其实是 rightBars 根之前发生的事——这不是 bug,是设计。

5 根
right=5 时,pivot 确认的延迟根数
na
尚未确认的当下 K 线返回值
2 个
背离最少需要的 pivot(上一个 + 这一个)

也就是说:你想比较「这一个 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 值准备好:

pine
//@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 并把上一个记起来:

pine
// 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 线的赋值都会覆写上去。最后判断背离并画上去:

pine
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)。要接自动下单建议直接用它:

pine
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 交叉那些更粗但更稳的信号。

收到背离信号之后,要不要进场

单一个背离信号本身不足以做决策,这是决策树的第一问;下面每一支都能整段拿掉不影响流程,但少任何一支胜率都会恶化。

1
信号来源是不是收盘后(barstate.isconfirmed)?
不是 →是实时中间值,等这根 K 线收再看。中间值下一秒可能消失
是 →往下一步
2
大时间框架的方向对吗(例如日线 RSI 过 50)?
不对 →逆势背离的假信号比例很高,通常直接跳过
对 →往下一步
3
有第二个确认条件吗(成交量、均线、更小时间框架转折)?
没有 →只有一个背离就进场,长期期望值通常打平或负;把它当观察不当进场
有 →照你的策略进场,用背离的 pivot 当初始止损参考

常见问题

参数与时间框架

left 与 right 要设多少?
常见范围 3~10。越小信号越多也越杂、延迟越短;越大信号越干净、但会漏掉快速转折。日线常用 5,1 小时以下常用 3。没有正确答案,一律在同一标的的 Strategy Tester 上比两、三组。
可以在 1 分钟框架上检测背离吗?
技术上可以,实务上信号比噪声多。5 分钟 RSI 一天可能背离 20 次,1 分钟会更严重。要用就搭配大时间框架 RSI 过滤,只在大周期方向对的时候动作。

信号与执行

MACD 背离跟 RSI 背离要选一个吗?
两个都可以,机制上一样——把 ta.pivotlow(rsi, left, right) 换成用 ta.macd() 出来的 MACD line 再取 pivot。MACD 通常比 RSI 早出一点,但假信号也多。有人两个同时背离才进场。
alert 发出去之后,执行端要注意什么?
两件事。TradingView 的 webhook 有延迟(几秒到十几秒),加上背离本身滞后,实际成交价不会是图上那个 pivot。同一根 K 线的信号可能被执行端去重挡掉——这是好事,跟 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