Bài viết gần đây
-
-
Dart Cú Pháp Cơ Bản Flutter Buổi 2 | var final const Null Safety
Tháng 7 27, 2026 -
EA MT5 Chậm Vì Multi-Pass | Siêu Cache 1-Pass Buổi 2
Tháng 7 27, 2026 -
Cache & Array MT5 Buổi 2 | Siêu Cache 1-Pass Grid Bot Hedging
Tháng 7 27, 2026 -
Python Dùng Để Làm Gì Trong Tài Chính? (CK, Fintech, Bot Trading)
Tháng 7 26, 2026
Trang chủ → Bài Viết → Thuật Toán 4: Trích Xuất Chỉ Số Tầng Lưới Từ Comment Vị Thế (Comment String Parser)
| Thuật Toán 4: Trích Xuất Chỉ Số Tầng Lưới Từ Comment Vị Thế (Comment String Parser)
Được viết bởi thanhdt vào ngày 01/06/2026 lúc 10:54 | 49 lượt xem
Vấn Đề: EA Bị Restart Thì Mất Bộ Nhớ
Hãy tưởng tượng robot giao dịch của bạn đang vận hành trơn tru, duy trì lưới 12 tầng trên EURUSD. Đột nhiên VPS bị reboot, MetaTrader crash, hoặc bạn phải cập nhật phiên bản EA mới. Khi EA khởi động lại, nó “không nhớ” mình đang ở tầng nào trong lưới — tất cả biến nội bộ đã bị xóa.
Đây là vấn đề cốt lõi mà Comment String Parser giải quyết: thay vì lưu trạng thái tầng trong bộ nhớ tạm thời của EA, ta ghi thông tin đó trực tiếp vào comment của lệnh trên sàn. Vì lệnh tồn tại trên server của broker, thông tin này không bao giờ bị mất dù EA restart bao nhiêu lần.
Ví dụ thực tế:
- Lệnh tầng 3 Buy: comment =
"s3" - Lệnh tầng -5 Sell (phía âm): comment =
"s-5" - Lệnh tầng 0 (lệnh gốc): comment =
"s0"
Khi EA restart, hàm GetStepFromComment() sẽ đọc comment này, parse ra số tầng, và khôi phục hoàn toàn trạng thái của lưới. Bài viết này trình bày toàn bộ cơ chế từ quy ước đặt tên comment đến code MQL5 hoàn chỉnh với xử lý lỗi đầy đủ.
Kiến Trúc Tổng Thể: Comment Parser Trong Hệ Thống Lưới
COMMENT STRING PARSER — LUỒNG HOẠT ĐỘNG
PHASE 1: MỞ LỆNH (Ghi Comment)
EA tính toán: step = 3
Mở lệnh Buy tầng 3
comment = “s” + IntegerToString(step)
→ comment = “s3″
OrderSend với comment=”s3” → Lưu trên broker server
PHASE 2: EA RESTART (Khôi Phục)
OnInit(): Quét tất cả vị thế
PositionGetString(POSITION_COMMENT)
GetStepFromComment(“s3”)
→ StringFind(“s”) + StringSubstr → 3
Khôi phục: step_map[ticket] = 3 ✓
Thời gian
PHASE 3: XỬ LÝ TẦNG ÂM (s-5, s-12, v.v.)
comment = “s-5”
Tầng -5 (phía Sell)
StringFind(comment, “s”) → pos=0
StringSubstr(comment, 1) → “-5”
StringToInteger(“-5”) → -5
step = -5
✓ Chính xác
Edge Cases: “s0” → 0 ✓ | “s-99” → -99 ✓ | “” (rỗng) → INT_MIN (lỗi) ✓ | “HEDGE_LOCK” → INT_MIN (bỏ qua) ✓
Lệnh thủ công không có “s” prefix → INT_MIN (bỏ qua, không xử lý)
Comment bị truncate bởi broker → dùng StringTrimRight() để xử lý ký tự thừa
Quy Ước Comment: Thiết Kế Quan Trọng Nhất
Trước khi viết code, cần quyết định quy ước định dạng comment. Đây là quyết định thiết kế quan trọng vì nó ảnh hưởng đến toàn bộ hệ thống. Một quy ước tốt phải thỏa mãn:
- Ngắn gọn: Nhiều broker giới hạn độ dài comment (thường 31-63 ký tự)
- Duy nhất: Không nhầm lẫn với comment của lệnh thủ công hoặc EA khác
- Hỗ trợ số âm: Tầng phía Sell thường dùng chỉ số âm
- Dễ parse: Code đọc lại phải đơn giản, không phụ thuộc vào thư viện ngoài
Quy Ước Được Chọn: Prefix “s” + Số Nguyên
| Tầng lưới | Comment được ghi | Ý nghĩa |
|---|---|---|
| Tầng 0 (gốc) | “s0” | Lệnh đặt tại giá gốc |
| Tầng 3 (Buy, lên) | “s3” | 3 bước lưới phía trên giá gốc |
| Tầng -5 (Sell, xuống) | “s-5” | 5 bước lưới phía dưới giá gốc |
| Tầng -12 (deep Sell) | “s-12” | 12 bước lưới phía dưới |
| Lệnh hedge | “HEDGE_LOCK_12345” | Lệnh đóng băng tài khoản (không parse) |
Hàm GetStepFromComment: Chi Tiết Kỹ Thuật
Đây là hàm trung tâm của toàn bộ thuật toán. Nó nhận vào một chuỗi comment và trả về số tầng tương ứng.
// Giá trị đặc biệt: trả về khi comment không hợp lệ
#define STEP_INVALID INT_MIN // -2147483648 — không bao giờ là số tầng hợp lệ
//+------------------------------------------------------------------+
//| Parse số tầng từ comment của lệnh |
//| Input: comment — chuỗi comment của vị thế |
//| Output: số tầng (có thể âm), hoặc STEP_INVALID nếu không hợp lệ |
//+------------------------------------------------------------------+
int GetStepFromComment(string comment)
{
// Làm sạch đầu vào
StringTrimLeft(comment);
StringTrimRight(comment);
// Kiểm tra rỗng
if(comment == "" || StringLen(comment) == 0)
return STEP_INVALID;
// Tìm vị trí của prefix "s"
int s_pos = StringFind(comment, "s", 0);
if(s_pos < 0)
{
// Không có "s" — không phải comment của hệ thống lưới
return STEP_INVALID;
}
// Trích xuất phần số sau "s"
string number_part = StringSubstr(comment, s_pos + 1);
// Làm sạch: xóa khoảng trắng và ký tự rác ở cuối
StringTrimRight(number_part);
// Kiểm tra chuỗi số có hợp lệ không
if(StringLen(number_part) == 0)
return STEP_INVALID;
// Kiểm tra ký tự hợp lệ (chỉ cho phép chữ số và dấu trừ ở đầu)
bool has_digit = false;
int start_idx = 0;
if(StringGetCharacter(number_part, 0) == '-')
start_idx = 1; // Bỏ qua dấu trừ
for(int i = start_idx; i = '0' && ch <= '9')
{
has_digit = true;
}
else
{
// Gặp ký tự không phải số → dừng ở đây
// Lấy phần đã xác nhận là số
number_part = StringSubstr(number_part, 0, i);
break;
}
}
if(!has_digit)
return STEP_INVALID;
// Chuyển đổi sang số nguyên
int step = (int)StringToInteger(number_part);
return step;
}
Giải Thích Từng Bước
Bước 1 — StringTrimLeft/Right: Một số broker thêm khoảng trắng hoặc ký tự xuống dòng vào comment. Trim trước để tránh parse lỗi.
Bước 2 — StringFind(comment, “s”, 0): Tìm vị trí đầu tiên của ký tự “s”. Nếu không tìm thấy (s_pos == -1), comment không thuộc hệ thống lưới.
Bước 3 — StringSubstr(comment, s_pos + 1): Lấy toàn bộ chuỗi sau ký tự “s”. Ví dụ: "s-5" → "-5", "s12" → "12".
Bước 4 — Kiểm tra ký tự hợp lệ: Duyệt từng ký tự để đảm bảo chỉ có chữ số và dấu trừ. Nếu gặp ký tự khác (ví dụ broker thêm suffix), cắt bỏ phần đó.
Bước 5 — StringToInteger: Chuyển chuỗi số thành int. MQL5 xử lý đúng dấu âm.
Hàm BuildStepComment: Ghi Comment Khi Mở Lệnh
Hàm đối xứng với GetStepFromComment() — tạo ra chuỗi comment từ số tầng để ghi vào lệnh khi mở.
//+------------------------------------------------------------------+
//| Tạo chuỗi comment từ số tầng |
//| Input: step — số tầng (âm hoặc dương) |
//| Output: chuỗi comment (ví dụ: "s3", "s-5", "s0") |
//+------------------------------------------------------------------+
string BuildStepComment(int step)
{
return "s" + IntegerToString(step);
}
// Ví dụ sử dụng:
// string comment = BuildStepComment(3); // → "s3"
// string comment = BuildStepComment(-5); // → "s-5"
// string comment = BuildStepComment(0); // → "s0"
Đơn giản như vậy. Sức mạnh của thiết kế tốt nằm ở chỗ hàm tạo ra comment (BuildStep) và hàm đọc lại comment (GetStep) hoàn toàn đối xứng với nhau.
Khôi Phục Trạng Thái Sau EA Restart: Hàm RestoreGridState
Đây là hàm được gọi trong OnInit() — nó quét toàn bộ vị thế đang mở và xây dựng lại bảng ánh xạ ticket → tầng:
// Cấu trúc lưu trữ thông tin tầng lưới
struct GridStep
{
ulong ticket; // Ticket của lệnh
int step; // Số tầng
double open_price; // Giá mở lệnh
double lot_size; // Khối lượng
ENUM_POSITION_TYPE type; // Buy hay Sell
};
GridStep g_steps[]; // Mảng toàn cục lưu trạng thái lưới
int g_step_count; // Số tầng đang hoạt động
//+------------------------------------------------------------------+
//| Khôi phục trạng thái lưới từ comment của các vị thế |
//| Gọi trong OnInit() sau khi EA restart |
//| Trả về số tầng đã khôi phục thành công |
//+------------------------------------------------------------------+
int RestoreGridState(long magic_number)
{
ArrayFree(g_steps);
g_step_count = 0;
int restored = 0;
int skipped = 0;
int total = PositionsTotal();
Print("RestoreGridState: Đang quét ", total, " vị thế...");
for(int i = 0; i < total; i++)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
// Chỉ xử lý vị thế của EA này
if(PositionGetInteger(POSITION_MAGIC) != magic_number) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
string comment = PositionGetString(POSITION_COMMENT);
int step = GetStepFromComment(comment);
if(step == STEP_INVALID)
{
Print(" Ticket ", ticket, " comment='", comment,
"' — không phải lệnh lưới, bỏ qua.");
skipped++;
continue;
}
// Thêm vào mảng
int idx = ArrayResize(g_steps, g_step_count + 1) - 1;
g_steps[idx].ticket = ticket;
g_steps[idx].step = step;
g_steps[idx].open_price = PositionGetDouble(POSITION_PRICE_OPEN);
g_steps[idx].lot_size = PositionGetDouble(POSITION_VOLUME);
g_steps[idx].type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
g_step_count++;
restored++;
Print(" Khôi phục: Ticket=", ticket,
" | Step=", step,
" | Type=", EnumToString(g_steps[idx].type),
" | Price=", g_steps[idx].open_price);
}
Print("RestoreGridState hoàn tất: Khôi phục=", restored,
" | Bỏ qua=", skipped);
return restored;
}
Tích Hợp Vào OnInit và OnTick
input long InpMagicNumber = 20240101;
input int InpMaxSteps = 20; // Số tầng tối đa mỗi chiều
input double InpLotBase = 0.01;
input double InpGridStep = 50; // Bước lưới (points)
//+------------------------------------------------------------------+
int OnInit()
{
Print("EA khởi động — bắt đầu khôi phục trạng thái...");
int restored = RestoreGridState(InpMagicNumber);
if(restored == 0)
{
Print("Không có vị thế nào — EA bắt đầu mới hoàn toàn.");
}
else
{
Print("Đã khôi phục ", restored, " tầng lưới từ comment.");
PrintCurrentGridState();
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Mở lệnh tại tầng cụ thể với comment đúng format |
//+------------------------------------------------------------------+
ulong OpenGridOrder(int step, ENUM_ORDER_TYPE order_type, double lot)
{
// Kiểm tra tầng đã tồn tại chưa (tránh mở trùng)
for(int i = 0; i < g_step_count; i++)
{
if(g_steps[i].step == step)
{
Print("Tầng ", step, " đã tồn tại (ticket=", g_steps[i].ticket, "), bỏ qua.");
return 0;
}
}
// Tạo comment theo quy ước
string comment = BuildStepComment(step);
// Lấy giá
double price = (order_type == ORDER_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
: SymbolInfoDouble(_Symbol, SYMBOL_BID);
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = lot;
request.type = order_type;
request.price = NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
request.deviation = 20;
request.magic = InpMagicNumber;
request.comment = comment; // GHI COMMENT QUY ƯỚC
if(!OrderSend(request, result) || result.retcode != TRADE_RETCODE_DONE)
{
Print("Lỗi mở lệnh tầng ", step, ": ", result.retcode,
" | ", result.comment);
return 0;
}
// Thêm vào danh sách tầng đang hoạt động
int idx = ArrayResize(g_steps, g_step_count + 1) - 1;
g_steps[idx].ticket = result.deal;
g_steps[idx].step = step;
g_steps[idx].open_price = result.price;
g_steps[idx].lot_size = lot;
g_steps[idx].type = (ENUM_POSITION_TYPE)((order_type == ORDER_TYPE_BUY) ? 0 : 1);
g_step_count++;
Print("Mở lệnh tầng ", step, " thành công. Ticket=", result.deal,
" | Comment=", comment);
return result.deal;
}
//+------------------------------------------------------------------+
//| In trạng thái hiện tại của lưới (debug) |
//+------------------------------------------------------------------+
void PrintCurrentGridState()
{
Print("=== TRẠNG THÁI LƯỚI HIỆN TẠI ===");
for(int i = 0; i < g_step_count; i++)
{
Print(" [", i, "] Step=", g_steps[i].step,
" | Ticket=", g_steps[i].ticket,
" | Type=", EnumToString(g_steps[i].type),
" | Price=", g_steps[i].open_price,
" | Lot=", g_steps[i].lot_size);
}
Print("=== TỔNG: ", g_step_count, " TẦNG ===");
}
Xử Lý Edge Cases Quan Trọng
Edge Case 1: Broker Truncate Comment
Một số broker giới hạn độ dài comment và tự động cắt bỏ phần thừa. Ví dụ: comment "s12345" có thể bị cắt thành "s123". Để tránh điều này, luôn dùng comment ngắn nhất có thể và kiểm tra xem broker có giới hạn không.
// Kiểm tra giới hạn comment của broker trong OnInit()
void CheckBrokerCommentLimit()
{
// Broker thường cho phép 31 ký tự cho comment
// "s" + "-" + 10 chữ số = 12 ký tự tối đa cho tầng
// Hoàn toàn an toàn với giới hạn 31 ký tự
int max_step_digits = 10;
string test_comment = "s-" + StringRepeat("9", max_step_digits);
Print("Comment tối đa có thể: '", test_comment,
"' (", StringLen(test_comment), " ký tự)");
}
Edge Case 2: Nhiều Lệnh Cùng Tầng (Sau Partial Close)
Sau khi chốt một phần lệnh (partial close), broker có thể chia lệnh thành nhiều phần nhỏ nhưng vẫn giữ nguyên comment. Kết quả là nhiều lệnh cùng comment "s3" trên tài khoản.
// Xử lý nhiều lệnh cùng tầng: lấy lệnh có khối lượng lớn nhất
int FindDominantStep(int step, long magic_number)
{
double max_lot = 0;
ulong dominant_ticket = 0;
int total = PositionsTotal();
for(int i = 0; i max_lot)
{
max_lot = lot;
dominant_ticket = ticket;
}
}
return (int)dominant_ticket;
}
Edge Case 3: Comment Bị Sàn Thay Đổi
Một số broker (đặc biệt là NDD/STP) tự động thêm prefix vào comment, ví dụ: "12345678:s3" hoặc "s3|ECN". Hàm GetStepFromComment() đã xử lý trường hợp này — nó tìm ký tự “s” ở bất kỳ vị trí nào trong chuỗi, không chỉ ở đầu.
| Comment nhận được | GetStepFromComment() | Kết quả |
|---|---|---|
| “s3” | StringFind → pos=0, substr=”3″ | 3 ✓ |
| “12345678:s3” | StringFind → pos=9, substr=”3″ | 3 ✓ |
| “s-5 ECN” | substr=”-5 ECN”, cắt tại ” ” → “-5” | -5 ✓ |
| “HEDGE_LOCK_123” | Không có “s” prefix → STEP_INVALID | STEP_INVALID ✓ |
| “” (rỗng) | Kiểm tra rỗng đầu tiên | STEP_INVALID ✓ |
| “manual trade” | Không có “s” — bỏ qua | STEP_INVALID ✓ |
Unit Test: Kiểm Tra Tính Đúng Đắn
Đây là bộ test quan trọng để xác minh hàm GetStepFromComment() hoạt động đúng trong mọi tình huống. Chạy trong OnInit() của EA test:
struct TestCase
{
string input;
int expected;
};
void RunCommentParserTests()
{
TestCase tests[] = {
{"s0", 0 },
{"s3", 3 },
{"s-5", -5 },
{"s-12", -12 },
{"s99", 99 },
{"s-99", -99 },
{"12345:s7", 7 }, // Broker prefix
{"s3 ECN", 3 }, // Broker suffix
{"", STEP_INVALID},
{"no_s_here", STEP_INVALID},
{"HEDGE_LOCK", STEP_INVALID},
{"sabc", STEP_INVALID}, // "s" nhưng không có số
{"s", STEP_INVALID}, // Chỉ có "s"
};
int passed = 0;
int failed = 0;
int total = ArraySize(tests);
Print("=== BẮT ĐẦU UNIT TEST: GetStepFromComment ===");
for(int i = 0; i < total; i++)
{
int result = GetStepFromComment(tests[i].input);
bool ok = (result == tests[i].expected);
if(ok)
{
Print(" [PASS] '", tests[i].input, "' → ", result);
passed++;
}
else
{
Print(" [FAIL] '", tests[i].input,
"' → Got=", result,
" Expected=", tests[i].expected);
failed++;
}
}
Print("=== KẾT QUẢ: ", passed, "/", total, " PASSED | ",
failed, " FAILED ===");
}
So Sánh Các Phương Pháp Lưu Trạng Thái
| Phương pháp | Tồn tại qua restart? | Phụ thuộc vào? | Nhược điểm |
|---|---|---|---|
| Comment Parser ✓ | ✓ Có (server broker) | Broker server | Broker có thể modify comment |
| GlobalVariable | ✓ Có (disk local) | Máy tính/VPS local | Mất nếu chuyển VPS hoặc xóa file |
| File CSV | ✓ Có (disk local) | Máy tính/VPS local | Ghi/đọc chậm hơn |
| Biến static | ✗ Không | RAM | Mất khi EA bị unload |
Kết luận: Comment Parser là phương pháp tốt nhất vì nó không phụ thuộc vào hạ tầng local. Ngay cả khi bạn chuyển EA sang VPS khác hoặc cài lại MetaTrader, miễn là các lệnh vẫn còn trên tài khoản thì trạng thái có thể khôi phục hoàn toàn.
Mối Liên Hệ Với Các Thuật Toán Khác
Kết Nối Với Volume Bias (Bài 3)
Khi lệnh Hedge Lock được mở (từ thuật toán Volume Bias), nó dùng comment "HEDGE_LOCK_magic". Comment Parser nhận diện đây không phải lệnh lưới (STEP_INVALID) và bỏ qua — đây là thiết kế có chủ đích để hai thuật toán không xung đột với nhau.
Kết Nối Với Reopen Buffer (Bài 5)
Reopen Buffer cần biết tầng nào đã được mở lại và tầng nào vẫn đang bị khóa. Thông tin này có thể được mã hóa vào comment: ví dụ "s3_CLOSED" cho tầng đã chốt lời và đang trong thời gian khóa.
Kết Nối Với FSM Chống Trùng Lệnh (Bài 6)
FSM sử dụng GetFSMState(magic, step, type) — hàm này cần biết step hiện tại, và giá trị step đó được lấy từ Comment Parser khi EA restart.
Tổng Kết Và Bài Học
Comment String Parser là giải pháp thanh lịch cho vấn đề “bộ nhớ phân tán” trong giao dịch tự động. Các điểm cốt lõi:
- Thiết kế quy ước trước, code sau: Format “s” + số nguyên đơn giản nhưng đủ mạnh
- StringFind + StringSubstr: Hai hàm cốt lõi của MQL5 cho việc parse string
- Xử lý lỗi toàn diện: STEP_INVALID cho mọi trường hợp không hợp lệ
- Luôn viết unit test: Với string parsing, unit test là bắt buộc
- Comment tồn tại trên broker server: Đây là lợi thế lớn nhất so với các phương pháp khác
Ở Bài 5, chúng ta sẽ khám phá thuật toán Reopen Buffer — cơ chế khóa tầng sau khi chốt lời và chỉ mở lại khi giá đã lùi đủ xa. Đây là thuật toán ngăn robot “tái mở lệnh” ngay lập tức sau khi chốt, một sai lầm phổ biến làm giảm hiệu quả của chiến lược lưới.
Muốn Học Toàn Bộ 9 Thuật Toán Với Source Code Hoàn Chỉnh?
Khóa học MQL5 Nâng Cao cung cấp source code đầy đủ, video giảng dạy chi tiết, và hỗ trợ Q&A trực tiếp. Từ Comment Parser đến Equity Lockdown — tất cả trong một khóa học.
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Đà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.