1025 lines
48 KiB
Plaintext
1025 lines
48 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| ICT EA – FVG Edition |
|
||
//| Architecture: 3 TF | 4 State Machine |
|
||
//| |
|
||
//| Flow: |
|
||
//| D1 Bias + H1 Trend đồng thuận |
|
||
//| → Tìm FVG trên H1 (EA_IDLE) |
|
||
//| → Chờ price chạm (EA_WAIT_TOUCH) |
|
||
//| → Chờ trigger M5 (EA_WAIT_TRIGGER) |
|
||
//| → Vào lệnh (EA_IN_TRADE) |
|
||
//+------------------------------------------------------------------+
|
||
#property strict
|
||
|
||
//====================================================
|
||
// INPUTS
|
||
//====================================================
|
||
|
||
input ENUM_TIMEFRAMES InpBiasTF = PERIOD_D1; // Timeframe xác định bias HTF (thường D1)
|
||
input ENUM_TIMEFRAMES InpMiddleTF = PERIOD_H1; // Timeframe tìm FVG + xác định trend MTF
|
||
input ENUM_TIMEFRAMES InpTriggerTF = PERIOD_M5; // Timeframe xác nhận entry (nến trigger)
|
||
|
||
input double InpRiskPercent = 1.0; // % balance chấp nhận rủi ro mỗi lệnh
|
||
input double InpRiskReward = 2.0; // Tỉ lệ TP/SL (TP = SL × RR)
|
||
input double InpMaxDailyLossPct = 3.0; // % loss tối đa trong ngày → dừng trade
|
||
|
||
input int InpLondonStartHour = 8; // Giờ mở phiên London (UTC)
|
||
input int InpLondonEndHour = 17; // Giờ đóng phiên London (UTC)
|
||
input int InpNYStartHour = 13; // Giờ mở phiên New York (UTC)
|
||
input int InpNYEndHour = 22; // Giờ đóng phiên New York (UTC)
|
||
input int InpSessionAvoidLastMin = 60; // Tránh N phút cuối mỗi phiên (không vào lệnh)
|
||
|
||
input int InpSwingRange = 2; // Số nến xác nhận mỗi bên của swing point
|
||
// VD: 2 → bar[i] phải cao hơn bar[i±1] và bar[i±2]
|
||
input int InpSwingLookback = 50; // Số bar MiddleTF quét ngược để tìm swing H/L
|
||
|
||
input int InpFVGMaxAliveMin = 4320; // FVG sống tối đa (phút) → 4320 = 72 giờ
|
||
input int InpFVGScanBars = 50; // Số bar MiddleTF quét ngược để tìm FVG candidate
|
||
input double InpFVGMinBodyPct = 60.0; // Nến giữa của pattern FVG: body/range >= X%
|
||
input int InpTriggerMaxBars = 30; // Timeout trigger: sau N bar TriggerTF → bỏ qua
|
||
|
||
input bool InpDebugLog = true; // In log chi tiết vào Journal
|
||
input bool InpDebugDraw = true; // Vẽ FVG, swing points, debug panel lên chart
|
||
|
||
//====================================================
|
||
// ENUMS
|
||
//====================================================
|
||
|
||
enum EAState
|
||
{
|
||
EA_IDLE, // Đang chờ: bias hợp lệ + MiddleTF trend đồng thuận + tìm FVG
|
||
EA_WAIT_TOUCH, // Đã có FVG candidate, đang chờ giá retracement chạm vùng FVG
|
||
EA_WAIT_TRIGGER, // Giá đã chạm FVG, đang chờ TriggerTF xác nhận entry
|
||
EA_IN_TRADE // Đang có lệnh mở, theo dõi cho đến khi đóng
|
||
};
|
||
|
||
enum HTFBias
|
||
{
|
||
BIAS_NONE = 0, // Chưa xác định được bias (chưa đủ bar)
|
||
BIAS_UP = 1, // Bias tăng – ưu tiên tìm lệnh Buy
|
||
BIAS_DOWN = -1, // Bias giảm – ưu tiên tìm lệnh Sell
|
||
BIAS_SIDEWAY = 2 // Sideway – không trade (không có bias rõ ràng)
|
||
};
|
||
|
||
enum MarketDir
|
||
{
|
||
DIR_NONE = 0, // Chưa xác định hướng
|
||
DIR_UP = 1, // Hướng tăng (uptrend hoặc bullish FVG)
|
||
DIR_DOWN = -1 // Hướng giảm (downtrend hoặc bearish FVG)
|
||
};
|
||
|
||
enum BlockReason
|
||
{
|
||
BLOCK_NONE, // Không bị block, EA hoạt động bình thường
|
||
BLOCK_SESSION, // Ngoài giờ giao dịch được phép
|
||
BLOCK_DAILY_LOSS, // Đã chạm mức loss tối đa trong ngày
|
||
BLOCK_BIAS_MISMATCH, // Bias D1 và MiddleTF trend ngược chiều nhau
|
||
BLOCK_NO_BIAS // Bias là SIDEWAY hoặc NONE → không có hướng rõ
|
||
};
|
||
|
||
//====================================================
|
||
// STRUCTS
|
||
//====================================================
|
||
|
||
struct BiasContext
|
||
{
|
||
HTFBias bias; // Bias hiện tại của BiasTF
|
||
double rangeHigh; // Đỉnh range khi bias = SIDEWAY (dùng để hiển thị)
|
||
double rangeLow; // Đáy range khi bias = SIDEWAY
|
||
datetime lastBarTime; // Thời điểm bar BiasTF cuối cùng được xử lý (guard chống update lặp)
|
||
};
|
||
|
||
struct MiddleTFTrend
|
||
{
|
||
MarketDir trend; // Trend hiện tại của MiddleTF: DIR_UP / DIR_DOWN / DIR_NONE
|
||
|
||
// Swing structure – 2 điểm gần nhất, [0] = mới hơn
|
||
double h0; // Swing High gần nhất (Higher High hoặc Lower High)
|
||
double h1; // Swing High trước đó
|
||
double l0; // Swing Low gần nhất (Higher Low hoặc Lower Low)
|
||
double l1; // Swing Low trước đó
|
||
|
||
int idxH0; // Bar index của h0 trên MiddleTF (dùng để vẽ)
|
||
int idxH1; // Bar index của h1
|
||
int idxL0; // Bar index của l0
|
||
int idxL1; // Bar index của l1
|
||
|
||
double keyLevel; // Level quan trọng nhất cần theo dõi:
|
||
// Uptrend → keyLevel = l0 (close < l0 = MSS, trend đổi)
|
||
// Downtrend→ keyLevel = h0 (close > h0 = MSS, trend đổi)
|
||
|
||
datetime lastBarTime; // Guard: chỉ recalculate khi có bar MiddleTF mới
|
||
};
|
||
|
||
struct FVGContext
|
||
{
|
||
bool active; // true = đang tracking một FVG (state >= WAIT_TOUCH)
|
||
bool touched; // true = giá đã chạm vào vùng FVG ít nhất 1 lần
|
||
|
||
MarketDir direction; // Hướng FVG: DIR_UP (bullish) hoặc DIR_DOWN (bearish)
|
||
double high; // Cạnh trên của vùng FVG
|
||
double low; // Cạnh dưới của vùng FVG
|
||
double mid; // Midpoint = (high+low)/2 – dùng làm entry tham chiếu
|
||
|
||
datetime createdTime; // Thời điểm nến phải của pattern đóng = FVG được confirm
|
||
datetime touchTime; // Thời điểm giá chạm FVG lần đầu
|
||
|
||
int barsAlive; // Số bar TriggerTF đã trôi qua kể từ khi touched
|
||
int touchBarIdx; // Bar index TriggerTF tại thời điểm touched (dùng đếm barsAlive)
|
||
};
|
||
|
||
struct TriggerContext
|
||
{
|
||
bool valid; // true = đã xác định được swing structure trên TriggerTF
|
||
MarketDir direction; // Hướng trigger đang chờ
|
||
double swingHigh; // Swing high gần nhất trên TriggerTF (sau khi touched FVG)
|
||
double swingLow; // Swing low gần nhất trên TriggerTF
|
||
int idxHigh; // Bar index của swingHigh
|
||
int idxLow; // Bar index của swingLow
|
||
double breakLevel; // Level cần bị phá vỡ để xác nhận trigger entry
|
||
};
|
||
|
||
struct OrderPlan
|
||
{
|
||
bool valid; // true = plan đã được tính, có thể gửi lệnh
|
||
int direction; // 1 = Buy, -1 = Sell
|
||
double entry; // Giá vào lệnh
|
||
double stopLoss; // Giá stop loss
|
||
double takeProfit; // Giá take profit
|
||
double lot; // Khối lượng lô
|
||
};
|
||
|
||
struct DailyRiskContext
|
||
{
|
||
double startBalance; // Balance lúc đầu ngày (chụp khi bar D1 mới mở)
|
||
double currentBalance; // Balance hiện tại (cache mỗi tick, tránh gọi API 2 lần)
|
||
datetime dayStartTime; // Thời điểm bar D1 hiện tại mở (ID của ngày)
|
||
bool limitHit; // true = đã chạm daily loss limit → không trade thêm hôm nay
|
||
};
|
||
|
||
//====================================================
|
||
// GLOBAL STATE
|
||
//====================================================
|
||
|
||
EAState g_State = EA_IDLE; // Trạng thái hiện tại của state machine
|
||
BlockReason g_BlockReason = BLOCK_NONE; // Lý do EA bị block (hiển thị trên debug panel)
|
||
|
||
BiasContext g_Bias; // Context bias HTF (D1)
|
||
MiddleTFTrend g_MiddleTrend; // Context trend + swing structure MTF (H1)
|
||
FVGContext g_FVG; // Context FVG đang được tracking
|
||
TriggerContext g_Trigger; // Context trigger entry (M5)
|
||
DailyRiskContext g_DailyRisk; // Context quản lý risk theo ngày
|
||
|
||
int g_TradeBarIndex = -1; // Bar index TriggerTF lúc gửi lệnh (guard chống double-entry)
|
||
|
||
// Tên cố định cho FVG drawing objects (chỉ 1 FVG active tại một thời điểm)
|
||
const string FVG_RECT_NAME = "FVG_RECT"; // Rectangle vùng FVG
|
||
const string FVG_MID_NAME = "FVG_MID"; // Đường midpoint (đứt nét)
|
||
const string FVG_LABEL_NAME = "FVG_LABEL"; // Text label "FVG ▲/▼"
|
||
|
||
// Prefix cho swing drawing objects – dùng ObjectsDeleteAll(0, SW_PREFIX) để xóa toàn bộ
|
||
const string SW_PREFIX = "SW_";
|
||
|
||
//====================================================
|
||
// SWING DETECTION HELPERS
|
||
//====================================================
|
||
|
||
// Trả về true nếu bar[i] là Swing High hợp lệ:
|
||
// bar[i].high > bar[i-k].high với mọi k trong [1..InpSwingRange] (nến bên phải / newer)
|
||
// bar[i].high > bar[i+k].high với mọi k trong [1..InpSwingRange] (nến bên trái / older)
|
||
// Ý nghĩa: đỉnh cục bộ được xác nhận bởi InpSwingRange nến ở mỗi phía
|
||
bool IsSwingHighAt(ENUM_TIMEFRAMES tf, int i)
|
||
{
|
||
double price = iHigh(_Symbol, tf, i);
|
||
for (int k = 1; k <= InpSwingRange; k++)
|
||
{
|
||
if (iHigh(_Symbol, tf, i - k) >= price) return false; // Nến newer (index thấp hơn) cao hơn → không phải đỉnh
|
||
if (iHigh(_Symbol, tf, i + k) >= price) return false; // Nến older (index cao hơn) cao hơn → không phải đỉnh
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Tương tự IsSwingHighAt nhưng cho đáy cục bộ
|
||
bool IsSwingLowAt(ENUM_TIMEFRAMES tf, int i)
|
||
{
|
||
double price = iLow(_Symbol, tf, i);
|
||
for (int k = 1; k <= InpSwingRange; k++)
|
||
{
|
||
if (iLow(_Symbol, tf, i - k) <= price) return false; // Nến newer thấp hơn → không phải đáy
|
||
if (iLow(_Symbol, tf, i + k) <= price) return false; // Nến older thấp hơn → không phải đáy
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//====================================================
|
||
// CONTEXT UPDATERS
|
||
//====================================================
|
||
|
||
// Phân loại bias dựa trên 2 bar BiasTF đã đóng gần nhất:
|
||
// b1 = bar[1] (vừa đóng), b2 = bar[2] (trước đó)
|
||
// Rule 1: b1.close > b2.high → BIAS_UP (breakout rõ ràng lên trên)
|
||
// Rule 2: b1.close < b2.low → BIAS_DOWN (breakout rõ ràng xuống dưới)
|
||
// Rule 3: b1.high > b2.high nhưng close < b2.high → BIAS_DOWN (sweep đỉnh rồi reject)
|
||
// Rule 4: b1.low < b2.low nhưng close > b2.low → BIAS_UP (sweep đáy rồi recover)
|
||
// Rule 5: không khớp rule nào → BIAS_SIDEWAY
|
||
HTFBias ResolveBias(double b1High, double b1Low, double b1Close,
|
||
double b2High, double b2Low)
|
||
{
|
||
if (b1Close > b2High) return BIAS_UP; // Đóng cửa vượt đỉnh bar trước
|
||
if (b1Close < b2Low) return BIAS_DOWN; // Đóng cửa xuyên đáy bar trước
|
||
if (b1High > b2High && b1Close < b2High) return BIAS_DOWN; // Wick phá đỉnh nhưng close quay về → bearish
|
||
if (b1Low < b2Low && b1Close > b2Low) return BIAS_UP; // Wick phá đáy nhưng close quay lại → bullish
|
||
return BIAS_SIDEWAY; // Không có tín hiệu rõ
|
||
}
|
||
|
||
void UpdateBiasContext()
|
||
{
|
||
datetime currentBarTime = iTime(_Symbol, InpBiasTF, 0);
|
||
if (currentBarTime == g_Bias.lastBarTime) return; // Guard: chỉ tính lại khi có bar BiasTF mới
|
||
|
||
g_Bias.lastBarTime = currentBarTime;
|
||
|
||
if (Bars(_Symbol, InpBiasTF) < 4) { g_Bias.bias = BIAS_NONE; return; } // Chưa đủ bar lịch sử
|
||
|
||
double b1High = iHigh (_Symbol, InpBiasTF, 1); // Bar đã đóng gần nhất
|
||
double b1Low = iLow (_Symbol, InpBiasTF, 1);
|
||
double b1Close = iClose(_Symbol, InpBiasTF, 1);
|
||
double b2High = iHigh (_Symbol, InpBiasTF, 2); // Bar trước đó
|
||
double b2Low = iLow (_Symbol, InpBiasTF, 2);
|
||
|
||
HTFBias prevBias = g_Bias.bias;
|
||
g_Bias.bias = ResolveBias(b1High, b1Low, b1Close, b2High, b2Low);
|
||
|
||
// Lưu range của b2 để hiển thị khi sideway (giúp trader thấy vùng congestion)
|
||
g_Bias.rangeHigh = (g_Bias.bias == BIAS_SIDEWAY) ? b2High : 0;
|
||
g_Bias.rangeLow = (g_Bias.bias == BIAS_SIDEWAY) ? b2Low : 0;
|
||
|
||
if (InpDebugLog && g_Bias.bias != prevBias) // Log khi bias thay đổi (không log mỗi bar)
|
||
PrintFormat("[BIAS] %s → %s | b1[H=%.5f L=%.5f C=%.5f] b2[H=%.5f L=%.5f]",
|
||
EnumToString(prevBias), EnumToString(g_Bias.bias),
|
||
b1High, b1Low, b1Close, b2High, b2Low);
|
||
}
|
||
|
||
//====================================================
|
||
// UPDATE MIDDLETF TREND
|
||
//
|
||
// Mục tiêu: xác định xu hướng cấu trúc (market structure) trên MiddleTF
|
||
//
|
||
// Quét ngược từ bar[SwingRange+1] trở về, thu thập:
|
||
// h0, h1 = 2 swing high gần nhất (h0 mới hơn h1)
|
||
// l0, l1 = 2 swing low gần nhất (l0 mới hơn l1)
|
||
//
|
||
// Điều kiện UPTREND (Higher High + Higher Low, key level chưa bị phá):
|
||
// h0 > h1 (đỉnh sau cao hơn đỉnh trước = Higher High)
|
||
// l0 > l1 (đáy sau cao hơn đáy trước = Higher Low)
|
||
// bar[1].close > l0 (giá chưa close dưới l0 → cấu trúc uptrend còn nguyên)
|
||
// keyLevel = l0 → nếu close < l0 thì MSS (Market Structure Shift) xảy ra
|
||
//
|
||
// Điều kiện DOWNTREND (Lower High + Lower Low):
|
||
// h0 < h1 (Lower High)
|
||
// l0 < l1 (Lower Low)
|
||
// bar[1].close < h0 (giá chưa close trên h0 → cấu trúc downtrend còn nguyên)
|
||
// keyLevel = h0 → nếu close > h0 thì MSS xảy ra
|
||
//====================================================
|
||
|
||
void UpdateMiddleTfTrendContext()
|
||
{
|
||
datetime currentBarTime = iTime(_Symbol, InpMiddleTF, 0);
|
||
if (currentBarTime == g_MiddleTrend.lastBarTime) return; // Guard: 1 lần/bar MiddleTF
|
||
|
||
g_MiddleTrend.lastBarTime = currentBarTime;
|
||
|
||
// Giới hạn vùng quét: không vượt quá số bar thực tế có sẵn
|
||
int maxBar = MathMin(InpSwingLookback, Bars(_Symbol, InpMiddleTF) - InpSwingRange - 2);
|
||
|
||
double highs[2]; int hiIdx[2]; int hc = 0; // Mảng lưu 2 swing high, hc = đếm số đã tìm được
|
||
double lows[2]; int loIdx[2]; int lc = 0;
|
||
|
||
// Bắt đầu từ SwingRange+1 để đảm bảo có đủ InpSwingRange nến bên phải (newer) xác nhận swing
|
||
for (int i = InpSwingRange + 1; i <= maxBar; i++)
|
||
{
|
||
if (hc < 2 && IsSwingHighAt(InpMiddleTF, i))
|
||
{
|
||
highs[hc] = iHigh(_Symbol, InpMiddleTF, i); // Lưu giá high của swing
|
||
hiIdx[hc] = i; // Lưu bar index để vẽ sau
|
||
hc++;
|
||
}
|
||
if (lc < 2 && IsSwingLowAt(InpMiddleTF, i))
|
||
{
|
||
lows[lc] = iLow(_Symbol, InpMiddleTF, i);
|
||
loIdx[lc] = i;
|
||
lc++;
|
||
}
|
||
if (hc == 2 && lc == 2) break; // Đã đủ 2 high + 2 low → dừng sớm, tiết kiệm CPU
|
||
}
|
||
|
||
if (hc < 2 || lc < 2) // Không đủ swing points trong vùng lookback
|
||
{
|
||
g_MiddleTrend.trend = DIR_NONE;
|
||
return;
|
||
}
|
||
|
||
// highs[0]/lows[0] = gần nhất (newer), highs[1]/lows[1] = xa hơn (older)
|
||
g_MiddleTrend.h0 = highs[0]; g_MiddleTrend.idxH0 = hiIdx[0];
|
||
g_MiddleTrend.h1 = highs[1]; g_MiddleTrend.idxH1 = hiIdx[1];
|
||
g_MiddleTrend.l0 = lows[0]; g_MiddleTrend.idxL0 = loIdx[0];
|
||
g_MiddleTrend.l1 = lows[1]; g_MiddleTrend.idxL1 = loIdx[1];
|
||
|
||
double bar1Close = iClose(_Symbol, InpMiddleTF, 1); // Bar đã đóng → dùng close để check MSS
|
||
MarketDir prevTrend = g_MiddleTrend.trend; // Lưu trend cũ để detect thay đổi khi log
|
||
|
||
if (g_MiddleTrend.h0 > g_MiddleTrend.h1 && // Higher High: đỉnh mới cao hơn đỉnh cũ
|
||
g_MiddleTrend.l0 > g_MiddleTrend.l1 && // Higher Low: đáy mới cao hơn đáy cũ
|
||
bar1Close > g_MiddleTrend.l0) // Close vẫn trên l0 → cấu trúc HH-HL chưa bị phá
|
||
{
|
||
g_MiddleTrend.trend = DIR_UP;
|
||
g_MiddleTrend.keyLevel = g_MiddleTrend.l0; // l0 là key level: phá = MSS → trend có thể đổi
|
||
}
|
||
else if (g_MiddleTrend.h0 < g_MiddleTrend.h1 && // Lower High: đỉnh mới thấp hơn đỉnh cũ
|
||
g_MiddleTrend.l0 < g_MiddleTrend.l1 && // Lower Low: đáy mới thấp hơn đáy cũ
|
||
bar1Close < g_MiddleTrend.h0) // Close vẫn dưới h0 → cấu trúc LH-LL chưa bị phá
|
||
{
|
||
g_MiddleTrend.trend = DIR_DOWN;
|
||
g_MiddleTrend.keyLevel = g_MiddleTrend.h0; // h0 là key level: phá = MSS → trend có thể đổi
|
||
}
|
||
else // Không rõ HH-HL hay LH-LL, hoặc key level đã bị close phá → cấu trúc không rõ ràng
|
||
{
|
||
g_MiddleTrend.trend = DIR_NONE;
|
||
g_MiddleTrend.keyLevel = 0;
|
||
}
|
||
|
||
if (InpDebugLog && g_MiddleTrend.trend != prevTrend)
|
||
PrintFormat("[MIDDLE TREND] %s → %s | H0=%.5f H1=%.5f L0=%.5f L1=%.5f | KeyLvl=%.5f",
|
||
EnumToString(prevTrend), EnumToString(g_MiddleTrend.trend),
|
||
g_MiddleTrend.h0, g_MiddleTrend.h1,
|
||
g_MiddleTrend.l0, g_MiddleTrend.l1,
|
||
g_MiddleTrend.keyLevel);
|
||
}
|
||
|
||
void UpdateDailyRiskContext()
|
||
{
|
||
datetime todayBarTime = iTime(_Symbol, PERIOD_D1, 0); // "ID" của ngày: thay đổi khi bar D1 mới mở
|
||
|
||
if (todayBarTime != g_DailyRisk.dayStartTime) // Bar D1 mới = ngày mới → reset daily risk
|
||
{
|
||
g_DailyRisk.dayStartTime = todayBarTime;
|
||
g_DailyRisk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Chụp balance đầu ngày
|
||
// Dùng BALANCE (không dùng EQUITY)
|
||
// → không bị ảnh hưởng bởi floating P&L
|
||
g_DailyRisk.limitHit = false; // Reset flag → cho phép trade ngày mới
|
||
if (InpDebugLog)
|
||
PrintFormat("[DAILY RISK] New day | startBalance=%.2f", g_DailyRisk.startBalance);
|
||
}
|
||
|
||
if (g_DailyRisk.limitHit) return; // Early exit: đã hit limit hôm nay → không cần tính lại mỗi tick
|
||
|
||
g_DailyRisk.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Cache vào struct
|
||
// → DrawContextDebug dùng lại,
|
||
// tránh gọi API 2 lần trong cùng tick
|
||
|
||
double lostPct = (g_DailyRisk.startBalance - g_DailyRisk.currentBalance)
|
||
/ g_DailyRisk.startBalance * 100.0; // % đã mất so với đầu ngày (giá trị dương = loss)
|
||
|
||
if (lostPct >= InpMaxDailyLossPct) // Chạm ngưỡng → lock trading cả ngày
|
||
{
|
||
g_DailyRisk.limitHit = true;
|
||
PrintFormat("[DAILY RISK] ⛔ Limit hit | lost=%.2f%% | limit=%.2f%% | balance=%.2f",
|
||
lostPct, InpMaxDailyLossPct, g_DailyRisk.currentBalance);
|
||
}
|
||
}
|
||
|
||
void UpdateAllContexts()
|
||
{
|
||
UpdateDailyRiskContext(); // Gọi mỗi tick (nhanh nhờ early-exit khi limitHit)
|
||
UpdateBiasContext(); // Gọi mỗi tick, chạy thực sự 1 lần/bar BiasTF (guard)
|
||
UpdateMiddleTfTrendContext(); // Gọi mỗi tick, chạy thực sự 1 lần/bar MiddleTF (guard)
|
||
}
|
||
|
||
//====================================================
|
||
// GUARDS
|
||
// Mỗi guard kiểm tra 1 điều kiện độc lập.
|
||
// EvaluateGuards() gọi tuần tự → điều kiện nào fail trước thì dừng.
|
||
// Thứ tự: Session → DailyRisk → Bias → MiddleTrend
|
||
//====================================================
|
||
|
||
bool IsSessionAllowed() { return true; /* TODO: kiểm tra London/NY session theo giờ UTC */ }
|
||
bool IsDailyLossOK() { return !g_DailyRisk.limitHit; } // false khi đã chạm daily loss limit
|
||
bool IsBiasValid() { return g_Bias.bias == BIAS_UP || g_Bias.bias == BIAS_DOWN; } // Chỉ trade khi có bias rõ (không trade sideway)
|
||
|
||
// Kiểm tra Bias D1 và MiddleTF trend có cùng hướng không
|
||
// Nguyên lý: chỉ trade theo trend đa khung thời gian đồng thuận
|
||
// Bias UP + Trend UP → cho phép tìm Buy setup
|
||
// Bias DOWN + Trend DOWN → cho phép tìm Sell setup
|
||
// Ngược chiều hoặc DIR_NONE → block (BLOCK_BIAS_MISMATCH)
|
||
bool IsMiddleTrendAligned()
|
||
{
|
||
if (g_MiddleTrend.trend == DIR_NONE) return false; // Trend chưa xác định
|
||
if (g_Bias.bias == BIAS_UP && g_MiddleTrend.trend == DIR_UP) return true;
|
||
if (g_Bias.bias == BIAS_DOWN && g_MiddleTrend.trend == DIR_DOWN) return true;
|
||
return false; // Bias và trend ngược chiều → không trade
|
||
}
|
||
|
||
bool EvaluateGuards()
|
||
{
|
||
g_BlockReason = BLOCK_NONE; // Reset trước khi check
|
||
if (!IsSessionAllowed()) { g_BlockReason = BLOCK_SESSION; return false; } // Ngoài giờ giao dịch
|
||
if (!IsDailyLossOK()) { g_BlockReason = BLOCK_DAILY_LOSS; return false; } // Đã hit daily loss
|
||
if (!IsBiasValid()) { g_BlockReason = BLOCK_NO_BIAS; return false; } // Bias = SIDEWAY/NONE
|
||
if (!IsMiddleTrendAligned()) { g_BlockReason = BLOCK_BIAS_MISMATCH; return false; } // Bias ≠ Trend
|
||
return true; // Tất cả guards pass → EA được phép trade
|
||
}
|
||
|
||
//====================================================
|
||
// TRANSITIONS
|
||
//====================================================
|
||
|
||
// Chuyển state và log transition (không chứa business logic)
|
||
void TransitionTo(EAState next)
|
||
{
|
||
if (InpDebugLog)
|
||
PrintFormat("[STATE] %s → %s", EnumToString(g_State), EnumToString(next));
|
||
g_State = next;
|
||
}
|
||
|
||
// Reset toàn bộ FVG context và quay về IDLE
|
||
// Gọi khi: FVG expired, FVG invalidated, FVG broken, trigger timeout, trade closed, guards fail
|
||
void ResetToIdle(string reason = "")
|
||
{
|
||
if (InpDebugLog && reason != "")
|
||
PrintFormat("[RESET] %s", reason);
|
||
|
||
ZeroMemory(g_FVG); // Xóa toàn bộ FVG data
|
||
ZeroMemory(g_Trigger); // Xóa trigger data
|
||
g_TradeBarIndex = -1; // Reset guard chống double-entry
|
||
|
||
// Xóa FVG drawing objects khỏi chart
|
||
ObjectDelete(0, FVG_RECT_NAME);
|
||
ObjectDelete(0, FVG_MID_NAME);
|
||
ObjectDelete(0, FVG_LABEL_NAME);
|
||
|
||
TransitionTo(EA_IDLE);
|
||
}
|
||
|
||
//====================================================
|
||
// FVG HELPERS
|
||
//====================================================
|
||
|
||
// Kiểm tra nến tại barIndex có body "mạnh" không (body/range >= InpFVGMinBodyPct%)
|
||
// Mục đích: loại bỏ FVG do nến doji, spinning top gây ra → chỉ lấy FVG từ nến impulse
|
||
bool IsCandleStrong(ENUM_TIMEFRAMES tf, int barIndex)
|
||
{
|
||
double high = iHigh (_Symbol, tf, barIndex);
|
||
double low = iLow (_Symbol, tf, barIndex);
|
||
double open = iOpen (_Symbol, tf, barIndex);
|
||
double close = iClose(_Symbol, tf, barIndex);
|
||
|
||
double range = high - low;
|
||
if (range < _Point) return false; // Nến có range gần 0 → bỏ qua (tránh chia cho 0)
|
||
|
||
return (MathAbs(close - open) / range * 100.0) >= InpFVGMinBodyPct;
|
||
}
|
||
|
||
// Kiểm tra FVG đã bị "fill" chưa = có bar nào sau FVG close vào trong vùng gap không
|
||
// fvgRightIdx = bar index của nến phải (newest) trong pattern 3 nến
|
||
// Scan từ fvgRightIdx-1 (bar ngay sau FVG) đến bar[1] (bar hiện tại đã đóng)
|
||
// Bullish FVG filled: có bar close <= fvgHigh (close vào trong gap từ trên xuống)
|
||
// Bearish FVG filled: có bar close >= fvgLow (close vào trong gap từ dưới lên)
|
||
bool IsFVGAlreadyFilled(ENUM_TIMEFRAMES tf, int fvgRightIdx,
|
||
double fvgHigh, double fvgLow, MarketDir dir)
|
||
{
|
||
for (int j = fvgRightIdx - 1; j >= 1; j--) // Scan từ ngay sau FVG đến bar gần nhất
|
||
{
|
||
double close = iClose(_Symbol, tf, j);
|
||
if (dir == DIR_UP && close <= fvgHigh) return true; // Close vào trong bullish gap
|
||
if (dir == DIR_DOWN && close >= fvgLow) return true; // Close vào trong bearish gap
|
||
}
|
||
return false; // Chưa có bar nào fill gap → FVG vẫn còn nguyên
|
||
}
|
||
|
||
// FVG expired = tồn tại quá InpFVGMaxAliveMin phút kể từ khi được confirm
|
||
// Lý do expire: FVG quá cũ thường mất relevance, rủi ro cao khi vào lệnh
|
||
bool IsFVGExpired()
|
||
{
|
||
if (g_FVG.createdTime <= 0) return false; // FVG chưa được set → không thể expire
|
||
return (int)(TimeCurrent() - g_FVG.createdTime) > InpFVGMaxAliveMin * 60;
|
||
}
|
||
|
||
// FVG invalidated = MiddleTF bar[1] close xuyên hoàn toàn qua vùng FVG ngược chiều
|
||
// Bullish FVG: close < FVG.low → giá đã xuyên xuống hoàn toàn qua gap → FVG không còn ý nghĩa
|
||
// Bearish FVG: close > FVG.high → giá đã xuyên lên hoàn toàn qua gap
|
||
bool IsFVGInvalidated()
|
||
{
|
||
double lastClose = iClose(_Symbol, InpMiddleTF, 1);
|
||
if (g_FVG.direction == DIR_UP && lastClose < g_FVG.low) return true;
|
||
if (g_FVG.direction == DIR_DOWN && lastClose > g_FVG.high) return true;
|
||
return false;
|
||
}
|
||
|
||
//====================================================
|
||
// FVG DETECTION
|
||
//
|
||
// Pattern 3 nến tại bar index i (i = nến giữa / impulse candle):
|
||
// bar[i+1] = nến trái (older) → đáy/đỉnh tạo một cạnh của gap
|
||
// bar[i] = nến giữa (impulse) → nến mạnh tạo ra gap
|
||
// bar[i-1] = nến phải (newer) → đáy/đỉnh tạo cạnh còn lại của gap
|
||
//
|
||
// Bullish FVG (gap tăng – giá nhảy lên):
|
||
// Điều kiện: bar[i+1].high < bar[i-1].low (đỉnh nến trái < đáy nến phải = có khoảng trống)
|
||
// gapLow = bar[i+1].high (đỉnh nến trái = cạnh dưới của gap)
|
||
// gapHigh = bar[i-1].low (đáy nến phải = cạnh trên của gap)
|
||
// → Khi retracement, giá về vùng [gapLow, gapHigh] để tìm buy
|
||
//
|
||
// Bearish FVG (gap giảm – giá nhảy xuống):
|
||
// Điều kiện: bar[i+1].low > bar[i-1].high
|
||
// gapHigh = bar[i+1].low (đáy nến trái = cạnh trên của gap)
|
||
// gapLow = bar[i-1].high (đỉnh nến phải = cạnh dưới của gap)
|
||
// → Khi retracement, giá về vùng [gapLow, gapHigh] để tìm sell
|
||
//
|
||
// Hàm scan từ i=2 → InpFVGScanBars, trả về FVG HỢP LỆ gần nhất (chưa fill, còn trong thời gian sống)
|
||
//====================================================
|
||
|
||
bool FindFVGCandidate(FVGContext &fvg)
|
||
{
|
||
MarketDir targetDir = g_MiddleTrend.trend; // Direction từ MiddleTF trend (đã pass guard đồng thuận với bias)
|
||
int maxBar = MathMin(InpFVGScanBars, Bars(_Symbol, InpMiddleTF) - 2); // Không scan quá số bar thực tế
|
||
|
||
for (int i = 2; i <= maxBar; i++) // i=2: nến phải là i-1=1 (bar đã đóng), an toàn để dùng
|
||
{
|
||
double leftHigh = iHigh (_Symbol, InpMiddleTF, i + 1); // Nến trái (older)
|
||
double leftLow = iLow (_Symbol, InpMiddleTF, i + 1);
|
||
double rightHigh = iHigh (_Symbol, InpMiddleTF, i - 1); // Nến phải (newer)
|
||
double rightLow = iLow (_Symbol, InpMiddleTF, i - 1);
|
||
double midOpen = iOpen (_Symbol, InpMiddleTF, i); // Nến giữa (impulse)
|
||
double midClose = iClose(_Symbol, InpMiddleTF, i);
|
||
|
||
double gapHigh, gapLow;
|
||
|
||
if (targetDir == DIR_UP) // Tìm Bullish FVG
|
||
{
|
||
if (leftHigh >= rightLow) continue; // Không có gap: đỉnh trái chạm/vượt đáy phải
|
||
if (midClose <= midOpen) continue; // Nến giữa phải bullish (close > open)
|
||
if (!IsCandleStrong(InpMiddleTF, i)) continue; // Nến giữa phải đủ mạnh (body >= X%)
|
||
gapLow = leftHigh; // Cạnh dưới gap = đỉnh nến trái
|
||
gapHigh = rightLow; // Cạnh trên gap = đáy nến phải
|
||
}
|
||
else // Tìm Bearish FVG
|
||
{
|
||
if (leftLow <= rightHigh) continue; // Không có gap
|
||
if (midClose >= midOpen) continue; // Nến giữa phải bearish (close < open)
|
||
if (!IsCandleStrong(InpMiddleTF, i)) continue;
|
||
gapHigh = leftLow; // Cạnh trên gap = đáy nến trái
|
||
gapLow = rightHigh; // Cạnh dưới gap = đỉnh nến phải
|
||
}
|
||
|
||
// Kiểm tra FVG chưa bị fill: scan từ nến phải (i-1) đến bar hiện tại
|
||
if (IsFVGAlreadyFilled(InpMiddleTF, i - 1, gapHigh, gapLow, targetDir))
|
||
continue; // Đã bị fill → bỏ qua, tìm FVG khác
|
||
|
||
// ── FVG hợp lệ → điền vào struct và trả về ──────────────────
|
||
fvg.active = true;
|
||
fvg.touched = false;
|
||
fvg.direction = targetDir;
|
||
fvg.high = gapHigh;
|
||
fvg.low = gapLow;
|
||
fvg.mid = (gapHigh + gapLow) / 2.0; // Midpoint dùng làm entry tham chiếu
|
||
fvg.createdTime = iTime(_Symbol, InpMiddleTF, i - 1); // Nến phải đóng = thời điểm FVG được confirm
|
||
fvg.touchTime = 0;
|
||
fvg.barsAlive = 0;
|
||
fvg.touchBarIdx = -1;
|
||
|
||
return true; // Trả về FVG gần nhất (i nhỏ nhất = gần bar hiện tại nhất)
|
||
}
|
||
|
||
return false; // Không tìm được FVG nào thỏa mãn
|
||
}
|
||
|
||
//====================================================
|
||
// STATE HANDLERS
|
||
//====================================================
|
||
|
||
// EA_IDLE: Quét tìm FVG candidate mỗi tick (nhanh nhờ guard đã lọc trước)
|
||
// Khi tìm được → lưu vào g_FVG → chuyển sang WAIT_TOUCH
|
||
void OnStateIdle()
|
||
{
|
||
FVGContext candidate;
|
||
ZeroMemory(candidate); // Đảm bảo struct sạch trước khi truyền vào FindFVGCandidate
|
||
if (!FindFVGCandidate(candidate)) return; // Không có FVG nào thỏa mãn → chờ tick tiếp
|
||
|
||
g_FVG = candidate; // Commit FVG candidate vào global state
|
||
|
||
if (InpDebugLog)
|
||
PrintFormat("[FVG FOUND] dir=%s | high=%.5f | low=%.5f | mid=%.5f | created=%s",
|
||
EnumToString(g_FVG.direction), g_FVG.high, g_FVG.low, g_FVG.mid,
|
||
TimeToString(g_FVG.createdTime));
|
||
|
||
TransitionTo(EA_WAIT_TOUCH);
|
||
}
|
||
|
||
// EA_WAIT_TOUCH: Theo dõi FVG đang active, chờ giá retracement vào vùng FVG
|
||
// Exit sớm (reset) nếu: FVG hết hạn sống | FVG bị invalidate (giá xuyên qua)
|
||
// Touch condition:
|
||
// Bullish FVG: giá bid <= FVG.high → giá đã retraced xuống chạm cạnh trên gap
|
||
// Bearish FVG: giá bid >= FVG.low → giá đã retraced lên chạm cạnh dưới gap
|
||
void OnStateWaitTouch()
|
||
{
|
||
if (IsFVGExpired()) { ResetToIdle("FVG expired"); return; } // Quá tuổi thọ → bỏ
|
||
if (IsFVGInvalidated()) { ResetToIdle("FVG invalidated"); return; } // Giá xuyên gap ngược chiều → bỏ
|
||
|
||
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // Dùng bid (không dùng ask) để check touch
|
||
|
||
bool touched = false;
|
||
if (g_FVG.direction == DIR_UP && bid <= g_FVG.high) touched = true; // Giá pullback xuống chạm cạnh trên gap
|
||
if (g_FVG.direction == DIR_DOWN && bid >= g_FVG.low) touched = true; // Giá pullback lên chạm cạnh dưới gap
|
||
|
||
if (!touched) return; // Chưa chạm → chờ tick tiếp
|
||
|
||
g_FVG.touched = true;
|
||
g_FVG.touchTime = TimeCurrent();
|
||
g_FVG.touchBarIdx = iBarShift(_Symbol, InpTriggerTF, TimeCurrent()); // Bar index TriggerTF lúc touched (dùng đếm timeout)
|
||
g_FVG.barsAlive = 0; // Reset counter
|
||
|
||
if (InpDebugLog)
|
||
PrintFormat("[FVG TOUCHED] dir=%s | price=%.5f | FVG[%.5f – %.5f]",
|
||
EnumToString(g_FVG.direction), bid, g_FVG.low, g_FVG.high);
|
||
|
||
TransitionTo(EA_WAIT_TRIGGER);
|
||
}
|
||
|
||
// EA_WAIT_TRIGGER: Giá đã chạm FVG, đang chờ TriggerTF xác nhận entry
|
||
void OnStateWaitTrigger()
|
||
{
|
||
// TODO: Nếu FVG bị giá phá xuyên (close hoàn toàn ra ngoài) → ResetToIdle("FVG broken")
|
||
// TODO: Nếu barsAlive > InpTriggerMaxBars → ResetToIdle("trigger timeout") (chờ quá lâu, momentum mất)
|
||
// TODO: Mỗi bar TriggerTF mới → cập nhật TriggerContext (swing structure + breakLevel)
|
||
// TODO: Khi breakLevel bị phá vỡ → BuildOrderPlan() → ExecuteOrder() → TransitionTo(EA_IN_TRADE)
|
||
}
|
||
|
||
// EA_IN_TRADE: Đang có lệnh mở, theo dõi cho đến khi lệnh đóng
|
||
void OnStateInTrade()
|
||
{
|
||
if (Bars(_Symbol, InpTriggerTF) <= g_TradeBarIndex) return; // Vẫn trong cùng bar lúc vừa gửi lệnh → skip
|
||
// TODO: Kiểm tra còn position/pending nào không → nếu không còn → ResetToIdle("trade closed")
|
||
}
|
||
|
||
//====================================================
|
||
// STATE MACHINE
|
||
//====================================================
|
||
|
||
void RunStateMachine()
|
||
{
|
||
switch (g_State)
|
||
{
|
||
case EA_IDLE: OnStateIdle(); break;
|
||
case EA_WAIT_TOUCH: OnStateWaitTouch(); break;
|
||
case EA_WAIT_TRIGGER: OnStateWaitTrigger(); break;
|
||
case EA_IN_TRADE: OnStateInTrade(); break;
|
||
}
|
||
}
|
||
|
||
//====================================================
|
||
// SWING DRAWING
|
||
//
|
||
// Mỗi swing point gồm tối đa 3 objects (đều dùng prefix SW_):
|
||
// SW_ARR_<tag> – Mũi tên OBJ_ARROW chỉ vào đỉnh/đáy
|
||
// SW_TXT_<tag> – Text label "H0"/"H1"/"L0"/"L1"
|
||
// SW_KL_<tag> – Đường ngang nét đứt OBJ_TREND (chỉ vẽ cho key level)
|
||
//
|
||
// Màu sắc (sáng = gần nhất, tối = cũ hơn):
|
||
// H0 = clrAqua | H1 = C'0,140,160'
|
||
// L0 = clrYellow | L1 = C'160,140,0'
|
||
//
|
||
// Key level logic:
|
||
// Uptrend → L0 là key level (vẽ đường ngang vàng → MSS nếu close < L0)
|
||
// Downtrend → H0 là key level (vẽ đường ngang cyan → MSS nếu close > H0)
|
||
//====================================================
|
||
|
||
void DrawOneSwing(
|
||
string tag, // "H0" | "H1" | "L0" | "L1"
|
||
bool isHigh, // true = swing high (mũi tên ▼), false = swing low (mũi tên ▲)
|
||
int barIdx, // Bar index trên MiddleTF
|
||
double price, // Giá high (nếu isHigh) hoặc low (nếu !isHigh) của swing
|
||
color clr, // Màu hiển thị
|
||
bool isKeyLevel) // true → vẽ thêm đường ngang nét đứt (key level / MSS line)
|
||
{
|
||
string arrName = SW_PREFIX + "ARR_" + tag;
|
||
string txtName = SW_PREFIX + "TXT_" + tag;
|
||
string lineName = SW_PREFIX + "KL_" + tag;
|
||
|
||
datetime t = iTime(_Symbol, InpMiddleTF, barIdx); // Thời điểm bar swing (dùng làm tọa độ X)
|
||
|
||
// ── Arrow: chỉ vào đỉnh/đáy ─────────────────────────────────────
|
||
if (ObjectFind(0, arrName) < 0)
|
||
ObjectCreate(0, arrName, OBJ_ARROW, 0, t, price);
|
||
|
||
ObjectSetInteger(0, arrName, OBJPROP_ARROWCODE, isHigh ? 234 : 233); // 234 = ▼ (đỉnh), 233 = ▲ (đáy)
|
||
ObjectSetInteger(0, arrName, OBJPROP_COLOR, clr);
|
||
ObjectSetInteger(0, arrName, OBJPROP_WIDTH, 2);
|
||
ObjectSetInteger(0, arrName, OBJPROP_ANCHOR, isHigh ? ANCHOR_BOTTOM : ANCHOR_TOP);
|
||
ObjectMove(0, arrName, 0, t, price); // Cập nhật vị trí mỗi lần vẽ (phòng khi barIdx thay đổi)
|
||
|
||
// ── Text label: hiển thị tên tag cạnh mũi tên ───────────────────
|
||
// Offset theo chiều dọc để text không đè lên mũi tên
|
||
double offset = (iHigh(_Symbol, InpMiddleTF, barIdx) - iLow(_Symbol, InpMiddleTF, barIdx)) * 0.3;
|
||
double txtY = isHigh ? price + offset : price - offset; // Đặt text trên đỉnh hoặc dưới đáy
|
||
|
||
if (ObjectFind(0, txtName) < 0)
|
||
ObjectCreate(0, txtName, OBJ_TEXT, 0, t, txtY);
|
||
|
||
ObjectMove(0, txtName, 0, t, txtY);
|
||
ObjectSetString (0, txtName, OBJPROP_TEXT, tag);
|
||
ObjectSetInteger(0, txtName, OBJPROP_COLOR, clr);
|
||
ObjectSetInteger(0, txtName, OBJPROP_FONTSIZE, 8);
|
||
ObjectSetInteger(0, txtName, OBJPROP_ANCHOR, isHigh ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
|
||
|
||
// ── Key Level line: đường ngang nét đứt từ swing đến bar hiện tại ─
|
||
if (isKeyLevel)
|
||
{
|
||
datetime tEnd = iTime(_Symbol, InpMiddleTF, 0); // Kéo đến bar hiện tại (sẽ cập nhật mỗi tick)
|
||
|
||
if (ObjectFind(0, lineName) < 0)
|
||
ObjectCreate(0, lineName, OBJ_TREND, 0, t, price, tEnd, price);
|
||
|
||
ObjectSetInteger(0, lineName, OBJPROP_COLOR, clr);
|
||
ObjectSetInteger(0, lineName, OBJPROP_STYLE, STYLE_DASH);
|
||
ObjectSetInteger(0, lineName, OBJPROP_WIDTH, 1);
|
||
ObjectSetInteger(0, lineName, OBJPROP_RAY_RIGHT, false); // Không kéo dài vô tận sang phải
|
||
ObjectMove(0, lineName, 0, t, price);
|
||
ObjectMove(0, lineName, 1, tEnd, price); // Cập nhật điểm phải mỗi tick
|
||
}
|
||
else
|
||
{
|
||
ObjectDelete(0, lineName); // Swing này không còn là key level → xóa đường ngang nếu có
|
||
}
|
||
}
|
||
|
||
void DrawSwingPoints()
|
||
{
|
||
if (!InpDebugDraw) return;
|
||
|
||
// Chưa đủ swing points (idxH0 = 0 sau ZeroMemory) → xóa tất cả swing objects
|
||
if (g_MiddleTrend.idxH0 <= 0 || g_MiddleTrend.idxH1 <= 0 ||
|
||
g_MiddleTrend.idxL0 <= 0 || g_MiddleTrend.idxL1 <= 0)
|
||
{
|
||
ObjectsDeleteAll(0, SW_PREFIX);
|
||
return;
|
||
}
|
||
|
||
bool uptrend = (g_MiddleTrend.trend == DIR_UP);
|
||
bool downtrend = (g_MiddleTrend.trend == DIR_DOWN);
|
||
|
||
DrawOneSwing("H0", true, g_MiddleTrend.idxH0, g_MiddleTrend.h0, clrAqua, downtrend); // H0 = key level khi downtrend
|
||
DrawOneSwing("H1", true, g_MiddleTrend.idxH1, g_MiddleTrend.h1, C'0,140,160', false);
|
||
DrawOneSwing("L0", false, g_MiddleTrend.idxL0, g_MiddleTrend.l0, clrYellow, uptrend); // L0 = key level khi uptrend
|
||
DrawOneSwing("L1", false, g_MiddleTrend.idxL1, g_MiddleTrend.l1, C'160,140,0', false);
|
||
}
|
||
|
||
//====================================================
|
||
// FVG DRAWING
|
||
//
|
||
// Vẽ rectangle vùng FVG + midpoint line + text label
|
||
//
|
||
// Cạnh trái rectangle = createdTime (cố định = thời điểm FVG confirmed)
|
||
// Cạnh phải rectangle = live khi WAIT_TOUCH (kéo đến bar hiện tại mỗi tick)
|
||
// = cố định khi WAIT_TRIGGER+ (touchTime + 1 bar MiddleTF)
|
||
// Màu: đậm khi candidate, sáng khi touched
|
||
// Bullish: xanh dương Bearish: cam
|
||
//====================================================
|
||
|
||
void DrawFVGRectangle()
|
||
{
|
||
if (!InpDebugDraw) return;
|
||
|
||
if (!g_FVG.active) // Không có FVG active → xóa objects cũ nếu còn
|
||
{
|
||
ObjectDelete(0, FVG_RECT_NAME);
|
||
ObjectDelete(0, FVG_MID_NAME);
|
||
ObjectDelete(0, FVG_LABEL_NAME);
|
||
return;
|
||
}
|
||
|
||
// ── Xác định cạnh phải (điểm kết thúc) của rectangle ───────────
|
||
datetime rectEnd;
|
||
if (!g_FVG.touched)
|
||
rectEnd = iTime(_Symbol, InpMiddleTF, 0); // Chưa touched → live: kéo tới bar hiện tại mỗi tick
|
||
else
|
||
{
|
||
int touchShift = iBarShift(_Symbol, InpMiddleTF, g_FVG.touchTime); // Bar MiddleTF tại thời điểm touch
|
||
rectEnd = iTime(_Symbol, InpMiddleTF, MathMax(touchShift - 1, 0)); // Bar tiếp theo sau touch (cố định)
|
||
}
|
||
|
||
color rectColor = !g_FVG.touched
|
||
? (g_FVG.direction == DIR_UP ? C'0,80,160' : C'140,40,0') // Màu đậm: FVG candidate chưa touched
|
||
: (g_FVG.direction == DIR_UP ? C'0,160,255' : C'255,100,0'); // Màu sáng: FVG đã được touched
|
||
|
||
// ── Rectangle ───────────────────────────────────────────────────
|
||
if (ObjectFind(0, FVG_RECT_NAME) < 0)
|
||
ObjectCreate(0, FVG_RECT_NAME, OBJ_RECTANGLE, 0,
|
||
g_FVG.createdTime, g_FVG.high, rectEnd, g_FVG.low);
|
||
|
||
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_COLOR, rectColor);
|
||
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_FILL, true); // Fill màu bên trong
|
||
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_BACK, true); // Vẽ dưới nến (không che chart)
|
||
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_WIDTH, 1);
|
||
ObjectMove(0, FVG_RECT_NAME, 0, g_FVG.createdTime, g_FVG.high); // Point 0 = góc trên trái (cố định)
|
||
ObjectMove(0, FVG_RECT_NAME, 1, rectEnd, g_FVG.low); // Point 1 = góc dưới phải (cập nhật)
|
||
|
||
// ── Midpoint line (đứt nét) ─────────────────────────────────────
|
||
if (ObjectFind(0, FVG_MID_NAME) < 0)
|
||
ObjectCreate(0, FVG_MID_NAME, OBJ_TREND, 0,
|
||
g_FVG.createdTime, g_FVG.mid, rectEnd, g_FVG.mid);
|
||
|
||
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_COLOR, clrSilver);
|
||
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_STYLE, STYLE_DOT); // Đường chấm chấm
|
||
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_WIDTH, 1);
|
||
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_RAY_RIGHT, false); // Không kéo vô tận
|
||
ObjectMove(0, FVG_MID_NAME, 0, g_FVG.createdTime, g_FVG.mid);
|
||
ObjectMove(0, FVG_MID_NAME, 1, rectEnd, g_FVG.mid);
|
||
|
||
// ── Text label ──────────────────────────────────────────────────
|
||
string labelText = StringFormat("FVG %s%s",
|
||
(g_FVG.direction == DIR_UP ? "▲" : "▼"),
|
||
(g_FVG.touched ? " [TOUCHED]" : "")); // Hiển thị trạng thái touched
|
||
|
||
if (ObjectFind(0, FVG_LABEL_NAME) < 0)
|
||
ObjectCreate(0, FVG_LABEL_NAME, OBJ_TEXT, 0, g_FVG.createdTime, g_FVG.high);
|
||
|
||
ObjectMove(0, FVG_LABEL_NAME, 0, g_FVG.createdTime, g_FVG.high);
|
||
ObjectSetString (0, FVG_LABEL_NAME, OBJPROP_TEXT, labelText);
|
||
ObjectSetInteger(0, FVG_LABEL_NAME, OBJPROP_COLOR, rectColor);
|
||
ObjectSetInteger(0, FVG_LABEL_NAME, OBJPROP_FONTSIZE, 8);
|
||
ObjectSetInteger(0, FVG_LABEL_NAME, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); // Text nằm ngay dưới cạnh trên
|
||
}
|
||
|
||
//====================================================
|
||
// DEBUG PANEL (góc trên bên TRÁI)
|
||
//
|
||
// Hiển thị toàn bộ context quan trọng để debug trực tiếp trên chart:
|
||
// Bias D1 | MiddleTF Trend + Key Level | Swing H/L values
|
||
// Daily Risk | Balance | EA State | FVG info | Block Reason
|
||
//
|
||
// Tất cả objects đều có prefix "DBG_" → xóa 1 lần trong OnDeinit
|
||
// CORNER_LEFT_UPPER + XDISTANCE=10 + YDISTANCE tăng dần = stack từ trên xuống
|
||
//====================================================
|
||
|
||
void DrawContextDebug()
|
||
{
|
||
if (!InpDebugDraw) return;
|
||
|
||
// Macro tạo hoặc cập nhật 1 OBJ_LABEL tại góc trên trái
|
||
// Tham số: tên object, text hiển thị, khoảng cách từ trên xuống (px), màu
|
||
#define SET_LABEL(name, text, ypos, clr) \
|
||
if (ObjectFind(0, name) < 0) \
|
||
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); \
|
||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); \
|
||
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 10); \
|
||
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, ypos); \
|
||
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9); \
|
||
ObjectSetInteger(0, name, OBJPROP_COLOR, clr); \
|
||
ObjectSetString (0, name, OBJPROP_TEXT, text);
|
||
|
||
SET_LABEL("DBG_HEADER", "── ICT EA Debug ──", 10, clrSilver)
|
||
|
||
// ── BIAS D1 ──────────────────────────────── y=30
|
||
// Màu phản ánh hướng: xanh = up, đỏ = down, cam = sideway, xám = none
|
||
color biasColor = (g_Bias.bias == BIAS_UP) ? clrLime :
|
||
(g_Bias.bias == BIAS_DOWN) ? clrTomato :
|
||
(g_Bias.bias == BIAS_SIDEWAY) ? clrOrange : clrGray;
|
||
SET_LABEL("DBG_BIAS", StringFormat("Bias : %s", EnumToString(g_Bias.bias)), 30, biasColor)
|
||
|
||
// ── MIDDLE TF TREND + KEY LEVEL ─────────── y=48
|
||
// KL = Key Level cần theo dõi để phát hiện MSS
|
||
color trendColor = (g_MiddleTrend.trend == DIR_UP) ? clrLime :
|
||
(g_MiddleTrend.trend == DIR_DOWN) ? clrTomato : clrGray;
|
||
SET_LABEL("DBG_TREND",
|
||
StringFormat("Trend : %s | KL=%.5f",
|
||
EnumToString(g_MiddleTrend.trend), g_MiddleTrend.keyLevel),
|
||
48, trendColor)
|
||
|
||
// ── SWING HIGH VALUES ───────────────────── y=66
|
||
// H1 (cũ) → H0 (mới): thấy rõ Higher High hay Lower High
|
||
SET_LABEL("DBG_SWING_H",
|
||
StringFormat("H1=%.5f H0=%.5f", g_MiddleTrend.h1, g_MiddleTrend.h0),
|
||
66, clrAqua)
|
||
|
||
// ── SWING LOW VALUES ────────────────────── y=84
|
||
// L1 (cũ) → L0 (mới): thấy rõ Higher Low hay Lower Low
|
||
SET_LABEL("DBG_SWING_L",
|
||
StringFormat("L1=%.5f L0=%.5f", g_MiddleTrend.l1, g_MiddleTrend.l0),
|
||
84, clrYellow)
|
||
|
||
// ── SIDEWAY RANGE (chỉ hiển thị khi bias = SIDEWAY) ─── y=102
|
||
if (g_Bias.bias == BIAS_SIDEWAY) {
|
||
SET_LABEL("DBG_BIAS_RANGE",
|
||
StringFormat(" Range : %.5f – %.5f", g_Bias.rangeLow, g_Bias.rangeHigh),
|
||
102, clrOrange)
|
||
}
|
||
else
|
||
ObjectDelete(0, "DBG_BIAS_RANGE"); // Ẩn khi không sideway
|
||
|
||
// ── DAILY RISK ──────────────────────────── y=102 (hoặc 120 nếu sideway hiện)
|
||
double lostPct = g_DailyRisk.startBalance > 0
|
||
? (g_DailyRisk.startBalance - g_DailyRisk.currentBalance)
|
||
/ g_DailyRisk.startBalance * 100.0 // % đã mất so với đầu ngày
|
||
: 0.0; // Chưa có startBalance (ngày đầu tiên) → hiển thị 0%
|
||
|
||
// Màu cảnh báo: đỏ khi hit, cam khi gần hit (>70% ngưỡng), xanh khi bình thường
|
||
color riskColor = g_DailyRisk.limitHit ? clrRed :
|
||
lostPct > InpMaxDailyLossPct * 0.7 ? clrOrange : clrLime;
|
||
|
||
SET_LABEL("DBG_RISK", StringFormat("Risk : %.2f%% / %.2f%%", lostPct, InpMaxDailyLossPct), 102, riskColor)
|
||
SET_LABEL("DBG_BAL", StringFormat("Bal : %.2f (start %.2f)", g_DailyRisk.currentBalance, g_DailyRisk.startBalance), 120, clrSilver)
|
||
SET_LABEL("DBG_LIMIT", g_DailyRisk.limitHit ? "⛔ DAILY LOSS HIT" : "✅ Loss OK", 138, g_DailyRisk.limitHit ? clrRed : clrLime)
|
||
|
||
// ── EA STATE ────────────────────────────── y=156
|
||
// Màu theo mức độ "hoạt động": xám = idle, cam = chờ, vàng = sắp vào, xanh = trong lệnh
|
||
color stateColor = (g_State == EA_IDLE) ? clrSilver :
|
||
(g_State == EA_WAIT_TOUCH) ? clrOrange :
|
||
(g_State == EA_WAIT_TRIGGER) ? clrYellow :
|
||
(g_State == EA_IN_TRADE) ? clrLime : clrGray;
|
||
SET_LABEL("DBG_STATE", StringFormat("State : %s", EnumToString(g_State)), 156, stateColor)
|
||
|
||
// ── FVG ACTIVE INFO ─────────────────────── y=174 (chỉ khi có FVG)
|
||
if (g_FVG.active)
|
||
{
|
||
color fvgColor = g_FVG.touched ? clrDeepSkyBlue : clrDodgerBlue; // Sáng hơn khi touched
|
||
SET_LABEL("DBG_FVG",
|
||
StringFormat("FVG : %s [%.5f – %.5f]",
|
||
EnumToString(g_FVG.direction), g_FVG.low, g_FVG.high),
|
||
174, fvgColor)
|
||
}
|
||
else
|
||
ObjectDelete(0, "DBG_FVG"); // Không có FVG → ẩn dòng này
|
||
|
||
// ── BLOCK REASON ────────────────────────── y=192 (chỉ khi bị block)
|
||
if (g_BlockReason != BLOCK_NONE) {
|
||
SET_LABEL("DBG_BLOCK",
|
||
StringFormat("Block : %s", EnumToString(g_BlockReason)),
|
||
192, clrTomato)
|
||
}
|
||
else
|
||
ObjectDelete(0, "DBG_BLOCK"); // Không bị block → ẩn dòng này
|
||
|
||
#undef SET_LABEL
|
||
|
||
ChartRedraw(0); // Force redraw để panel hiển thị ngay lập tức
|
||
}
|
||
|
||
void DrawVisuals()
|
||
{
|
||
DrawSwingPoints(); // Vẽ H0/H1/L0/L1 và key level line trên chart
|
||
DrawFVGRectangle(); // Vẽ FVG zone, midpoint, label
|
||
DrawContextDebug(); // Cập nhật debug panel góc trên trái
|
||
}
|
||
|
||
//====================================================
|
||
// EA LIFECYCLE
|
||
//====================================================
|
||
|
||
int OnInit()
|
||
{
|
||
// Khởi tạo tất cả context về trạng thái mặc định (tất cả số = 0, bool = false)
|
||
ZeroMemory(g_Bias);
|
||
ZeroMemory(g_MiddleTrend);
|
||
ZeroMemory(g_FVG);
|
||
ZeroMemory(g_Trigger);
|
||
ZeroMemory(g_DailyRisk);
|
||
|
||
g_State = EA_IDLE; // Bắt đầu ở trạng thái chờ
|
||
g_BlockReason = BLOCK_NONE;
|
||
g_TradeBarIndex = -1; // -1 = chưa có lệnh nào
|
||
|
||
UpdateAllContexts(); // Tính bias + trend ngay khi load (không đợi tick đầu tiên)
|
||
DrawVisuals(); // Vẽ ngay khi load (không đợi tick đầu tiên → không thấy "trống")
|
||
|
||
PrintFormat("✅ EA initialized | Bias=%s | MiddleTrend=%s | State=%s",
|
||
EnumToString(g_Bias.bias),
|
||
EnumToString(g_MiddleTrend.trend),
|
||
EnumToString(g_State));
|
||
|
||
return INIT_SUCCEEDED;
|
||
}
|
||
|
||
void OnTick()
|
||
{
|
||
UpdateAllContexts(); // Cập nhật bias + trend + daily risk mỗi tick
|
||
|
||
if (EvaluateGuards()) // Tất cả điều kiện pass → chạy state machine
|
||
RunStateMachine();
|
||
else if (g_State != EA_IDLE) // Đang ở state khác IDLE mà bị block → reset về IDLE
|
||
ResetToIdle(EnumToString(g_BlockReason)); // Chỉ reset 1 lần (lần tiếp theo g_State = IDLE → không reset lại)
|
||
|
||
if (InpDebugDraw)
|
||
DrawVisuals(); // Cập nhật visuals mỗi tick (FVG rect cạnh phải live, debug panel)
|
||
}
|
||
|
||
void OnDeinit(const int reason)
|
||
{
|
||
// Xóa tất cả objects đã tạo khi EA bị gỡ khỏi chart
|
||
ObjectDelete(0, FVG_RECT_NAME);
|
||
ObjectDelete(0, FVG_MID_NAME);
|
||
ObjectDelete(0, FVG_LABEL_NAME);
|
||
ObjectsDeleteAll(0, SW_PREFIX); // Xóa toàn bộ SW_ARR_*, SW_TXT_*, SW_KL_*
|
||
ObjectsDeleteAll(0, "DBG_"); // Xóa toàn bộ debug panel labels
|
||
ChartRedraw(0);
|
||
PrintFormat("EA deinitialized | reason=%d", reason);
|
||
} |