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/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 頁逐筆可查。
免費開始使用