🔥 🎁 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)
Bot Python nhận webhook TradingView: code mẫu Flask đầy đủ 2026
ĐTĐược viết bởi Đặng Trí Thanhvào ngày 06/10/2026 lúc 11:00|… lượt xem
TradingView06/10/2026 · 19 phút đọc

Bot Python nhận webhook TradingView: code mẫu Flask đầy đủ

Sau khi đã tạo được Alert gửi webhook, câu hỏi tiếp theo luôn là: viết cái gì ở phía nhận? Rất nhiều người dừng lại ở một endpoint 10 dòng — đủ để "hello world" nhưng không đủ để chạy tiền thật, vì thiếu xác thực, thiếu chống trùng, thiếu quản lý rủi ro và không có log để tra khi lệnh sai.

Bài viết này là bản code mẫu đầy đủ cho bot Python + Flask nhận webhook TradingView: từ cài môi trường, endpoint có HMAC, parse và kiểm tra dữ liệu, lớp adapter gọi API sàn (MT5, Binance/Bybit, chứng khoán Việt Nam), tính khối lượng theo % rủi ro, đặt SL/TP, log có cấu trúc, thông báo Telegram, cho tới đóng gói chạy như service trên VPS.

Đọc kèm để nắm bối cảnh: TradingView Webhook là gì? và Tạo Alert TradingView gửi Webhook JSON.

Trả lời nhanh

Bot nhận webhook TradingView bằng Python thường là một ứng dụng Flask (hoặc FastAPI) với 5 thành phần: (1) endpoint POST xác thực chữ ký HMAC, (2) lớp chống trùng tín hiệu, (3) bộ chuẩn hoá và kiểm tra dữ liệu, (4) lớp adapter gọi API sàn, (5) logging + thông báo.

Bản tối thiểu chạy được chỉ khoảng 150–250 dòng: nhận body → xác thực → parse JSON → kiểm tra khoá trùng → tính khối lượng → gọi API sàn → đặt SL/TP → trả 200. Nhưng để chạy tiền thật bạn cần thêm 4 thứ mà code mẫu trên mạng thường bỏ qua: giới hạn tần suất, trần lỗ trong ngày, kiểm tra giá hợp lý và đối soát vị thế định kỳ.

Nguyên tắc quan trọng nhất về kiến trúc: endpoint chỉ nên nhận và đẩy vào hàng đợi, worker mới xử lý. Nếu bạn gọi API sàn ngay trong request, một lần sàn chậm 20 giây là đủ để TradingView coi webhook thất bại.


Kiến trúc bot webhook cần có gì

Trước khi viết dòng code đầu tiên, hãy hình dung bot của bạn gồm những khối nào. Một bot "đủ dùng cho tiền thật" có 7 khối:

KhốiNhiệm vụNếu thiếu thì sao
EndpointNhận HTTP POST, xác thực, trả 200 nhanhNgười khác bắn lệnh giả, hoặc TradingView báo lỗi gửi
Hàng đợiĐệm tín hiệu để xử lý tuần tựSàn chậm làm request timeout, mất tín hiệu
Chuẩn hoá & kiểm traÉp dữ liệu về một dạng thống nhất, loại payload rácBot crash vì một trường thiếu
Quản lý rủi roTính khối lượng, giới hạn vị thế, trần lỗ ngàyVào lệnh quá lớn, cháy tài khoản trong một phiên
Adapter sànBọc API của từng sàn sau một interface chungMuốn thêm sàn phải sửa khắp nơi
LoggingGhi vết mọi tín hiệu và mọi lệnhKhông biết lỗi ở đâu khi lệnh sai
Giám sátHeartbeat, cảnh báo Telegram khi bot chếtBot dừng mà bạn không biết

Điểm đáng lưu ý: ba khối giữa (quản lý rủi ro, adapter, logging) là phần chiếm 70% công sức thực tế, nhưng lại là phần bị bỏ qua nhiều nhất trong các ví dụ "bot webhook 30 dòng" trên mạng.


Cài đặt môi trường

Trên Ubuntu/Debian hoặc VPS Windows, các bước cài đặt tương tự nhau:

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install flask python-dotenv requests
pip install MetaTrader5            # chỉ trên Windows, nếu giao dịch MT5
pip install python-binance         # nếu dùng Binance

Và file .env (không bao giờ commit file này):

TV_WEBHOOK_SECRET=doi-thanh-chuoi-dai-ngau-nhien-64-ky-tu
TELEGRAM_BOT_TOKEN=123456:ABC...
TELEGRAM_CHAT_ID=987654321
BINANCE_API_KEY=...
BINANCE_API_SECRET=...
RISK_PER_TRADE=0.005
MAX_OPEN_POSITIONS=3
MAX_DAILY_LOSS_PCT=0.03
DRY_RUN=true

Ba tham số bạn sẽ chỉnh nhiều nhất: RISK_PER_TRADE (rủi ro mỗi lệnh theo % tài khoản — 0,5% là mức khởi đầu hợp lý), MAX_OPEN_POSITIONS (số lệnh mở đồng thời tối đa) và MAX_DAILY_LOSS_PCT (trần lỗ trong ngày — chạm ngưỡng là bot tự dừng).

Riêng DRY_RUN hãy để true trong suốt quá trình phát triển. Nó khiến bot chạy hết logic nhưng không gửi lệnh thật — cách rẻ nhất để phát hiện lỗi mà không mất tiền.


Endpoint nhận webhook: code đầy đủ

Đây là phần lõi. Bản dưới đây có xác thực HMAC, chống trùng, giới hạn tần suất và đẩy vào hàng đợi:

import hashlib, hmac, json, logging, os, time
from collections import deque
from flask import Flask, request, abort
from dotenv import load_dotenv

load_dotenv()
log = logging.getLogger("bot")
SECRET = os.environ["TV_WEBHOOK_SECRET"].encode()

app = Flask(__name__)
recent = deque(maxlen=500)          # khoá chống trùng gần đây
hits = {}                           # symbol -> timestamps cho rate limit

def verify(raw: bytes, sig: str) -> bool:
    if not sig:
        return False
    mac = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, sig.lower())

def rate_ok(symbol: str, limit=10, window=60) -> bool:
    now = time.time()
    q = [t for t in hits.get(symbol, []) if now - t < window]
    q.append(now); hits[symbol] = q
    return len(q) <= limit

@app.post("/tv-signal")
def tv_signal():
    raw = request.get_data()
    signature = request.headers.get("X-Signature", "")
    if not verify(raw, signature):
        log.warning("Từ chối request: chữ ký không hợp lệ, ip=%s", request.remote_addr)
        abort(401)

    try:
        data = json.loads(raw)
    except json.JSONDecodeError as e:
        log.error("Payload không phải JSON hợp lệ: %s", e)
        return {"ok": False, "reason": "bad-json"}, 400

    if not rate_ok(data.get("symbol", "?")):
        log.warning("Vượt giới hạn tần suất: %s", data.get("symbol"))
        return {"ok": False, "reason": "rate-limited"}, 429

    key = (data.get("symbol"), data.get("action"), data.get("ts") or data.get("time"))
    if key in recent:
        log.info("Bỏ qua tín hiệu trùng: %s", key)
        return {"ok": True, "skipped": "duplicate"}, 200
    recent.append(key)

    queue.put(data)                 # worker xử lý, endpoint trả về ngay
    return {"ok": True}, 200

Năm chi tiết quyết định chất lượng đoạn code này:

  1. `request.get_data()` lấy raw bytes. Đừng request.json ngay, vì bạn cần raw bytes để kiểm chữ ký HMAC. Parse JSON sau khi đã xác thực.
  2. `hmac.compare_digest` thay vì `==`. So sánh chuỗi thường có thể bị tấn công timing; compare_digest là hàm chống điều đó.
  3. Rate limit theo symbol chứ không theo IP — vì request đến từ hạ tầng TradingView, IP không giúp phân biệt gì.
  4. Trả `200` ngay sau khi đẩy vào queue. TradingView chỉ cần biết "đã nhận". Không để nó chờ API sàn.
  5. Trả body JSON ngắn cho mọi trường hợp, kể cả 401/429. Nhưng không trả về chi tiết nội bộ (như stack trace) để tránh lộ thông tin.

Chuẩn hoá và kiểm tra dữ liệu

Payload bạn thiết kế có thể "sạch", nhưng thực tế luôn có tín hiệu méo: thiếu trường, giá bằng 0, action viết hoa, thời gian sai định dạng. Hãy chuẩn hoá ở một chỗ duy nhất:

from dataclasses import dataclass

@dataclass
class Signal:
    symbol: str
    action: str          # buy | sell
    price: float
    sl: float | None
    tp: float | None
    tf: str
    strategy: str

def parse_signal(d: dict) -> Signal:
    action = str(d.get("action", "")).strip().lower()
    if action not in ("buy", "sell"):
        raise ValueError(f"action không hợp lệ: {action!r}")

    def num(v):
        return float(v) if v not in (None, "", "0") else None

    s = Signal(
        symbol=str(d["symbol"]).upper(),
        action=action,
        price=float(d["price"]),
        sl=num(d.get("sl")),
        tp=num(d.get("tp")),
        tf=str(d.get("tf", "")),
        strategy=str(d.get("strategy", "unknown")),
    )
    if s.price <= 0:
        raise ValueError("price phải > 0")
    if s.sl and s.action == "buy" and s.sl >= s.price:
        raise ValueError("SL của lệnh buy phải thấp hơn giá vào")
    if s.sl and s.action == "sell" and s.sl <= s.price:
        raise ValueError("SL của lệnh sell phải cao hơn giá vào")
    return s

Hai kiểm tra cuối cùng (SL đúng phía) nghe có vẻ thừa, nhưng chúng bắt được lỗi thực tế: chỉ báo vẽ plot sai, hoặc bạn sửa Pine Script và vô tình đảo hai biến. Một lệnh có SL sai phía sẽ bị sàn từ chối (với MT5), hoặc tệ hơn là được chấp nhận và đóng ngay lập tức.


Lớp adapter gọi API sàn

Đây là chỗ quyết định bot của bạn có mở rộng được hay không. Hãy định nghĩa một interface chung, mỗi sàn cài đặt riêng:

class Broker:
    def balance(self) -> float: ...
    def open_positions(self, symbol: str) -> int: ...
    def market_order(self, s: Signal, qty: float) -> dict: ...
    def set_sl_tp(self, order_id: str, sl: float, tp: float) -> dict: ...

class Mt5Broker(Broker):
    def market_order(self, s, qty):
        import MetaTrader5 as mt5
        req = {"action": mt5.TRADE_ACTION_DEAL, "symbol": s.symbol,
               "volume": qty, "type": mt5.ORDER_TYPE_BUY if s.action == "buy" else mt5.ORDER_TYPE_SELL,
               "price": mt5.symbol_info_tick(s.symbol).ask, "deviation": 20,
               "type_filling": mt5.ORDER_FILLING_IOC}
        return mt5.order_send(req)._asdict()

class BinanceBroker(Broker):
    def market_order(self, s, qty):
        from binance.client import Client
        c = Client(os.environ["BINANCE_API_KEY"], os.environ["BINANCE_API_SECRET"])
        side = "BUY" if s.action == "buy" else "SELL"
        return c.create_order(symbol=s.symbol, side=side, type="MARKET", quantity=qty)

Vì sao nên làm theo cách này thay vì gọi API trực tiếp trong worker? Vì khi bạn muốn thêm chứng khoán Việt Nam, bạn chỉ việc viết SsiBroker và đăng ký vào một map. Không sửa một dòng logic nào ở phần tính khối lượng hay quản lý rủi ro.

Với MT5, có hai điểm thực chiến cần nhớ: `deviation` (mức trượt giá tối đa chấp nhận) và `type_filling` (chế độ khớp lệnh — nhiều sàn chỉ chấp nhận một giá trị nhất định, sai là lệnh bị trả về lỗi 10030). Với Binance/Bybit, cần chú ý lọc giá (tick size) và bước khối lượng (step size): gửi khối lượng không đúng bước sẽ bị từ chối.


Tính khối lượng và đặt SL/TP

Đây là phần "quản lý rủi ro" — cũng là phần phân biệt bot nghiệp dư với bot dùng được. Công thức chuẩn:

khối lượng = (số dư × % rủi ro) / |giá vào − giá SL|

Ví dụ: tài khoản 10.000 USD, rủi ro 0,5% = 50 USD, SL cách giá vào 100 pips (tương đương 1 USD/pip với 0,1 lot vàng) → khối lượng sao cho 100 pips lỗ = 50 USD.

def position_size(balance: float, risk_pct: float, price: float,
                  sl: float, value_per_unit: float = 1.0) -> float:
    risk_amount = balance * risk_pct
    stop_dist = abs(price - sl)
    if stop_dist <= 0:
        raise ValueError("SL trùng giá vào")
    raw = risk_amount / (stop_dist * value_per_unit)
    return round(raw, 6)

Sau khi có khối lượng, bot vào lệnh rồi đặt SL/TP thật ở sàn — và phải kiểm tra kết quả đặt:

def execute(broker: Broker, s: Signal) -> None:
    if len(broker.open_positions(s.symbol)) >= MAX_OPEN:
        log.info("Đã đủ %s vị thế cho %s, bỏ qua", MAX_OPEN, s.symbol)
        return

    qty = position_size(broker.balance(), RISK_PER_TRADE, s.price, s.sl or s.price * 0.99)
    if DRY_RUN:
        log.info("[DRY_RUN] sẽ %s %s qty=%s sl=%s tp=%s", s.action, s.symbol, qty, s.sl, s.tp)
        return

    res = broker.market_order(s, qty)
    if not res.get("ok"):
        log.error("Đặt lệnh thất bại: %s", res)
        notify(f"⚠️ Lệnh {s.action} {s.symbol} thất bại: {res.get('error')}")
        return

    if s.sl or s.tp:
        sl_tp = broker.set_sl_tp(res["order_id"], s.sl, s.tp)
        if not sl_tp.get("ok"):
            # Tình huống nguy hiểm nhất: đã vào lệnh nhưng chưa có SL
            log.error("KHÔNG đặt được SL/TP: %s", sl_tp)
            notify(f"🚨 {s.symbol} vào lệnh nhưng CHƯA có SL — kiểm tra ngay")
    notify(f"✅ {s.action.upper()} {s.symbol} qty={qty} @ {s.price}")

Chỗ đáng chú ý là nhánh "vào lệnh được nhưng đặt SL thất bại". Đây là trạng thái nguy hiểm nhất trong toàn bộ hệ thống: vị thế đang mở mà không có bảo vệ. Bot bắt buộc phải phát hiện và báo động ngay, đồng thời có thể thử lại vài lần hoặc đóng vị thế nếu vượt quá thời gian cho phép.


Logging có cấu trúc và thông báo Telegram

Log vô dụng nếu bạn không tìm được gì trong đó. Hãy log dạng JSON, một dòng một sự kiện:

import json as _json, datetime as _dt

def jlog(event: str, **kv):
    log.info(_json.dumps({"t": _dt.datetime.utcnow().isoformat(), "event": event, **kv},
                         ensure_ascii=False))

# dùng
jlog("signal_received", symbol=s.symbol, action=s.action, strategy=s.strategy, tf=s.tf)
jlog("order_sent", symbol=s.symbol, qty=qty, price=s.price)
jlog("sl_tp_set", symbol=s.symbol, sl=s.sl, tp=s.tp)

Với log dạng này, bạn có thể đếm nhanh mọi thứ bằng công cụ dòng lệnh: số tín hiệu mỗi ngày, số lệnh bị từ chối, chênh lệch giá tín hiệu và giá khớp.

Thông báo Telegram thì giữ ở mức "ít nhưng đúng việc":

import requests

def notify(text: str) -> None:
    requests.post(
        f"https://api.telegram.org/bot{os.environ['TELEGRAM_BOT_TOKEN']}/sendMessage",
        json={"chat_id": os.environ["TELEGRAM_CHAT_ID"], "text": text}, timeout=5)

Chỉ nên gửi thông báo cho 4 loại sự kiện: lệnh đã vào, lệnh bị từ chối, lỗi đặt SL/TP, và heartbeat mỗi giờ. Nếu bạn gửi thông báo cho mọi tín hiệu nhận được, bạn sẽ tắt thông báo sau ba ngày — và khi đó mất luôn cả cảnh báo quan trọng.


Chạy bot như một service

Bot phải tự chạy lại khi VPS khởi động lại hoặc khi tiến trình chết. Trên Linux dùng systemd:

# /etc/systemd/system/tvbot.service
[Unit]
Description=TradingView webhook bot
After=network-online.target

[Service]
User=bot
WorkingDirectory=/opt/tvbot
EnvironmentFile=/opt/tvbot/.env
ExecStart=/opt/tvbot/.venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 app:app
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Trên Windows dùng NSSM:

nssm install TVBot "C:\tvbot\.venv\Scripts\python.exe" "-m" "gunicorn" "-b" "127.0.0.1:8000" "app:app"
nssm set TVBot AppDirectory C:\tvbot
nssm set TVBot AppStdout C:\tvbot\logs\out.log
nssm set TVBot AppStderr C:\tvbot\logs\err.log
nssm set TVBot AppRestartDelay 5000

Ba chi tiết dễ bỏ sót: (1) `Restart=always` — không có nó, bot chết là chết hẳn; (2) chạy sau reverse proxy (Nginx/Caddy) để có HTTPS và không lộ port trực tiếp; (3) `-w 2` worker chỉ nên dùng khi bot không giữ state trong RAM — nếu bạn chống trùng bằng biến trong bộ nhớ như ví dụ trên, chạy nhiều worker có thể cho hai tín hiệu giống nhau vào hai worker khác nhau. Khi cần nhiều worker, hãy chuyển khoá chống trùng vào Redis hoặc database.


Kiểm thử: 4 tầng trước khi chạy tiền thật

Tầng 1 — Unit test cho parse và position_size. Hai hàm này là nơi sinh lỗi nhiều nhất. Viết test cho các trường hợp biên: SL lớn hơn giá vào, price = 0, action viết hoa, thiếu trường sl.

def test_reject_bad_sl():
    try:
        parse_signal({"symbol": "XAUUSD", "action": "buy", "price": 2400, "sl": 2500})
        assert False, "phải raise lỗi"
    except ValueError:
        pass

Tầng 2 — Test endpoint bằng request giả. Dùng app.test_client() của Flask, gửi body JSON cùng chữ ký HMAC đúng và sai, xác nhận 401 với chữ ký sai, 200 với chữ ký đúng, và 200 + skipped cho tín hiệu trùng.

Tầng 3 — DRY_RUN trên dữ liệu thật. Cho alert thật chạy vào bot với DRY_RUN=true trong 2–3 ngày. Đọc log để xem khối lượng tính ra có hợp lý không, SL/TP có đúng phía không, tín hiệu trùng có bị chặn không.

Tầng 4 — Tài khoản demo ít nhất 3 ngày. Cho bot vào lệnh thật trên demo, qua cả phiên Á, Âu, Mỹ. Đây là tầng bắt được các vấn đề về độ trễ, khớp lệnh, và các lỗi API chỉ xuất hiện khi thị trường biến động.

Đừng bỏ qua tầng 3. Nó miễn phí, chạy song song với cuộc sống hằng ngày của bạn, và phát hiện phần lớn lỗi logic.


Cấu trúc thư mục bot hoàn chỉnh

tvbot/
├── app.py               # Flask app + endpoint
├── worker.py            # vòng lặp xử lý queue
├── broker/
│   ├── base.py          # interface Broker
│   ├── mt5.py
│   ├── binance.py
│   └── ssi.py
├── risk.py              # position_size, giới hạn vị thế, trần lỗ ngày
├── notify.py            # Telegram / Zalo / email
├── validate.py          # parse_signal + kiểm tra
├── reconcile.py         # đối soát vị thế định kỳ
├── tests/
├── .env
├── .env.example
└── requirements.txt

Việc tách worker.py khỏi app.py không phải để "cho đẹp": nó cho phép bạn restart endpoint mà không mất tín hiệu đang chờ, và cho phép chạy nhiều worker nếu sau này cần. Nó cũng giúp ích khi debug — bạn chạy riêng worker với payload tái hiện được.


8 lỗi thường gặp khi deploy bot webhook

LỗiTriệu chứngCách xử lý
Dùng request.json trước khi verifyKhông kiểm được chữ kýĐọc raw bằng request.get_data() rồi mới parse
Xử lý nặng ngay trong requestTradingView báo gửi thất bạiĐẩy vào queue, trả 200 ngay
Chống trùng bằng biến RAM + nhiều workerVào lệnh 2 lầnĐưa khoá chống trùng vào Redis/DB
Không set type_filling cho MT5Lỗi 10030 khi đặt lệnhĐọc yêu cầu của sàn, chọn đúng chế độ
Khối lượng không đúng step sizeAPI từ chối lệnhLàm tròn theo step size của symbol
SL/TP sai phíaSàn từ chối hoặc lệnh đóng ngayKiểm tra trong parse_signal
Không kiểm kết quả đặt SLVị thế mở không bảo vệBắt buộc kiểm và báo động
Log không có timestamp/mã lệnhKhông tra được sự cốLog JSON, một dòng một sự kiện

Một lỗi nữa ít thấy trong bảng nhưng hay gặp trên thực tế: bot chạy đúng nhưng VPS hết dung lượng đĩa do log không được xoay vòng. Hãy cấu hình logrotate hoặc giới hạn kích thước file log ngay từ đầu.


Checklist bảo mật cho bot

  1. ✅ HTTPS bắt buộc, có reverse proxy, không mở port trực tiếp.
  2. ✅ Xác thực HMAC mọi request; token đủ dài và đổi định kỳ.
  3. ✅ Rate limit theo symbol; chặn payload quá lớn.
  4. ✅ API key chỉ quyền giao dịch, không rút tiền; khoá IP nếu sàn hỗ trợ.
  5. ✅ .env nằm ngoài mã nguồn, quyền file 600, không có trong Git.
  6. ✅ Kiểm tra giá hợp lý: tín hiệu lệch quá X% so với giá thị trường thì từ chối.
  7. ✅ Trần khối lượng tối đa mỗi lệnh, bất kể công thức tính ra bao nhiêu.
  8. ✅ Nút dừng khẩn cấp: endpoint yêu cầu chữ ký, đóng toàn bộ vị thế và tắt bot.

Điểm 6 và 7 là "van an toàn cuối". Chúng không thay thế công thức tính khối lượng, mà bảo vệ bạn khi công thức đó nhận dữ liệu vô lý (ví dụ SL bị gửi thành 0 khiến khối lượng tính ra khổng lồ).


Mở rộng: khi nào cần hàng đợi và nhiều tài khoản

Bản code mẫu ở trên chạy tốt cho một tài khoản và vài chiến lược. Bạn cần nâng cấp khi:

  • Nhiều tài khoản ở nhiều sàn. Thêm bảng accounts với cấu hình riêng; worker chọn broker theo tài khoản trong tín hiệu.
  • Cần xử lý lại khi sàn lỗi. Dùng hàng đợi bền (Redis Streams, RabbitMQ, hoặc đơn giản là bảng signals trong Postgres) với trạng thái pending/processed/failed.
  • Cần audit đầy đủ. Ghi mọi tín hiệu và mọi lệnh vào database, để có thể tái hiện một ngày giao dịch bất kỳ sau này.
  • Chạy nhiều VPS. Tách endpoint (nhận) và worker (xử lý) thành hai dịch vụ riêng, dùng chung hàng đợi.

Với đa số người học và trader cá nhân, mức "một VPS, một tài khoản, ba chiến lược" là đủ và nên dừng ở đó. Nâng cấp kiến trúc chỉ đáng làm khi bạn đã chạy thật ổn định ít nhất vài tháng.


Luồng xử lý từng bước: từ request đến lệnh khớp

Để bạn hình dung rõ chỗ nào dễ hỏng, đây là hành trình đầy đủ của một tín hiệu:

1. TradingView gửi POST. Body là nội dung Message bạn đã viết trong alert, kèm header chữ ký nếu bạn cấu hình. Thời điểm này: t0.

2. Reverse proxy nhận. Nginx/Caddy kiểm HTTPS, chuyển tiếp tới Flask. Nếu bạn cấu hình sai header hoặc giới hạn client_max_body_size quá nhỏ, request bị chặn ở đây — và triệu chứng là "alert báo gửi lỗi" trong khi bot không thấy gì.

3. Flask xác thực chữ ký. Sai chữ ký → 401 và log cảnh báo. Đúng → parse JSON. Nếu JSON hỏng, trả 400 kèm lý do; đừng trả 500 vì bạn muốn phân biệt "lỗi phía gửi" và "lỗi phía bot".

4. Kiểm tra tần suất và trùng lặp. Vượt rate limit → 429. Trùng khoá → 200 kèm skipped. Lưu ý: vẫn trả 200 cho tín hiệu trùng để TradingView không đánh dấu lỗi gửi.

5. Đẩy vào hàng đợi và trả 200. Từ đây TradingView đã xong việc. Thời điểm t1 — khoảng cách t1 - t0 nên dưới 300ms.

6. Worker lấy tín hiệu ra xử lý. Bước này chạy bất đồng bộ. Worker kiểm tra: còn đủ số vị thế mở không, có vượt trần lỗ trong ngày không, giá tín hiệu có lệch quá mức cho phép so với giá thị trường không.

7. Tính khối lượng. Dùng số dư hiện tại và khoảng cách SL. Đây là chỗ cần đọc số dư tươi từ sàn, không dùng cache cũ — vì số dư đổi sau mỗi lệnh.

8. Gửi lệnh và đặt SL/TP. Nhận kết quả, kiểm tra mã lỗi. Thành công → ghi log + thông báo. Thất bại → log rõ lý do, không thử lại vô hạn.

9. Ghi log và đối soát. Mọi bước ở trên được ghi lại với mã tín hiệu để sau này bạn lần theo được. Thời điểm t2 — từ t0 đến lúc lệnh khớp thường 1–5 giây.

Khi có sự cố, bạn chỉ cần so t0 → t1 → t2 trong log: nếu t1 - t0 lớn, vấn đề nằm ở endpoint; nếu t2 - t1 lớn, vấn đề nằm ở sàn hoặc mạng.


Đối soát vị thế: khối bị bỏ quên nhiều nhất

Hầu hết bot tự viết đều thiếu bước đối soát — và đó là lý do chúng thường "chạy tốt vài tuần rồi có ngày lệch không hiểu vì sao".

Đối soát là gì? Định kỳ (mỗi 1–5 phút), bot so vị thế đang mở thực tế ở sàn với sổ log của mình. Ba loại chênh lệch thường gặp:

Chênh lệchNguyên nhân điển hìnhCách xử lý
Có lệnh ở sàn, không có trong logBot restart giữa chừng, hoặc bạn vào tayGhi nhận vào log với cờ unmanaged, không tự đóng
Có trong log, không có ở sànLệnh đã đóng (SL/TP) mà bot chưa cập nhật, hoặc bị sàn huỷĐánh dấu đã đóng, giải phóng slot vị thế
Khối lượng lệchKhớp một phần, hoặc lệnh bị sửa tayCảnh báo cho bạn quyết định, không tự "sửa"

Nguyên tắc quan trọng: bot không tự ý đóng những vị thế nó không quản lý. Nếu bạn vừa vào một lệnh tay để phòng hộ, một bot "tự dọn dẹp" sẽ đóng lệnh đó và làm hỏng kế hoạch của bạn.

Cách cài đặt đơn giản nhất: một job chạy mỗi 2 phút, lấy danh sách vị thế từ sàn, so với bảng positions trong database, ghi lại mọi khác biệt và gửi thông báo nếu có chênh lệch chưa từng thấy. Chỉ cần vậy là bạn phát hiện được phần lớn sự cố trước khi nó thành lỗ thật.


Đọc log để tìm lỗi: ba câu hỏi cần trả lời được

Khi có vấn đề, log phải trả lời được ba câu hỏi sau. Nếu không, log của bạn chưa đủ tốt:

Câu 1 — Tín hiệu có tới bot không? Tìm signal_received theo mã tín hiệu. Nếu không có dòng nào trong khi bạn thấy alert đã phát, vấn đề nằm ở đường ống: bot chết, HTTPS lỗi, hoặc alert chưa bật webhook.

Câu 2 — Bot có quyết định vào lệnh không? Tìm order_sent hoặc lý do bỏ qua (skipped: duplicate, rate-limited, max-positions, daily-loss-limit). Đây là chỗ phân biệt "bot bị lỗi" với "bot làm đúng quy tắc của bạn".

Câu 3 — Lệnh có khớp đúng như mong đợi không? So price trong tín hiệu với giá khớp thật, và kiểm tra sl_tp_set có thành công không. Chênh lệch trung bình giữa hai giá là chỉ số slippage của chiến lược — nếu nó lớn hơn biên lợi nhuận trung bình mỗi lệnh, chiến lược sẽ lỗ khi chạy thật dù backtest đẹp.

Mẹo nhỏ: dùng một mã định danh duy nhất xuyên suốt (trace_id) sinh ra ở endpoint và gắn vào mọi dòng log liên quan. Khi tra sự cố, bạn chỉ cần grep một chuỗi thay vì ghép thời gian bằng mắt.


Dấu hiệu bạn nên viết lại bot

Không phải lúc nào cũng nên thêm tính năng. Có bốn dấu hiệu cho thấy viết lại rẻ hơn sửa:

  1. Số dư sai lệch thường xuyên mà nguyên nhân nằm rải rác ở nhiều chỗ trong code — thường là do state không có một nguồn chân lý duy nhất.
  2. Mỗi lần thêm sàn phải sửa 5–6 file. Nghĩa là bạn chưa tách lớp adapter.
  3. Không dám restart bot vì sợ mất tín hiệu. Nghĩa là chưa có hàng đợi bền.
  4. Bạn không tra được sự cố cũ. Log thiếu trace_id hoặc không lưu đủ lâu.

Ngược lại, đừng viết lại chỉ vì "code chưa đẹp". Bot đang chạy ổn định, có log, có cảnh báo và có trần rủi ro thì nên giữ nguyên và cải tiến từng phần. Viết lại một hệ thống đang chạy luôn rủi ro hơn nó trông như vậy.


Tổng kết

Bot Python nhận webhook TradingView không phải bài toán khó — nó là bài toán làm đủ các bước nhỏ. Endpoint đúng, xác thực đúng, chống trùng đúng, tính khối lượng đúng, đặt SL/TP đúng và log đủ để tra lại.

Năm việc cần làm, theo thứ tự:

  1. Dựng endpoint có HMAC + chống trùng + rate limit.
  2. Viết parse_signal với đầy đủ kiểm tra biên.
  3. Tách lớp adapter sàn để mở rộng dễ.
  4. Tính khối lượng theo % rủi ro và luôn kiểm tra kết quả đặt SL/TP.
  5. Chạy như service trên VPS, có heartbeat và cảnh báo.

Nếu bạn muốn dựng trọn hệ thống này với người hướng dẫn từng buổi — từ tạo alert, viết bot, nối API sàn tới đưa lên VPS chạy 24/7 — 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 phần mềm giám sát VPS FindMe VPS Guardian. Kết thúc khoá, bạn có một bot chạy thật — không phải bài tập mẫu.

Đọ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
  • Khóa Lập trình Bot Auto Trading không cần biết code

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

Bot nhận webhook TradingView viết bằng gì là tốt nhất?

Python + Flask (hoặc FastAPI) là lựa chọn phổ biến nhất vì hệ sinh thái thư viện sàn rất đầy đủ: MetaTrader5 cho MT5, python-binance cho Binance. Node.js hoặc .NET cũng dùng được nếu bạn đã quen, miễn là bạn triển khai đủ xác thực, chống trùng và quản lý rủi ro.

Cần bao nhiêu dòng code để có bot chạy được?

Bản tối thiểu chạy được khoảng 150–250 dòng. Bản dùng được cho tiền thật thường 600–1.200 dòng khi đã có lớp adapter sàn, quản lý rủi ro, logging, đối soát và test.

Vì sao phải đẩy tín hiệu vào hàng đợi thay vì xử lý ngay?

Vì TradingView chỉ chờ trong thời gian ngắn. Nếu bot gọi API sàn ngay trong request và sàn phản hồi chậm, TradingView coi như gửi thất bại. Đẩy vào hàng đợi rồi trả 200 ngay giúp webhook luôn "thành công" và tín hiệu không bị mất.

Bot của tôi vào 2 lệnh cho cùng một tín hiệu, sửa thế nào?

Thêm khoá chống trùng theo (symbol, action, time) — cùng một tín hiệu chỉ xử lý một lần. Nếu bạn chạy nhiều worker hoặc nhiều tiến trình, phải lưu khoá này ở Redis hoặc database thay vì biến trong RAM.

Làm sao kiểm tra bot mà không mất tiền?

Dùng cờ DRY_RUN=true để bot chạy hết logic nhưng không gọi API sàn, chỉ ghi log "lẽ ra tôi sẽ đặt lệnh X". Sau 2–3 ngày đọc log, chuyển sang tài khoản demo ít nhất 3 ngày rồi mới chạy tiền thật với khối lượng nhỏ nhất.

Đặt SL/TP thất bại thì bot nên làm gì?

Đây là trạng thái nguy hiểm nhất: vị thế đã mở mà không có bảo vệ. Bot phải log rõ lỗi, gửi cảnh báo ngay, thử lại vài lần, và nếu vẫn thất bại trong khoảng thời gian cho phép thì đóng vị thế để tránh rủi ro không kiểm soát.

Bot có cần database không?

Không bắt buộc ở mức cơ bản — log JSON là đủ để tra cứu. Nhưng khi bạn cần audit đầy đủ (mọi tín hiệu, mọi lệnh, trạng thái xử lý) hoặc chạy nhiều worker, một bảng signals trong Postgres giúp việc đối soát và tái hiện sự cố dễ hơn nhiều.

Chạy bot trên VPS Windows hay Linux?

MT5 chỉ chạy trên Windows, nên nếu bạn giao dịch Forex qua MT5 thì VPS Windows là bắt buộc. Với Binance/Bybit và chứng khoán Việt Nam qua REST API, VPS Linux nhẹ và rẻ hơn, dễ cấu hình systemd và logrotate hơn.

Đọ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.