| 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

Hình 1: Luồng hoạt động của Comment String Parser — từ ghi comment khi mở lệnh đến khôi phục trạng thái sau restart

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:

  1. Ngắn gọn: Nhiều broker giới hạn độ dài comment (thường 31-63 ký tự)
  2. Duy nhất: Không nhầm lẫn với comment của lệnh thủ công hoặc EA khác
  3. Hỗ trợ số âm: Tầng phía Sell thường dùng chỉ số âm
  4. 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.

Xem Khóa Học MQL5 Nâng Cao →

Đặng Trí Thanh

Đặng Trí Thanh

Giám đốc Công nghệ · DNT Digital · Giảng viên HNDL
1.322 Bài viết
15.4k Người theo dõi
120k+ Lượt đọc

Đà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.