| Bài 7: Thuật Toán Reopen & Vùng Đệm Khoảng Lùi An Toàn (Reopen Buffer)

Được viết bởi thanhdt vào ngày 01/06/2026 lúc 10:17 | 34 lượt xem

Giới Thiệu: Cờ Đóng Lệnh Và Điều Kiện Mở Lại

Bài 5, chúng ta đã xây dựng cơ chế Reopen Buffer — khóa tầng sau khi chốt lời và chỉ mở lại khi giá lùi đủ xa. Bài này mở rộng khái niệm đó trong context của series Bài 4-9, nơi có hai loại chốt lời khác nhau với điều kiện mở lại khác nhau:

  1. Gặt X → để lại cờ _X_CLOSED_ khi đóng
  2. Trailing Plowing → để lại cờ _PLOW_CLOSED_ khi đóng

Tại sao cần phân biệt? Vì hành vi thị trường sau hai kiểu chốt lời này hoàn toàn khác nhau:

  • Sau Gặt X (chốt tại điểm cố định): Giá có thể tiếp tục theo hướng cũ hoặc reverse — cần buffer ngắn hơn
  • Sau Trailing Plowing (chốt sau pullback từ đỉnh): Thị trường đang có dấu hiệu reversal rõ ràng — cần buffer dài hơn trước khi mở lại

Nhờ hai cờ riêng biệt, hàm CheckReopenBuffer() có thể áp dụng ngưỡng buffer khác nhau cho từng trường hợp — tăng độ chính xác và giảm rủi ro tái mở lệnh sai thời điểm.


Kiến Trúc: Hai Cờ, Hai Ngưỡng Buffer

REOPEN BUFFER — HAI CỜ, HAI NGƯỠNG BUFFER

LUỒNG GẶT X

CheckHarvestX() → Đóng tầng s

Set “_X_CLOSED_{magic}_{step}”
= close_price (giá chốt)

Buffer áp dụng: InpXBuffer% × StepSize

LUỒNG TRAILING PLOWING

CheckTrailingPlowing() → Đóng tầng s

Set “_PLOW_CLOSED_{magic}_{step}”
= close_price (giá chốt)

Buffer áp dụng: InpPlowBuffer% × StepSize (lớn hơn)

CheckReopenBuffer() — mỗi tick
Đọc cờ → Chọn buffer% → So sánh giá → Xóa cờ

Giá ra khỏi
vùng buffer?

Xóa cờ → Tầng sẵn sàng mở lại ✅

NO

Tiếp tục
khóa

Hình 1: Hai cờ đóng lệnh (_X_CLOSED_ và _PLOW_CLOSED_) được xử lý bởi CheckReopenBuffer() với ngưỡng buffer khác nhau

Thiết Kế GlobalVariable Keys Cho Hai Cờ


// Key conventions cho hai loại cờ:
// _X_CLOSED_{magic}_{step}    = giá chốt bởi Gặt X
// _PLOW_CLOSED_{magic}_{step} = giá chốt bởi Trailing Plowing

string GetXClosedKey(long magic, int step)
{
    return "_X_CLOSED_" + IntegerToString((int)magic)
           + "_" + IntegerToString(step);
}

string GetPlowClosedKey(long magic, int step)
{
    return "_PLOW_CLOSED_" + IntegerToString((int)magic)
           + "_" + IntegerToString(step);
}

//+------------------------------------------------------------------+
//| Đặt cờ sau khi Gặt X đóng tầng thành công                      |
//+------------------------------------------------------------------+
void SetXClosedFlag(long magic, int step, double close_price)
{
    // Xóa cờ Plow nếu có (tránh conflict)
    string plow_key = GetPlowClosedKey(magic, step);
    if(GlobalVariableCheck(plow_key)) GlobalVariableDel(plow_key);

    GlobalVariableSet(GetXClosedKey(magic, step), close_price);
    Print("Flag _X_CLOSED_ set: Tầng=", step, " Price=", close_price);
}

//+------------------------------------------------------------------+
//| Đặt cờ sau khi Trailing Plowing đóng tầng thành công            |
//+------------------------------------------------------------------+
void SetPlowClosedFlag(long magic, int step, double close_price)
{
    // Xóa cờ X nếu có
    string x_key = GetXClosedKey(magic, step);
    if(GlobalVariableCheck(x_key)) GlobalVariableDel(x_key);

    GlobalVariableSet(GetPlowClosedKey(magic, step), close_price);
    Print("Flag _PLOW_CLOSED_ set: Tầng=", step, " Price=", close_price);
}

//+------------------------------------------------------------------+
//| Kiểm tra loại cờ đang active cho tầng này                       |
//+------------------------------------------------------------------+
enum ENUM_CLOSE_FLAG
{
    FLAG_NONE  = 0,
    FLAG_X     = 1,
    FLAG_PLOW  = 2
};

ENUM_CLOSE_FLAG GetActiveFlag(long magic, int step)
{
    if(GlobalVariableCheck(GetXClosedKey(magic, step)))    return FLAG_X;
    if(GlobalVariableCheck(GetPlowClosedKey(magic, step))) return FLAG_PLOW;
    return FLAG_NONE;
}

double GetFlagClosePrice(long magic, int step, ENUM_CLOSE_FLAG flag)
{
    if(flag == FLAG_X)   return GlobalVariableGet(GetXClosedKey(magic, step));
    if(flag == FLAG_PLOW) return GlobalVariableGet(GetPlowClosedKey(magic, step));
    return 0.0;
}

Tham Số Hai Buffer: XBuffer Và PlowBuffer


// Hai tham số buffer riêng biệt
input double InpXBuffer    = 25.0;   // Buffer sau Gặt X (% của StepSize)
input double InpPlowBuffer = 50.0;   // Buffer sau Plowing (% của StepSize) — lớn hơn
input double InpGridStep   = 50.0;   // Bước lưới (points)
input long   InpMagicNumber = 20240101;
input int    InpMaxStep    = 20;

// Lý do InpPlowBuffer > InpXBuffer:
// Plowing chốt sau khi giá đã pullback từ đỉnh.
// Điều này cho thấy xu hướng đang yếu dần.
// Cần buffer lớn hơn để đảm bảo xu hướng thực sự đảo chiều
// trước khi mở lại tầng.

Hàm CheckReopenBuffer: Logic Phân Biệt Hai Cờ


//+------------------------------------------------------------------+
//| Kiểm tra và xóa cờ Reopen khi đủ điều kiện (gọi trong OnTick)  |
//| Trả về số tầng đã được mở khóa                                   |
//+------------------------------------------------------------------+
int CheckReopenBuffer()
{
    double bid   = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    double ask   = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double mid   = (bid + ask) / 2.0;
    double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

    int unlocked = 0;

    for(int step = -InpMaxStep; step  upper || mid < lower);

        if(outside)
        {
            string flag_name = (flag == FLAG_X) ? "_X_CLOSED_" : "_PLOW_CLOSED_";
            Print("ReOpenBuffer: Tầng ", step,
                  " | Flag=", flag_name,
                  " | ClosePrice=", close_price,
                  " | Buffer=", buffer_pct, "%",
                  " | Range=[", lower, ",", upper, "]",
                  " | Current=", mid,
                  " → MỞ KHÓA");

            // Xóa cờ
            if(flag == FLAG_X)
                GlobalVariableDel(GetXClosedKey(InpMagicNumber, step));
            else
                GlobalVariableDel(GetPlowClosedKey(InpMagicNumber, step));

            unlocked++;
        }
    }

    return unlocked;
}

Cập Nhật CloseStepWithLock: Phân Biệt Lý Do Đóng

Hàm CloseStepWithLock() từ Bài 5 cần được mở rộng để nhận thêm tham số close_reason:


enum ENUM_CLOSE_REASON
{
    CLOSE_REASON_HARVEST_X = 0,   // Đóng bởi Gặt X
    CLOSE_REASON_PLOWING   = 1,   // Đóng bởi Trailing Plowing
    CLOSE_REASON_BASKET_TP = 2,   // Đóng bởi Basket TP
    CLOSE_REASON_MANUAL    = 3    // Đóng thủ công
};

bool CloseStepWithLock(ulong ticket, int step, long magic,
                       ENUM_CLOSE_REASON reason = CLOSE_REASON_HARVEST_X)
{
    if(!PositionSelectByTicket(ticket)) return false;

    double volume     = PositionGetDouble(POSITION_VOLUME);
    double profit     = PositionGetDouble(POSITION_PROFIT);
    ENUM_POSITION_TYPE pos_type =
        (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

    double close_price = (pos_type == POSITION_TYPE_BUY)
                         ? SymbolInfoDouble(_Symbol, SYMBOL_BID)
                         : SymbolInfoDouble(_Symbol, SYMBOL_ASK);

    // Tạo và gửi request đóng lệnh
    MqlTradeRequest request = {};
    MqlTradeResult  result  = {};
    request.action    = TRADE_ACTION_DEAL;
    request.symbol    = _Symbol;
    request.volume    = volume;
    request.type      = (pos_type == POSITION_TYPE_BUY)
                        ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
    request.position  = ticket;
    request.price     = NormalizeDouble(close_price,
                        (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
    request.deviation = 20;
    request.magic     = magic;
    request.comment   = "CLOSE_s" + IntegerToString(step);

    if(!OrderSend(request, result) || result.retcode != TRADE_RETCODE_DONE)
    {
        Print("CloseStep thất bại: ", result.retcode);
        return false;
    }

    double actual_close = result.price;

    // === ĐẶT CỜ PHÙ HỢP VỚI LÝ DO ĐÓNG ===
    switch(reason)
    {
        case CLOSE_REASON_HARVEST_X:
            SetXClosedFlag(magic, step, actual_close);
            break;

        case CLOSE_REASON_PLOWING:
            SetPlowClosedFlag(magic, step, actual_close);
            break;

        case CLOSE_REASON_BASKET_TP:
            // Basket TP dùng buffer riêng (xem Bài 10)
            SetXClosedFlag(magic, step, actual_close);
            break;

        case CLOSE_REASON_MANUAL:
            // Không đặt cờ — trader tự quyết định khi nào mở lại
            break;
    }

    Print("✅ Đóng tầng ", step,
          " | Lý do=", EnumToString(reason),
          " | Profit=", profit,
          " | Close@", actual_close);

    return true;
}

Cập Nhật Các Hàm Caller


// Trong CheckHarvestX() — truyền CLOSE_REASON_HARVEST_X
bool closed = CloseStepWithLock(ticket, step, InpMagicNumber,
                                 CLOSE_REASON_HARVEST_X);

// Trong CheckTrailingPlowing() — truyền CLOSE_REASON_PLOWING
bool closed = CloseStepWithLock(ticket, step, InpMagicNumber,
                                 CLOSE_REASON_PLOWING);

Xử Lý Edge Cases Đặc Trưng

Edge Case 1: Tầng Đóng Hai Lần Liên Tiếp

Tầng s được Gặt X đóng → cờ _X_CLOSED_ được đặt. Trước khi buffer xóa cờ, trader mở lại tầng thủ công → Gặt X lại đóng tầng đó → cờ mới được set với giá chốt mới. Đây là hành vi đúng — cờ cũ bị ghi đè bởi giá chốt mới.


// SetXClosedFlag() tự động xử lý: ghi đè GV cũ
void SetXClosedFlag(long magic, int step, double close_price)
{
    // GlobalVariableSet() ghi đè nếu key đã tồn tại
    GlobalVariableSet(GetXClosedKey(magic, step), close_price);
}

Edge Case 2: Cả Hai Cờ Cùng Active

Không thể xảy ra về mặt logic (một tầng không thể bị đóng hai lần cùng lúc). Tuy nhiên, để an toàn, các hàm SetXClosedFlag()SetPlowClosedFlag() đều xóa cờ kia trước khi set cờ mới.

Edge Case 3: Buffer Quá Lớn — Giá Không Bao Giờ Ra Ngoài

Nếu InpPlowBuffer = 200% và thị trường sideway trong dải hẹp, tầng có thể bị khóa vô thời hạn. Giải pháp: thêm timeout theo thời gian như là fallback:


// Thêm timestamp vào khi set cờ
void SetXClosedFlagWithTime(long magic, int step, double close_price)
{
    GlobalVariableSet(GetXClosedKey(magic, step), close_price);
    // Lưu thêm timestamp
    string ts_key = "_X_CLOSED_TS_" + IntegerToString((int)magic)
                    + "_" + IntegerToString(step);
    GlobalVariableSet(ts_key, (double)TimeCurrent());
}

// Trong CheckReopenBuffer() — thêm kiểm tra timeout
datetime flag_time = (datetime)GlobalVariableGet(ts_key);
int hours_elapsed = (int)((TimeCurrent() - flag_time) / 3600);

if(hours_elapsed >= InpMaxLockHours)  // Ví dụ: 24 giờ
{
    Print("Timeout: Tầng ", step, " đã khóa ", hours_elapsed, " giờ → Mở khóa.");
    GlobalVariableDel(GetXClosedKey(magic, step));
    unlocked++;
}

Edge Case 4: EA Restart — Khôi Phục Trạng Thái Khóa


int OnInit()
{
    // ... (khôi phục grid state từ Comment Parser)

    // Kiểm tra các cờ Reopen đang active
    int x_locked    = 0;
    int plow_locked = 0;

    for(int step = -InpMaxStep; step <= InpMaxStep; step++)
    {
        ENUM_CLOSE_FLAG flag = GetActiveFlag(InpMagicNumber, step);
        if(flag == FLAG_X)
        {
            double cp = GetFlagClosePrice(InpMagicNumber, step, FLAG_X);
            Print("Khôi phục: Tầng ", step,
                  " đang bị _X_CLOSED_ @ ", cp);
            x_locked++;
        }
        else if(flag == FLAG_PLOW)
        {
            double cp = GetFlagClosePrice(InpMagicNumber, step, FLAG_PLOW);
            Print("Khôi phục: Tầng ", step,
                  " đang bị _PLOW_CLOSED_ @ ", cp);
            plow_locked++;
        }
    }

    Print("Reopen Buffer: ", x_locked, " tầng X_CLOSED, ",
          plow_locked, " tầng PLOW_CLOSED.");

    return INIT_SUCCEEDED;
}

Tích Hợp Hoàn Chỉnh Trong OnTick


void OnTick()
{
    // 1. Kiểm tra Hedge Lock (Volume Bias — Bài 3)
    if(InpEnableHedge)
    {
        CheckVolumeBias(InpMagicNumber);
        if(GlobalVariableGet("_LOCKED_" + IntegerToString((int)InpMagicNumber)) == 1.0)
        {
            Comment("⛔ HEDGE LOCK ACTIVE");
            return;
        }
    }

    // 2. Cập nhật Reopen Buffer (Bài 5 + 9)
    int unlocked = CheckReopenBuffer();
    if(unlocked > 0)
        Print("Mở khóa ", unlocked, " tầng từ Reopen Buffer.");

    // 3. Quét các vị thế đang mở
    int total = PositionsTotal();
    for(int i = total - 1; i >= 0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(ticket == 0) continue;
        if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
        if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;

        string comment = PositionGetString(POSITION_COMMENT);
        int step = GetStepFromComment(comment);
        if(step == STEP_INVALID) continue;

        // 4. Trailing Plowing (Bài 8)
        bool plowed = CheckTrailingPlowing(ticket, step);
        if(plowed) continue;

        // 5. Gặt X (Bài 7) — chỉ nếu không plowing
        // (CheckHarvestX() duyệt riêng bên ngoài vòng lặp này)
    }

    // 6. Gặt X cho tất cả vị thế
    CheckHarvestX();

    // 7. Mở lệnh mới cho các tầng đã được mở khóa
    // (Logic Grid Engine — không trình bày ở đây)
}

So Sánh Buffer Theo Loại Đóng

Loại đóng Cờ đặt Buffer khuyến nghị Lý do
Gặt X (tại điểm cố định) _X_CLOSED_ 20-30% × StepSize Giá có thể tiếp tục, mở lại sớm hơn
Trailing Plowing (sau pullback) _PLOW_CLOSED_ 40-60% × StepSize Pullback có thể tiếp tục, cần đợi lâu hơn
Basket TP (đóng toàn cluster) _X_CLOSED_ Tùy cấu hình Coi như Gặt X cho mục đích buffer
Đóng thủ công Không đặt cờ Không có buffer Trader tự quyết định khi nào mở lại

Tổng Kết Series Bài 4-9

Sau 6 bài viết, chúng ta đã xây dựng một hệ thống hoàn chỉnh với các thành phần phối hợp chặt chẽ:

Bài Thuật toán Vai trò
Bài 4 FSM Chống Trùng Người gác cổng — không lệnh trùng
Bài 5 Gặt X Chốt lời từng tầng tại điểm cố định
Bài 6 Trailing Plowing Theo đuôi xu hướng, chốt sau pullback
Bài 7 (này) Reopen Buffer (2 cờ) Kiểm soát thời điểm mở lại theo lý do đóng

Bài 10, chúng ta sẽ đến với Basket TP — giải pháp chốt toàn bộ cluster khi tổng lợi nhuận thực + nổi đạt target. Đây là thuật toán cuối cùng trước khi kết thúc series với Equity Lockdown ở Bài 11.


Xây Dựng EA Hoàn Chỉnh Với Toàn Bộ 9 Thuật Toán

Khóa học MQL5 Nâng Cao hướng dẫn bạn kết hợp tất cả các thành phần — từ Comment Parser, FSM, Gặt X, Trailing Plowing, Reopen Buffer đến Basket TP và Equity Lockdown — thành một EA thực chiến hoàn chỉnh.

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.