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ối | Nhiệm vụ | Nếu thiếu thì sao |
|---|---|---|
| Endpoint | Nhận HTTP POST, xác thực, trả 200 nhanh | Ngườ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ác | Bot crash vì một trường thiếu |
| Quản lý rủi ro | Tính khối lượng, giới hạn vị thế, trần lỗ ngày | Vào lệnh quá lớn, cháy tài khoản trong một phiên |
| Adapter sàn | Bọc API của từng sàn sau một interface chung | Muốn thêm sàn phải sửa khắp nơi |
| Logging | Ghi vết mọi tín hiệu và mọi lệnh | Không biết lỗi ở đâu khi lệnh sai |
| Giám sát | Heartbeat, cảnh báo Telegram khi bot chết | Bot 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 BinanceVà 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=trueBa 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}, 200Năm chi tiết quyết định chất lượng đoạn code này:
- `request.get_data()` lấy raw bytes. Đừng
request.jsonngay, vì bạn cần raw bytes để kiểm chữ ký HMAC. Parse JSON sau khi đã xác thực. - `hmac.compare_digest` thay vì `==`. So sánh chuỗi thường có thể bị tấn công timing;
compare_digestlà hàm chống điều đó. - 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ì.
- Trả `200` ngay sau khi đẩy vào queue. TradingView chỉ cần biết "đã nhận". Không để nó chờ API sàn.
- 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 sHai 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.targetTrê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 5000Ba 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:
passTầ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.txtViệ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ỗi | Triệu chứng | Cách xử lý |
|---|---|---|
Dùng request.json trước khi verify | Không kiểm được chữ ký | Đọc raw bằng request.get_data() rồi mới parse |
| Xử lý nặng ngay trong request | TradingView 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 worker | Vào lệnh 2 lần | Đưa khoá chống trùng vào Redis/DB |
Không set type_filling cho MT5 | Lỗ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 size | API từ chối lệnh | Làm tròn theo step size của symbol |
| SL/TP sai phía | Sàn từ chối hoặc lệnh đóng ngay | Kiểm tra trong parse_signal |
| Không kiểm kết quả đặt SL | Vị thế mở không bảo vệ | Bắt buộc kiểm và báo động |
| Log không có timestamp/mã lệnh | Khô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
- ✅ HTTPS bắt buộc, có reverse proxy, không mở port trực tiếp.
- ✅ Xác thực HMAC mọi request; token đủ dài và đổi định kỳ.
- ✅ Rate limit theo symbol; chặn payload quá lớn.
- ✅ API key chỉ quyền giao dịch, không rút tiền; khoá IP nếu sàn hỗ trợ.
- ✅
.envnằm ngoài mã nguồn, quyền file600, không có trong Git. - ✅ 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.
- ✅ 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.
- ✅ 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
accountsvớ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
signalstrong Postgres) với trạng tháipending/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ệch | Nguyên nhân điển hình | Cách xử lý |
|---|---|---|
| Có lệnh ở sàn, không có trong log | Bot restart giữa chừng, hoặc bạn vào tay | Ghi nhận vào log với cờ unmanaged, không tự đóng |
| Có trong log, không có ở sàn | Lệ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ệch | Khớp một phần, hoặc lệnh bị sửa tay | Cả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:
- 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.
- 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.
- 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.
- Bạn không tra được sự cố cũ. Log thiếu
trace_idhoặ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ự:
- Dựng endpoint có HMAC + chống trùng + rate limit.
- Viết
parse_signalvới đầy đủ kiểm tra biên. - Tách lớp adapter sàn để mở rộng dễ.
- Tính khối lượng theo % rủi ro và luôn kiểm tra kết quả đặt SL/TP.
- 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.
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.
