Bot gửi lệnh hai lần: năm tình huống, bảng mã trả về và bốn lớp chống trùng
Vận hành14/09/2026 · 18 phút đọc

Bot gửi lệnh hai lần: năm tình huống, bảng mã trả về và bốn lớp chống trùng

Có một loại lỗi trong vận hành bot mà bạn chỉ phát hiện khi nhìn vào lịch sử giao dịch và thấy hai lệnh giống hệt nhau, cách nhau vài giây, ở hai mức giá khác nhau. Không có cảnh báo nào. Không có lỗi nào trong log. Bot vẫn "chạy đúng như code".

Đó là gửi lệnh trùng — và nó là một trong những nguyên nhân nhân đôi rủi ro phổ biến nhất, đặc biệt với bot chạy qua đêm hoặc qua kết nối không ổn định.

Điều khiến lỗi này khó chịu là nó không phải lỗi của code. Code của bạn có thể hoàn toàn đúng: chỉ gửi lệnh khi có tín hiệu, chỉ gửi một lần cho mỗi tín hiệu. Vấn đề nằm ở chỗ khác: bot không biết lệnh đã tới sàn hay chưa. Trong giao dịch, "không biết" phải được xử lý như "có thể đã khớp" — và hầu hết bot không làm vậy.

Bài này đi qua năm tình huống thực tế gây trùng lệnh, bảng quyết định cho từng mã trả về (khi nào được phép gửi lại), và bốn lớp chống trùng nên dùng cùng lúc. Cuối bài có code MQL5 cho lớp kiểm tra vị thế và lớp khoá theo tín hiệu.


1. Năm tình huống khiến bot gửi lệnh hai lần

Năm tình huống khiến bot gửi lệnh hai lần
Năm tình huống khiến bot gửi lệnh hai lần

Tình huống 1 — Timeout khi gửi. Đây là nguyên nhân phổ biến nhất. Bot gọi OrderSend, lệnh tới sàn, sàn khớp, nhưng phản hồi không về kịp trong thời gian chờ. MT5 trả về lỗi timeout. Bot — được viết để "thử lại khi lỗi" — gửi lại. Lần thứ hai cũng khớp. Bây giờ bạn có hai lệnh.

Điểm mấu chốt: timeout không có nghĩa là lệnh không vào. Nó chỉ có nghĩa là bạn không nhận được câu trả lời. Hai chuyện hoàn toàn khác nhau.

Tình huống 2 — Mất mạng đúng lúc gửi. VPS mất mạng trong một giây, ngay sau khi gói tin gửi lệnh đã đi. Sàn nhận và khớp. Bot không nhận được gì, và ở lần tick tiếp theo, tín hiệu vẫn đúng (vì giá chưa đổi đủ để tín hiệu tắt). Bot gửi lại.

Tình huống 3 — Requote rồi khớp. Sàn trả về requote (mã 138). Bot thử lại với giá mới — lần này khớp. Nhưng nếu lần thử đầu thật ra đã khớp ở phía sàn mà bot hiểu nhầm là bị từ chối, bạn có hai lệnh. Với requote, rủi ro này nhỏ hơn nhưng không bằng không.

Tình huống 4 — MT5 treo rồi chạy lại. Trong lúc treo, một tick được xử lý dở. Khi terminal sống lại, tick đó được xử lý lại từ đầu. Nếu logic gửi lệnh không có trạng thái "đã gửi", bot sẽ gửi lại.

Tình huống 5 — Khởi động lại giữa chuỗi. Đây là tình huống tệ nhất: bot khởi động lại (do VPS reboot, do watchdog tự sửa) và không biết mình đã làm gì trước đó. Tín hiệu vẫn còn hiệu lực, nên bot gửi lại toàn bộ những gì nó cho là cần gửi. Nếu trước khi restart bot đang mở ba lệnh, sau khi restart nó có thể mở thêm ba lệnh nữa.

Năm tình huống này có một điểm chung đáng nhớ: trong cả năm, bot đều đang làm đúng theo logic của nó. Vấn đề không nằm ở logic gửi, mà ở chỗ thiếu một bước kiểm tra trạng thái thật trước khi gửi.

2. Mã trả về nào thì được phép gửi lại?

Mã trả về nào thì được phép gửi lại
Mã trả về nào thì được phép gửi lại

Đây là bảng quyết định quan trọng nhất trong bài. Nếu bạn chỉ sửa một thứ, hãy sửa cách bot phản ứng với các mã này.

Mã trả vềNghĩaĐược gửi lại?
10009 doneKhớp lệnhKHÔNG — lệnh đã vào
10004 requoteGiá đổi, chưa khớpĐược, kèm giá mới
10006 rejectedBị từ chốiĐược, nhưng kiểm tra lý do trước
10008 placedĐã đặt lệnh chờKHÔNG
Không có phản hồi / timeoutKHÔNG BIẾTKHÔNG — phải kiểm tra vị thế trước
10016 invalid stopsTham số saiCó, sau khi sửa tham số
10019 / 134 not enough moneyKhông đủ ký quỹKHÔNG — giảm khối lượng trước

Ba dòng cần khắc vào đầu:

Dòng "không có phản hồi" là dòng nguy hiểm nhất. Hầu hết bot được viết theo cấu trúc: nếu OrderSend trả về false → thử lại. Nhưng "false" gộp chung hai trường hợp rất khác nhau: sàn từ chối (an toàn để thử lại) và bạn không biết (nguy hiểm). Phải tách hai trường hợp này ra.

`10009` phải được xử lý như "đã xong", không phải "cần kiểm tra thêm". Nếu code của bạn kiểm tra lại vị thế sau khi nhận 10009 và vì độ trễ mà chưa thấy vị thế mới, bạn có thể kết luận sai rồi gửi lại.

`10016` (invalid stops) là loại lỗi cần sửa tham số, không phải thử lại. Gửi lại y nguyên tham số sai sẽ nhận lại đúng lỗi đó — và nếu bạn có vòng lặp thử lại, nó chỉ làm đầy log.

3. Bốn lớp chống trùng

Bốn lớp chống trùng — dùng cùng lúc, không chọn một
Bốn lớp chống trùng — dùng cùng lúc, không chọn một

Lớp 1 — Khoá theo tín hiệu. Mỗi tín hiệu cần một mã duy nhất: mã nến (thời gian mở nến), cộng với symbol và magic number. Trước khi gửi, kiểm tra xem mã này đã được xử lý chưa; nếu rồi, thoát.

Điểm cần chú ý: mã tín hiệu phải bền qua lần khởi động lại. Nếu bạn lưu "đã xử lý tín hiệu X" trong biến toàn cục (trong RAM), khởi động lại là mất. Phải lưu ra file hoặc dùng biến toàn cục của terminal (GlobalVariableSet, tồn tại qua restart).

Lớp 2 — Kiểm tra vị thế trước khi gửi. Đây là lớp rẻ nhất và hiệu quả nhất, và cũng là lớp bị bỏ qua nhiều nhất. Trước mỗi OrderSend, đếm số vị thế đang mở theo magic number và symbol, rồi so với số mong đợi của chiến lược.

Ví dụ: chiến lược của bạn chỉ mở tối đa một lệnh cho mỗi symbol. Trước khi gửi, kiểm tra PositionsTotal(). Nếu đã có một lệnh với magic của bạn trên symbol đó — thoát. Đơn giản vậy thôi, và nó chặn được cả năm tình huống ở mục 1.

Hạn chế của lớp này: nó chỉ đúng nếu bot được thiết kế để biết số lệnh mong đợi. Với chiến lược grid hoặc DCA (nhiều lệnh có chủ đích), bạn phải kiểm tra theo cách khác — ví dụ kiểm tra đã có lệnh nào ở mức giá này chưa.

Lớp 3 — Ghi trạng thái ra file. Trước khi gửi: ghi "đang gửi tín hiệu X". Sau khi có kết quả: ghi "tín hiệu X đã khớp, ticket Y". Khi khởi động lại, đọc file để biết mình đang ở đâu.

Đây là lớp duy nhất xử lý được tình huống 5 (restart giữa chuỗi). Nhược điểm: ghi file có độ trễ và có thể thất bại (ổ đĩa đầy). Vì vậy nó bổ trợ cho lớp 2, không thay thế.

Một chi tiết thiết kế quan trọng: ghi "đang gửi" trước khi gọi OrderSend, không phải sau. Nếu ghi sau, một lần mất điện đúng lúc sẽ để lại trạng thái sai — bot tưởng chưa gửi.

Lớp 4 — Chống trùng phía sàn. Một số sàn hỗ trợ mã định danh lệnh do khách hàng đặt (client order id), và sẽ từ chối hai lệnh có cùng mã. MT5 không đảm bảo có tính năng này cho mọi loại tài khoản, nên bạn cần kiểm tra với broker của mình.

Nếu không có, lớp gần nhất là: kiểm tra lại lần cuối ngay trước `OrderSend` — đếm vị thế lần nữa trong cùng một khối lệnh, không có Sleep ở giữa.

4. Code: lớp kiểm tra vị thế và lớp khoá theo tín hiệu

Bắt đầu với lớp 2 — kiểm tra vị thế. Hàm này là thứ bạn gọi trước mọi lệnh gửi:

mql5
// Đếm vị thế đang mở của CHÍNH bot này (theo magic + symbol).
// Không dùng PositionsTotal() trần: tài khoản có thể đang có lệnh của tay hoặc của bot khác.
int DemLenhCuaBot(ulong magic, string symbol)
{
   int dem = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) != symbol) continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != magic)    continue;
      dem++;
   }
   return(dem);
}

Và lớp 1 — khoá theo tín hiệu, lưu bằng biến toàn cục nên tồn tại qua lần khởi động lại:

mql5
// Mã tín hiệu = thời gian mở nến + magic. Dùng biến toàn cục của terminal để sống qua restart.
bool DaXuLyTinHieu(ulong magic, string symbol, datetime moNen)
{
   string khoa = StringFormat("FM_SIG_%I64u_%s_%d", magic, symbol, (int)moNen);
   return(GlobalVariableCheck(khoa));
}

void DanhDauDaXuLy(ulong magic, string symbol, datetime moNen)
{
   string khoa = StringFormat("FM_SIG_%I64u_%s_%d", magic, symbol, (int)moNen);
   GlobalVariableSet(khoa, (double)TimeLocal());
}

Và hàm gửi lệnh gộp cả bốn lớp, với quy tắc rõ ràng cho trường hợp "không có phản hồi":

mql5
bool GuiLenhAnToan(ENUM_ORDER_TYPE loai, double kl, double gia, double sl, double tp,
                   ulong magic, string symbol, datetime moNen, int soLenhToiDa)
{
   // Lớp 1: tín hiệu này đã xử lý chưa?
   if(DaXuLyTinHieu(magic, symbol, moNen))
   {
      PrintFormat("BỎ QUA: tín hiệu %d của %s đã xử lý.", (int)moNen, symbol);
      return(false);
   }
   // Lớp 2: đã đủ số lệnh mong đợi chưa?
   if(DemLenhCuaBot(magic, symbol) >= soLenhToiDa)
   {
      PrintFormat("BỎ QUA: đã có %d lệnh (tối đa %d) cho %s.",
                  DemLenhCuaBot(magic, symbol), soLenhToiDa, symbol);
      DanhDauDaXuLy(magic, symbol, moNen);   // đánh dấu để lần sau không thử lại
      return(false);
   }

   // Lớp 3: ghi "đang gửi" TRƯỚC khi gửi
   GhiTrangThai(moNen, "dang_gui", 0);

   MqlTradeRequest req; MqlTradeResult res;
   ZeroMemory(req); ZeroMemory(res);
   req.action = TRADE_ACTION_DEAL; req.symbol = symbol; req.volume = kl;
   req.type = loai; req.price = gia; req.sl = sl; req.tp = tp;
   req.deviation = 20; req.magic = magic; req.comment = StringFormat("sig%d", (int)moNen);

   bool ok = OrderSend(req, res);
   uint  rc = res.retcode;

   // Trường hợp NGUY HIỂM: không nhận được phản hồi → KHÔNG gửi lại, phải kiểm tra
   if(!ok && rc == 0)
   {
      Print("KHÔNG RÕ KẾT QUẢ (timeout). KHÔNG gửi lại — chờ kiểm tra vị thế.");
      // KHÔNG đánh dấu đã xử lý: để lần tick sau kiểm tra lại vị thế thật
      return(false);
   }
   // Sàn đã trả lời rằng lệnh CHƯA vào → an toàn để thử lại
   if(!ok && (rc == TRADE_RETCODE_REQUOTE || rc == TRADE_RETCODE_REJECT))
   {
      PrintFormat("Chưa vào lệnh (retcode=%u). Được phép thử lại với giá mới.", rc);
      GhiTrangThai(moNen, "chua_vao", rc);
      return(false);
   }
   if(!ok)
   {
      PrintFormat("Lỗi %u — kiểm tra tham số trước khi thử lại.", rc);
      GhiTrangThai(moNen, "loi", rc);
      return(false);
   }

   DanhDauDaXuLy(magic, symbol, moNen);
   GhiTrangThai(moNen, "da_khop", res.order);
   PrintFormat("ĐÃ KHỚP: %.2f %s @%.5f ticket=%I64u", kl, symbol, res.price, res.order);
   return(true);
}

Ba điểm trong đoạn code này đáng nhấn:

`rc == 0` là trường hợp đặc biệt. Khi OrderSend trả về false và retcode bằng 0, nghĩa là không có phản hồi từ sàn — không phải bị từ chối. Code trên không đánh dấu đã xử lý, và cũng không gửi lại; nó để tick sau kiểm tra vị thế thật. Đây là hành vi đúng: chờ có bằng chứng thay vì đoán.

`DanhDauDaXuLy` khi đã đủ số lệnh. Khi tín hiệu tới nhưng đã đủ lệnh, ta vẫn đánh dấu đã xử lý — để tín hiệu đó không bị thử lại mỗi tick.

`comment` có mã tín hiệu. Nhờ vậy, khi nhìn vào danh sách vị thế trên MT5, bạn biết ngay lệnh nào thuộc tín hiệu nào — kể cả ba tháng sau.

5. Trường hợp khó: chiến lược nhiều lệnh (grid, DCA)

Với chiến lược chỉ mở một lệnh, lớp 2 (đếm số lệnh) giải quyết gần hết. Nhưng nếu chiến lược của bạn cố ý mở nhiều lệnh — grid, DCA, martingale — thì "đếm số lệnh" không còn đủ, vì mở thêm lệnh là hành vi đúng.

Cách xử lý: thay vì đếm tổng số lệnh, hãy kiểm tra ở mức giá cụ thể. Trước khi mở lệnh ở mức giá P, kiểm tra xem đã có lệnh nào trong khoảng ±X điểm quanh P chưa. Nếu có, bỏ qua.

Một cách khác, chặt hơn: gán mỗi mức giá của grid một mã riêng (ví dụ chỉ số mức: grid_3), rồi dùng khoá theo tín hiệu như mục 4 với mã là grid_3 thay vì thời gian nến. Cách này đòi hỏi lưới giá của bạn phải ổn định (không thay đổi giữa các phiên), nếu không mã sẽ đổi và bot lại mở thêm lệnh.

Với martingale, cần cẩn thận hơn nữa: số lần nhân khối lượng là thứ phải được lưu bền, vì nếu bot khởi động lại và mất số lần đã nhân, nó có thể bắt đầu lại từ bước 1 trong khi tài khoản đang ở bước 4 — hoặc tệ hơn, nhân tiếp lên bước 5 ngoài kế hoạch.

6. Khoá trạng thái nên lưu ở đâu: ba lựa chọn và cái giá của từng cái

Đây là quyết định kỹ thuật quan trọng nhất của cả bài, vì nó quyết định lớp 1 và lớp 3 có thật sự hoạt động hay không.

Lựa chọn 1 — biến trong RAM (biến toàn cục của EA). Nhanh nhất, không đụng ổ đĩa. Nhược điểm chí mạng: mất khi bot khởi động lại. Mà khởi động lại chính là tình huống 5 — trường hợp bạn cần nó nhất. Kết luận: dùng được như lớp phụ (chống trùng trong cùng một phiên), không dùng được làm lớp chính.

Lựa chọn 2 — biến toàn cục của terminal (`GlobalVariableSet`). Đây là lựa chọn tốt nhất cho hầu hết trường hợp. Biến toàn cục của MT5 được lưu xuống đĩa và tồn tại qua lần khởi động lại terminal. Tốc độ gần như biến RAM, không cần tự xử lý file, và có sẵn hàm để kiểm tra tồn tại (GlobalVariableCheck).

Hai điểm cần chú ý: (a) biến toàn cục gắn với terminal, không gắn với tài khoản — nếu bạn chạy nhiều terminal trên cùng máy thì mỗi terminal có kho riêng; (b) MT5 có giới hạn số lượng biến toàn cục và tổng dung lượng, nên đừng lưu cả lịch sử dài — chỉ lưu trạng thái gần đây và dọn định kỳ.

Lựa chọn 3 — file trạng thái. Bền nhất và đọc được từ bên ngoài MT5 (đây là điểm quan trọng: script giám sát bên ngoài có thể đọc file để biết bot đang ở trạng thái nào). Nhược điểm: chậm hơn, có thể thất bại (ổ đĩa đầy, sandbox chặn ghi), và bạn phải tự xử lý việc ghi hỏng.

Cách dùng thực tế: kết hợp 2 và 3. Dùng biến toàn cục làm lớp chính (nhanh, bền qua restart), và ghi file như bản sao để bên ngoài đọc được. Nếu ghi file thất bại, bot vẫn chạy đúng nhờ biến toàn cục — và bạn có log để biết file đang có vấn đề.

Một chi tiết nhỏ nhưng hay bị bỏ: dọn trạng thái cũ. Nếu bạn lưu khoá theo mã tín hiệu và không bao giờ xoá, sau một năm bạn có hàng nghìn biến toàn cục. Hãy dọn những khoá cũ hơn một tuần trong OnInit, và ghi lại số lượng đã dọn.

Sáu dấu hiệu trong log cho thấy bot đang có nguy cơ trùng lệnh

Những dấu hiệu này không phải lỗi — chúng là cảnh báo sớm. Thấy hai dấu hiệu trở lên trong cùng một tuần, hãy rà lại bốn lớp chống trùng.

1. Có dòng `UNKNOWN sig=...` trong ngày. Nghĩa là đã có ít nhất một lần bot không nhận được phản hồi. Bản thân việc đó chưa tệ — điều bạn cần kiểm tra là sau đó bot có gửi lại không.

2. Cùng một mã tín hiệu xuất hiện nhiều lần với `SEND`. Nếu bạn thấy hai dòng SEND sig=... cho cùng một mã, bot đã gửi hai lần cho một tín hiệu. Tìm xem lớp kiểm tra vị thế có bị bỏ qua ở nhánh nào không.

3. Nhiều dòng `SKIP ... da_du_lenh` trong một ngày. Đây là dấu hiệu bot đang bị đánh giá tín hiệu quá thường xuyên — có thể sau mỗi tick, dù tín hiệu chỉ nên được xét mỗi nến. Không nguy hiểm về trùng lệnh (vì đã bị chặn), nhưng làm log nhiễu và tăng tải.

4. Số lệnh đang mở lớn hơn số lệnh mong đợi. Đây là dấu hiệu muộn nhất nhưng chắc chắn nhất, và là lý do bạn nên đưa con số này vào báo cáo mỗi sáng. Nếu bot của bạn chỉ nên mở một lệnh mà đang có hai, hãy dừng và rà lại ngay — đừng để nó thành trạng thái bình thường mới.

5. Độ trễ gửi lệnh tăng dần. Độ trễ tăng nghĩa là tần suất timeout sẽ tăng theo. Đây là chỉ số duy nhất trong danh sách có thể báo trước khi lỗi trùng lệnh xảy ra.

6. Có lần bot gửi lệnh mà không có dòng `dang_gui` trong file trạng thái. Nghĩa là thứ tự ghi trạng thái đã bị đảo — ghi sau khi gửi thay vì trước khi gửi. Đây là lỗi thiết kế cần sửa, và nó chỉ lộ ra khi có sự cố tiếp theo.

Khi bot chạy nhiều symbol: khoá theo symbol hay theo tín hiệu?

Câu trả lời ngắn: khoá theo tín hiệu, nhưng mã tín hiệu phải bao gồm symbol. Đây là lỗi tôi thấy khá phổ biến, và nó gây ra kiểu trùng lệnh khó phát hiện nhất.

Hãy tưởng tượng bot của bạn chạy XAUUSD và EURUSD, và mã tín hiệu chỉ là thời gian mở nến. Vào nến 15 giờ, cả hai symbol đều có tín hiệu. Bot xử lý XAUUSD trước, đánh dấu “nến 15 giờ đã xử lý”. Đến EURUSD, bot thấy mã đã tồn tại và bỏ qua tín hiệu thật — bạn mất một lệnh, không phải gửi thừa.

Nếu bạn đảo ngược thứ tự kiểm tra (kiểm tra rồi mới đánh dấu theo cách khác), bạn lại có thể gửi hai lệnh — một cho mỗi symbol — dù chiến lược chỉ cho phép một. Cả hai chiều đều sai.

Cách làm đúng rất đơn giản: mã tín hiệu = symbol + magic + thời gian mở nến. Nhờ có symbol trong mã, mỗi symbol có không gian trạng thái riêng, và bạn tránh được cả hai lỗi trên.

Một trường hợp nữa cần lưu ý: nếu EA của bạn xét tín hiệu trên nhiều khung thời gian (ví dụ H1 cho xu hướng, M15 cho điểm vào), thì mã tín hiệu nên bao gồm cả khung thời gian tạo tín hiệu. Nếu không, tín hiệu M15 lúc 15:00 và tín hiệu H1 lúc 15:00 sẽ trùng mã, và một trong hai bị bỏ qua nhầm.

6. Những gì bạn nên ghi log để phát hiện trùng lệnh

Phát hiện trùng lệnh sau khi nó xảy ra khó hơn nhiều so với ngăn nó. Có ba dòng log giúp ích:

  • Trước khi gửi: SEND sig=<mã tín hiệu> kl=<kl> gia=<giá> soLenhHienTai=<n>. Nhìn lại, bạn thấy ngay lúc đó bot nghĩ đang có bao nhiêu lệnh.
  • Khi không rõ kết quả: UNKNOWN sig=<mã> — chờ kiểm tra vị thế. Dòng này là dấu hiệu bạn cần kiểm tra ngay.
  • Khi bỏ qua vì đã đủ lệnh: SKIP sig=<mã> ly-do=da_du_lenh. Nếu dòng này xuất hiện quá nhiều, có thể bot đang bị gọi lại liên tục cho cùng một tín hiệu.

Ngoài ra, hãy dùng magic number khác nhau cho mỗi chiến lược và giữ một bảng tra magic → chiến lược trong tài liệu vận hành. Khi tài khoản có nhiều lệnh, magic là thứ duy nhất phân biệt lệnh của bot nào — và nếu bạn dùng cùng một magic cho nhiều chiến lược, mọi phép đếm ở mục 4 sẽ sai.

7. Kiểm thử: tự tạo một lần trùng lệnh để chứng minh

Cách kiểm thử hiệu quả nhất là tạo ra tình huống 1 (timeout) một cách nhân tạo trên tài khoản demo:

  1. Trong GuiLenhAnToan, tạm thời đặt req.deviation = 1 và gửi lệnh khi thị trường đang biến động mạnh — để tạo requote.
  2. Hoặc đơn giản hơn: trong code, tạm bỏ dòng DanhDauDaXuLy sau khi khớp, rồi để bot chạy trong phiên có tín hiệu. Bot sẽ gửi lặp.
  3. Xác nhận rằng hệ thống giám sát của bạn phát hiện được điều đó — ví dụ cảnh báo khi cùng một magic có nhiều lệnh hơn số mong đợi.

Bài kiểm thử thứ ba quan trọng nhất: nó kiểm tra không phải bot, mà khả năng phát hiện của bạn. Một lỗi trùng lệnh không bị phát hiện trong demo sẽ không bị phát hiện khi chạy tiền thật — chỉ là hậu quả đắt hơn.

Và một lưu ý cuối: hãy kiểm tra lại số lệnh mong đợi mỗi ngày. Một dòng trong báo cáo sáng: "tài khoản 1234567: 2 lệnh đang mở (mong đợi 1)". Nếu con số đó sai, bạn biết trước khi nó thành thói quen.

Bốn bài kiểm tra trước khi cho bot chạy tiền thật

Bốn bài này mất khoảng 30 phút trên tài khoản demo, và chúng kiểm tra đúng những thứ dễ hỏng nhất.

Bài 1 — gửi lệnh hai lần bằng tay. Gọi hàm gửi lệnh hai lần liên tiếp cho cùng một tín hiệu. Kết quả mong đợi: lần thứ hai bị chặn và có dòng log BỎ QUA. Nếu cả hai đều chạy, lớp 1 hoặc lớp 2 đang không hoạt động.

Bài 2 — giả lập không có phản hồi. Cho hàm gửi lệnh trả về thất bại với retcode = 0 (mô phỏng timeout) và quan sát: bot có gửi lại không? Kết quả mong đợi: không gửi lại, và có dòng KHÔNG RÕ KẾT QUẢ. Đây là bài kiểm tra quan trọng nhất trong bốn bài.

Bài 3 — khởi động lại giữa chuỗi. Gửi một lệnh, sau đó khởi động lại MT5 (đóng và mở lại), rồi để bot chạy tiếp. Kết quả mong đợi: bot không gửi lại tín hiệu cũ. Nếu có, trạng thái của bạn đang không bền qua restart — kiểm tra lại xem bạn lưu bằng biến RAM hay bằng biến toàn cục/file.

Bài 4 — hai symbol cùng lúc. Cho tín hiệu xuất hiện ở hai symbol trong cùng một nến, và xác nhận cả hai đều được xử lý (mỗi symbol một lệnh). Nếu chỉ một lệnh được gửi, mã tín hiệu của bạn đang thiếu phần symbol — lỗi ở mục 5.

Ghi lại kết quả bốn bài này vào nhật ký vận hành. Khi bạn sửa logic gửi lệnh lần sau, chạy lại bốn bài này. Chúng rẻ hơn rất nhiều so với việc phát hiện lỗi trùng lệnh bằng tiền thật.

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

Hỏi: Bot tôi dùng `OrderSend` trả về `TRADE_RETCODE_DONE` là chắc chắn chỉ có một lệnh đúng không? Trả về DONE nghĩa là lệnh đó đã thành công — nhưng nó không nói gì về việc bạn đã gọi hàm bao nhiêu lần. Nếu bạn gọi ba lần và cả ba lần thành công, bạn có ba lệnh và ba dòng DONE. Câu trả lời nằm ở lớp chống trùng phía trước, không nằm ở mã trả về.

Hỏi: Tôi có thể dựa vào thời gian chờ (cooldown) thay cho mã tín hiệu không? Không nên. Thời gian chờ đủ để chặn hầu hết trường hợp, nhưng nó có hai vấn đề: nó chặn cả tín hiệu thật nếu hai tín hiệu hợp lệ đến gần nhau, và nó không chặn gì nếu bot khởi động lại trong khoảng chờ (vì trạng thái mất). Thời gian chờ là lớp bổ sung tốt, không phải lớp chính.

Hỏi: Nếu broker không trả về mã lệnh ở retcode 10009 thì tôi kiểm tra bằng gì? Kiểm tra hai thứ: (a) số lượng vị thế đang mở với đúng magic và đúng symbol — nếu tăng so với trước khi gửi, lệnh đã được ghi; (b) lịch sử lệnh trong 60 giây gần nhất. Với hai thông tin này bạn xác định được kết quả trong hầu hết trường hợp, và phần còn lại thuộc về quy trình xử lý thủ công.

Hỏi: Xử lý thủ công khi không rõ kết quả — có cách nào tốt hơn? Có, nếu bot của bạn dùng lệnh có mã định danh riêng (ví dụ đưa mã tín hiệu vào phần comment của lệnh). Khi đó bạn có thể tìm trong lịch sử theo comment để trả lời câu hỏi “tín hiệu này đã lên sàn chưa” một cách chắc chắn. Đây là thói quen nhỏ, tốn 0 đồng, nhưng biến tình huống mơ hồ nhất thành tình huống xác định được.

8. Checklist và tóm lại

Checklist 10 điểm chống trùng lệnh:

  1. Có phân biệt "sàn từ chối" với "không nhận được phản hồi"
  2. Không bao giờ gửi lại khi không rõ kết quả — phải kiểm tra vị thế trước
  3. Có khoá theo tín hiệu, lưu bền qua restart (biến toàn cục hoặc file)
  4. Ghi "đang gửi" trước khi gọi OrderSend
  5. Đếm vị thế theo magic + symbol, không đếm tổng số lệnh tài khoản
  6. Mỗi chiến lược một magic number, có bảng tra magic → chiến lược
  7. Có mã tín hiệu trong comment của lệnh
  8. Với grid/DCA: kiểm tra theo mức giá, không đếm tổng
  9. Có log cho ba dòng: SEND, UNKNOWN, SKIP
  10. Đã thử tạo trùng lệnh trên demo và hệ thống phát hiện được

Bốn ý cần nhớ:

  1. Trùng lệnh không phải lỗi logic — nó là lỗi xử lý "không biết". Bot gửi lại vì nó được lập trình để "lỗi thì thử lại", mà "không có phản hồi" bị gộp chung với "bị từ chối".
  2. Chờ có bằng chứng thay vì đoán. Không rõ kết quả → kiểm tra vị thế. Đây là bước mà hầu hết bot thiếu.
  3. Lớp đếm vị thế là lớp rẻ nhất và hiệu quả nhất, chặn được cả năm tình huống — nhưng phải đếm theo magic, không đếm tổng.
  4. Trạng thái phải sống qua lần khởi động lại. Biến trong RAM mất khi bot restart — và restart lại chính là tình huống tệ nhất.

Nếu bạn muốn đi tiếp: Đọc log MT5 để tìm nguyên nhân có bảng tra mã lỗi đầy đủ, Audit log cho bot trading hướng dẫn ghi lại để điều tra khi có lệnh lạ, và Bot chết lúc 3h sáng là bảng tra nguyên nhân tổng quát.

Trong giao dịch, "tôi không biết" không phải câu trả lời an toàn. Nó là câu trả lời nguy hiểm nhất.
Đọc xong rồi? Hãy thử ngay — miễn phí 7 ngày.