Bài viết gần đây
| Mô hình Máy Trạng Thái Hữu Hạn (FSM) trong Bot Trading
Được viết bởi Đặng Trí Thanh vào ngày 04/02/2026 lúc 08:50 | 322 lượt xem
Mô hình Máy Trạng Thái Hữu Hạn (Finite State Machine – FSM) là kiến trúc lõi của mọi bot giao dịch tự động chuyên nghiệp. Bài viết này — bản mở rộng đầy đủ — giải thích FSM từ khái niệm đến triển khai thực chiến trong Python và MT5, kèm code mẫu, case study và các lỗi cần tránh.
1. FSM là gì?
FSM là mô hình toán học mô tả một hệ thống chỉ ở một trạng thái tại một thời điểm, và chuyển trạng thái khi có sự kiện và điều kiện thỏa. Ví dụ quen thuộc: máy bán nước tự động (chờ tiền → nhận tiền → trả hàng), đèn giao thông (xanh → vàng → đỏ).
Áp vào bot trading: FSM giúp trả lời “bot đang ở đâu trong vòng đời giao dịch?” — chờ tín hiệu, vào lệnh, giữ lệnh, thoát lệnh hay nghỉ ngắn.
1.1. Ba thành phần của FSM
- States (trạng thái): tập hữu hạn các trạng thái bot có thể ở.
- Events (sự kiện): tín hiệu, thay đổi giá, hết giờ, lỗi kết nối…
- Transitions (chuyển trạng thái): quy tắc “từ A + sự kiện E → B”.
1.2. Vì sao bot cần FSM — không chỉ là “code gọn”
- Chống nhồi lệnh: bot chỉ vào lệnh khi ở IDLE.
- Giảm bug: thay vì hàng chục if lồng nhau, mỗi trạng thái một hành vi rõ ràng.
- Dễ debug: log trạng thái để biết bot “đang nghĩ gì”.
- An toàn: mất kết nối → giữ trạng thái, không hành động sai.
- Mở rộng: thêm chiến lược = thêm trạng thái, không đụng code cũ.
2. Các trạng thái cơ bản của bot
| Trạng thái | Ý nghĩa | Hành động | Sự kiện ra |
|---|---|---|---|
| IDLE | Chờ đợi | Quét tín hiệu | có tín hiệu → SIGNAL |
| SIGNAL | Có tín hiệu | Kiểm tra bộ lọc | đủ điều kiện → ENTER; không → IDLE |
| ENTER | Đặt lệnh | Gửi lệnh | thành công → IN_POSITION; lỗi → IDLE |
| IN_POSITION | Giữ lệnh | Theo dõi SL/TP/trailing | chạm SL/TP → EXIT |
| EXIT | Thoát lệnh | Đóng lệnh, ghi log | đóng xong → COOLDOWN |
| COOLDOWN | Nghỉ ngắn | Chờ hết giờ | hết giờ → IDLE |
3. Code FSM đầy đủ bằng Python
3.1. Bản hoàn chỉnh có bộ lọc và cooldown
import time, datetime
class BotFSM:
def __init__(self, magic=6111, cooldown=120, max_spread=0.5):
self.state = "IDLE"
self.magic = magic
self.cooldown = cooldown
self.max_spread = max_spread
self.cooldown_until = 0
self.log = []
def _log(self, msg):
self.log.append(f"{datetime.datetime.now()} [{self.state}] {msg}")
def on_tick(self, bid, ask, signal, adx, ma20):
self._log(f"tick bid={bid} ask={ask} signal={signal}")
spread = ask - bid
if self.state == "IDLE":
if signal == "BUY" and adx > 25 and spread < self.max_spread:
self.state = "ENTER"
elif self.state == "ENTER":
self.place_order("BUY", ask)
self.state = "IN_POSITION"
elif self.state == "IN_POSITION":
if bid <= self.stop_price:
self.close_order()
self.state = "EXIT"
elif self.state == "EXIT":
self.cooldown_until = time.time() + self.cooldown
self.state = "COOLDOWN"
elif self.state == "COOLDOWN":
if time.time() >= self.cooldown_until:
self.state = "IDLE"
def place_order(self, side, price):
self._log(f"PLACE {side} @ {price} magic={self.magic}")
def close_order(self):
self._log("CLOSE ALL")
bot = BotFSM()
# mô phỏng vài tick
for p in [67000, 67050, 66980]:
bot.on_tick(p, p + 0.3, "BUY", 30, 67100)
print("
".join(bot.log))
3.2. Lưu trạng thái khi dùng MT5 (Global Variable)
MetaTrader 5 chạy bot theo tick; nếu terminal restart, biến trong bộ nhớ bị mất. Giải pháp: lưu trạng thái vào Global Variable (MQL5):
int state = (int)GlobalVariableGet("PyBot_State");
// sau mỗi lần đổi trạng thái:
GlobalVariableSet("PyBot_State", (double)state);
// khi OnTick bắt đầu, đọc lại:
state = (int)GlobalVariableGet("PyBot_State");
Với Python + API, bạn có thể lưu vào file hoặc cơ sở dữ liệu, hoặc dùng thư viện MetaTrader5 đọc Global Variable qua global_variable_get().
4. FSM cho bot hedging đa cụm (nâng cao)
Với chiến lược hedging rải nhiều cụm, mỗi cụm lệnh là một “bot con” với FSM riêng. Bot mẹ điều phối bằng global state:
- ClusterState: mỗi cụm (theo magic/comment) có IDLE → ENTER → IN_POSITION → SURPLUS/HEDGE → EXIT.
- Surplus: khi giá hồi về vùng lời, bot chốt từng phần lệnh trong cụm.
- Hedge: khi giá đi ngược quá sâu, mở lệnh đối ứng để khóa rủi ro.
# ví dụ: cập nhật trạng thái từng cụm sau mỗi tick
for cluster in bot.clusters:
if cluster.state == "IDLE" and signal_for(cluster):
cluster.state = "ENTER"
elif cluster.state == "IN_POSITION" and cluster.surplus_ready():
cluster.take_profit_partial()
cluster.state = "SURPLUS"
5. Case study: PyBot PyNhiQuaiBot thực chiến
PyBot dùng FSM để xử lý đồng thời hàng chục lệnh trên nhiều cụm: quét tín hiệu → lọc spread/ADX → vào lệnh → quản lý lưới → thoát theo step/X-level. Nhờ FSM, bot không nhồi lệnh dù tick về dồn dập, và khi VPS restart, bot đọc lại trạng thái từ Global Variable để tiếp tục đúng vòng đời — không sót lệnh, không đóng nhầm.
6. Các lỗi thường gặp và cách tránh
- Quên cập nhật trạng thái: hành động nhưng không đổi state → lặp hành động. Luôn đổi state ngay sau hành động.
- Thiếu COOLDOWN: bot vào lại ngay sau khi cắt lỗ → cháy tài khoản. Luôn có nghỉ ngắn.
- Không lưu state khi restart: bot “mất trí nhớ”. Dùng Global Variable/DB.
- So sánh sai kiểu: giá là float, đừng so với int 67000.
- Không log: khó truy vết lỗi. Log mọi chuyển trạng thái.
- Chuyển trạng thái bất hợp lệ: chỉ cho phép các đường chuyển trong bảng.
7. Kiểm thử FSM trước khi chạy real
- Vẽ bảng chuyển trạng thái, duyệt mọi cặp (state, event) hợp lệ.
- Mô phỏng: mất kết nối, lỗi lệnh, tin lớn, spread rộng.
- Chạy backtest trên dữ liệu lịch sử, quan sát log chuyển trạng thái.
- Paper trade 2–4 tuần trước khi chạy real.
8. Câu hỏi thường gặp (FAQ)
FSM có làm bot chậm không? Không — chỉ là so sánh trạng thái đơn giản.
Bot 1 lệnh đơn giản có cần FSM không? Chưa cần, nhưng khi có lưới/hedging/trailing thì nên.
FSM khác gì state machine của MT5? MT5 có sẵn cơ chế (OnTick + global vars); FSM là cách tổ chức logic, áp dụng được cả Python lẫn MQL5.
Làm sao xử lý lệnh tay lẫn lệnh bot? Dùng magic number để FSM chỉ quản lý lệnh đúng magic.
Học FSM ở đâu bài bản? Khóa Python Hedging / Bot Auto Trading tại Hướng Nghiệp Dữ Liệu dạy trọn kiến trúc này kèm thực hành.
8.1. Bảng chuyển trạng thái đầy đủ cho bot lưới + hedging
Dưới đây là bảng chuyển trạng thái mở rộng cho bot lưới kết hợp hedging — mỗi cụm lệnh là một FSM con:
| Trạng thái cụm | Sự kiện | Điều kiện | Trạng thái mới | Hành động |
|---|---|---|---|---|
| IDLE | tín hiệu vào | spread < 0.3, ADX > 25 | ENTER | đặt lệnh mở cụm |
| ENTER | khớp lệnh | — | GRID_OPEN | chờ giá hồi |
| GRID_OPEN | giá đi ngược 1 step | số bậc < max_grid | GRID_ADD | thêm lệnh cùng cụm |
| GRID_ADD | giá hồi về profit | — | GRID_OPEN | theo dõi tiếp |
| GRID_OPEN | giá hồi về vùng lời | đạt surplus_pct | SURPLUS | chốt từng phần |
| SURPLUS | chốt đủ | — | EXIT | đóng toàn cụm |
| GRID_OPEN | đi ngược quá sâu | loss > hedge_threshold | HEDGE | mở lệnh đối ứng |
| HEDGE | giá hồi về vùng an toàn | — | SURPLUS | chốt dần |
8.1.1. Vì sao phải có bảng này trước khi code
Bảng là “hợp đồng” giữa bạn và bot: mọi hành vi đều được định nghĩa trước, code chỉ là hiện thực hoá. Khi đổi chiến lược, bạn sửa bảng trước rồi sửa code theo — tránh vô vàn nhánh if mâu thuẫn nhau. Đây chính là điểm mà nhiều trader mới bỏ qua, dẫn tới bot hoạt động “theo kiểu ngẫu hứng”.
8.2. Template Expert Advisor (MQL5) dùng FSM
Áp dụng đúng mô hình trên trong MQL5 — bot đọc/ghi trạng thái qua Global Variable để sống sót qua restart:
enum EAState { S_IDLE, S_ENTER, S_GRID, S_SURPLUS, S_HEDGE, S_EXIT };
int g_state;
int OnInit() {
g_state = (int)GlobalVariableGet("EA_State");
return INIT_SUCCEEDED;
}
void OnTick() {
switch (g_state) {
case S_IDLE:
if (SignalBuy() && SpreadOK()) {
OpenFirst();
g_state = S_ENTER;
SaveState();
}
break;
case S_GRID:
if (PriceRetraced()) { g_state = S_SURPLUS; SaveState(); }
else if (DeepLoss()) { OpenHedge(); g_state = S_HEDGE; SaveState(); }
else if (GridStepHit()) { AddGridOrder(); }
break;
// ... các trạng thái khác
}
}
void SaveState() {
GlobalVariableSet("EA_State", (double)g_state);
}
Điểm mấu chốt: mọi chuyển trạng thái đều gọi SaveState(). Nếu VPS restart giữa chừng, OnInit đọc lại đúng trạng thái và tiếp tục vòng đời — không sót lệnh, không đóng nhầm.
8.3. Lưu trạng thái bằng JSON để dễ debug
Với Python, lưu toàn bộ trạng thái bot (kể cả lịch sử từng cụm) ra JSON giúp bạn xem lại “bot đã nghĩ gì” từng giây:
import json
def save_state(bot):
payload = {
"state": bot.state,
"clusters": [c.to_dict() for c in bot.clusters],
"updated": time.strftime("%Y-%m-%d %H:%M:%S"),
}
with open("bot_state.json", "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
def load_state(bot):
try:
with open("bot_state.json", "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return {}
8.4. So sánh FSM với các kiến trúc khác
| Kiến trúc | Độ phức tạp | Dễ debug | Khi nào dùng |
|---|---|---|---|
| If/else lồng nhau | Thấp | Thấp (rối) | Bot demo, vài điều kiện |
| FSM | Trung bình | Cao (log state) | Bot thực chiến 5–30 trạng thái |
| Behavior Tree | Cao | Rất cao | Bot đa chiến lược, nhiều mục tiêu |
Với đa số bot trading cá nhân, FSM là điểm cân bằng tốt nhất giữa đơn giản và đủ mạnh — không quá khó như Behavior Tree nhưng vượt trội so với if/else rối rắm.
Ví dụ hoàn chỉnh: FSM điều khiển bot lưới XAUUSD
Kết hợp tất cả các phần trên, dưới đây là một bot lưới XAUUSD có FSM đầy đủ — bạn có thể chạy thử trên tài khoản demo:
import time
class GridBot:
def __init__(self, symbol="XAUUSD", lot=0.01, step=300, max_grid=4,
max_spread=0.30):
self.symbol = symbol
self.state = "IDLE"
self.grid = [] # các lệnh trong cụm
self.step = step
self.max_grid = max_grid
self.max_spread = max_spread
self.cooldown_until = 0
def on_tick(self, bid, ask):
spread = ask - bid
if self.state == "IDLE":
if self.signal() and spread < self.max_spread:
self.open_order("BUY", ask)
self.state = "GRID_OPEN"
elif self.state == "GRID_OPEN":
if len(self.grid) < self.max_grid and bid < self.last_price() - self.step:
self.open_order("BUY", ask) # thêm bậc
if self.surplus_ready():
self.take_profit_partial()
self.state = "SURPLUS"
elif self.state == "SURPLUS":
if not self.grid:
self.state = "COOLDOWN"
self.cooldown_until = time.time() + 300
elif self.state == "COOLDOWN":
if time.time() >= self.cooldown_until:
self.state = "IDLE"
def signal(self): return True
def open_order(self, side, price): self.grid.append(price)
def last_price(self): return self.grid[-1] if self.grid else 0
def surplus_ready(self): return len(self.grid) > 0
def take_profit_partial(self):
# chốt bậc đầu khi giá hồi về vùng lời
self.grid.pop(0)
Hãy chú ý: mỗi nhánh elif tương ứng đúng một dòng trong bảng chuyển trạng thái ở mục 8.1 — không có nhánh “lạc trôi” nào ngoài thiết kế.
FSM kết hợp quản lý vốn theo trạng thái
Rủi ro nên thay đổi theo trạng thái: khi IDLE thì rủi ro theo kế hoạch; khi SURPLUS (đang có lời) có thể tăng dần; khi HEDGE (phòng thủ) phải giảm lot hoặc khóa lệnh:
def lot_size(self):
if self.state == "HEDGE": return self.base_lot * 0.5
if self.state == "SURPLUS": return self.base_lot * 1.2 # dùng lời phóng to
return self.base_lot
Quy tắc vàng: không bao giờ tăng lot khi đang lỗ sâu. FSM giúp bạn ép điều này bằng code thay vì dựa vào kỷ luật tự thân — vốn dễ sai khi cảm xúc lên xuống.
Kiến trúc đề xuất cho bot 24/7
- VPS Linux gần broker, chạy Python service (systemd) tự restart khi crash.
- FSM lõi + kết nối MT5/Binance qua API.
- Lưu state JSON/DB mỗi lần chuyển trạng thái.
- Log xoay vòng (RotatingFileHandler) + cảnh báo Telegram khi lỗi.
- Dashboard đơn giản đọc log để xem trạng thái hiện tại.
Với kiến trúc này, bot chạy nhiều tháng không cần can thiệp, và mọi sự cố đều có dấu vết để truy vết nhanh.
FSM và vòng đời đầy đủ của một lệnh giao dịch
Hãy đi theo vòng đời hoàn chỉnh của một lệnh từ lúc bot nhận tick đến khi chốt lời, để thấy FSM điều phối từng bước như thế nào trong thực tế. Giả sử bot chạy trên XAUUSD, tài khoản demo:
- 09:00:00 — IDLE: bot quét từng tick, tính MA20, ADX và spread. Chưa có tín hiệu nên trạng thái giữ nguyên IDLE, không làm gì khác ngoài cập nhật bộ đếm.
- 09:07:33 — IDLE → ENTER: giá đóng nến 5 phút trên MA20, ADX = 28, spread = 0.21 (nhỏ hơn 0.30). Đủ 3 điều kiện, bot chuyển ENTER và ghi log kèm lý do.
- 09:07:34 — ENTER: bot kiểm tra margin tự do đủ, magic đúng, không có tin lớn trong 15 phút tới. Gửi lệnh BUY 0.01 lot.
- 09:07:35 — ENTER → IN_POSITION: lệnh khớp tại 2350.5. Bot lưu giá vào lệnh, chuyển IN_POSITION, đặt SL 2343 và TP 2364 theo kế hoạch R:R 1:3.
- 09:07:35 → 11:30:00 — IN_POSITION: bot chỉ theo dõi giá, kiểm tra mỗi tick xem giá có chạm SL/TP không. Không vào lệnh mới trong lúc này (tránh nhồi lệnh).
- 11:30:00 — IN_POSITION → EXIT: giá chạm TP 2364. Bot đóng lệnh, ghi nhận lợi nhuận +13.5 USD (0.01 lot × 13.5).
- 11:30:01 — EXIT → COOLDOWN: bot đặt cooldown 120 giây để tránh vào lệnh vội vàng ngay sau khi thoát.
- 11:32:01 — COOLDOWN → IDLE: hết giờ nghỉ, bot quay lại quét tín hiệu từ đầu.
Chính nhờ FSM, mỗi bước đều có một trạng thái rõ ràng — không có nhánh code “tự do” nào có thể làm bot hành động sai lúc nửa đêm khi không ai canh.
Vì sao vòng đời này quan trọng với backtest
Khi bạn chạy backtest, hãy mô phỏng đúng vòng đời này: bot chỉ được phép vào lệnh khi ở IDLE, chỉ được phép đóng khi ở IN_POSITION. Nhiều bot lập trình kiểu if/else lộn xộn sẽ “đặt lệnh khi đang giữ lệnh” — backtest cho kết quả đẹp giả tạo, live thì vỡ mặt. FSM buộc bạn mô phỏng đúng trật tự, nên số liệu backtest đáng tin cậy hơn.
FSM với lệnh tay và lệnh bot — tránh xung đột
Một tình huống thực tế: bạn mở MetaTrader, tay đặt thêm một lệnh BUY trong lúc bot đang ở IN_POSITION. Nếu bot không biết lệnh đó tồn tại, nó có thể đóng nhầm hoặc để sót. Giải pháp chuẩn là tách theo magic number:
MY_MAGIC = 6111
def my_positions():
return [p for p in positions_get() if p.magic == MY_MAGIC]
def on_tick():
if state == "IN_POSITION":
mine = my_positions()
if not mine: # lệnh bot đã bị đóng tay/khác
state = "EXIT"
log("Không thấy lệnh magic 6111 - đồng bộ lại")
Quy tắc: bot chỉ quản lý lệnh đúng magic của mình. Lệnh tay của bạn để nguyên — không đụng tới. Cách này tránh tai họa “bot đóng nhầm lệnh tay đang lời to”.
Đồng bộ FSM với trạng thái thật trên sàn (Reconciliation)
Sai lầm nguy hiểm nhất: FSM trong bộ nhớ tưởng là đang IN_POSITION nhưng thực tế lệnh đã bị sàn đóng (do SL phía server, do quản trị, do hết margin). Bot phải có bước đối soát định kỳ:
def reconcile():
real = {p.ticket for p in positions_get()}
expected = my_expected_tickets()
lost = expected - real
for t in lost:
log(f"Lệnh {t} không còn trên sàn - chuyển trạng thái")
handle_lost_order(t)
Gọi reconcile() mỗi 30 giây hoặc sau mỗi sự kiện quan trọng. Nếu FSM và thực tế lệch, ưu tiên theo thực tế rồi cập nhật lại FSM — không cố chấp giữ trạng thái ảo.
Kiểm thử FSM theo bảng (table-driven test)
Cách kiểm thử chuyên nghiệp nhất: liệt kê mọi (state, event) trong bảng chuyển trạng thái rồi tự động chạy từng trường hợp:
import itertools
CASES = [
("IDLE", "BUY_SIGNAL", True, True),
("IDLE", "BUY_SIGNAL", False, False), # spread rộng -> giữ IDLE
("ENTER", "OK", True, True),
("ENTER", "FAIL", True, True),
("IN_POSITION", "SL", True, True),
("COOLDOWN", "TIMEOUT", False, True),
]
def run_tests(bot):
failed = 0
for state, event, cond, expect in CASES:
bot.state = state
bot.on_event(event, cond)
ok = (bot.state_changed == expect)
if not ok:
failed += 1
print(f"FAIL: {state}+{event} -> {bot.state}")
return failed
Chạy test này mỗi khi sửa code FSM. Nếu quên một nhánh chuyển trạng thái, test báo lỗi ngay — thay vì phát hiện ra sau 3 tháng khi bot làm hỏng tài khoản.
Các câu hỏi nâng cao về FSM
FSM có giới hạn số trạng thái không? Không có giới hạn cứng, nhưng trên ~30 trạng thái thì nên tách thành nhiều FSM con (nested) hoặc chuyển sang Behavior Tree cho dễ quản lý.
Bot dùng FSM có chậm hơn if/else không? Không đáng kể — một phép so sánh trạng thái mất vài nano giây, trong khi tick đến mỗi 100ms. Vấn đề hiệu năng nằm ở kết nối API chứ không phải logic FSM.
Có nên dùng thư viện FSM có sẵn (vd transitions, python-statemachine) không? Nên. Thư viện giúp bạn khai báo bảng chuyển trạng thái gọn gàng, tự vẽ được sơ đồ và tránh lỗi thủ công. Nhưng hãy hiểu nguyên lý trước khi dùng thư viện — đừng phụ thuộc mù.
Làm sao biết bot đang ở trạng thái nào khi xem log? Luôn ghi trạng thái vào mỗi dòng log (vd [IN_POSITION] giá chạm TP). Lọc log theo trạng thái để dựng biểu đồ phân bố thời gian.
FSM xử lý tin tức lớn thế nào? Thêm trạng thái NEWS_BLOCK: trước tin lớn 15 phút chuyển sang đó, chặn vào lệnh mới nhưng vẫn quản lý lệnh đang mở; hết tin thì về lại trạng thái cũ.
Kết luận mở rộng
FSM không chỉ là “một mẹo code gọn” — nó là cách tư duy giúp bot của bạn có vòng đời rõ ràng, an toàn và dễ bảo trì. Bắt đầu từ 5–6 trạng thái cơ bản, thêm dần các trạng thái phòng thủ (NEWS_BLOCK, reconcile, cooldown) khi chiến lược phức tạp. Luôn: vẽ bảng chuyển trạng thái trước → code theo bảng → test theo bảng → log mọi chuyển động → đồng bộ với sàn. Làm đúng quy trình này, bạn có thể tin tưởng bot chạy 24/7 mà không cần canh từng phút.
9. Kết luận
FSM là nền tảng để bot hoạt động đúng, an toàn và dễ bảo trì. Bắt đầu từ 5–6 trạng thái, thêm dần khi chiến lược phức tạp. Luôn lưu trạng thái, log mọi chuyển động và kiểm thử kỹ mọi đường chuyển trước khi đưa vào tài khoản thật.
📌 Muốn làm chủ kiến trúc bot + hedging, tham gia khóa Python Hedging / Bot Auto Trading tại Hướng Nghiệp Dữ Liệu — hotline 0934.145.100 · Zalo 0934.145.100.
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.
Đặng Trí Thanh
Giám đốc Công nghệ · DNT Digital · Giảng viên HNDLĐặng Trí Thanh — Founder & CTO · Hướng Nghiệp Dữ Liệu - DNT Digital. Chuyên đào tạo và triển khai thực chiến Python, MT5 và hệ thống bot auto trading / IB cho học viên và doanh nghiệp.