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
| Thuật Toán 2: Quét Tìm Biên Độ Giá Trị Cực Hạn (Min-Max) Của Lưới Vị Thế
Được viết bởi thanhdt vào ngày 01/06/2026 lúc 10:53 | 61 lượt xem
Tại Sao Bot Lưới Cần Biết “Lệnh Xa Nhất Đang Ở Đâu”?
Trong giao dịch lưới đa chiều (Multi-Layer Grid Trading), bot không quản lý một lệnh đơn lẻ — nó quản lý cả một tập hợp vị thế phân bố theo chiều giá. Mỗi khi thị trường đi ngược chiều, bot mở thêm một tầng lưới mới cách tầng cũ một khoảng cố định. Kết quả là sau vài giờ giao dịch, tài khoản có thể đang gồng 5, 10, thậm chí 15 lệnh Buy đang lỗ trải dài từ giá 2000 đến 1970 (đối với vàng XAUUSD).
Để ra quyết định đúng trong tình huống này, bot cần trả lời hai câu hỏi cốt lõi trong từng mili-giây:
- “Lệnh Buy mở thấp nhất đang ở giá nào?” (MinPrice) — để quyết định có nên mở thêm tầng mới không: chỉ mở khi giá hiện tại thấp hơn MinPrice một khoảng bằng GridStep.
- “Tổng lợi nhuận của cả cụm đã đủ để chốt lời tập thể chưa?” — phụ thuộc vào khoảng cách từ MaxPrice đến giá hiện tại.
Đây chính là nhiệm vụ của Thuật Toán 2 — Quét Min-Max Biên Độ Lưới: một vòng lặp thời gian thực liên tục dò tìm giá trị cực hạn (Min/Max) trong tập hợp vị thế đang hoạt động, đảm bảo bot luôn có dữ liệu chính xác tuyệt đối để đưa ra quyết định giao dịch.
1. Toán Học Của Bài Toán Tìm Min-Max Trong Grid
1.1. Định Nghĩa Chính Thức
Cho tập hợp vị thế Buy đang mở P = {p₁, p₂, …, pₙ}, mỗi vị thế có giá mở cửa OpenPrice(pᵢ):
- MinPrice(Buy) = min{OpenPrice(pᵢ)} — giá mở của lệnh Buy thấp nhất (xa nhất theo hướng lỗ)
- MaxPrice(Buy) = max{OpenPrice(pᵢ)} — giá mở của lệnh Buy cao nhất (gần nhất với giá hiện tại)
Tương tự cho cụm Sell (nhưng chiều ngược lại — lệnh Sell lỗ khi giá tăng):
- MinPrice(Sell) = min{OpenPrice(pᵢ)} — lệnh Sell mở thấp nhất
- MaxPrice(Sell) = max{OpenPrice(pᵢ)} — lệnh Sell mở cao nhất (xa nhất theo hướng lỗ)
1.2. Ứng Dụng 1: Điều Kiện Mở Tầng Lưới Mới
Bot Nhị Quái V6 Pro chỉ mở thêm tầng Buy mới khi thỏa mãn đồng thời:
// Điều kiện nhồi lệnh Buy:
bool CanOpenNewBuy = (current_ask < MinPrice(Buy) - GridStep)
&& (CountBuyPositions(magic) RequiredMargin);
Nếu không có MinPrice chính xác, bot có thể mở lệnh quá gần tầng cũ (nhồi kép) hoặc bỏ lỡ cơ hội nhồi tại vùng giá hợp lý. Cả hai đều gây hại nghiêm trọng cho hiệu suất hệ thống.
1.3. Ứng Dụng 2: Tính Điểm Basket TP
Basket TP (chốt lời tập thể toàn cụm) được kích hoạt khi tổng P&L của cụm đạt ngưỡng mục tiêu. Điểm kích hoạt được tính từ MinPrice và MaxPrice:
// Giá kích hoạt Basket TP cho cụm Buy:
double BasketTP_Price = MinPrice(Buy) + GridStep * BasketTPMultiplier;
// Khi giá hiện tại >= BasketTP_Price → đóng toàn bộ cụm Buy
1.4. Ứng Dụng 3: Đo Rộng Vùng Gồng Lỗ (Drawdown Width)
double DrawdownWidth = MaxPrice(Buy) - MinPrice(Buy);
// = Khoảng cách từ lệnh đầu tiên đến lệnh cuối cùng
// Dùng để estimate margin đang bị giam và mức độ rủi ro
2. Thiết Kế Thuật Toán Quét Vị Thế
2.1. Vì Sao Phải Quét Trực Tiếp Từ Sàn?
Câu hỏi hợp lý: “Tại sao không lưu MinPrice/MaxPrice vào biến global khi mở từng lệnh, thay vì quét lại toàn bộ mỗi tick?”
Câu trả lời: VPS crash, mất điện, mạng ngắt đột ngột. Khi bot khởi động lại sau sự cố, tất cả biến trong RAM đều mất sạch. Nếu MinPrice/MaxPrice chỉ được lưu trong biến, bot khởi động lại sẽ không biết các lệnh hiện có đang ở đâu — dẫn đến:
- Mở lệnh nhồi tại vị trí sai (không biết tầng thấp nhất đang ở đâu)
- Tính toán Basket TP sai (điểm chốt lời lệch)
- Equity Lockdown không kích hoạt đúng lúc
Quét trực tiếp từ danh sách vị thế đang mở trên server sàn (PositionsTotal()) đảm bảo dữ liệu luôn đúng 100% bất kể lịch sử khởi động. Đây là nguyên tắc “stateless resilience” — bot không phụ thuộc vào memory nội bộ mà luôn đọc truth từ nguồn đáng tin cậy nhất là sàn giao dịch.
2.2. Thuật Toán Linear Scan O(n)
Với n lệnh đang mở, thuật toán quét tuyến tính đơn giản nhất có độ phức tạp O(n):
void GetGridExtremes(int magic, ENUM_POSITION_TYPE pos_type,
double &min_price, double &max_price, int &count) {
min_price = DBL_MAX; // Khởi tạo cực đại (để so sánh nhỏ hơn)
max_price = DBL_MIN; // Khởi tạo cực tiểu (để so sánh lớn hơn)
count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
// Lọc theo symbol và magic number
if(!m_position.SelectByIndex(i)) continue;
if(m_position.Symbol() != _Symbol) continue;
if(m_position.Magic() != magic) continue;
if(m_position.PositionType() != pos_type) continue;
double open_price = m_position.PriceOpen();
if(open_price max_price) max_price = open_price;
count++;
}
// Reset nếu không có vị thế
if(count == 0) {
min_price = 0.0;
max_price = 0.0;
}
}
Lưu ý về hướng duyệt: Duyệt ngược từ PositionsTotal()-1 xuống 0 thay vì xuôi từ 0 lên. Lý do: khi có lệnh đóng trong quá trình duyệt (do execution ở luồng khác), index của các lệnh còn lại sẽ thay đổi. Duyệt ngược tránh được lỗi skip lệnh do index shift.
3. Mã Nguồn MQL5 Đầy Đủ Thực Chiến
3.1. Hàm Cốt Lõi GetGridExtremes() Phiên Bản Nâng Cao
//+------------------------------------------------------------------+
//| Struct lưu toàn bộ thông tin biên độ lưới |
//+------------------------------------------------------------------+
struct SGridInfo {
double min_open; // Giá mở thấp nhất trong cụm
double max_open; // Giá mở cao nhất trong cụm
double total_volume; // Tổng khối lượng đang gồng
double total_profit; // Tổng P&L hiện tại (tính cả swap)
int count; // Số lệnh đang mở
double avg_open; // Giá mở trung bình (VWAP)
void Reset() {
min_open = 0.0;
max_open = 0.0;
total_volume = 0.0;
total_profit = 0.0;
count = 0;
avg_open = 0.0;
}
};
//+------------------------------------------------------------------+
//| Quét toàn bộ thông tin biên độ lưới cho một cụm vị thế |
//+------------------------------------------------------------------+
SGridInfo GetGridInfo(int magic, ENUM_POSITION_TYPE pos_type) {
SGridInfo info;
info.min_open = DBL_MAX;
info.max_open = -DBL_MAX;
info.total_volume = 0.0;
info.total_profit = 0.0;
info.count = 0;
double vwap_numerator = 0.0; // Cho tính VWAP: sum(price * volume)
for(int i = PositionsTotal() - 1; i >= 0; i--) {
if(!m_position.SelectByIndex(i)) continue;
if(m_position.Symbol() != _Symbol) continue;
if(m_position.Magic() != magic) continue;
if(m_position.PositionType() != pos_type) continue;
double open = m_position.PriceOpen();
double vol = m_position.Volume();
double profit = m_position.Profit() + m_position.Swap();
// Cập nhật min/max
if(open info.max_open) info.max_open = open;
// Tích lũy
info.total_volume += vol;
info.total_profit += profit;
vwap_numerator += open * vol;
info.count++;
}
if(info.count > 0) {
// Tính giá mở trung bình có trọng số khối lượng (VWAP)
info.avg_open = NormalizeDouble(vwap_numerator / info.total_volume,
(int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
} else {
info.min_open = 0.0;
info.max_open = 0.0;
}
return info;
}
3.2. Sử Dụng Trong OnTick()
void OnTick() {
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
// Quét thông tin cụm Buy
SGridInfo buy_info = GetGridInfo(InpMagicBuy, POSITION_TYPE_BUY);
// Quét thông tin cụm Sell
SGridInfo sell_info = GetGridInfo(InpMagicSell, POSITION_TYPE_SELL);
// === Logic mở lệnh Buy mới ===
if(buy_info.count == 0) {
// Không có lệnh Buy nào → mở lệnh đầu tiên
OpenFirstBuy(ask);
} else if(buy_info.count < InpMaxLayers) {
// Kiểm tra điều kiện nhồi tầng mới
double next_buy_level = buy_info.min_open - InpGridStep * _Point;
if(ask 0 && buy_info.total_profit >= InpBasketTP_USD) {
CloseAllBuyPositions();
PrintFormat("Basket TP triggered: %d lệnh, Profit = %.2f USD",
buy_info.count, buy_info.total_profit);
}
// === Hiển thị thông tin trên dashboard ===
ShowDashboard(buy_info, sell_info, bid, ask);
}
3.3. Tối Ưu: Cache Kết Quả Trong Tick Hiện Tại
// Vấn đề: Nếu nhiều function trong OnTick() đều gọi GetGridInfo()
// → quét PositionsTotal() nhiều lần/tick → tốn CPU không cần thiết
// Giải pháp: Cache kết quả trong struct global, refresh mỗi tick một lần
SGridInfo g_buy_cache;
SGridInfo g_sell_cache;
datetime g_cache_time = 0;
int g_cache_tick = 0;
void RefreshGridCache() {
int current_tick = (int)SymbolInfoInteger(_Symbol, SYMBOL_SESSION_DEALS);
if(current_tick == g_cache_tick) return; // Đã refresh tick này rồi
g_buy_cache = GetGridInfo(InpMagicBuy, POSITION_TYPE_BUY);
g_sell_cache = GetGridInfo(InpMagicSell, POSITION_TYPE_SELL);
g_cache_tick = current_tick;
g_cache_time = TimeCurrent();
}
void OnTick() {
RefreshGridCache(); // Chỉ quét 1 lần/tick
// Sử dụng cache ở nhiều nơi mà không tốn CPU
if(NeedOpenNewBuy(g_buy_cache)) OpenBuy();
if(NeedBasketTP(g_buy_cache)) CloseBuyCluster();
if(NeedLockdown(g_buy_cache, g_sell_cache)) ActivateLockdown();
ShowDashboard(g_buy_cache, g_sell_cache);
}
4. Sơ Đồ Thuật Toán Quét Min-Max
Thuật Toán Quét Min-Max Biên Độ Lưới Vị Thế
OnTick() gọi GetGridInfo()
min=DBL_MAX, max=-DBL_MAX, count=0
for i = PositionsTotal()-1 → 0
Symbol && Magic && Type OK?
Skip → tiếp
if price < min → min = price
if price > max → max = price
count++, total_vol += vol
YES
NO
i–
5. Edge Cases Quan Trọng
5.1. Bot Mới Khởi Động — Chưa Có Lệnh Nào
// Trường hợp: count = 0 sau khi quét
// Kết quả: min_open = DBL_MAX, max_open = -DBL_MAX (giá trị khởi tạo)
// Bot phải kiểm tra count > 0 trước khi dùng min/max:
if(buy_info.count > 0) {
double next_level = buy_info.min_open - InpGridStep * _Point;
// ... logic nhồi lệnh
} else {
// Không có lệnh → mở lệnh đầu tiên tại giá hiện tại
OpenFirstBuy(ask);
}
5.2. Lệnh Đang Trong Trạng Thái Pending (Chưa Khớp)
// PositionsTotal() CHỈ trả về lệnh đã KHỚP (market orders)
// Pending orders (limit, stop) nằm ở OrdersTotal()
// → Nếu bot dùng pending orders, cần bổ sung logic riêng:
void GetPendingExtremes(int magic, ENUM_ORDER_TYPE order_type,
double &min_price, double &max_price) {
min_price = DBL_MAX;
max_price = -DBL_MAX;
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(!OrderSelect(i)) continue;
if(OrderMagicNumber() != magic) continue;
if(OrderType() != order_type) continue;
double op = OrderOpenPrice();
if(op max_price) max_price = op;
}
}
5.3. Nhiều Magic Number Trên Cùng Một Symbol
// Nhị Quái V6 Pro thường dùng nhiều magic số để tách biệt cụm lệnh:
// Magic 1001 = Cụm Buy EURUSD chiến lược A
// Magic 1002 = Cụm Sell EURUSD chiến lược A
// Magic 2001 = Cụm Buy XAUUSD chiến lược B
// GetGridInfo() đã lọc theo magic → tự động tách biệt
// Không cần thêm logic phân tách thủ công
5.4. Lệnh Đang Trong Quá Trình Đóng (Partial Close)
// Khi bot thực hiện đóng một phần lệnh, volume giảm xuống
// Nhưng lệnh vẫn xuất hiện trong PositionsTotal() với volume mới
// → GetGridInfo() tự động cập nhật total_volume đúng
// Edge case cần chú ý: sau partial close, một lệnh có thể có
// volume rất nhỏ (0.01) nhưng vẫn "chiếm" min/max price
// → Có thể thêm filter volume tối thiểu nếu cần:
if(m_position.Volume() >= InpMinVolumeToCount) {
// Chỉ tính vào min/max nếu volume đủ lớn
}
6. Benchmark Hiệu Năng Thực Tế
6.1. Chi Phí CPU Của Vòng Lặp Quét
Trên VPS điển hình với 500 vị thế đang mở (tình huống cực đoan với nhiều cặp tiền):
| Số lệnh đang mở | Thời gian quét | Ảnh hưởng |
|---|---|---|
| 5 lệnh (tiêu biểu) | < 1 μs | Không đáng kể |
| 20 lệnh | 2-5 μs | Không đáng kể |
| 100 lệnh | 10-20 μs | Rất nhỏ |
| 500 lệnh | 50-100 μs | Nên dùng cache |
Kết luận thực tế: Với bot lưới thông thường (5-20 lệnh/cụm, 1-4 cặp tiền), tổng thời gian quét 10 cặp tiền với mỗi cặp > 50 lệnh.
6.2. Tối Ưu Nâng Cao: Incremental Update
// Thay vì quét lại toàn bộ mỗi tick,
// chỉ update khi có sự kiện thực sự thay đổi vị thế:
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result) {
// Sự kiện mở lệnh mới hoặc đóng lệnh
if(trans.type == TRADE_TRANSACTION_DEAL_ADD) {
// Đánh dấu cache cần refresh
g_cache_valid = false;
}
}
// OnTick() chỉ refresh cache khi cần thiết
void OnTick() {
if(!g_cache_valid) {
g_buy_cache = GetGridInfo(InpMagicBuy, POSITION_TYPE_BUY);
g_cache_valid = true;
}
// Sử dụng g_buy_cache mà không tốn CPU
}
7. Tích Hợp Với Các Thuật Toán Khác Trong V6 Pro
7.1. Mối Quan Hệ Với Thuật Toán 1 (Chuẩn Hóa)
MinPrice và MaxPrice trả về từ GetGridInfo() cần được chuẩn hóa trước khi dùng trong tính toán cấp độ lưới:
// Tính giá tầng lưới tiếp theo (không cần chuẩn hóa thêm vì
// open_price của lệnh đã được normalize khi gửi — Thuật Toán 1)
double next_level = buy_info.min_open - InpGridStep * _Point;
// Nhưng bước nhảy tính từ _Point cần normalize:
next_level = NormalizePrice(next_level); // Từ Thuật Toán 1
7.2. Đầu Vào Cho Thuật Toán 3 (Volume Bias)
GetGridInfo() cũng trả về total_volume — đây là đầu vào trực tiếp cho Thuật Toán 3 (tính Delta Volume giữa Buy và Sell để kích hoạt Equity Lockdown hedging).
7.3. Đầu Vào Cho Basket TP (Bài 8)
total_profit trong SGridInfo là tổng P&L tức thời của toàn cụm — tham số chính để Basket TP quyết định đóng tập thể khi đạt ngưỡng lợi nhuận mục tiêu.
8. Debugging Và Logging
// Thêm vào Dashboard hoặc Print để debug
void PrintGridStatus(const SGridInfo &info, string cluster_name) {
if(info.count == 0) {
PrintFormat("[%s] Không có vị thế nào", cluster_name);
return;
}
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double spread = (info.max_open - info.min_open) / point;
PrintFormat("[%s] Count=%d | Vol=%.2f | P&L=%.2f USD",
cluster_name, info.count, info.total_volume, info.total_profit);
PrintFormat("[%s] Min=%.{%d}f | Max=%.{%d}f | Spread=%.1f pts | AvgOpen=%.{%d}f",
cluster_name,
info.min_open, digits,
info.max_open, digits,
spread,
info.avg_open, digits);
}
9. Kết Luận: Đôi Mắt Của Bot Nhị Quái
Thuật Toán Quét Min-Max là “đôi mắt” của Bot Nhị Quái V6 Pro — không có nó, bot mù hoàn toàn về vị trí của chính các lệnh mình đang gồng. Với vòng lặp O(n) đơn giản nhưng hiệu quả, bot luôn có:
- MinPrice chính xác để quyết định nhồi tầng hay không
- MaxPrice để tính khoảng cách drawdown
- TotalVolume để phát hiện mất cân bằng và kích hoạt Lockdown Hedging
- TotalProfit để kích hoạt Basket TP đúng thời điểm
- VWAP (giá trung bình có trọng số) để phân tích hiệu suất
Điều quan trọng nhất: thuật toán này stateless — kết quả luôn chính xác dù bot restart bao nhiêu lần, dù VPS mất điện giữa chừng. Đây là nền tảng của sự ổn định trong hệ thống giao dịch tự động thực chiến.
Trong bài tiếp theo, chúng ta sẽ đi vào Thuật Toán 3 — Tính Toán Độ Lệch Khối Lượng Ròng (Volume Bias): cách bot đo lường sự mất cân bằng giữa cụm Buy và Sell để kích hoạt chiến lược hedge bảo vệ tài khoản trong tình huống thị trường biến động cực mạnh.
🎓 Học Xây Dựng Bot Nhị Quái V6 Pro Từ A Đến Z
Khóa học Lập trình MT5 Nâng cao — Hedging & Tự động hóa dạy toàn bộ 5 thuật toán cốt lõi, kiến trúc FSM đa tầng, Basket TP và Equity Lockdown theo chuẩn thực chiến.
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.