🔥 🎁 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)
Vận hành bot 24/7 trên VPS và bảo mật API key 2026
ĐTĐược viết bởi Đặng Trí Thanhvào ngày 10/10/2026 lúc 20:00|… lượt xem
TradingView10/10/2026 · 20 phút đọc

Vận hành bot 24/7 trên VPS và bảo mật API key

Một bot auto trading chỉ hoàn thành khoảng một phần ba công việc khi code chạy được trên máy bạn. Hai phần còn lại là vận hành (bot sống, không mất state, tự hồi phục) và bảo mật (API key không lộ, webhook không bị giả mạo). Thiếu hai phần này, chiến lược tốt đến đâu cũng có thể kết thúc bằng một tài khoản bị rút sạch.

Đây là những gì thực sự xảy ra khi bot chạy thật: VPS khởi động lại sau khi nhà cung cấp bảo trì và bot mất trạng thái vị thế; mạng chập và bot bỏ lỡ ba tín hiệu; log đầy ổ đĩa và bot dừng; hoặc tệ nhất — API key bị lộ qua một lần git push và kẻ khác rút tiền trong đêm.

Bài viết này là toàn bộ phần vận hành: chọn VPS, kiến trúc tham chiếu, chạy bằng systemd/Docker, lưu state đúng cách, heartbeat và cảnh báo, log JSON, bảy nguyên tắc bảo mật API key, xác thực webhook chống replay, gia cố VPS và checklist go-live.

Đọc trước: Bot Python nhận webhook TradingView (Flask) và Quản trị rủi ro tự động: SL, TP và position sizing.

Trả lời nhanh

Vận hành bot 24/7 gồm năm trụ:

  1. Chạy như một dịch vụ — systemd (Linux), Windows Service, hoặc Docker có restart: unless-stopped. Không bao giờ chạy bằng một cửa sổ terminal rồi tắt máy.
  2. State nằm trên đĩa — vị thế, stop hiện tại, số lệnh trong ngày phải ghi xuống file/SQLite sau mỗi thay đổi, để bot restart vẫn biết mình đang giữ gì.
  3. Giám sát chủ động — bot gửi heartbeat định kỳ; thiếu heartbeat là có cảnh báo Telegram, không phải đợi bạn tự phát hiện.
  4. Log có cấu trúc — JSON một dòng cho mỗi sự kiện, có trace_id, có rotate; tuyệt đối không log secret.
  5. Bảo mật hai đầu — API key với quyền tối thiểu + IP whitelist, và webhook phải có shared secret + timestamp/nonce để chống replay.

Câu hỏi đầu tiên nên trả lời không phải "VPS nào" mà là "bot cần chạy 24/7 hay chỉ cần chạy trong phiên?". Crypto cần 24/7; Forex cần 24/5; chứng khoán Việt Nam chỉ cần khoảng 9:00–15:00 mỗi ngày làm việc — và đôi khi chạy theo phiên lại là lựa chọn an toàn hơn.


Chọn VPS: những gì thực sự quan trọng

Đa số bot cá nhân không cần cấu hình cao. Thứ quan trọng là ổn định và vị trí địa lý, không phải số nhân CPU.

Yếu tốKhuyến nghị cho bot cá nhânLý do
CPU1–2 vCPUBot chỉ chờ webhook, không tính toán nặng
RAM1–2 GB (2–4 GB nếu Windows)Python + SQLite + log + agent giám sát
Ổ đĩa20–40 GB SSDLog và dữ liệu nến tích luỹ theo thời gian
Vị tríCùng khu vực với broker/sànGiảm độ trễ đặt lệnh
Băng thôngBất kỳ, mức thấp cũng đủChỉ là JSON vài KB
Hệ điều hànhLinux (Ubuntu/Debian) ưu tiênỔn định, systemd, dễ tự động hoá
Uptime SLA≥ 99,9%Bot chết lúc thị trường biến động là rủi ro thật

Ba lưu ý ít người để ý:

  • Vị trí VPS quan trọng hơn cấu hình. Đặt VPS ở Singapore khi broker bạn ở Tokyo thêm 30–60 ms mỗi request. Với bot gửi lệnh theo nhịp, khoảng trễ tích luỹ này là thật.
  • Windows tốn RAM hơn nhiều. Nếu bot dùng MT5 thì cần Windows, nhưng 2 GB RAM là mức tối thiểu thực dụng và bạn nên có 4 GB nếu chạy thêm agent/terminal.
  • Đừng chạy bot ngay trên máy tính cá nhân. Máy tắt, mất mạng, Windows Update khởi động lại — cả ba đều là "bot chết" trong mắt thị trường. (Nếu bạn không muốn tự quản VPS, dịch vụ dạng FindMe VPS Guardian mà bootcamp tặng kèm là lựa chọn đã cấu hình sẵn.)

Về nhà cung cấp: hãy chọn nơi có console/snapshot và cảnh báo email khi VPS down. Bạn không cần nhà cung cấp "tốt nhất", bạn cần nơi bạn biết bot của mình đang chết khi nó chết.


Kiến trúc tham chiếu cho bot chạy 24/7

Đừng nhồi tất cả vào một luồng. Kiến trúc tối thiểu nhưng đủ chắc cho bot cá nhân:

TradingView (alert)
        |
        v
 [ webhook receiver ]  <-- HTTPS, verify secret, chong replay
        |  (ghi tin hieu vao queue/DB)
        v
 [ signal queue ]  (SQLite table / Redis list)
        |
        v
 [ trade worker ]  <-- validate, position sizing, gui lenh
        |  (ghi state + trade log)
        v
 [ state store ]  (SQLite file / JSON)  <-- bot restart doc lai tu day
        |
        v
 [ monitor ]  <-- heartbeat, alert Telegram, kiem tra lech vi the
        |
        v
 [ log JSON ]  -->  rotate  -->  backup

Bốn nguyên tắc kiến trúc:

  1. Receiver phải nhanh. TradingView chờ phản hồi HTTP trong thời gian ngắn và có thể coi alert là thất bại nếu bạn xử lý quá lâu. Receiver chỉ nên xác thực, ghi vào queue, trả 200; mọi xử lý nặng (gọi API broker) để worker làm.
  2. Worker chạy tách riêng. Nếu worker chết hoặc chậm, receiver vẫn nhận tín hiệu — bạn không mất tín hiệu, chỉ xử lý chậm.
  3. State tập trung một chỗ. Mọi thứ cần sống sót sau restart nằm ở state store, không nằm trong biến RAM.
  4. Monitor độc lập. Nếu monitor nằm chung tiến trình với worker, worker chết là monitor chết — bạn không nhận được cảnh báo nào.

Nếu bạn muốn bắt đầu đơn giản: một tiến trình FastAPI + một tiến trình worker + SQLite là đủ cho bot cá nhân giao dịch 1–3 mã. Chỉ cần tách khi thấy dấu hiệu nghẽn thật (queue dài, timeout webhook).


Chạy bot như một dịch vụ: systemd, Docker, Windows Service

Chỉ có một cách sai: mở terminal, gõ python bot.py, rồi để đó. Mọi thứ khác đều tốt hơn.

systemd (Ubuntu/Debian) — lựa chọn ổn định nhất:

# /etc/systemd/system/tvbot.service
[Unit]
Description=TradingView Webhook Bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=tvbot
WorkingDirectory=/opt/tvbot
EnvironmentFile=/opt/tvbot/.env
ExecStart=/opt/tvbot/.venv/bin/python -m app.worker
Restart=always
RestartSec=5
StartLimitBurst=10
StartLimitIntervalSec=300
# Gioi han quyen: bot khong can ghi vao he thong
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Ba tham số đáng chú ý: Restart=always để tự bật lại khi crash, RestartSec=5 để không bật quá nhanh, và `StartLimitBurst`/`StartLimitIntervalSec` để tránh vòng lặp restart vô hạn khi bot lỗi cấu hình (nếu bật lại 100 lần mỗi phút, bạn sẽ làm ngập API broker và có thể bị khoá tạm).

Sau khi tạo file: sudo systemctl daemon-reload, sudo systemctl enable --now tvbot, và xem log bằng journalctl -u tvbot -f.

Docker Compose — lựa chọn linh hoạt, dễ chuyển máy:

# docker-compose.yml
services:
  receiver:
    build: .
    command: ["python", "-m", "app.receiver"]
    env_file: [.env]
    ports: ["127.0.0.1:8080:8080"]   # chi mo noi bo, TLS do reverse proxy lo
    restart: unless-stopped
    volumes:
      - ./state:/app/state
      - ./logs:/app/logs
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "5" }

  worker:
    build: .
    command: ["python", "-m", "app.worker"]
    env_file: [.env]
    restart: unless-stopped
    depends_on: [receiver]
    volumes:
      - ./state:/app/state
      - ./logs:/app/logs

  watchtower:
    image: containrrr/watchtower
    restart: unless-stopped
    volumes: ["/var/run/docker.sock:/var/run/docker.sock"]
    command: ["--interval", "3600", "--cleanup"]

Hai chi tiết quan trọng: `ports: 127.0.0.1:8080` (không đưa cổng bot ra Internet trực tiếp) và giới hạn log của Docker — mặc định Docker không giới hạn và có thể ăn hết ổ đĩa của bạn trong vài tuần.

Windows Service — dùng khi bot cần MT5. Cách gọn nhất là tạo service bằng NSSM hoặc sc.exe trỏ tới python.exe, với thư mục làm việc là thư mục dự án. Nhớ đặt Restart on failure trong phần Recovery của service, và tắt chế độ tự động cập nhật/khởi động lại của Windows trong khung giờ giao dịch.


State: thứ quyết định bot có "quên" hay không

Đây là lỗi vận hành gây thiệt hại nhiều nhất: bot restart và mất trí nhớ. Nó quên rằng đã vào lệnh, quên stop đã dời lên hoà vốn, quên rằng hôm nay đã thua 3 lệnh. Kết quả: vào lệnh trùng, hoặc tệ hơn, đóng lệnh đúng lúc đang lãi.

Ba loại state phải lưu:

StateVí dụNếu mất
Vị thếsymbol, side, qty, entry, stop, tp, stateVào lệnh trùng, không quản lý stop
Đếm rủi roequity_open, trades_today, consecutive_lossesCircuit breaker mất tác dụng
Chống trùng tín hiệusymbol + tf + bar đã xử lý gần nhấtNhận lại tín hiệu cũ, vào lệnh lần hai

Cách lưu tối thiểu mà vẫn an toàn: SQLite (một file, không cần cài server) với ghi atomic.

import json, os, sqlite3, tempfile

DB = "/opt/tvbot/state/bot.db"

def init_db(conn: sqlite3.Connection) -> None:
    conn.execute("PRAGMA journal_mode=WAL")          # an toan khi bot restart giua chung
    conn.execute("""CREATE TABLE IF NOT EXISTS positions(
        symbol TEXT PRIMARY KEY, side TEXT, qty REAL, entry REAL,
        stop REAL, tp REAL, state TEXT, opened_at TEXT
    )""")
    conn.execute("""CREATE TABLE IF NOT EXISTS meta(
        key TEXT PRIMARY KEY, value TEXT
    )""")
    conn.execute("""CREATE TABLE IF NOT EXISTS signals_seen(
        key TEXT PRIMARY KEY, seen_at TEXT
    )""")
    conn.commit()

def save_position(conn, pos: dict) -> None:
    conn.execute("""INSERT INTO positions(symbol, side, qty, entry, stop, tp, state, opened_at)
        VALUES(:symbol, :side, :qty, :entry, :stop, :tp, :state, :opened_at)
        ON CONFLICT(symbol) DO UPDATE SET
            qty=excluded.qty, stop=excluded.stop, tp=excluded.tp, state=excluded.state""", pos)
    conn.commit()      # ghi ngay, khong doi cuoi phien

def save_json_atomic(path: str, data: dict) -> None:
    """Ghi JSON kieu atomic: khong bao gio de file nua voi."""
    d = os.path.dirname(path)
    fd, tmp = tempfile.mkstemp(dir=d)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False)
        f.flush()
        os.fsync(f.fileno())
    os.replace(tmp, path)     # thay the nguyen tu

Hai kỹ thuật trong đoạn code trên đáng nhớ:

  • `PRAGMA journal_mode=WAL` giúp SQLite chịu được việc tiến trình bị kill giữa lúc ghi — chuyện xảy ra thường xuyên với Restart=always.
  • Ghi atomic qua file tạm + `os.replace` tránh trường hợp bot bị tắt đúng lúc đang ghi và để lại file JSON hỏng. Nếu bạn ghi trực tiếp vào file thật, một lần mất điện là mất toàn bộ state.

Và một nguyên tắc bắt buộc: đối soát state với sàn khi khởi động. Bot đọc state trên đĩa, truy vấn vị thế thật từ API, rồi so sánh:

  • Vị thế có trên đĩa nhưng không có trên sàn → đã bị đóng (bởi stop thật, thanh lý, hoặc thao tác tay). Ghi log, xoá khỏi state.
  • Có trên sàn nhưng không có trên đĩa → bot mất state hoặc có người vào lệnh tay. Không tự động đóng; hãy cảnh báo và để người quyết định.
  • Số lượng lệch → cảnh báo ngay, đừng đoán.

Heartbeat và giám sát: để bot tự nói "tôi còn sống"

Cách giám sát tệ nhất là mở mắt nhìn dashboard. Cách đúng là bot chủ động báo về, và sự im lặng bị coi là sự cố.

Hai cơ chế nên có:

1. Heartbeat gửi ra ngoài. Cứ 60 giây, bot gửi một request tới dịch vụ healthcheck (hoặc một endpoint bạn tự dựng). Nếu quá 3 phút không có heartbeat, dịch vụ đó gửi cảnh báo Telegram/email cho bạn. Điểm hay của thiết kế này: nó báo cả khi VPS mất điện hoàn toàn — điều mà một script trên chính VPS không thể làm.

2. Watchdog nội bộ kiểm tra logic, không chỉ tiến trình. Tiến trình còn sống không có nghĩa là bot còn hoạt động đúng: vòng lặp có thể bị treo ở một lệnh HTTP không timeout, hoặc queue đầy.

import threading, time, requests

class Watchdog(threading.Thread):
    def __init__(self, check_fn, interval=60, stale_after=300):
        super().__init__(daemon=True)
        self.check_fn = check_fn
        self.interval = interval
        self.stale_after = stale_after

    def run(self):
        while True:
            try:
                health = self.check_fn()      # {"last_tick": ts, "queue": n, "api_ok": bool}
                if time.time() - health["last_tick"] > self.stale_after:
                    self.alert(f"Bot treo: khong tick trong {self.stale_after}s")
                if not health["api_ok"]:
                    self.alert("Khong goi duoc API broker")
                if health["queue"] > 50:
                    self.alert(f"Queue tin hieu dang ton {health['queue']} muc")
            except Exception as e:
                self.alert(f"Watchdog loi: {e}")
            time.sleep(self.interval)

    @staticmethod
    def alert(msg: str):
        # Gui Telegram; khong dung print vi khong ai doc journal luc 3h sang
        requests.post(
            f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
            json={"chat_id": TELEGRAM_CHAT_ID, "text": msg}, timeout=10,
        )

Bốn chỉ số nên đưa vào cảnh báo:

  • Không có tín hiệu trong bao lâu so với tần suất bình thường (cảnh báo nếu vắng bất thường — dấu hiệu alert TradingView hết hạn hoặc bị tắt).
  • Số tín hiệu trong queue (tồn đọng nghĩa là worker chậm hoặc đang lỗi).
  • Kết nối API broker (lỗi xác thực, hết hạn token, bảo trì).
  • Lệch số dư/vị thế giữa state và sàn.

Một lưu ý rất thực tế: cảnh báo quá nhiều sẽ khiến bạn bỏ qua cảnh báo thật. Hãy dùng hai mức — thông tin (heartbeat hàng ngày, tổng kết phiên) và khẩn cấp (bot chết, lệch vị thế, chạm trần lỗ ngày). Chỉ mức khẩn cấp mới nên có âm thanh đặc biệt trên điện thoại.


Log: cấu trúc, rotate và những gì không được ghi

Log của bot không phải để "cho biết đã chạy". Nó là bằng chứng để đối soát. Vì vậy hãy log dạng JSON một dòng, có khoá rõ ràng:

import json, logging, time, uuid

class JsonFormatter(logging.Formatter):
    def format(self, record):
        payload = {
            "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z", time.localtime(record.created)),
            "level": record.levelname,
            "msg": record.getMessage(),
            "trace_id": getattr(record, "trace_id", None),
            "event": getattr(record, "event", None),
        }
        extra = getattr(record, "extra_data", None)
        if extra:
            payload.update(extra)
        return json.dumps(payload, ensure_ascii=False)

handler = logging.handlers.RotatingFileHandler("/opt/tvbot/logs/bot.log", maxBytes=10_000_000, backupCount=10)
handler.setFormatter(JsonFormatter())
log = logging.getLogger("tvbot")
log.addHandler(handler)
log.setLevel(logging.INFO)

def new_trace():
    return uuid.uuid4().hex[:12]

# Vi du dung
trace = new_trace()
log.info("Nhan tin hieu", extra={"trace_id": trace, "event": "signal_received",
        "extra_data": {"symbol": "BTCUSDT", "side": "buy", "tf": "60", "bar": "2026-10-10T12:00:00Z"}})
log.info("Da gui lenh", extra={"trace_id": trace, "event": "order_sent",
        "extra_data": {"order_id": "12345", "qty": 0.05, "price": 60120.5}})

Với trace_id xuyên suốt từ tín hiệu tới lệnh khớp, bạn trả lời được câu hỏi quan trọng nhất khi có sự cố: "Tín hiệu nào đã dẫn tới lệnh này, và ở bước nào nó bị biến dạng?"

Những thứ tuyệt đối không được ghi vào log: API key, secret, token, chữ ký HMAC, toàn bộ header Authorization, và nội dung request webhook thô nếu trong đó có secret. Một bản log bị lộ không nên trở thành chìa khoá tài khoản.

SECRET_KEYS = {"api_key", "api_secret", "secret", "token", "password", "signature", "authorization"}

def redact(data: dict) -> dict:
    out = {}
    for k, v in data.items():
        if any(s in k.lower() for s in SECRET_KEYS):
            out[k] = "***"
        elif isinstance(v, dict):
            out[k] = redact(v)
        else:
            out[k] = v
    return out

Về rotate: dùng RotatingFileHandler (như trên) hoặc logrotate của hệ điều hành. Đừng bao giờ để một file log phình tới vài GB — bot sẽ dừng vì hết ổ đĩa, và điều này xảy ra đúng vào lúc bạn cần nó chạy nhất.


Bảy nguyên tắc bảo mật API key

Đây là phần quan trọng nhất của bài viết. API key của bạn là tiền. Hãy đối xử với nó như vậy.

1. Không bao giờ commit key vào Git. Dùng .env và thêm vào .gitignore ngay từ commit đầu tiên:

.env
*.env
state/
logs/
*.db
*.db-wal
*.db-shm

Nếu đã lỡ commit: thu hồi (revoke) key ngay lập tức, rồi tạo key mới. Xoá commit không đủ — lịch sử Git vẫn còn, và các dịch vụ quét GitHub tìm key lộ hoạt động trong vài giây.

2. Cấp quyền tối thiểu. Ba quy tắc cho mọi sàn/broker:

  • Bật giao dịch, TẮT rút tiền. Tuyệt đối không bật withdrawal cho key của bot.
  • Tắt chuyển khoản nội bộ nếu sàn cho phép tách quyền.
  • Key riêng cho bot, không dùng chung key cho cả việc đọc số dư, bot và các script thử nghiệm.

3. Lọc theo IP. Hầu hết sàn/broker cho phép giới hạn key theo IP. Hãy gắn key với IP tĩnh của VPS. Nếu key bị lộ nhưng kẻ tấn công không ở trên IP đó, key vô dụng. Đây là lớp bảo vệ có giá trị cao nhất so với công sức bỏ ra.

4. Đừng dùng key của tài khoản chính. Hãy tạo tài khoản/sub-account riêng cho bot, chỉ nạp số vốn bạn chấp nhận rủi ro cho hệ thống đó. Đây là "circuit breaker" ở tầng tài khoản: dù mọi thứ khác thất bại, thiệt hại tối đa đã được xác định.

5. Rotate định kỳ và sau mọi sự cố. Đặt lịch thu hồi + tạo key mới mỗi 3–6 tháng, và ngay lập tức nếu: bạn chia sẻ màn hình, copy .env qua chat, đổi nhà cung cấp VPS, hoặc nghi ngờ có truy cập bất thường.

6. Bảo vệ file `.env` ở tầng hệ điều hành. Quyền file phải là 600 và chủ sở hữu là user chạy bot:

sudo chown tvbot:tvbot /opt/tvbot/.env
sudo chmod 600 /opt/tvbot/.env

7. Không truyền key qua tham số dòng lệnh. python bot.py --api-key xxx sẽ xuất hiện trong ps aux và trong log shell history. Hãy dùng biến môi trường hoặc file secret.


Code: nạp secret đúng cách

Nguyên tắc: secret đến từ môi trường hoặc file, không từ code, và bot phải kiểm tra ngay khi khởi động — thiếu secret thì dừng, không chạy với cấu hình nửa vời.

import os
from pathlib import Path

REQUIRED = ["BROKER_API_KEY", "BROKER_API_SECRET", "WEBHOOK_SECRET", "TELEGRAM_TOKEN", "TELEGRAM_CHAT_ID"]

def load_env(path: str = "/opt/tvbot/.env") -> None:
    """Doc .env don gian, khong phu thuoc thu vien ngoai."""
    p = Path(path)
    if not p.exists():
        return
    for line in p.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))

def require_secrets() -> dict:
    load_env()
    missing = [k for k in REQUIRED if not os.environ.get(k)]
    if missing:
        # Khong in gia tri, chi in ten bien thieu
        raise SystemExit(f"Thieu bien moi truong: {', '.join(missing)}")
    return {k: os.environ[k] for k in REQUIRED}

SECRETS = require_secrets()

Lưu ý SystemExit khi thiếu secret: bot dừng ngay thay vì chạy với key rỗng rồi gửi hàng loạt request lỗi (có thể khiến IP bị chặn tạm). Với hệ thống lớn hơn, hãy dùng secret manager của nhà cung cấp cloud, nhưng với bot cá nhân thì .env quyền 600 là mức đủ thực dụng.


Bảo vệ webhook: chống giả mạo và chống replay

Đây là lỗ hổng bị bỏ qua nhiều nhất trong các bot tự viết. Nếu endpoint webhook của bạn mở công khai và không xác thực, bất kỳ ai biết URL đều có thể gửi lệnh cho bot của bạn. Kịch bản tấn công rất đơn giản: kẻ xấu gửi tín hiệu buy liên tục để bot mua ở đỉnh, hoặc gửi sell để đóng vị thế của bạn.

Ba lớp bảo vệ, nên dùng đồng thời:

  1. URL bí mật (một token dài trong đường dẫn) — lớp yếu nhất, nhưng chặn được quét tự động.
  2. Shared secret trong payload hoặc header — bot so sánh bằng so sánh hằng thời gian.
  3. Timestamp + nonce chống replay — bắt buộc nếu bạn muốn chặn việc gửi lại payload cũ.
import hmac, time, hashlib
from fastapi import FastAPI, Request, HTTPException, Header

app = FastAPI()
WEBHOOK_SECRET = SECRETS["WEBHOOK_SECRET"].encode()
MAX_SKEW_SECONDS = 30
seen_nonces: dict[str, float] = {}     # nen thay bang SQLite/Redis khi chay that

def verify(raw_body: bytes, sig_header: str, ts_header: str, nonce: str) -> None:
    # 1) timestamp phai gan hien tai -> chan replay payload cu
    try:
        ts = int(ts_header)
    except (TypeError, ValueError):
        raise HTTPException(400, "timestamp khong hop le")
    if abs(time.time() - ts) > MAX_SKEW_SECONDS:
        raise HTTPException(401, "timestamp qua cu")

    # 2) nonce chua tung thay
    if nonce in seen_nonces:
        raise HTTPException(409, "nonce da dung (replay)")
    seen_nonces[nonce] = time.time()

    # 3) chu ky HMAC tren body + timestamp + nonce
    mac = hmac.new(WEBHOOK_SECRET, raw_body + ts_header.encode() + nonce.encode(), hashlib.sha256)
    if not hmac.compare_digest(mac.hexdigest(), sig_header or ""):
        raise HTTPException(401, "chu ky khong dung")

@app.post("/webhook/{token}")
async def webhook(token: str, request: Request,
                  x_signature: str = Header(default=""),
                  x_timestamp: str = Header(default=""),
                  x_nonce: str = Header(default="")):
    if token != os.environ.get("WEBHOOK_PATH_TOKEN", ""):
        raise HTTPException(404)
    raw = await request.body()
    verify(raw, x_signature, x_timestamp, x_nonce)
    # ... xac thuc xong moi ghi vao queue, tra 200 nhanh
    return {"ok": True}

Bốn chi tiết quan trọng:

  • `hmac.compare_digest` thay vì == để tránh tấn công đo thời gian.
  • Timestamp chỉ lệch tối đa 30 giây. Điều này có nghĩa bot của bạn cần đồng hồ chính xác — nên bật NTP/chrony trên VPS. Lệch giờ là nguyên nhân rất phổ biến khiến xác thực thất bại trong khi code đúng.
  • Nonce phải lưu bền (SQLite/Redis), không chỉ trong RAM; nếu bot restart, bộ nhớ nonce mất và cửa sổ replay mở lại.
  • Đừng log raw body của webhook nếu body chứa secret; dùng hàm redact() đã viết ở trên.

Nếu dùng TradingView, hãy đặt secret vào header (TradingView cho phép tuỳ chỉnh một số header trong cấu hình alert) hoặc vào chính payload JSON, rồi tính chữ ký phía gửi nếu bạn có hệ thống trung gian. Trong trường hợp không thể tạo chữ ký HMAC, tối thiểu hãy dùng token dài trong URL + kiểm tra timestamp/nonce trong payload + whitelist IP của TradingView.


Gia cố VPS: những bước tối thiểu bắt buộc

Danh sách dưới đây không phải "nên làm" — với một VPS giữ API key giao dịch, đây là mức tối thiểu.

# 1) Tao user rieng, khong chay bot bang root
sudo adduser --disabled-password --gecos "" tvbot
sudo mkdir -p /opt/tvbot && sudo chown -R tvbot:tvbot /opt/tvbot

# 2) SSH: chi cho phep dang nhap bang key
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

# 3) Firewall: chi mo SSH (va 443 neu can webhook ra ngoai)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 443/tcp
sudo ufw enable

# 4) Chan brute force
sudo apt install -y fail2ban

# 5) Cap nhat dinh ky (va bat unattended-upgrades cho security patch)
sudo apt update && sudo apt upgrade -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

# 6) Dong ho chinh xac cho HMAC/timestamp
sudo apt install -y chrony && sudo systemctl enable --now chrony

Với phần webhook ra Internet, đừng mở cổng 8080 trực tiếp. Hãy để reverse proxy xử lý TLS:

# Caddyfile - tu dong lay va gia han chung chi TLS
bot.example.com {
    reverse_proxy 127.0.0.1:8080
    header {
        Strict-Transport-Security "max-age=31536000"
        X-Content-Type-Options "nosniff"
    }
    log {
        output file /var/log/caddy/bot.log {
            roll_size 10mb
            roll_keep 5
        }
    }
}

Với Nginx, cấu hình tương đương cần proxy_pass http://127.0.0.1:8080; cùng ssl_certificate do certbot quản lý. Điểm cốt lõi giống nhau: bot chỉ nghe trên localhost, TLS do reverse proxy làm, và không expose cổng nào khác.

Bảng tóm tắt mức độ ưu tiên:

Lớp bảo vệChặn đượcCông sức
Tắt quyền rút tiền trên API keyToàn bộ thiệt hại tối đaRất thấp
IP whitelist cho keyDùng key lộ từ nơi khácThấp
Xác thực webhook (HMAC + nonce)Gửi lệnh giả, replayTrung bình
TLS + chỉ nghe localhostNghe lén, tấn công trực tiếpThấp
Chạy bằng user riêng, không rootLeo thang đặc quyềnThấp
Rotate key định kỳKey bị lộ âm thầmThấp

Những sự cố vận hành điển hình và cách xử lý

Sự cốDấu hiệuXử lý đúng
Bot restart, mất stateVào lệnh trùng, stop "quên"State trên đĩa + đối soát với sàn khi khởi động
Log đầy ổ đĩa, bot dừngBot chết, ổ đĩa 100%Rotate log + cảnh báo dung lượng
Mất mạng tới API brokerLệnh không gửi đượcRetry có backoff + cảnh báo + circuit breaker
Webhook hết hạn / alert bị tắtKhông có tín hiệu nàoCảnh báo khi vắng tín hiệu bất thường
Đồng hồ VPS lệchXác thực HMAC thất bạichrony/NTP + kiểm tra drift
Tín hiệu trùngNhiều lệnh cùng lúcKhoá symbol+tf+bar lưu bền
Vòng lặp restartBị khoá API tạm thờiStartLimitBurst + sửa lỗi cấu hình
Key bị lộTruy cập lạThu hồi ngay, tạo key mới, bật IP whitelist
Nhà cung cấp VPS bảo trìBot ngừng hoạt độngThông báo + tạm khoá vào lệnh mới

Một điểm quan trọng ít được nói: retry có backoff là bắt buộc, nhưng retry cho lệnh giao dịch thì phải rất cẩn thận. Retry một lệnh đã được gửi thành công nhưng timeout ở phía nhận có thể tạo lệnh thứ hai. Luôn dùng idempotency key (client_order_id) do bạn tự sinh, và kiểm tra trạng thái lệnh trước khi gửi lại.


Chi phí vận hành thực tế

KhoảnMức phổ biếnGhi chú
VPS Linux 1–2 vCPU, 1–2 GB5–15 USD/thángĐủ cho bot cá nhân 1–3 mã
VPS Windows 2–4 GB (nếu dùng MT5)15–40 USD/thángCần cho MT5/EA
Tên miền + TLS~10 USD/nămLet's Encrypt miễn phí chứng chỉ
Giám sát uptime ngoài0–10 USD/thángCó dịch vụ miễn phí với gói cơ bản
Backup snapshot1–3 USD/thángNên bật, ít nhất hàng tuần
TradingView gói có webhookTuỳ góiCần thiết để gửi webhook
Tổng tối thiểu~7–20 USD/thángChưa tính phí giao dịch

Đối chiếu đáng suy nghĩ: chi phí vận hành một bot cả năm thường nhỏ hơn một lần trượt giá do mất kết nối. Đây không phải khoản nên tiết kiệm bằng cách chạy trên máy cá nhân.


Checklist go-live 15 điểm

  1. ✅ Bot chạy như dịch vụ (systemd/Windows Service/Docker) với restart tự động và giới hạn số lần restart.
  2. ✅ State lưu trên đĩa (SQLite/JSON atomic), ghi ngay sau mỗi thay đổi.
  3. ✅ Có bước đối soát state với sàn khi khởi động, có cảnh báo khi lệch.
  4. ✅ Heartbeat gửi ra ngoài; im lặng quá 3 phút là có cảnh báo.
  5. ✅ Watchdog kiểm tra logic (tick, queue, API), không chỉ "tiến trình còn sống".
  6. ✅ Log JSON có trace_id, có rotate, không chứa secret.
  7. ✅ API key: đã tắt quyền rút tiền, có IP whitelist, quyền file .env = 600.
  8. ✅ Key nằm trong .gitignore; đã kiểm tra lịch sử Git không có secret.
  9. ✅ Webhook xác thực bằng HMAC + timestamp + nonce; nonce lưu bền.
  10. ✅ Đồng hồ VPS đồng bộ NTP; sai lệch dưới 1 giây.
  11. ✅ TLS qua reverse proxy, bot chỉ nghe localhost; firewall chỉ mở 22 (+443).
  12. ✅ SSH khoá-only, không cho root đăng nhập, đã cài fail2ban.
  13. ✅ Có retry với backoff và idempotency key cho lệnh gửi đi.
  14. ✅ Circuit breaker (trần lỗ ngày, chuỗi thua, số lệnh/ngày) đã hoạt động và đã test.
  15. ✅ Có kịch bản xử lý sự cố bằng văn bản: mất mạng, API lỗi, bot chết, key bị lộ.

Hãy test điểm 2, 3, 4 và 9 bằng cách tự phá hệ thống: kill bot, rút mạng, gửi webhook sai chữ ký. Nếu bạn chưa từng thử, bạn chưa biết bot của mình phản ứng thế nào.


Vận hành định kỳ: làm gì hàng ngày, hàng tuần, hàng tháng

Hàng ngày (5 phút). Kiểm tra bot còn sống, xem số tín hiệu nhận được so với kỳ vọng, xem có cảnh báo nào bị bỏ qua, đối soát vị thế nếu có giao dịch.

Hàng tuần (15 phút). Đọc danh sách lệnh kèm R-multiple, so expectancy tuần này với backtest, kiểm tra dung lượng ổ đĩa và log rotate có hoạt động, xác nhận alert TradingView còn bật và chưa hết hạn.

Hàng tháng (30 phút). Cập nhật hệ điều hành, xem lại phân bố lợi nhuận theo tuần, kiểm tra sai lệch giữa trượt giá giả định và trượt giá thật, và quyết định có cần điều chỉnh phí giả định trong backtest.

Hàng quý. Rotate API key (hoặc ngay khi có sự cố), rà soát lại danh sách mã bot giao dịch, đọc lại toàn bộ sự cố đã ghi và xem có mẫu chung nào cần sửa trong code.

Routine này quan trọng vì nó biến vận hành từ "phản ứng khi có chuyện" thành "phát hiện trước khi có chuyện".


Tổng kết

Đưa bot lên VPS 24/7 không khó, nhưng có ba thứ bạn không được bỏ qua:

  • Chạy như dịch vụ, state trên đĩa, đối soát khi khởi động — để bot không bao giờ "quên" vị thế.
  • Giám sát chủ động — heartbeat gửi ra ngoài và watchdog kiểm tra logic; sự im lặng phải bị coi là sự cố.
  • Bảo mật hai đầu — API key với quyền tối thiểu + IP whitelist, và webhook có HMAC + timestamp + nonce.

Nếu bạn làm đúng ba điều này, bot của bạn sẽ ổn định ở mức mà hầu hết hệ thống tự làm không đạt tới. Phần còn lại — tối ưu chiến lược, thêm thị trường, tăng vốn — chỉ có ý nghĩa khi nền tảng vận hành đã chắc.


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

Cần cấu hình VPS thế nào để chạy bot 24/7?

Với bot cá nhân 1–3 mã, 1–2 vCPU và 1–2 GB RAM là đủ. Yếu tố quan trọng hơn là vị trí địa lý (chọn cùng khu vực với broker/sàn để giảm độ trễ) và độ ổn định của nhà cung cấp. Nếu dùng MT5 thì cần Windows với 2–4 GB RAM.

Bot nên chạy bằng systemd, Docker hay Windows Service?

systemd là lựa chọn ổn định và nhẹ nhất trên Linux. Docker phù hợp khi bạn muốn nhiều tiến trình (receiver + worker) và dễ chuyển máy. Windows Service là bắt buộc khi bot cần MT5. Cả ba đều tốt hơn hẳn việc chạy bằng terminal — điều quan trọng là có restart tự động.

Làm sao để bot không mất trạng thái khi VPS khởi động lại?

Lưu vị thế, stop hiện tại và các bộ đếm rủi ro xuống SQLite hoặc file JSON ghi atomic, ngay sau mỗi thay đổi. Khi khởi động, bot đọc lại state rồi đối soát với sàn: vị thế có trên đĩa nhưng không có trên sàn thì đánh dấu đã đóng; có trên sàn nhưng không có trên đĩa thì cảnh báo cho người, không tự động đóng.

Làm sao biết bot đã chết mà không phải ngồi nhìn màn hình?

Cho bot gửi heartbeat ra một dịch vụ giám sát bên ngoài mỗi 60 giây; nếu quá 3 phút không có heartbeat, dịch vụ đó gửi cảnh báo Telegram/email. Cách này báo được cả khi VPS mất điện hoàn toàn — điều mà một script chạy trên chính VPS không làm được.

Bảo mật API key cho bot quan trọng nhất là việc gì?

Ba việc theo thứ tự giá trị: tắt quyền rút tiền trên key, bật IP whitelist chỉ cho IP của VPS, và không bao giờ commit key vào Git (dùng .env quyền 600 + .gitignore). Nếu key đã lộ, thu hồi ngay và tạo key mới — xoá commit là không đủ.

Webhook TradingView có bị giả mạo được không?

Có, nếu endpoint của bạn không xác thực. Bất kỳ ai biết URL đều có thể gửi lệnh cho bot. Cần dùng đồng thời: token dài trong URL, shared secret, và HMAC + timestamp + nonce để chống replay. Đừng quên đồng bộ đồng hồ VPS bằng NTP, vì lệch giờ làm xác thực timestamp thất bại.

Có nên chạy bot trên máy tính cá nhân không?

Không nên. Máy bị tắt, mất mạng, Windows Update khởi động lại, bạn mang laptop đi họp — tất cả đều là "bot chết" với thị trường. Chi phí một VPS (khoảng 5–15 USD/tháng) nhỏ hơn nhiều so với một lần mất kết nối đúng lúc cần cắt lỗ.

Log bot nên chứa những gì?

Log dạng JSON một dòng cho mỗi sự kiện, có trace_id xuyên suốt từ tín hiệu tới lệnh khớp, kèm symbol, tf, bar, price, qty, phí và trạng thái lệnh. Tuyệt đối không log API key, secret, token hay chữ ký. Dùng rotate để log không ăn hết ổ đĩa, và giữ tối thiểu vài tuần để đối soát.


Tất cả nội dung bài này — từ systemd, state, heartbeat, log JSON tới HMAC webhook và gia cố VPS — đều là phần thực hành trong Bootcamp TradingView Webhook Bot: 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:

  • Backtest tín hiệu TradingView trước khi chạy thật
  • Quản trị rủi ro tự động: SL, TP và position sizing
  • 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.