🔥 🎁 QUÀ TẶNG khi đăng ký: Phần mềm DNT Digital SaaS FindMe VPS Guardian 12 tháng (~240 USD) và 24 tháng (~480 USD)
Pine Script cơ bản: tự tạo chỉ báo và alert cho bot 2026
ĐTĐược viết bởi Đặng Trí Thanhvào ngày 07/10/2026 lúc 20:00|… lượt xem
TradingView07/10/2026 · 19 phút đọc

Pine Script cơ bản: tự tạo chỉ báo và alert cho bot

Mọi bot auto trading đều bắt đầu từ một tín hiệu. Nếu tín hiệu sai, phần còn lại của hệ thống — webhook, API, quản trị rủi ro — chỉ giúp bạn sai nhanh hơn và đều đặn hơn.

Với TradingView, tín hiệu được sinh ra bởi Pine Script: ngôn ngữ lập trình riêng của TradingView, viết trực tiếp trong trình soạn thảo Pine Editor ngay dưới chart. Bạn không cần biết lập trình trước; bạn cần hiểu ba khái niệm: series, chỉ báo và alert.

Bài viết này đi từ con số 0 tới lúc có một tín hiệu đủ chuẩn để bot đọc: viết chỉ báo EMA cross có lọc RSI, thoát lệnh trailing ATR, tín hiệu đa khung, đóng gói payload JSON, và quan trọng nhất — cách tránh repaint để tín hiệu backtest không khác gì tín hiệu chạy thật.

Đọc trước: TradingView Webhook là gì? Hướng dẫn A–Z và Tạo Alert TradingView gửi Webhook JSON.

Trả lời nhanh

Pine Script cơ bản để làm bot gồm ba việc:

  1. Khai báo script bằng //@version=6 + indicator(...), rồi tính chỉ báo bằng các hàm ta.* (như ta.ema, ta.rsi, ta.atr).
  2. Xác định điều kiện tín hiệu — ví dụ ta.crossover(fast, slow) — và chỉ phát tín hiệu khi nến đã đóng (barstate.isconfirmed) để tránh tín hiệu đổi giữa chừng.
  3. Gửi tín hiệu ra ngoài bằng alert() với nội dung là chuỗi JSON; TradingView sẽ POST chuỗi đó tới webhook của bot.

Ba lỗi khiến 90% tín hiệu Pine "chạy đẹp trên chart nhưng bot lỗ" là: repaint (tín hiệu đổi sau khi nến đóng), dùng `request.security` không đúng cách (lấy dữ liệu khung lớn chưa đóng), và gửi tín hiệu mỗi tick thay vì mỗi nến đóng.

Nguyên tắc duy nhất cần nhớ: bot chỉ nên nhận tín hiệu một lần cho mỗi nến đã đóng, và nến đóng ở khung nào thì bot phải biết khung đó.


Pine Script là gì và nó nằm ở đâu trong hệ thống bot?

Pine Script là ngôn ngữ chuyên dụng cho biểu đồ: bạn không viết ứng dụng, không đọc file, không mở kết nối mạng. Bạn chỉ mô tả điều kiện trên dữ liệu giá, rồi TradingView hiển thị và phát cảnh báo.

Vì vậy hãy đặt Pine đúng vị trí của nó trong kiến trúc:

Thành phầnNhiệm vụCông cụ
Tín hiệuPhát hiện điều kiện trên giáPine Script (TradingView)
Vận chuyểnĐẩy tín hiệu ra ngoàiAlert + webhook (HTTPS POST)
Xử lýXác thực, chuẩn hoá, tính khối lượngBot (Python/Node)
Thực thiĐặt/huỷ lệnhAPI broker/sàn (Binance, Bybit, MT5, SSI…)
Giám sátLog, cảnh báo, đối soátTelegram + log + dashboard

Pine không đặt lệnh. Pine không biết bạn đang lỗ hay lãi bao nhiêu. Pine chỉ trả lời một câu: "Điều kiện X vừa đúng, ở giá Y, tại thời điểm Z, trên khung thời gian W." Mọi thứ khác là việc của bot.

Hiểu được ranh giới này giúp bạn tránh một sai lầm rất phổ biến: cố nhồi quản trị rủi ro, tính khối lượng, kiểm tra số dư… vào Pine. Những thứ đó phải nằm ở bot, nơi có dữ liệu tài khoản thật.

Một lợi ích ít người để ý: alert của TradingView chạy trên server của TradingView, không cần máy bạn bật. Bạn tắt máy đi làm, tín hiệu vẫn được gửi tới webhook của bot — miễn là bot (chạy trên VPS) vẫn sống.


Cấu trúc một script Pine tối thiểu

Mở Pine Editor (dưới chart), dán script dưới đây và bấm "Add to chart":

//@version=6
indicator("Script dau tien", overlay=true)

len = input.int(20, "Chu ky SMA")
smaValue = ta.sma(close, len)

plot(smaValue, "SMA", color=color.orange)

Giải thích từng dòng:

  • //@version=6 — chỉ định phiên bản Pine. Luôn ghi rõ; TradingView mặc định có thể dùng phiên bản khác và script sẽ lỗi cú pháp.
  • indicator(...) — khai báo đây là chỉ báo. overlay=true nghĩa là vẽ lên chart giá; để mặc định (false) thì script nằm ở panel riêng bên dưới (như RSI).
  • input.int(...) — tham số người dùng chỉnh được, hiện trong bảng Settings. Đây là cách bạn thay đổi hành vi mà không phải sửa code.
  • ta.sma(close, len) — hàm tính chỉ báo; close là series giá đóng cửa.
  • plot(...) — vẽ ra chart.

Ba loại script bạn cần phân biệt:

LoạiKhai báoDùng khi
Indicatorindicator()Muốn phát alert cho bot — dùng cái này
Strategystrategy()Muốn backtest bằng Strategy Tester
Librarylibrary()Xuất hàm dùng lại cho script khác

Với bot, hãy viết `indicator()`. Strategy Tester rất tốt để thử nghiệm, nhưng mô hình khớp lệnh của nó là giả định của TradingView, không phải của broker bạn; số liệu đẹp trên Tester không đảm bảo bot chạy có lãi.


Series, kiểu dữ liệu và những hàm bạn sẽ dùng 90% thời gian

Điểm khác biệt lớn nhất giữa Pine và ngôn ngữ thông thường: hầu hết biến trong Pine là "series" — tức là một dãy giá trị theo thời gian, không phải một con số duy nhất. close không phải "giá hiện tại", mà là toàn bộ lịch sử giá đóng cửa; khi bạn viết close > open, Pine hiểu đó là so sánh từng nến và trả về một series đúng/sai.

Kiểu dữ liệu cơ bản:

  • int, float, bool, string, color — kiểu đơn giản.
  • series — dãy giá trị (mặc định với dữ liệu giá).
  • simple / const — giá trị cố định; nhiều hàm yêu cầu tham số simple, nên nếu truyền biến series vào sẽ lỗi.

Nhóm hàm bạn dùng nhiều nhất:

// Xu huong
ta.sma(src, len)          // trung binh dong
ta.ema(src, len)          // trung binh ham mu
ta.rma(src, len)          // dung trong RSI/ATR

// Dong luong
ta.rsi(close, 14)
ta.macd(close, 12, 26, 9)

// Bien dong
ta.atr(14)                // trung binh bien do thuc
ta.stdev(close, 20)       // do lech chuan
ta.highest(high, 20)      // dinh cao nhat
ta.lowest(low, 20)        // day thap nhat

// Su kien
ta.crossover(a, b)        // a vua cat len b
ta.crossunder(a, b)       // a vua cat xuong b
ta.barssince(cond)        // so nen tu lan cond dung gan nhat
ta.change(close)          // close - close[1]

// Thoi gian
timeframe.period          // "15", "60", "1D"
hour, minute, dayofweek   // gio, phut, thu

// Dinh dang chuoi
str.tostring(x, "#.##")
str.format("{0} {1}", a, b)

Ba mẹo tiết kiệm thời gian:

  1. Chỉ mục lịch sử `[n]` là công cụ mạnh nhất: high[1] là đỉnh nến trước, close[2] là giá đóng cửa cách đây 2 nến. Dùng để tránh "nhìn vào nến hiện tại" khi xác định breakout.
  2. `na` là giá trị "không có" (tương tự null). Phải kiểm tra na() trước khi tính toán, nếu không bot sẽ nhận giá trị rác.
  3. `var` để giữ trạng thái giữa các nến — ví dụ mức trailing stop. Không có var, biến sẽ bị tính lại từ đầu mỗi nến.

Ví dụ về var — đây là chỗ quyết định script có "nhớ" được vị thế hay không:

var float trailingStop = na   // giu gia tri qua cac nen
var int   viThe = 0           // 0: khong co lenh, 1: dang mua

Chỉ báo đầu tiên: EMA cross có lọc RSI và ATR

Đây là script hoàn chỉnh, chạy được, và có tín hiệu đủ để nối vào bot:

//@version=6
indicator("EMA Cross + RSI Filter (bot ready)", overlay=true)

fastLen = input.int(20,  "EMA nhanh")
slowLen = input.int(50,  "EMA cham")
rsiLen  = input.int(14,  "RSI")
rsiMin  = input.int(50,  "RSI toi thieu de mua")
atrLen  = input.int(14,  "ATR")
atrMult = input.float(2.0, "He so ATR cho stop")

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
rsi  = ta.rsi(close, rsiLen)
atr  = ta.atr(atrLen)

// Chi vao lenh khi xu huong va dong luong cung huong
longCond  = ta.crossover(fast, slow) and rsi > rsiMin
shortCond = ta.crossunder(fast, slow) and rsi < (100 - rsiMin)

// Stop loss va take profit tinh san de gui kem trong payload
stopLong  = close - atr * atrMult
targetLong = close + atr * atrMult * 2

plot(fast, "EMA nhanh", color=color.new(#ff9800, 0))
plot(slow, "EMA cham",  color=color.new(#2196f3, 0))
plot(longCond  ? stopLong  : na, "SL long",  color=color.red,   style=plot.style_linebr)
plot(longCond  ? targetLong : na, "TP long", color=color.green, style=plot.style_linebr)

Điểm đáng lưu ý với người mới:

  • rsi > rsiMin là bộ lọc xu hướng, không phải điều kiện vào lệnh. Nó loại bớt tín hiệu cắt EMA khi thị trường đi ngang.
  • atr * 2 là khoảng cách stop theo biến động, tốt hơn con số cố định như "20 pip" vì tự co giãn theo thị trường.
  • style=plot.style_linebr để đường SL/TP không nối liền giữa các tín hiệu — chi tiết nhỏ nhưng giúp bạn đọc chart chính xác hơn.

Sau khi thêm vào chart, hãy xem bằng mắt ít nhất 50 tín hiệu gần nhất. Nếu bạn thấy tín hiệu xuất hiện ở những chỗ bạn tuyệt đối không muốn bot vào lệnh, sửa điều kiện trước khi nghĩ tới webhook.


Từ chỉ báo sang tín hiệu: alert() và alertcondition()

Pine có hai cách phát cảnh báo, và chúng không thay thế nhau:

alertcondition()alert()
Nội dung gửi điChỉ có tên/mô tả điều kiệnChuỗi tự do — gửi được JSON
Dùng với webhook botKhông phù hợpĐúng cách
Số lượngMỗi điều kiện cần một alert riêngMột alert, gửi mọi nội dung
Tần suấtPhụ thuộc cấu hình alertChọn được bằng alert.freq_*

Với bot, hãy dùng alert():

if longCond
    alert("LONG " + syminfo.ticker, alert.freq_once_per_bar_close)

if shortCond
    alert("SHORT " + syminfo.ticker, alert.freq_once_per_bar_close)

Các giá trị tần suất cần biết:

  • alert.freq_once_per_bar_close — dùng cái này cho bot. Tín hiệu chỉ gửi khi nến đã đóng.
  • alert.freq_once_per_bar — gửi ngay lần đầu điều kiện đúng trong nến, kể cả nến chưa đóng → nguy cơ repaint.
  • alert.freq_all — gửi mỗi lần script tính lại. Chỉ dùng khi bạn thật sự hiểu mình đang làm gì; với bot, điều này thường nghĩa là gửi hàng chục tín hiệu trùng.

Sau khi có alert(), cấu hình alert trong TradingView: chọn script, chọn "Any alert() function call", đặt Webhook URL trỏ tới bot của bạn, và bật. Webhook là tính năng của các gói trả phí (tuỳ thời điểm và gói, TradingView có thể không cho webhook ở gói miễn phí) — hãy kiểm tra lại gói bạn đang dùng.

Giới hạn số alert đang hoạt động tuỳ gói (gói miễn phí chỉ có một số rất ít). Điều này ảnh hưởng trực tiếp tới thiết kế: tốt hơn là một script, một alert gửi JSON có trường action, thay vì 8 alert cho 8 mã.


Đóng gói payload JSON trong Pine

Đây là phần quan trọng nhất khi nối Pine với bot: nội dung alert chính là body JSON mà bot sẽ nhận.

//@version=6
indicator("Webhook JSON Signal", overlay=true)

fastLen = input.int(20, "EMA nhanh")
slowLen = input.int(50, "EMA cham")
atrLen  = input.int(14, "ATR")
atrMult = input.float(2.0, "He so ATR")

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
atr  = ta.atr(atrLen)

longCond  = ta.crossover(fast, slow)
shortCond = ta.crossunder(fast, slow)

// Ham tao payload: gom du thong tin bot can, khong thua khong thieu
f_payload(action) =>
    sl = action == "buy" ? close - atr * atrMult : close + atr * atrMult
    tp = action == "buy" ? close + atr * atrMult * 2 : close - atr * atrMult * 2
    '{"symbol":"'  + syminfo.ticker +
     '","exchange":"' + syminfo.prefix +
     '","action":"' + action +
     '","price":'   + str.tostring(close, "#.####") +
     ',"sl":'       + str.tostring(sl, "#.####") +
     ',"tp":'       + str.tostring(tp, "#.####") +
     ',"tf":"'      + timeframe.period +
     '","bar":"'    + str.format_time(time_close, "yyyy-MM-dd'T'HH:mm:ssZ") +
     '","strategy":"ema_cross_v1"}'

if longCond
    alert(f_payload("buy"), alert.freq_once_per_bar_close)

if shortCond
    alert(f_payload("sell"), alert.freq_once_per_bar_close)

Những điều cần chú ý về payload:

  • Dùng dấu nháy đơn `'` cho chuỗi JSON trong Pine để dấu nháy kép " bên trong JSON không phải escape. Đây là mẹo giúp code sạch hơn hẳn.
  • Luôn gửi kèm `tf` (khung thời gian) và `bar` (thời điểm nến) — bot cần cả hai để chống trùng lệnh và để log đối soát.
  • Làm tròn số bằng `str.tostring(x, "#.####")` trước khi nhét vào JSON. Nếu không, bạn sẽ gặp những giá trị như 1.2345678901234 và bot phải tự cắt.
  • Gửi `sl` và `tp` ngay trong tín hiệu. Vì bot không biết ATR của Pine, nếu bạn không gửi thì bot buộc phải tính lại bằng công thức khác → hai bên lệch nhau.

Một chi tiết kỹ thuật nên biết: giá trị sl = close - atr * mult được tính tại nến tín hiệu. Bot sẽ dùng con số này làm ngưỡng giám sát (stop-watcher), chứ không đặt một lệnh stop trên sàn — vì nhiều sàn không hỗ trợ, và giá stop cố định sẽ không bám theo thị trường.


Ví dụ thực chiến 1: Donchian breakout + trailing ATR

Chiến lược breakout kênh Donchian rất hợp với bot vì tín hiệu rõ ràng, ít tham số, và điều kiện vào lệnh chỉ dùng dữ liệu quá khứ.

//@version=6
indicator("Donchian Breakout + ATR Trail", overlay=true)

len     = input.int(20,   "Chu ky kenh Donchian")
atrLen  = input.int(14,   "ATR")
atrMult = input.float(2.5,"He so trailing")
useTrail = input.bool(true, "Bat trailing stop")

// Lay dinh/day cua len-1 nen truoc => khong nhin vao nen hien tai
upper = ta.highest(high, len)[1]
lower = ta.lowest(low,  len)[1]
atr   = ta.atr(atrLen)

var int   viThe  = 0
var float trail  = na

breakUp   = close > upper and viThe == 0
breakDown = close < lower and viThe == 0

if breakUp
    viThe := 1
    trail := close - atr * atrMult
if breakDown
    viThe := -1
    trail := close + atr * atrMult

// Cap nhat trailing theo huong dang giu
if viThe == 1
    trail := math.max(trail, close - atr * atrMult)
    if close < trail
        viThe := 0

if viThe == -1
    trail := math.min(trail, close + atr * atrMult)
    if close > trail
        viThe := 0

plot(upper, "Dinh Donchian", color=color.new(color.red, 40))
plot(lower, "Day Donchian",  color=color.new(color.green, 40))
plot(viThe != 0 ? trail : na, "Trailing", color=color.new(color.blue, 0), style=plot.style_linebr)

alert('{"event":"entry","side":"buy","symbol":"' + syminfo.ticker + '","price":' + str.tostring(close, "#.####") + ',"sl":' + str.tostring(trail, "#.####") + ',"tf":"' + timeframe.period + '"}', alert.freq_once_per_bar_close)

Lưu ý quan trọng: ta.highest(high, len)[1] — dấu [1] là điều khác biệt giữa một chiến lược breakout có thể giao dịch và một chiến lược nhìn trộm tương lai. Nếu bạn viết ta.highest(high, len) (không có [1]), đỉnh kênh đã bao gồm chính nến hiện tại, và close > upper gần như không bao giờ đúng — hoặc tệ hơn, đúng trong những điều kiện vô nghĩa.

Trong thực tế, bạn nên tách alert entry và alert exit thành hai alert() riêng (hoặc dùng trường event) để bot biết cần mở vị thế hay đóng.


Ví dụ thực chiến 2: Mean reversion cho khung 15 phút

Với khung ngắn và thị trường đi ngang, chiến lược đảo chiều về trung bình thường hiệu quả hơn đuổi breakout:

//@version=6
indicator("Mean Reversion 15m", overlay=true)

basisLen = input.int(20,   "Chu ky SMA")
devMult  = input.float(2.0,"Do lech chuan")
rsiLen   = input.int(14,   "RSI")
rsiBuy   = input.int(30,   "RSI mua")
rsiSell  = input.int(70,   "RSI ban")

basis = ta.sma(close, basisLen)
dev   = ta.stdev(close, basisLen)
upper = basis + devMult * dev
lower = basis - devMult * dev
rsi   = ta.rsi(close, rsiLen)

buyCond  = ta.crossunder(close, lower) and rsi < rsiBuy
sellCond = ta.crossover(close, upper)  and rsi > rsiSell

plot(basis, "SMA 20", color=color.new(color.gray, 0))
plot(upper, "Upper", color=color.new(color.red, 50))
plot(lower, "Lower", color=color.new(color.green, 50))

if buyCond
    alert('{"event":"entry","side":"buy","symbol":"' + syminfo.ticker + '","price":' + str.tostring(close, "#.####") + ',"tp":' + str.tostring(basis, "#.####") + ',"atr":' + str.tostring(ta.atr(14), "#.####") + ',"tf":"' + timeframe.period + '"}', alert.freq_once_per_bar_close)

if sellCond
    alert('{"event":"entry","side":"sell","symbol":"' + syminfo.ticker + '","price":' + str.tostring(close, "#.####") + ',"tp":' + str.tostring(basis, "#.####") + ',"atr":' + str.tostring(ta.atr(14), "#.####") + ',"tf":"' + timeframe.period + '"}', alert.freq_once_per_bar_close)

Hai điều cần lưu ý với mean reversion khi giao cho bot:

  1. Không có stop tự nhiên. Khác breakout (stop dưới đáy kênh), đảo chiều trung bình có thể "đúng về lâu dài nhưng cháy tài khoản trước". Hãy đặt stop theo ATR trong payload và để bot giám sát.
  2. Gửi `atr` trong payload để bot tự tính khối lượng theo rủi ro cố định (ví dụ rủi ro 0,5% tài khoản cho mỗi lệnh), thay vì bạn nhập khối lượng bằng tay.

Ví dụ thực chiến 3: Tín hiệu đa khung (xu hướng khung lớn, vào lệnh khung nhỏ)

Đây là kỹ thuật "ăn tiền" nhất cũng là chỗ nhiều người làm sai nhất. Cách đúng:

//@version=6
indicator("HTF Trend + LTF Entry", overlay=true)

htf        = input.timeframe("240", "Khung xu huong")
htfEmaLen  = input.int(200, "EMA khung lon")
entryLen   = input.int(20,  "EMA khung nho")

htfEma   = request.security(syminfo.tickerid, htf, ta.ema(close, htfEmaLen), lookahead=barmerge.lookahead_off)
trendUp  = close > htfEma

entryEma = ta.ema(close, entryLen)
pullBack = low <= entryEma and close > entryEma   // cham EMA roi dong cua tren

buyCond = trendUp and pullBack
plot(entryEma, "EMA entry", color=color.new(color.orange, 0))

if buyCond
    alert('{"event":"entry","side":"buy","symbol":"' + syminfo.ticker + '","price":' + str.tostring(close, "#.####") + ',"htf":"' + htf + '","htf_trend":"up","tf":"' + timeframe.period + '"}', alert.freq_once_per_bar_close)

Ba điều bắt buộc:

  • `lookahead=barmerge.lookahead_off`. Nếu để lookahead_on, Pine sẽ "biết" giá trị EMA khung 4H của tương lai — backtest sẽ cực đẹp, chạy thật sẽ thất vọng.
  • Dùng `request.security` với `barmerge.gaps_off` (mặc định) và hiểu rằng giá trị khung lớn chỉ đổi sau khi nến khung lớn đóng. Trong lúc nến 4H chưa đóng, EMA khung 4H của nến hiện tại vẫn là số liệu tạm.
  • Gửi `htf` trong payload để bot ghi log biết tín hiệu đến từ cặp khung nào. Ba tháng sau bạn sẽ cần đúng thông tin này để tìm ra vì sao bot vào lệnh ngược xu hướng.

Gửi stop loss và position sizing ngay trong tín hiệu

Bot cần biết rủi ro, không chỉ hướng. Cách sạch nhất là để Pine gửi khoảng cách stop, bot tự quy đổi thành khối lượng:

//@version=6
indicator("Risk Ready Signal", overlay=true)

riskPerTrade = input.float(0.5, "Rui ro moi lenh (% tai khoan)")
atrLen       = input.int(14,   "ATR")
slMult       = input.float(1.5, "He so SL theo ATR")

atr    = ta.atr(atrLen)
emaUp  = ta.crossover(ta.ema(close, 20), ta.ema(close, 50))
slDist = atr * slMult

if emaUp
    alert('{"event":"entry","side":"buy","symbol":"' + syminfo.ticker + '","price":' + str.tostring(close, "#.####") + ',"sl_distance":' + str.tostring(slDist, "#.####") + ',"risk_pct":' + str.tostring(riskPerTrade, "#.##") + ',"atr":' + str.tostring(atr, "#.####") + ',"tf":"' + timeframe.period + '"}', alert.freq_once_per_bar_close)

Phía bot, công thức khối lượng luôn là:

khối lượng = (tài khoản × rủi ro %) / khoảng cách stop

Ví dụ tài khoản 10.000 USDT, rủi ro 0,5% = 50 USDT, khoảng cách stop = 1.200 USDT (BTC) → khối lượng ≈ 0,0416 BTC.

Vì sao không gửi khối lượng từ Pine? Vì Pine không biết số dư, không biết giá trị pip/point, không biết đòn bẩy và không biết bạn đang có vị thế nào. Khối lượng là quyết định của bot, dựa trên tài khoản thật tại thời điểm lệnh.

Một biến thể hữu ích: nếu bạn muốn bot "im lặng" vào những giờ thanh khoản kém, chặn luôn ở Pine:

trongPhien = not na(time(timeframe.period, "0700-2000:12345", "Asia/Ho_Chi_Minh"))
buyCond    = emaUp and trongPhien

Repaint — kẻ thù số một của bot dùng Pine

Repaint là hiện tượng tín hiệu xuất hiện trên chart, rồi biến mất hoặc đổi hướng sau khi nến đóng. Với trader ngồi nhìn chart, repaint gây khó chịu. Với bot, repaint gây lệnh sai đã được gửi đi — và không thể rút lại.

Ba nguyên nhân phổ biến, theo thứ tự nguy hiểm:

  1. Điều kiện dựa trên nến chưa đóng. ta.crossover(fast, slow) tính trên nến hiện tại có thể đúng lúc giá 1.234, rồi sai lúc giá 1.180. Cách chặn: chỉ phát tín hiệu khi barstate.isconfirmed, hoặc dùng alert.freq_once_per_bar_close.
  2. `calc_on_every_tick=true`. Script tính lại theo từng tick → điều kiện có thể "nhấp nháy" nhiều lần trong một nến. Với bot, hãy để mặc định (false) hoặc dùng xác nhận nến.
  3. `request.security(..., lookahead=barmerge.lookahead_on)`. Script đọc được giá trị tương lai của khung lớn → backtest huyền thoại, thực tế không tồn tại. Luôn dùng `lookahead_off`.

Một số hàm tự nhiên repaint: những hàm dùng giá trị chưa chốt của nến hiện tại (close, high, low) trong điều kiện vào lệnh; các chỉ báo có thành phần làm mượt mạnh; và các tín hiệu dùng ta.highest(high, n) mà không có [1].

Bộ ba bảo vệ nên dùng đồng thời:

// 1. Chi phat tin hieu khi nen da dong
confirmed = barstate.isconfirmed

// 2. Chi vao lenh mot lan cho moi nen
var int lastBar = na
newBar = na(lastBar) or bar_index > lastBar

// 3. Luu vet thoi diem phat tin hieu de doi soat
if longCond and confirmed and newBar
    lastBar := bar_index
    alert(f_payload("buy"), alert.freq_once_per_bar_close)

Cách kiểm chứng không có repaint, làm trong 5 phút:

  1. Mở chart, ghi lại tín hiệu bạn thấy ở 20 điểm gần nhất (mã + nến giờ).
  2. Tải lại trang (F5) và so danh sách.
  3. Chuyển khung thời gian đi rồi quay lại — so lại lần nữa.
  4. Mở Bar Replay, chạy từng nến và xem tín hiệu có xuất hiện đúng lúc nến đóng hay xuất hiện rồi biến mất.

Nếu tín hiệu đổi giữa các lần kiểm tra này, đừng nối vào bot. Không có API nào chịu được tín hiệu không xác định.


Những lỗi Pine hay gặp khi làm bot

LỗiTriệu chứngCách sửa
Dùng alertcondition cho JSONKhông gửi được payloadChuyển sang alert()
alert.freq_allBot nhận hàng chục tín hiệu trùngDùng freq_once_per_bar_close
Không gửi tfBot không biết tín hiệu khung nào, chống trùng saiLuôn thêm trường tf
Không gửi bar/timeKhông đối soát được với log brokerThêm time_close định dạng ISO
Nhét " trong JSON không escapeJSON hỏng, bot trả 400Dùng ' cho chuỗi Pine
Số dài không làm trònGiá lệnh lệ lệchstr.tostring(x, "#.####")
Script vẽ nhiều mãMột alert không biết mã nàoDùng syminfo.ticker trong payload, hoặc một alert mỗi mã
Đặt logic quản trị vốn vào PineBot không kiểm soát được rủi roChuyển sang bot (có dữ liệu tài khoản)
Alert tạo xong quên bậtKhông có tín hiệu nào đến botChecklist: script → alert → webhook → test
Tín hiệu gửi khi thị trường đóngBot đặt lệnh rácLọc bằng time(timeframe.period, "session")

Một lỗi rất hay gặp nữa là dùng alert để test: người mới thường tạo alert với freq_all để "xem có hoạt động không", rồi quên đổi lại. Hãy test bằng công cụ "Test webhook" của TradingView, hoặc bằng cách gọi thủ công webhook từ terminal.


Kiểm thử tín hiệu trước khi cho bot chạy thật

Ba tầng kiểm thử, mỗi tầng trả lời một câu hỏi khác nhau:

  1. Xem bằng mắt (mắt người). Bật script trên chart, xem 50–100 tín hiệu gần nhất. Câu hỏi: tín hiệu có xuất hiện ở chỗ hợp lý không?
  2. Strategy Tester. Chuyển logic sang strategy() để có thống kê (net profit, drawdown, win rate, profit factor). Câu hỏi: logic này có "sống" qua nhiều giai đoạn thị trường không? Nhưng nhớ: khớp lệnh của TradingView là giả định, chưa tính phí, chưa tính trượt giá, chưa tính T+2,5 hay biên độ. Số đẹp ở đây không phải bằng chứng có lãi.
  3. Chạy song song (shadow). Bot thật nhận tín hiệu, log lại, không gửi lệnh trong 1–2 tuần, rồi so log bot với danh sách tín hiệu trên TradingView. Câu hỏi: bot có nhận đủ và chỉ nhận đúng những tín hiệu tôi thấy không? Đây là tầng phát hiện lỗi webhook, lỗi trùng, lỗi múi giờ.

Chỉ khi cả ba tầng đều qua, hãy cho bot gửi lệnh với khối lượng tối thiểu.

Một mẹo đối soát rất hiệu quả: cho Pine gửi kèm bar (thời điểm nến đóng) và strategy (tên phiên bản logic, ví dụ ema_cross_v1). Khi bot ghi log theo hai trường này, bạn có thể dựng ngay một bảng so sánh giữa tín hiệu và lệnh thật để tìm lệch.


Checklist bàn giao từ Pine sang bot

Trước khi coi một script Pine là "xong", hãy kiểm đủ:

  1. ✅ //@version=6 ghi rõ ở dòng đầu.
  2. ✅ Dùng indicator(), không nhồi logic tài khoản vào Pine.
  3. ✅ Tín hiệu chỉ phát khi nến đóng (barstate.isconfirmed / freq_once_per_bar_close).
  4. ✅ Không có lookahead_on; kiểm tra đã chuyển hết sang lookahead_off.
  5. ✅ Mọi giá trị na đã được xử lý trước khi đưa vào payload.
  6. ✅ Payload có đủ: symbol, exchange, action, price, sl, tf, bar, strategy.
  7. ✅ Số trong payload đã làm tròn (#.####).
  8. ✅ Đã kiểm tra repaint bằng cách F5 và bằng Bar Replay.
  9. ✅ Alert đã tạo, đã chọn "Any alert() function call", đã điền webhook và đã bật.
  10. ✅ Đã test webhook bằng 1 lệnh thủ công và xem log bot ghi nhận.
  11. ✅ Có tên phiên bản logic trong payload để đối soát khi bạn đổi điều kiện tín hiệu.

Sau này khi bạn sửa điều kiện tín hiệu, hãy tăng phiên bản (ema_cross_v2) chứ đừng sửa ngầm. Khi bot lỗ, câu hỏi đầu tiên luôn là: thay đổi tín hiệu nào đã gây ra chuyện này?


Tổng kết

Pine Script không khó, nhưng nó đòi hỏi bạn suy nghĩ khác: bạn đang mô tả điều kiện trên dãy dữ liệu, và bot chỉ nên nhận một tín hiệu cho mỗi nến đã đóng.

Bốn việc tạo nên một script Pine "bot ready": dùng ta.* để tính chỉ báo, xác định điều kiện bằng crossover/crossunder hoặc so sánh mức, phát tín hiệu bằng alert() với payload JSON đầy đủ, và bảo vệ tín hiệu khỏi repaint bằng xác nhận nến + lookahead_off + log có tf và thời gian nến.

Nếu bạn làm đúng bốn việc đó, phần còn lại của hệ thống — webhook, bot Python, API broker — trở nên đơn giản hơn rất nhiều. Nếu bạn bỏ qua bước bảo vệ repaint, mọi phần sau đều vô nghĩa.


Câu hỏi thường gặp

Pine Script có khó học không?

Không khó nếu bạn học đúng thứ tự: cấu trúc indicator(), series và [n], nhóm hàm ta.*, rồi alert(). Người mới thường mất 1–2 tuần để viết được chỉ báo có lọc tín hiệu và gửi webhook. Phần khó không nằm ở cú pháp mà ở hiểu repaint và điều kiện nến đã đóng.

Tôi có cần biết lập trình mới học được Pine Script không?

Không cần kinh nghiệm lập trình trước, nhưng cần chấp nhận tư duy logic: biến, điều kiện, vòng if. Pine có sẵn rất nhiều hàm chỉ báo dựng sẵn nên bạn có thể tạo tín hiệu hữu ích chỉ với 10–15 dòng. Kỹ năng quan trọng hơn là hiểu chiến lược giao dịch của mình.

alert() và alertcondition() khác nhau thế nào?

alertcondition() chỉ thông báo rằng một điều kiện đã đúng, nội dung thông báo là văn bản mô tả nên không truyền được JSON. alert() gửi được chuỗi tự do, nên bạn có thể đóng gói đầy đủ trường dữ liệu cho bot. Với bot webhook, luôn dùng alert().

Làm sao biết script của tôi bị repaint?

Ba dấu hiệu: tín hiệu biến mất hoặc đổi hướng sau khi nến đóng; danh sách tín hiệu khác nhau giữa hai lần tải lại chart; tín hiệu đẹp bất thường khi xem backtest nhưng kém khi chạy thật. Cách kiểm tra chắc chắn là dùng Bar Replay chạy từng nến và xem tín hiệu xuất hiện đúng lúc nến đóng.

Tôi nên đặt khối lượng lệnh trong Pine hay trong bot?

Đặt trong bot. Pine không biết số dư, đòn bẩy, vị thế đang mở hay giá trị pip/point của tài khoản bạn. Cách đúng là Pine gửi price và sl (hoặc sl_distance, atr), còn bot quy đổi thành khối lượng theo rủi ro cố định.

Một alert có gửi được tín hiệu cho nhiều mã không?

Có, nếu payload chứa symbol và exchange lấy từ syminfo.ticker và syminfo.prefix. Cách này tiết kiệm alert, nhưng đòi hỏi bot phải đọc mã từ payload và xác thực danh sách mã được phép giao dịch. Đừng để bot chạy lệnh trên bất kỳ mã nào nó nhận được mà không có whitelist.

Tại sao bot nhận hai tín hiệu giống nhau cho cùng một nến?

Thường do alert đặt ở alert.freq_all hoặc freq_once_per_bar, hoặc do tín hiệu tính lại theo tick. Cách xử lý kép: ở Pine dùng freq_once_per_bar_close, và ở bot chống trùng bằng khoá symbol + tf + bar — chỉ nhận tín hiệu đầu tiên cho mỗi khoá.

Tín hiệu TradingView có bị trễ không?

Alert được gửi từ server TradingView nên thường chỉ chậm vài trăm mili giây tới vài giây. Độ trễ lớn hơn đến từ dữ liệu: một số gói và một số nguồn dữ liệu có thể trễ (thường là 15 phút). Hãy kiểm tra mức dữ liệu thời gian thực của gói bạn dùng trước khi xây chiến lược khung ngắn.


Muốn tự tay viết chỉ báo, tạo alert webhook và nối vào bot chạy thật — từ crypto, Forex tới chứng khoán Việt Nam — Bootcamp TradingView Webhook Bot gồm 6 buổi online (20:00–22:00 Thứ 2, khai giảng 19/10/2026), học phí 990.000đ, tặng kèm 12 tháng FindMe VPS Guardian.

Đọc tiếp trong cụm bài:

  • TradingView Webhook là gì? Hướng dẫn A–Z
  • Tạo Alert TradingView gửi Webhook JSON
  • Bot Python nhận webhook TradingView (Flask)
  • Khóa Lập trình Bot Auto Trading không cần biết code
Đọc xong rồi? Hãy thử ngay — miễn phí 7 ngày.
📬

Weekly Digest — Nhận Bản Tin Hàng Tuần

Nhận các bài viết phân tích kỹ thuật chuyên sâu, thuật toán giao dịch tự động (Trading Bot) và các giải pháp công nghệ mới nhất từ Hướng Nghiệp Dữ Liệu.