Files
Bell-PriceActionWithEma-EA/Experts/EA_ICT_CL.mq5
T

1465 lines
71 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//+------------------------------------------------------------------+
//| ICT EA FVG Edition (MQL5) v4.0 |
//| Architecture : BiasTF(D1) + MiddleTF(H1) + TriggerTF(M5) |
//| State Machine: IDLE → WAIT_TOUCH → WAIT_TRIGGER → IN_TRADE |
//| |
//| Flow tổng thể: |
//| 1. D1 bias (UP/DOWN) đồng thuận H1 trend |
//| 2. H1 FVG thuận xu hướng → chờ price retrace vào FVG |
//| 3. Giá touch H1 FVG → chờ M5 MSS xác nhận đảo chiều |
//| 4. M5 MSS = keyLevel sóng hồi bị phá: |
//| - H1 UP: M5 đang downtrend (hồi), tH0 bị phá (close>tH0) |
//| - H1 DOWN: M5 đang uptrend (hồi), tL0 bị phá (close<tL0) |
//| 5. Entry = limit tại keyLevel vừa bị phá (tH0 hoặc tL0) |
//| 6. SL = swing extreme sóng hồi (tL0 cho buy, tH0 cho sell) |
//| 7. TP = Entry ± 2R |
//| |
//| Drawing: objects KHÔNG BAO GIỜ bị xóa, chỉ update |
//+------------------------------------------------------------------+
#property copyright "Bell's ICT EA"
#property version "4.00"
#define MAX_FVG_POOL 30
//====================================================
// INPUTS
//====================================================
input ENUM_TIMEFRAMES InpBiasTF = PERIOD_D1; // Bias TF (HTF)
input ENUM_TIMEFRAMES InpMiddleTF = PERIOD_H1; // FVG + trend TF (MTF)
input ENUM_TIMEFRAMES InpTriggerTF = PERIOD_M5; // Entry confirmation (LTF)
input double InpRiskPercent = 1.0; // Risk % per trade
input double InpRiskReward = 2.0; // TP/SL ratio
input double InpMaxDailyLossPct = 3.0; // Max daily loss %
input int InpLondonStartHour = 8; // London open (UTC)
input int InpLondonEndHour = 17; // London close (UTC)
input int InpNYStartHour = 13; // NY open (UTC)
input int InpNYEndHour = 22; // NY close (UTC)
input int InpSwingRange = 3; // Bars each side for swing confirm
input int InpSwingLookback = 50; // MiddleTF swing scan bars
input int InpTriggerSwingLookback = 30; // TriggerTF swing scan bars
input int InpFVGMaxAliveMin = 4320; // Max PENDING lifetime (min) = 72h
input int InpFVGScanBars = 50; // MiddleTF bars to scan for FVGs
input double InpFVGMinBodyPct = 60.0; // Mid-candle min body %
input int InpTriggerMaxBars = 30; // Trigger timeout (TriggerTF bars)
input long InpMagicNumber = 20250308; // EA magic number
input int InpSlippage = 5; // Max slippage (points)
input bool InpDebugLog = true; // Journal logging
input bool InpDebugDraw = true; // Chart drawing
//====================================================
// ENUMS
//====================================================
enum EAState
{
EA_IDLE, // Tìm FVG tốt nhất
EA_WAIT_TOUCH, // Chờ giá retrace vào MiddleTF FVG
EA_WAIT_TRIGGER, // Chờ TriggerTF MSS xác nhận
EA_IN_TRADE // Đang có lệnh / position
};
enum HTFBias { BIAS_NONE=0, BIAS_UP=1, BIAS_DOWN=-1, BIAS_SIDEWAY=2 };
enum MarketDir { DIR_NONE=0, DIR_UP=1, DIR_DOWN=-1 };
enum BlockReason
{
BLOCK_NONE, BLOCK_SESSION, BLOCK_DAILY_LOSS,
BLOCK_BIAS_MISMATCH, BLOCK_NO_BIAS
};
enum FVGStatus
{
FVG_PENDING, // Hình thành, chưa touch
FVG_TOUCHED, // Giá đã vào vùng gap
FVG_USED // 0=expired 1=broken 2=triggered
};
//====================================================
// STRUCTS
//====================================================
struct BiasContext
{
HTFBias bias; // Hướng bias: UP/DOWN/SIDEWAY/NONE
double rangeHigh, rangeLow; // Biên range khi SIDEWAY (dùng b2 H/L)
datetime lastBarTime; // Guard: chỉ recalc khi D1 bar mới mở
};
//----------------------------------------------------------------------
// TFTrendContext Swing structure + MSS
//
// v4 MSS (đơn giản):
// MSS = keyLevel sóng hồi trên TriggerTF bị phá vỡ
// - H1 UP → M5 đang DOWN (hồi) → tH0 bị phá (close > tH0) = bull MSS
// - H1 DOWN → M5 đang UP (hồi) → tL0 bị phá (close < tL0) = bear MSS
//
// Entry = giá keyLevel vừa bị phá (đặt limit chờ giá hồi)
// SL = swing extreme sóng hồi (tL0 cho buy, tH0 cho sell)
// Cả 2 được capture TRƯỚC khi re-scan swing.
//----------------------------------------------------------------------
struct TFTrendContext
{
// ── Swing structure ────────────────────────────────────────────
MarketDir trend; // DIR_UP / DIR_DOWN / DIR_NONE
double h0, h1, l0, l1; // 2 swing highs + 2 swing lows gần nhất
int idxH0, idxH1, idxL0, idxL1; // Bar indices tương ứng
double keyLevel; // L0 (uptrend) hoặc H0 (downtrend)
datetime lastBarTime; // Guard: chỉ recalc trên bar mới
// ── MSS tracking ───────────────────────────────────────────────
datetime lastMssTime; // Open time cây nến phá keyLevel
double lastMssLevel; // KeyLevel bị phá (= giá entry)
MarketDir lastMssBreak; // DIR_UP = bull MSS, DIR_DOWN = bear MSS
double mssSLSwing; // v4: swing SL tại thời điểm MSS
// (L0 cho bull, H0 cho bear)
};
struct FVGRecord
{
int id; // Unique FVG ID, auto-increment
FVGStatus status; // PENDING → TOUCHED → USED lifecycle
int usedCase; // 0=expired 1=broken(H1 close phá) 2=triggered(MSS)
MarketDir direction; // DIR_UP=bullish gap, DIR_DOWN=bearish gap
double high, low, mid; // Biên trên, biên dưới, midpoint của gap
datetime createdTime; // Thời gian cây nến bên phải gap (i-1)
datetime touchTime; // Thời gian bid đầu tiên vào vùng gap
datetime usedTime; // Thời gian FVG bị consumed (broken/expired/triggered)
MarketDir triggerTrendAtTouch; // M5 trend tại thời điểm touch (dùng cho case2 check)
};
struct OrderPlan
{
bool valid; // true khi đã build thành công, sẵn sàng gửi
int direction; // +1 = BUY LIMIT, -1 = SELL LIMIT
double entry, stopLoss, takeProfit, lot; // Giá entry, SL, TP, lot size
int parentFVGId; // ID FVG gốc → ghi vào comment lệnh
};
struct DailyRiskContext
{
double startBalance, currentBalance; // Balance đầu ngày vs hiện tại
datetime dayStartTime; // Open time D1 bar hiện tại
bool limitHit; // true → dừng trade hết ngày
};
//====================================================
// GLOBALS
//====================================================
EAState g_State = EA_IDLE; // State machine hiện tại
BlockReason g_BlockReason = BLOCK_NONE; // Lý do bị block (nếu có)
BiasContext g_Bias; // D1 bias context
TFTrendContext g_MiddleTrend; // H1 trend + swing + MSS
TFTrendContext g_TriggerTrend; // M5 trend + swing + MSS (entry trigger)
DailyRiskContext g_DailyRisk; // Daily drawdown tracking
FVGRecord g_FVGPool[MAX_FVG_POOL]; // Pool chứa tối đa 30 FVG records
int g_FVGCount = 0; // Số FVG hiện có trong pool
int g_NextFVGId = 0; // ID tiếp theo cho FVG mới
int g_ActiveFVGIdx = -1; // Index FVG đang theo dõi (-1 = không có)
int g_TradeBarIndex = -1; // Bar index lúc đặt lệnh (dùng cho timeout)
OrderPlan g_OrderPlan; // Kế hoạch lệnh hiện tại
ulong g_PendingTicket = 0; // Ticket pending order (0 = không có)
// Chart object prefixes
const string SW_PREFIX = "SW_"; // MiddleTF swing arrows/labels
const string TS_PREFIX = "TS_"; // TriggerTF swing arrows/labels
const string MSS_PREFIX = "MSS_"; // MSS markers
const string FVGP_PREFIX = "FVGP_"; // MiddleTF FVG rectangles
const string ORD_PREFIX = "ORD_"; // Order visualization (TradingView-style)
const string DBG_PREFIX = "DBG_"; // Debug panel labels
//+------------------------------------------------------------------+
//| MQL5 HELPER: iBarShift replacement |
//+------------------------------------------------------------------+
int MyBarShift(string symbol, ENUM_TIMEFRAMES tf, datetime time, bool exact = false)
{
datetime arr[]; // Mảng chứa open time tất cả bars
int maxCopy = MathMin(Bars(symbol, tf), 5000); // Giới hạn 5000 bars tránh quá tải bộ nhớ
int copied = CopyTime(symbol, tf, 0, maxCopy, arr); // Copy toàn bộ bar times vào arr[]
if (copied <= 0) return -1; // Không có data → trả -1
for (int i = copied - 1; i >= 0; i--) // Duyệt từ mới nhất → cũ nhất
{
if (arr[i] <= time) // Tìm bar đầu tiên có time <= target
return copied - 1 - i; // Convert array index → bar shift (0=newest)
}
return exact ? -1 : copied - 1; // exact=true: ko tìm thấy→-1, false→bar cũ nhất
}
//+------------------------------------------------------------------+
//| SECTION 1 SWING HELPERS |
//+------------------------------------------------------------------+
bool IsSwingHighAt(ENUM_TIMEFRAMES tf, int i)
{
double p = iHigh(_Symbol, tf, i); // Giá high tại bar i (ứng viên swing)
for (int k = 1; k <= InpSwingRange; k++) // Kiểm tra InpSwingRange bars 2 bên
if (iHigh(_Symbol, tf, i-k) >= p || iHigh(_Symbol, tf, i+k) >= p) return false; // Có bar cao hơn → không phải swing
return true; // Bar i là đỉnh cao nhất trong vùng
}
bool IsSwingLowAt(ENUM_TIMEFRAMES tf, int i)
{
double p = iLow(_Symbol, tf, i); // Giá low tại bar i (ứng viên swing)
for (int k = 1; k <= InpSwingRange; k++) // Kiểm tra InpSwingRange bars 2 bên
if (iLow(_Symbol, tf, i-k) <= p || iLow(_Symbol, tf, i+k) <= p) return false; // Có bar thấp hơn → không phải swing
return true; // Bar i là đáy thấp nhất trong vùng
}
bool ScanSwingStructure(
ENUM_TIMEFRAMES tf, int lookback,
double &h0, double &h1, int &idxH0, int &idxH1,
double &l0, double &l1, int &idxL0, int &idxL1)
{
int maxBar = MathMin(lookback, Bars(_Symbol, tf) - InpSwingRange - 2); // Giới hạn scan range
double highs[2]; int hiIdx[2]; int hc = 0; // 2 swing highs gần nhất (h0=mới, h1=cũ)
double lows [2]; int loIdx[2]; int lc = 0; // 2 swing lows gần nhất (l0=mới, l1=cũ)
for (int i = InpSwingRange + 1; i <= maxBar; i++) // Bắt đầu từ bar đủ xa để có range 2 bên
{
if (hc < 2 && IsSwingHighAt(tf, i)) { highs[hc] = iHigh(_Symbol, tf, i); hiIdx[hc] = i; hc++; } // Thu thập swing high
if (lc < 2 && IsSwingLowAt (tf, i)) { lows [lc] = iLow (_Symbol, tf, i); loIdx[lc] = i; lc++; } // Thu thập swing low
if (hc == 2 && lc == 2) break; // Đủ 2 cặp → dừng scan
}
if (hc < 2 || lc < 2) return false; // Thiếu swing → không xác định được trend
h0 = highs[0]; idxH0 = hiIdx[0]; // Swing high gần nhất (most recent)
h1 = highs[1]; idxH1 = hiIdx[1]; // Swing high trước đó (để so sánh HH/LH)
l0 = lows [0]; idxL0 = loIdx[0]; // Swing low gần nhất
l1 = lows [1]; idxL1 = loIdx[1]; // Swing low trước đó (để so sánh HL/LL)
return true;
}
void ResolveTrendFromSwings(
ENUM_TIMEFRAMES tf,
double h0, double h1, double l0, double l1,
MarketDir &trend, double &keyLevel)
{
double c1 = iClose(_Symbol, tf, 1);
if (h0 > h1 && l0 > l1 && c1 > l0) { trend = DIR_UP; keyLevel = l0; } // HH+HL → uptrend, KL=L0
else if (h0 < h1 && l0 < l1 && c1 < h0) { trend = DIR_DOWN; keyLevel = h0; } // LH+LL → downtrend, KL=H0
else { trend = DIR_NONE; keyLevel = 0; }
}
//+------------------------------------------------------------------+
//| SECTION 2 CONTEXT UPDATERS |
//+------------------------------------------------------------------+
HTFBias ResolveBias(double b1H, double b1L, double b1C, double b2H, double b2L)
{
if (b1C > b2H) return BIAS_UP; // Close trên high hôm qua
if (b1C < b2L) return BIAS_DOWN; // Close dưới low hôm qua
if (b1H > b2H && b1C < b2H) return BIAS_DOWN; // False break up → bearish
if (b1L < b2L && b1C > b2L) return BIAS_UP; // False break down → bullish
return BIAS_SIDEWAY;
}
void UpdateBiasContext()
{
datetime t0 = iTime(_Symbol, InpBiasTF, 0); // Open time bar D1 hiện tại
if (t0 == g_Bias.lastBarTime) return; // Đã tính rồi → skip (chỉ tính 1 lần/ngày)
g_Bias.lastBarTime = t0; // Đánh dấu đã xử lý bar này
if (Bars(_Symbol, InpBiasTF) < 4) { g_Bias.bias = BIAS_NONE; return; } // Chưa đủ data
double b1H = iHigh (_Symbol, InpBiasTF, 1); // D1 bar hôm qua: high
double b1L = iLow (_Symbol, InpBiasTF, 1); // low
double b1C = iClose(_Symbol, InpBiasTF, 1); // close
double b2H = iHigh (_Symbol, InpBiasTF, 2); // D1 bar hôm kia: high (reference range)
double b2L = iLow (_Symbol, InpBiasTF, 2); // low (reference range)
HTFBias prev = g_Bias.bias; // Lưu bias cũ để log khi thay đổi
g_Bias.bias = ResolveBias(b1H, b1L, b1C, b2H, b2L); // So sánh b1 close vs b2 range
g_Bias.rangeHigh = (g_Bias.bias == BIAS_SIDEWAY) ? b2H : 0; // SIDEWAY: lưu biên trên
g_Bias.rangeLow = (g_Bias.bias == BIAS_SIDEWAY) ? b2L : 0; // SIDEWAY: lưu biên dưới
if (InpDebugLog && g_Bias.bias != prev)
PrintFormat("[BIAS] %s → %s | b1[H=%.5f L=%.5f C=%.5f] b2[H=%.5f L=%.5f]",
EnumToString(prev), EnumToString(g_Bias.bias), b1H, b1L, b1C, b2H, b2L);
}
//----------------------------------------------------------------------
// UpdateTFTrendContext
//
// Gọi mỗi bar mới. Thực hiện TRƯỚC khi re-scan swing:
//
// v4 MSS check (TriggerTF only):
// - M5 đang downtrend (hồi khi H1 UP): keyLevel = tH0
// → close > tH0 = bull MSS → entry BUY LIMIT tại tH0
// - M5 đang uptrend (hồi khi H1 DOWN): keyLevel = tL0
// → close < tL0 = bear MSS → entry SELL LIMIT tại tL0
//
// Chỉ nhận MSS thuận chiều MiddleTF (filter duy nhất).
// Entry price = keyLevel bị phá (tH0 hoặc tL0).
// SL = swing extreme sóng hồi (tL0 cho buy, tH0 cho sell).
// Cả entry + SL capture TRƯỚC re-scan vì sau đó swing thay đổi.
//----------------------------------------------------------------------
void UpdateTFTrendContext(ENUM_TIMEFRAMES tf, int lookback, TFTrendContext &ctx)
{
datetime t0 = iTime(_Symbol, tf, 0);
if (t0 == ctx.lastBarTime) return; // Chỉ chạy 1 lần/bar
ctx.lastBarTime = t0;
double bar1C = iClose(_Symbol, tf, 1); // Bar vừa đóng: close
double bar1H = iHigh (_Symbol, tf, 1); // high
double bar1L = iLow (_Symbol, tf, 1); // low
datetime bar1T = iTime (_Symbol, tf, 1); // time
// ══════════════════════════════════════════════════════════════════
// MSS check (TRƯỚC re-scan, dùng OLD keyLevel + swing)
//
// Logic:
// trend hiện tại có keyLevel → nếu close phá keyLevel:
// breakDir = hướng phá (UP nếu phá lên, DOWN nếu phá xuống)
// Chỉ nhận nếu breakDir == MiddleTF trend (TriggerTF only)
//
// Capture tại thời điểm MSS:
// lastMssLevel = keyLevel bị phá = ENTRY PRICE cho limit order
// mssSLSwing = swing extreme phía đối diện = SL level
// Bull MSS: SL = L0 (đáy sóng hồi)
// Bear MSS: SL = H0 (đỉnh sóng hồi)
// ══════════════════════════════════════════════════════════════════
if (ctx.trend != DIR_NONE && ctx.keyLevel > 0)
{
bool mssHit =
(ctx.trend == DIR_UP && bar1C < ctx.keyLevel) || // Uptrend KL=L0 → break below
(ctx.trend == DIR_DOWN && bar1C > ctx.keyLevel); // Downtrend KL=H0 → break above
if (mssHit && bar1T != ctx.lastMssTime)
{
MarketDir breakDir = (ctx.trend == DIR_UP) ? DIR_DOWN : DIR_UP;
// TriggerTF: chỉ nhận MSS thuận chiều MiddleTF
bool accept = true;
if (tf == InpTriggerTF && g_MiddleTrend.trend != DIR_NONE)
{
if (breakDir != (MarketDir)g_MiddleTrend.trend)
accept = false; // Ngược chiều → bỏ qua
}
if (accept)
{
ctx.lastMssTime = bar1T; // Thời gian cây nến MSS
ctx.lastMssLevel = ctx.keyLevel; // KeyLevel bị phá = ENTRY
ctx.lastMssBreak = breakDir; // Hướng break
// v4: Capture SL swing TRƯỚC re-scan
if (breakDir == DIR_UP)
ctx.mssSLSwing = ctx.l0; // Bull MSS → SL dưới L0 (đáy hồi)
else
ctx.mssSLSwing = ctx.h0; // Bear MSS → SL trên H0 (đỉnh hồi)
if (InpDebugLog)
PrintFormat("[%s MSS] %s | entry=%.5f SL_swing=%.5f | close=%.5f | %s",
EnumToString(tf),
(breakDir == DIR_UP) ? "▲ Bull" : "▼ Bear",
ctx.lastMssLevel, ctx.mssSLSwing,
bar1C, TimeToString(bar1T));
}
else if (InpDebugLog)
{
PrintFormat("[%s MSS skip] %s ngược H1=%s | KL=%.5f",
EnumToString(tf),
(breakDir == DIR_UP) ? "▲" : "▼",
EnumToString(g_MiddleTrend.trend), ctx.keyLevel);
}
}
}
// ══════════════════════════════════════════════════════════════════
// Re-scan swing structure
// ══════════════════════════════════════════════════════════════════
double h0, h1, l0, l1;
int idxH0, idxH1, idxL0, idxL1;
if (!ScanSwingStructure(tf, lookback, h0, h1, idxH0, idxH1, l0, l1, idxL0, idxL1))
{ ctx.trend = DIR_NONE; return; }
ctx.h0 = h0; ctx.idxH0 = idxH0;
ctx.h1 = h1; ctx.idxH1 = idxH1;
ctx.l0 = l0; ctx.idxL0 = idxL0;
ctx.l1 = l1; ctx.idxL1 = idxL1;
MarketDir prev = ctx.trend;
ResolveTrendFromSwings(tf, h0, h1, l0, l1, ctx.trend, ctx.keyLevel);
if (InpDebugLog && ctx.trend != prev)
PrintFormat("[%s TREND] %s → %s | H0=%.5f H1=%.5f L0=%.5f L1=%.5f | KL=%.5f",
EnumToString(tf), EnumToString(prev), EnumToString(ctx.trend),
h0, h1, l0, l1, ctx.keyLevel);
}
void UpdateDailyRiskContext()
{
datetime today = iTime(_Symbol, PERIOD_D1, 0); // Open time ngày hiện tại
if (today != g_DailyRisk.dayStartTime) // Ngày mới → reset tracking
{
g_DailyRisk.dayStartTime = today; // Đánh dấu ngày mới
g_DailyRisk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Snapshot balance đầu ngày
g_DailyRisk.limitHit = false; // Reset flag daily loss
if (InpDebugLog) PrintFormat("[DAILY RISK] New day | start=%.2f", g_DailyRisk.startBalance);
}
if (g_DailyRisk.limitHit) return; // Đã hit limit → không cần check nữa
g_DailyRisk.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Balance hiện tại
double lostPct = (g_DailyRisk.startBalance - g_DailyRisk.currentBalance) // Tính % loss trong ngày
/ g_DailyRisk.startBalance * 100.0;
if (lostPct >= InpMaxDailyLossPct) // Vượt ngưỡng → dừng trade
{
g_DailyRisk.limitHit = true; // Set flag → EvaluateGuards() sẽ block
PrintFormat("[DAILY RISK] ⛔ Limit hit | lost=%.2f%% | bal=%.2f",
lostPct, g_DailyRisk.currentBalance);
}
}
void UpdateAllContexts()
{
UpdateDailyRiskContext(); // 1. Check daily drawdown trước tiên
UpdateBiasContext(); // 2. D1 bias (UP/DOWN/SIDEWAY)
UpdateTFTrendContext(InpMiddleTF, InpSwingLookback, g_MiddleTrend); // 3. H1 swing+trend+MSS
UpdateTFTrendContext(InpTriggerTF, InpTriggerSwingLookback, g_TriggerTrend); // 4. M5 swing+trend+MSS (entry signal)
}
//+------------------------------------------------------------------+
//| SECTION 3 GUARDS |
//+------------------------------------------------------------------+
bool IsSessionAllowed() { return true; /* TODO: London/NY session filter dựa trên InpLondon/NYHour */ }
bool IsDailyLossOK() { return !g_DailyRisk.limitHit; } // false nếu đã vượt InpMaxDailyLossPct
bool IsBiasValid() { return g_Bias.bias == BIAS_UP || g_Bias.bias == BIAS_DOWN; } // Cần direction rõ ràng
bool IsMiddleTrendAligned() // H1 trend phải cùng hướng D1 bias
{
if (g_MiddleTrend.trend == DIR_NONE) return false; // H1 chưa xác định → block
return (g_Bias.bias == BIAS_UP && g_MiddleTrend.trend == DIR_UP) || // D1 UP + H1 UP ✓
(g_Bias.bias == BIAS_DOWN && g_MiddleTrend.trend == DIR_DOWN); // D1 DOWN + H1 DOWN ✓
}
bool EvaluateGuards() // Kiểm tra tất cả điều kiện trước khi trade
{
g_BlockReason = BLOCK_NONE; // Reset reason
if (!IsSessionAllowed()) { g_BlockReason = BLOCK_SESSION; return false; } // Ngoài giờ
if (!IsDailyLossOK()) { g_BlockReason = BLOCK_DAILY_LOSS; return false; } // Vượt loss/ngày
if (!IsBiasValid()) { g_BlockReason = BLOCK_NO_BIAS; return false; } // D1 sideway/none
if (!IsMiddleTrendAligned()) { g_BlockReason = BLOCK_BIAS_MISMATCH; return false; } // H1 ngược D1
return true; // Tất cả OK → cho phép trade
}
//+------------------------------------------------------------------+
//| SECTION 4 STATE MACHINE HELPERS |
//+------------------------------------------------------------------+
void TransitionTo(EAState next)
{
if (InpDebugLog)
PrintFormat("[STATE] %s → %s", EnumToString(g_State), EnumToString(next));
g_State = next;
}
void ResetToIdle(string reason = "")
{
if (InpDebugLog && reason != "")
PrintFormat("[RESET→IDLE] %s", reason);
g_ActiveFVGIdx = -1; // Bỏ FVG đang theo dõi
g_TradeBarIndex = -1; // Reset bar index
g_PendingTicket = 0; // Không còn pending order
ZeroMemory(g_OrderPlan); // Xóa order plan
TransitionTo(EA_IDLE); // Quay về IDLE tìm FVG mới
}
//+------------------------------------------------------------------+
//| SECTION 5 FVG HELPERS |
//+------------------------------------------------------------------+
bool IsCandleStrong(ENUM_TIMEFRAMES tf, int i)
{
double h = iHigh(_Symbol, tf, i), l = iLow(_Symbol, tf, i); // Range = high - low
double o = iOpen(_Symbol, tf, i), c = iClose(_Symbol, tf, i); // Body = |close - open|
double range = h - l; // Tổng chiều dài nến
if (range < _Point) return false; // Nến quá nhỏ → bỏ qua
return (MathAbs(c - o) / range * 100.0) >= InpFVGMinBodyPct; // Body >= 60% range → strong
}
bool IsFVGInPool(datetime created)
{
for (int j = 0; j < g_FVGCount; j++)
if (g_FVGPool[j].createdTime == created) return true;
return false;
}
//+------------------------------------------------------------------+
//| SECTION 6 FVG POOL: SCAN & REGISTER |
//+------------------------------------------------------------------+
void ScanAndRegisterFVGs()
{
static datetime s_lastScan = 0; // Guard: chỉ scan 1 lần mỗi bar H1
datetime t0 = iTime(_Symbol, InpMiddleTF, 0); // Open time bar H1 hiện tại
if (t0 == s_lastScan) return; // Đã scan bar này rồi → skip
s_lastScan = t0; // Đánh dấu đã scan
MarketDir dir = g_MiddleTrend.trend; // Chỉ scan FVG thuận chiều H1 trend
if (dir == DIR_NONE) return; // Không có trend → không scan
int maxBar = MathMin(InpFVGScanBars, Bars(_Symbol, InpMiddleTF) - 2); // Giới hạn scan range
for (int i = 2; i <= maxBar; i++)
{
double leftH = iHigh (_Symbol, InpMiddleTF, i + 1); // Cây bên trái gap
double leftL = iLow (_Symbol, InpMiddleTF, i + 1);
double rightH = iHigh (_Symbol, InpMiddleTF, i - 1); // Cây bên phải gap
double rightL = iLow (_Symbol, InpMiddleTF, i - 1);
double midO = iOpen (_Symbol, InpMiddleTF, i); // Cây giữa (tạo gap)
double midC = iClose(_Symbol, InpMiddleTF, i);
double gH = 0, gL = 0;
if (dir == DIR_UP) // Bullish FVG
{
if (leftH >= rightL) continue; // Không có gap
if (midC <= midO) continue; // Cây giữa phải bullish
if (!IsCandleStrong(InpMiddleTF, i)) continue; // Body đủ lớn
gL = leftH; gH = rightL; // Gap = leftH → rightL
}
else // Bearish FVG
{
if (leftL <= rightH) continue;
if (midC >= midO) continue; // Cây giữa phải bearish
if (!IsCandleStrong(InpMiddleTF, i)) continue;
gH = leftL; gL = rightH; // Gap = rightH → leftL
}
datetime created = iTime(_Symbol, InpMiddleTF, i - 1); // FVG created = open time cây phải
if (IsFVGInPool(created)) continue; // Đã có trong pool → skip trùng
// ── Evict oldest USED if pool full ──────────────────────────────
if (g_FVGCount >= MAX_FVG_POOL) // Pool đầy 30 slot
{
int evict = -1; datetime oldest = TimeCurrent(); // Tìm USED cũ nhất để xóa
for (int j = 0; j < g_FVGCount; j++)
if (g_FVGPool[j].status == FVG_USED && g_FVGPool[j].createdTime < oldest)
{ oldest = g_FVGPool[j].createdTime; evict = j; } // Ghi nhớ slot cũ nhất
if (evict < 0) { if (InpDebugLog) Print("[FVG POOL] Full"); break; } // Không có USED → stop
for (int j = evict; j < g_FVGCount - 1; j++) g_FVGPool[j] = g_FVGPool[j + 1]; // Shift array
g_FVGCount--; // Giảm count
if (g_ActiveFVGIdx > evict) g_ActiveFVGIdx--; // Fix active index sau shift
else if (g_ActiveFVGIdx == evict) g_ActiveFVGIdx = -1; // Active bị evict → clear
}
// ── Build record ─────────────────────────────────────────────────
FVGRecord rec;
ZeroMemory(rec); // Init tất cả field = 0
rec.id = g_NextFVGId++; rec.direction = dir; // Assign ID tự tăng + hướng
rec.high = gH; rec.low = gL; rec.mid = (gH + gL) / 2.0; // Biên gap + midpoint
rec.createdTime = created; // Thời gian tạo
int rightBar = i - 1; // Cây bên phải gap (dùng cho scan status)
// P1: case1 Kiểm tra đã bị phá xuyên chưa (H1 close qua gap)
bool c1Hit = false; datetime c1T = 0; // Flag + thời gian bị phá
for (int j = rightBar - 1; j >= 1; j--) // Scan từ cây phải → hiện tại
{
double cl = iClose(_Symbol, InpMiddleTF, j); // Close mỗi bar H1
if ((rec.direction == DIR_UP && cl < rec.low) || // Bull FVG: close dưới biên dưới = phá
(rec.direction == DIR_DOWN && cl > rec.high)) // Bear FVG: close trên biên trên = phá
{ c1Hit = true; c1T = iTime(_Symbol, InpMiddleTF, j); break; } // Ghi nhận thời gian phá
}
if (c1Hit) { rec.status = FVG_USED; rec.usedCase = 1; rec.usedTime = c1T; } // Case1 = broken
else
{
// P2: touch Kiểm tra giá đã wick vào gap chưa
bool tdHit = false; datetime tdT = 0; // Flag + thời gian touch
for (int j = rightBar - 1; j >= 1; j--) // Scan từ cây phải → hiện tại
{
bool inGap = (rec.direction == DIR_UP && iLow (_Symbol, InpMiddleTF, j) <= rec.high) || // Bull: low ≤ gap top
(rec.direction == DIR_DOWN && iHigh(_Symbol, InpMiddleTF, j) >= rec.low); // Bear: high ≥ gap bottom
if (inGap) { tdHit = true; tdT = iTime(_Symbol, InpMiddleTF, j); break; } // Đã touch
}
if (tdHit) { rec.status = FVG_TOUCHED; rec.touchTime = tdT; rec.triggerTrendAtTouch = g_TriggerTrend.trend; } // Ghi nhận M5 trend lúc touch
else
{
rec.status = FVG_PENDING; // Chưa touch, chưa bị phá → chờ
if ((int)(TimeCurrent() - rec.createdTime) > InpFVGMaxAliveMin * 60) // Quá 72h
{ rec.status = FVG_USED; rec.usedCase = 0; rec.usedTime = TimeCurrent(); } // Case0 = expired
}
}
g_FVGPool[g_FVGCount] = rec; // Thêm vào pool
g_FVGCount++; // Tăng count
if (InpDebugLog)
PrintFormat("[FVG +] #%d %s [%.5f%.5f] %s | %s",
rec.id, EnumToString(rec.direction), rec.low, rec.high,
EnumToString(rec.status), TimeToString(rec.createdTime));
}
}
//+------------------------------------------------------------------+
//| SECTION 7 FVG POOL: UPDATE STATUSES (every tick) |
//+------------------------------------------------------------------+
void UpdateFVGStatuses()
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // Giá bid hiện tại (dùng cho touch check)
double midC1 = iClose(_Symbol, InpMiddleTF, 1); // H1 bar vừa đóng: close (dùng cho case1)
datetime midT1 = iTime (_Symbol, InpMiddleTF, 1); // H1 bar vừa đóng: time
for (int i = 0; i < g_FVGCount; i++) // Duyệt toàn bộ pool
{
if (g_FVGPool[i].status == FVG_USED) continue; // Đã consumed → skip
// ── 1. Case1: H1 close phá xuyên FVG → broken ──────────────────
bool c1 = (g_FVGPool[i].direction == DIR_UP && midC1 < g_FVGPool[i].low) || // Bull: close < bottom
(g_FVGPool[i].direction == DIR_DOWN && midC1 > g_FVGPool[i].high); // Bear: close > top
if (c1)
{
g_FVGPool[i].status = FVG_USED;
g_FVGPool[i].usedCase = 1;
g_FVGPool[i].usedTime = midT1;
if (InpDebugLog)
PrintFormat("[FVG #%d] BROKEN | close=%.5f", g_FVGPool[i].id, midC1);
continue;
}
if (g_FVGPool[i].status == FVG_PENDING)
{
// ── 2. Expire ────────────────────────────────────────────────────
int age = (int)(TimeCurrent() - g_FVGPool[i].createdTime);
if (age > InpFVGMaxAliveMin * 60)
{
g_FVGPool[i].status = FVG_USED; g_FVGPool[i].usedCase = 0;
g_FVGPool[i].usedTime = TimeCurrent();
continue;
}
// ── 3. Touch: bid vào vùng gap ───────────────────────────────────
bool touched = (g_FVGPool[i].direction == DIR_UP && bid <= g_FVGPool[i].high) || // Bull: bid retrace xuống ≤ gap top
(g_FVGPool[i].direction == DIR_DOWN && bid >= g_FVGPool[i].low); // Bear: bid retrace lên ≥ gap bottom
if (touched)
{
g_FVGPool[i].status = FVG_TOUCHED; // Chuyển sang TOUCHED
g_FVGPool[i].touchTime = TimeCurrent(); // Ghi nhận thời gian touch
g_FVGPool[i].triggerTrendAtTouch = g_TriggerTrend.trend; // Snapshot M5 trend lúc touch
if (InpDebugLog)
PrintFormat("[FVG #%d] TOUCHED | bid=%.5f [%.5f%.5f]",
g_FVGPool[i].id, bid, g_FVGPool[i].low, g_FVGPool[i].high);
}
}
else if (g_FVGPool[i].status == FVG_TOUCHED)
{
// ── 4. Case2: M5 MSS đã xảy ra SAU touch → triggered ───────────
//
// v4: Kiểm tra g_TriggerTrend.lastMssTime > touchTime
// VÀ hướng MSS phù hợp FVG direction
//
bool hasMSS =
g_TriggerTrend.lastMssTime > g_FVGPool[i].touchTime && // MSS sau touch
g_TriggerTrend.lastMssBreak == g_FVGPool[i].direction; // Cùng hướng
if (hasMSS)
{
g_FVGPool[i].status = FVG_USED;
g_FVGPool[i].usedCase = 2; // Case2 = triggered
g_FVGPool[i].usedTime = TimeCurrent();
if (InpDebugLog)
PrintFormat("[FVG #%d] TRIGGERED | MSS %s @ %s",
g_FVGPool[i].id,
(g_TriggerTrend.lastMssBreak == DIR_UP) ? "▲" : "▼",
TimeToString(g_TriggerTrend.lastMssTime, TIME_MINUTES));
continue;
}
// ── 5. Trigger timeout ────────────────────────────────────────────
int bars = (int)((TimeCurrent() - g_FVGPool[i].touchTime) / PeriodSeconds(InpTriggerTF));
if (bars > InpTriggerMaxBars
&& g_ActiveFVGIdx >= 0
&& g_FVGPool[g_ActiveFVGIdx].id == g_FVGPool[i].id)
{
if (InpDebugLog)
PrintFormat("[FVG #%d] Timeout (%d bars)", g_FVGPool[i].id, bars);
g_ActiveFVGIdx = -1;
}
}
}
}
//+------------------------------------------------------------------+
//| SECTION 7B ORDER PLAN & EXECUTION |
//| |
//| v4: Entry = MSS broken keyLevel (tH0 hoặc tL0) |
//| SL = swing extreme sóng hồi (capture lúc MSS) |
//| TP = Entry ± InpRiskReward * |Entry - SL| |
//+------------------------------------------------------------------+
double CalcLotFromRisk(double entry, double sl)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE); // Balance hiện tại
double riskMoney = balance * InpRiskPercent / 100.0; // Số tiền chấp nhận mất (VD: 1% of 10000 = 100)
double slPips = MathAbs(entry - sl) / _Point; // SL tính bằng points (VD: 50 points)
if (slPips < 1) return 0; // SL quá nhỏ → invalid
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); // Giá trị 1 tick/lot
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); // Kích thước 1 tick
if (tickValue <= 0 || tickSize <= 0) return 0; // Broker data invalid
double pipValue = tickValue * (_Point / tickSize); // Giá trị 1 point cho 1 lot
double rawLot = riskMoney / (slPips * pipValue); // Lot = riskMoney / (points × value/point)
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); // VD: 0.01
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); // VD: 100.0
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); // VD: 0.01
if (lotStep <= 0) lotStep = 0.01; // Fallback
rawLot = MathFloor(rawLot / lotStep) * lotStep; // Làm tròn xuống theo lotStep
rawLot = MathMax(minLot, MathMin(maxLot, rawLot)); // Clamp trong [min, max]
return NormalizeDouble(rawLot, 2); // Trả về lot 2 chữ số thập phân
}
bool BuildOrderPlan(int fvgId, MarketDir dir)
{
ZeroMemory(g_OrderPlan); // Reset plan cũ
// v4: Entry = keyLevel vừa bị phá (đã capture trong MSS context)
double entry = NormalizeDouble(g_TriggerTrend.lastMssLevel, _Digits); // tH0 (buy) hoặc tL0 (sell)
// v4: SL = swing extreme sóng hồi (đã capture trong MSS context)
double sl = g_TriggerTrend.mssSLSwing; // tL0 (buy) hoặc tH0 (sell), captured TRƯỚC re-scan
if (entry <= 0 || sl <= 0) return false; // Data invalid → không đặt lệnh
double tp;
if (dir == DIR_UP) // ── BUY LIMIT setup ──
{
if (sl >= entry) return false; // SL phải dưới entry cho buy
sl = NormalizeDouble(sl - 2 * _Point, _Digits); // Buffer 2 points dưới swing low
double riskDist = entry - sl; // Khoảng cách risk (points)
tp = NormalizeDouble(entry + InpRiskReward * riskDist, _Digits); // TP = entry + 2R (mặc định)
}
else // ── SELL LIMIT setup ──
{
if (sl <= entry) return false; // SL phải trên entry cho sell
sl = NormalizeDouble(sl + 2 * _Point, _Digits); // Buffer 2 points trên swing high
double riskDist = sl - entry; // Khoảng cách risk (points)
tp = NormalizeDouble(entry - InpRiskReward * riskDist, _Digits); // TP = entry - 2R
}
double lot = CalcLotFromRisk(entry, sl); // Tính lot từ risk% và SL distance
if (lot <= 0) return false; // Lot invalid → skip
g_OrderPlan.valid = true; // Plan sẵn sàng
g_OrderPlan.direction = (dir == DIR_UP) ? 1 : -1; // +1=buy, -1=sell
g_OrderPlan.entry = entry; // Giá đặt limit order
g_OrderPlan.stopLoss = sl; // SL đã có buffer
g_OrderPlan.takeProfit = tp; // TP theo R:R ratio
g_OrderPlan.lot = lot; // Lot size theo risk management
g_OrderPlan.parentFVGId = fvgId; // Link về FVG gốc
if (InpDebugLog)
PrintFormat("[ORDER PLAN] %s | entry=%.5f SL=%.5f TP=%.5f lot=%.2f | FVG#%d",
(dir == DIR_UP) ? "BUY LIMIT" : "SELL LIMIT",
entry, sl, tp, lot, fvgId);
return true;
}
ulong ExecuteLimitOrder()
{
if (!g_OrderPlan.valid) return 0;
ENUM_ORDER_TYPE cmd = (g_OrderPlan.direction > 0)
? ORDER_TYPE_BUY_LIMIT
: ORDER_TYPE_SELL_LIMIT;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Validate: BUY LIMIT < Ask, SELL LIMIT > Bid
if (cmd == ORDER_TYPE_BUY_LIMIT && g_OrderPlan.entry >= ask)
{
if (InpDebugLog) PrintFormat("[ORDER] BUY LIMIT entry %.5f >= ask %.5f → skip",
g_OrderPlan.entry, ask);
return 0;
}
if (cmd == ORDER_TYPE_SELL_LIMIT && g_OrderPlan.entry <= bid)
{
if (InpDebugLog) PrintFormat("[ORDER] SELL LIMIT entry %.5f <= bid %.5f → skip",
g_OrderPlan.entry, bid);
return 0;
}
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request); // Init tất cả field = 0
ZeroMemory(result);
request.action = TRADE_ACTION_PENDING; // Đặt lệnh chờ (không market order)
request.symbol = _Symbol; // Symbol hiện tại trên chart
request.volume = g_OrderPlan.lot; // Lot size đã tính từ risk%
request.type = cmd; // BUY_LIMIT hoặc SELL_LIMIT
request.price = g_OrderPlan.entry; // Giá đặt lệnh = MSS keyLevel
request.sl = g_OrderPlan.stopLoss; // SL = swing hồi + buffer
request.tp = g_OrderPlan.takeProfit; // TP = entry ± R:R ratio
request.deviation = (ulong)InpSlippage; // Slippage tối đa (points)
request.magic = InpMagicNumber; // Magic number để identify EA's orders
request.comment = StringFormat("ICT#%d", g_OrderPlan.parentFVGId); // Comment link FVG
request.type_filling = ORDER_FILLING_RETURN; // Return unfilled volume (broker-safe)
request.type_time = ORDER_TIME_GTC; // Good Till Cancel (không hết hạn)
if (!OrderSend(request, result))
{
PrintFormat("[ORDER] ❌ retcode=%u", result.retcode);
return 0;
}
if (result.retcode != TRADE_RETCODE_DONE && result.retcode != TRADE_RETCODE_PLACED)
{
PrintFormat("[ORDER] ❌ rejected retcode=%u: %s", result.retcode, result.comment);
return 0;
}
PrintFormat("[ORDER] ✅ %s #%llu | %.2f @ %.5f SL=%.5f TP=%.5f",
(cmd == ORDER_TYPE_BUY_LIMIT) ? "BUY_LIM" : "SELL_LIM",
result.order, g_OrderPlan.lot, g_OrderPlan.entry,
g_OrderPlan.stopLoss, g_OrderPlan.takeProfit);
return result.order;
}
//+------------------------------------------------------------------+
//| SECTION 8 BEST FVG SELECTOR |
//+------------------------------------------------------------------+
int GetBestActiveFVGIdx()
{
int bestIdx = -1; datetime bestTime = 0; bool foundTouch = false; // Track best candidate
for (int i = 0; i < g_FVGCount; i++)
{
if (g_FVGPool[i].status == FVG_USED) continue; // Skip FVG đã consumed
if (g_FVGPool[i].status == FVG_TOUCHED) // TOUCHED được ưu tiên hơn PENDING
{
if (!foundTouch || g_FVGPool[i].createdTime > bestTime) // Mới nhất trong TOUCHED
{ foundTouch = true; bestIdx = i; bestTime = g_FVGPool[i].createdTime; }
}
else if (!foundTouch && g_FVGPool[i].createdTime > bestTime) // PENDING: chỉ khi chưa có TOUCHED
{ bestIdx = i; bestTime = g_FVGPool[i].createdTime; }
}
return bestIdx; // -1 nếu không có FVG nào khả dụng
}
//+------------------------------------------------------------------+
//| SECTION 9 STATE HANDLERS |
//+------------------------------------------------------------------+
void OnStateIdle()
{
int idx = GetBestActiveFVGIdx();
if (idx < 0) return;
g_ActiveFVGIdx = idx;
if (InpDebugLog)
PrintFormat("[ACTIVE FVG] #%d %s [%.5f%.5f] %s",
g_FVGPool[idx].id, EnumToString(g_FVGPool[idx].direction),
g_FVGPool[idx].low, g_FVGPool[idx].high, EnumToString(g_FVGPool[idx].status));
TransitionTo(g_FVGPool[idx].status == FVG_TOUCHED ? EA_WAIT_TRIGGER : EA_WAIT_TOUCH);
}
void OnStateWaitTouch()
{
if (g_ActiveFVGIdx < 0) { ResetToIdle("active lost"); return; }
int ai = g_ActiveFVGIdx;
if (g_FVGPool[ai].status == FVG_USED)
{ ResetToIdle(StringFormat("FVG #%d broken/expired", g_FVGPool[ai].id)); return; }
if (g_FVGPool[ai].status == FVG_TOUCHED)
{ TransitionTo(EA_WAIT_TRIGGER); return; }
// Kiểm tra FVG mới tốt hơn
int better = GetBestActiveFVGIdx();
if (better >= 0 && better != ai)
{
bool bTouch = (g_FVGPool[better].status == FVG_TOUCHED);
bool bNewer = (g_FVGPool[better].createdTime > g_FVGPool[ai].createdTime);
if (bTouch || bNewer)
{
if (InpDebugLog)
PrintFormat("[SWITCH] FVG #%d → #%d", g_FVGPool[ai].id, g_FVGPool[better].id);
g_ActiveFVGIdx = better;
if (bTouch) TransitionTo(EA_WAIT_TRIGGER);
}
}
}
void OnStateWaitTrigger()
{
if (g_ActiveFVGIdx < 0) { ResetToIdle("active lost"); return; }
int ai = g_ActiveFVGIdx;
if (g_FVGPool[ai].status == FVG_USED)
{
if (g_FVGPool[ai].usedCase == 2) // Case2 = MSS triggered
{
if (InpDebugLog)
PrintFormat("[ENTRY SIGNAL] FVG #%d %s [%.5f%.5f] | MSS entry=%.5f",
g_FVGPool[ai].id, EnumToString(g_FVGPool[ai].direction),
g_FVGPool[ai].low, g_FVGPool[ai].high,
g_TriggerTrend.lastMssLevel);
// v4: Build order plan dùng MSS data (entry=keyLevel, SL=swing hồi)
if (BuildOrderPlan(g_FVGPool[ai].id, g_FVGPool[ai].direction))
{
ulong ticket = ExecuteLimitOrder();
if (ticket > 0)
{
g_PendingTicket = ticket;
g_TradeBarIndex = Bars(_Symbol, InpTriggerTF);
TransitionTo(EA_IN_TRADE);
}
else
ResetToIdle(StringFormat("FVG #%d order failed", g_FVGPool[ai].id));
}
else
ResetToIdle(StringFormat("FVG #%d invalid plan", g_FVGPool[ai].id));
}
else
ResetToIdle(StringFormat("FVG #%d case%d", g_FVGPool[ai].id, g_FVGPool[ai].usedCase));
return;
}
if (g_FVGPool[ai].status == FVG_PENDING) { TransitionTo(EA_WAIT_TOUCH); return; }
}
void OnStateInTrade()
{
// ── 1. Pending order còn active? ───────────────────────────────
if (g_PendingTicket > 0)
{
bool foundPending = false;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (OrderGetTicket(i) == g_PendingTicket) { foundPending = true; break; }
}
if (!foundPending)
{
// Kiểm tra đã fill → position
bool posFound = false;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong pt = PositionGetTicket(i);
if (pt > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber
&& PositionGetString(POSITION_SYMBOL) == _Symbol)
{
posFound = true;
g_PendingTicket = 0; // Filled
if (InpDebugLog) PrintFormat("[TRADE] Filled → position #%llu", pt);
break;
}
}
if (!posFound)
{
HistorySelect(TimeCurrent() - 86400, TimeCurrent());
// Cancelled?
bool cancelled = false;
for (int i = HistoryOrdersTotal() - 1; i >= 0; i--)
{
ulong ht = HistoryOrderGetTicket(i);
if (ht == g_PendingTicket)
{
long st = HistoryOrderGetInteger(ht, ORDER_STATE);
if (st == ORDER_STATE_CANCELED || st == ORDER_STATE_EXPIRED || st == ORDER_STATE_REJECTED)
cancelled = true;
break;
}
}
if (cancelled) { ResetToIdle("order cancelled"); return; }
// Position closed?
bool closed = false;
for (int i = HistoryDealsTotal() - 1; i >= 0; i--)
{
ulong dt = HistoryDealGetTicket(i);
if (HistoryDealGetInteger(dt, DEAL_MAGIC) == InpMagicNumber
&& HistoryDealGetString(dt, DEAL_SYMBOL) == _Symbol
&& HistoryDealGetInteger(dt, DEAL_ENTRY) == DEAL_ENTRY_OUT)
{
double profit = HistoryDealGetDouble(dt, DEAL_PROFIT);
if (InpDebugLog) PrintFormat("[TRADE] Closed | profit=%.2f", profit);
closed = true; break;
}
}
ResetToIdle(closed ? "trade closed" : "order lost");
return;
}
}
return; // Pending alive → wait
}
// ── 2. Track position ──────────────────────────────────────────
bool hasPos = false;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong pt = PositionGetTicket(i);
if (pt > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber
&& PositionGetString(POSITION_SYMBOL) == _Symbol)
{ hasPos = true; break; }
}
if (!hasPos) ResetToIdle("position closed");
}
//+------------------------------------------------------------------+
//| SECTION 10 STATE MACHINE RUNNER |
//+------------------------------------------------------------------+
void RunStateMachine()
{
UpdateFVGStatuses(); // Cập nhật trạng thái FVG pool (mỗi tick)
ScanAndRegisterFVGs(); // Scan H1 bars cho FVG mới (mỗi bar H1)
switch (g_State) // Dispatch theo state hiện tại
{
case EA_IDLE: OnStateIdle(); break; // Tìm FVG tốt nhất → WAIT_TOUCH/TRIGGER
case EA_WAIT_TOUCH: OnStateWaitTouch(); break; // Chờ giá retrace vào FVG
case EA_WAIT_TRIGGER: OnStateWaitTrigger(); break; // Chờ M5 MSS → đặt limit order
case EA_IN_TRADE: OnStateInTrade(); break; // Track pending/position → close
}
}
//+------------------------------------------------------------------+
//| SECTION 11 DRAWING |
//+------------------------------------------------------------------+
void DrawOneSwingPoint(
string prefix, ENUM_TIMEFRAMES tf,
string tag, bool isHigh, int barIdx, double price,
color clr, bool isKL, int arrowSz = 2, int fontSize = 8)
{
string arrN = prefix + "ARR_" + tag;
string txtN = prefix + "TXT_" + tag;
string klN = prefix + "KL_" + tag;
datetime t = iTime(_Symbol, tf, barIdx);
if (ObjectFind(0, arrN) < 0) ObjectCreate(0, arrN, OBJ_ARROW, 0, t, price);
ObjectSetInteger(0, arrN, OBJPROP_ARROWCODE, isHigh ? 234 : 233);
ObjectSetInteger(0, arrN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, arrN, OBJPROP_WIDTH, arrowSz);
ObjectSetInteger(0, arrN, OBJPROP_ANCHOR, isHigh ? ANCHOR_BOTTOM : ANCHOR_TOP);
ObjectMove(0, arrN, 0, t, price);
double rng = iHigh(_Symbol, tf, barIdx) - iLow(_Symbol, tf, barIdx);
double txtY = isHigh ? price + rng * 0.3 : price - rng * 0.3;
if (ObjectFind(0, txtN) < 0) ObjectCreate(0, txtN, OBJ_TEXT, 0, t, txtY);
ObjectMove(0, txtN, 0, t, txtY);
ObjectSetString (0, txtN, OBJPROP_TEXT, tag);
ObjectSetInteger(0, txtN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, txtN, OBJPROP_FONTSIZE, fontSize);
ObjectSetInteger(0, txtN, OBJPROP_ANCHOR, isHigh ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
if (isKL)
{
datetime tEnd = iTime(_Symbol, tf, 0);
if (ObjectFind(0, klN) < 0) ObjectCreate(0, klN, OBJ_TREND, 0, t, price, tEnd, price);
ObjectSetInteger(0, klN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, klN, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, klN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, klN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, klN, 0, t, price);
ObjectMove(0, klN, 1, tEnd, price);
}
else ObjectDelete(0, klN);
}
void DrawMiddleSwingPoints()
{
if (!InpDebugDraw) return;
if (g_MiddleTrend.idxH0 <= 0 || g_MiddleTrend.idxH1 <= 0 ||
g_MiddleTrend.idxL0 <= 0 || g_MiddleTrend.idxL1 <= 0)
{ ObjectsDeleteAll(0, SW_PREFIX); return; }
bool isUp = (g_MiddleTrend.trend == DIR_UP);
bool isDown = (g_MiddleTrend.trend == DIR_DOWN);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "H0", true, g_MiddleTrend.idxH0, g_MiddleTrend.h0, clrAqua, isDown, 2, 8);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "H1", true, g_MiddleTrend.idxH1, g_MiddleTrend.h1, C'0,140,160', false, 2, 8);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "L0", false, g_MiddleTrend.idxL0, g_MiddleTrend.l0, clrYellow, isUp, 2, 8);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "L1", false, g_MiddleTrend.idxL1, g_MiddleTrend.l1, C'160,140,0', false, 2, 8);
}
void DrawTriggerSwingPoints()
{
if (!InpDebugDraw) return;
if (g_TriggerTrend.idxH0 <= 0 || g_TriggerTrend.idxH1 <= 0 ||
g_TriggerTrend.idxL0 <= 0 || g_TriggerTrend.idxL1 <= 0)
{ ObjectsDeleteAll(0, TS_PREFIX); return; }
bool isUp = (g_TriggerTrend.trend == DIR_UP);
bool isDown = (g_TriggerTrend.trend == DIR_DOWN);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tH0", true, g_TriggerTrend.idxH0, g_TriggerTrend.h0, C'180,100,255', isDown, 1, 7);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tH1", true, g_TriggerTrend.idxH1, g_TriggerTrend.h1, C'100,60,160', false, 1, 7);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tL0", false, g_TriggerTrend.idxL0, g_TriggerTrend.l0, C'255,160,40', isUp, 1, 7);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tL1", false, g_TriggerTrend.idxL1, g_TriggerTrend.l1, C'160,100,20', false, 1, 7);
}
//----------------------------------------------------------------------
// DrawMSSMarker MSS arrow + label + keyLevel line
//----------------------------------------------------------------------
void DrawMSSMarker(
string mssId, ENUM_TIMEFRAMES tf,
datetime mssTime, double mssLevel, MarketDir mssBreak)
{
if (!InpDebugDraw || mssTime == 0) return;
string arrN = MSS_PREFIX + mssId + "_ARR";
string lblN = MSS_PREFIX + mssId + "_LBL";
string klN = MSS_PREFIX + mssId + "_KL";
bool isBull = (mssBreak == DIR_UP);
color clr = isBull ? clrLime : clrTomato;
int shift = MyBarShift(_Symbol, tf, mssTime);
if (shift < 0) return;
double closeAtMss = iClose(_Symbol, tf, shift);
// Arrow tại close cây nến MSS
if (ObjectFind(0, arrN) < 0)
ObjectCreate(0, arrN, OBJ_ARROW, 0, mssTime, closeAtMss);
ObjectSetInteger(0, arrN, OBJPROP_ARROWCODE, isBull ? 233 : 234);
ObjectSetInteger(0, arrN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, arrN, OBJPROP_WIDTH, 2);
ObjectSetInteger(0, arrN, OBJPROP_ANCHOR, isBull ? ANCHOR_TOP : ANCHOR_BOTTOM);
ObjectMove(0, arrN, 0, mssTime, closeAtMss);
// Label "▲MSS" / "▼MSS"
double rng = iHigh(_Symbol, tf, shift) - iLow(_Symbol, tf, shift);
double lblY = isBull ? closeAtMss - rng * 0.4 : closeAtMss + rng * 0.4;
if (ObjectFind(0, lblN) < 0)
ObjectCreate(0, lblN, OBJ_TEXT, 0, mssTime, lblY);
ObjectMove(0, lblN, 0, mssTime, lblY);
ObjectSetString (0, lblN, OBJPROP_TEXT, isBull ? "▲MSS" : "▼MSS");
ObjectSetInteger(0, lblN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, lblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, lblN, OBJPROP_ANCHOR, isBull ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER);
// Dotted line tại broken keyLevel
datetime tEnd = iTime(_Symbol, tf, 0);
if (ObjectFind(0, klN) < 0)
ObjectCreate(0, klN, OBJ_TREND, 0, mssTime, mssLevel, tEnd, mssLevel);
ObjectSetInteger(0, klN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, klN, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, klN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, klN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, klN, 0, mssTime, mssLevel);
ObjectMove(0, klN, 1, tEnd, mssLevel);
}
void DrawMSSMarkers()
{
if (!InpDebugDraw) return;
if (g_MiddleTrend.lastMssTime > 0)
{
string mid = "M_" + IntegerToString((long)g_MiddleTrend.lastMssTime);
DrawMSSMarker(mid, InpMiddleTF, g_MiddleTrend.lastMssTime, g_MiddleTrend.lastMssLevel, g_MiddleTrend.lastMssBreak);
}
if (g_TriggerTrend.lastMssTime > 0)
{
string tid = "T_" + IntegerToString((long)g_TriggerTrend.lastMssTime);
DrawMSSMarker(tid, InpTriggerTF, g_TriggerTrend.lastMssTime, g_TriggerTrend.lastMssLevel, g_TriggerTrend.lastMssBreak);
}
}
//----------------------------------------------------------------------
// DrawOrderVisualization TradingView-style Entry/SL/TP
//----------------------------------------------------------------------
void DrawOrderVisualization()
{
if (!InpDebugDraw) return;
string tpZoneN = ORD_PREFIX + "TP_ZONE";
string slZoneN = ORD_PREFIX + "SL_ZONE";
string entLineN = ORD_PREFIX + "ENTRY_LINE";
string slLineN = ORD_PREFIX + "SL_LINE";
string tpLineN = ORD_PREFIX + "TP_LINE";
string entLblN = ORD_PREFIX + "ENTRY_LBL";
string slLblN = ORD_PREFIX + "SL_LBL";
string tpLblN = ORD_PREFIX + "TP_LBL";
string infoLblN = ORD_PREFIX + "INFO_LBL";
if (!g_OrderPlan.valid || g_State == EA_IDLE)
{
ObjectDelete(0, tpZoneN); ObjectDelete(0, slZoneN);
ObjectDelete(0, entLineN); ObjectDelete(0, slLineN); ObjectDelete(0, tpLineN);
ObjectDelete(0, entLblN); ObjectDelete(0, slLblN); ObjectDelete(0, tpLblN);
ObjectDelete(0, infoLblN);
return;
}
bool isBuy = (g_OrderPlan.direction > 0);
double entry = g_OrderPlan.entry;
double sl = g_OrderPlan.stopLoss;
double tp = g_OrderPlan.takeProfit;
datetime tStart = (g_TriggerTrend.lastMssTime > 0) ? g_TriggerTrend.lastMssTime : iTime(_Symbol, InpTriggerTF, 20);
datetime tEnd = iTime(_Symbol, InpTriggerTF, 0) + PeriodSeconds(InpTriggerTF) * 25;
double slPips = MathAbs(entry - sl) / _Point;
double tpPips = MathAbs(tp - entry) / _Point;
double rr = (slPips > 0) ? tpPips / slPips : 0;
color entryClr = isBuy ? C'33,150,243' : C'255,152,0';
color tpFill = C'15,65,35';
color slFill = C'85,15,15';
color tpClr = C'38,166,91';
color slClr = C'229,57,53';
// TP zone
double tpTop = isBuy ? tp : entry, tpBot = isBuy ? entry : tp;
if (ObjectFind(0, tpZoneN) < 0) ObjectCreate(0, tpZoneN, OBJ_RECTANGLE, 0, tStart, tpTop, tEnd, tpBot);
ObjectSetInteger(0, tpZoneN, OBJPROP_COLOR, tpFill); ObjectSetInteger(0, tpZoneN, OBJPROP_FILL, true);
ObjectSetInteger(0, tpZoneN, OBJPROP_BACK, true);
ObjectMove(0, tpZoneN, 0, tStart, tpTop); ObjectMove(0, tpZoneN, 1, tEnd, tpBot);
// SL zone
double slTop = isBuy ? entry : sl, slBot = isBuy ? sl : entry;
if (ObjectFind(0, slZoneN) < 0) ObjectCreate(0, slZoneN, OBJ_RECTANGLE, 0, tStart, slTop, tEnd, slBot);
ObjectSetInteger(0, slZoneN, OBJPROP_COLOR, slFill); ObjectSetInteger(0, slZoneN, OBJPROP_FILL, true);
ObjectSetInteger(0, slZoneN, OBJPROP_BACK, true);
ObjectMove(0, slZoneN, 0, tStart, slTop); ObjectMove(0, slZoneN, 1, tEnd, slBot);
// Entry line
if (ObjectFind(0, entLineN) < 0) ObjectCreate(0, entLineN, OBJ_TREND, 0, tStart, entry, tEnd, entry);
ObjectSetInteger(0, entLineN, OBJPROP_COLOR, entryClr); ObjectSetInteger(0, entLineN, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, entLineN, OBJPROP_WIDTH, 2); ObjectSetInteger(0, entLineN, OBJPROP_RAY_RIGHT, true);
ObjectMove(0, entLineN, 0, tStart, entry); ObjectMove(0, entLineN, 1, tEnd, entry);
// TP line
if (ObjectFind(0, tpLineN) < 0) ObjectCreate(0, tpLineN, OBJ_TREND, 0, tStart, tp, tEnd, tp);
ObjectSetInteger(0, tpLineN, OBJPROP_COLOR, tpClr); ObjectSetInteger(0, tpLineN, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, tpLineN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, tpLineN, OBJPROP_RAY_RIGHT, true);
ObjectMove(0, tpLineN, 0, tStart, tp); ObjectMove(0, tpLineN, 1, tEnd, tp);
// SL line
if (ObjectFind(0, slLineN) < 0) ObjectCreate(0, slLineN, OBJ_TREND, 0, tStart, sl, tEnd, sl);
ObjectSetInteger(0, slLineN, OBJPROP_COLOR, slClr); ObjectSetInteger(0, slLineN, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, slLineN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, slLineN, OBJPROP_RAY_RIGHT, true);
ObjectMove(0, slLineN, 0, tStart, sl); ObjectMove(0, slLineN, 1, tEnd, sl);
// Labels
datetime lblT = tEnd;
string eTxt = StringFormat("%s %s | %.2f lot", isBuy?"▶ BUY LIM":"▶ SELL LIM", DoubleToString(entry,_Digits), g_OrderPlan.lot);
if (ObjectFind(0, entLblN) < 0) ObjectCreate(0, entLblN, OBJ_TEXT, 0, lblT, entry);
ObjectMove(0, entLblN, 0, lblT, entry); ObjectSetString(0, entLblN, OBJPROP_TEXT, eTxt);
ObjectSetInteger(0, entLblN, OBJPROP_COLOR, entryClr); ObjectSetInteger(0, entLblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, entLblN, OBJPROP_ANCHOR, ANCHOR_LEFT);
string tTxt = StringFormat("◎ TP %s +%.0fp (%.1fR)", DoubleToString(tp,_Digits), tpPips, rr);
if (ObjectFind(0, tpLblN) < 0) ObjectCreate(0, tpLblN, OBJ_TEXT, 0, lblT, tp);
ObjectMove(0, tpLblN, 0, lblT, tp); ObjectSetString(0, tpLblN, OBJPROP_TEXT, tTxt);
ObjectSetInteger(0, tpLblN, OBJPROP_COLOR, tpClr); ObjectSetInteger(0, tpLblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, tpLblN, OBJPROP_ANCHOR, isBuy?ANCHOR_LEFT_LOWER:ANCHOR_LEFT_UPPER);
string sTxt = StringFormat("✕ SL %s -%.0fp", DoubleToString(sl,_Digits), slPips);
if (ObjectFind(0, slLblN) < 0) ObjectCreate(0, slLblN, OBJ_TEXT, 0, lblT, sl);
ObjectMove(0, slLblN, 0, lblT, sl); ObjectSetString(0, slLblN, OBJPROP_TEXT, sTxt);
ObjectSetInteger(0, slLblN, OBJPROP_COLOR, slClr); ObjectSetInteger(0, slLblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, slLblN, OBJPROP_ANCHOR, isBuy?ANCHOR_LEFT_UPPER:ANCHOR_LEFT_LOWER);
string iTxt = StringFormat("Risk %.1f%% | %.0f:%.0f pips | %.1fR", InpRiskPercent, slPips, tpPips, rr);
if (ObjectFind(0, infoLblN) < 0) ObjectCreate(0, infoLblN, OBJ_TEXT, 0, lblT, (entry+sl)/2.0);
ObjectMove(0, infoLblN, 0, lblT, (entry+sl)/2.0); ObjectSetString(0, infoLblN, OBJPROP_TEXT, iTxt);
ObjectSetInteger(0, infoLblN, OBJPROP_COLOR, C'160,160,160'); ObjectSetInteger(0, infoLblN, OBJPROP_FONTSIZE, 7);
ObjectSetInteger(0, infoLblN, OBJPROP_ANCHOR, ANCHOR_LEFT);
}
//----------------------------------------------------------------------
// DrawOneFVGRecord MiddleTF FVG rectangle + midline + label
//----------------------------------------------------------------------
void DrawOneFVGRecord(int idx)
{
if (!InpDebugDraw || idx < 0 || idx >= g_FVGCount) return;
string sid = IntegerToString(g_FVGPool[idx].id);
string rectN = FVGP_PREFIX + "RECT_" + sid;
string midN = FVGP_PREFIX + "MID_" + sid;
string lblN = FVGP_PREFIX + "LBL_" + sid;
datetime rectEnd;
if (g_FVGPool[idx].status == FVG_PENDING)
rectEnd = iTime(_Symbol, InpMiddleTF, 0);
else if (g_FVGPool[idx].touchTime > 0)
{
int shift = MyBarShift(_Symbol, InpTriggerTF, g_FVGPool[idx].touchTime);
rectEnd = (shift >= 0) ? iTime(_Symbol, InpTriggerTF, shift) : iTime(_Symbol, InpMiddleTF, 0);
}
else
{
int shift = MyBarShift(_Symbol, InpMiddleTF, g_FVGPool[idx].usedTime);
rectEnd = (shift >= 0) ? iTime(_Symbol, InpMiddleTF, shift) : iTime(_Symbol, InpMiddleTF, 0);
}
if (rectEnd <= g_FVGPool[idx].createdTime) rectEnd = iTime(_Symbol, InpMiddleTF, 0);
color fillColor;
if (g_FVGPool[idx].status == FVG_PENDING) fillColor = (g_FVGPool[idx].direction == DIR_UP) ? C'0,50,110' : C'90,25,0';
else if (g_FVGPool[idx].status == FVG_TOUCHED) fillColor = (g_FVGPool[idx].direction == DIR_UP) ? C'0,120,220' : C'220,75,0';
else if (g_FVGPool[idx].usedCase == 2) fillColor = C'0,100,0';
else if (g_FVGPool[idx].usedCase == 1) fillColor = C'70,0,0';
else fillColor = C'50,50,50';
if (ObjectFind(0, rectN) < 0)
ObjectCreate(0, rectN, OBJ_RECTANGLE, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high, rectEnd, g_FVGPool[idx].low);
ObjectSetInteger(0, rectN, OBJPROP_COLOR, fillColor); ObjectSetInteger(0, rectN, OBJPROP_FILL, true);
ObjectSetInteger(0, rectN, OBJPROP_BACK, true);
ObjectMove(0, rectN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high);
ObjectMove(0, rectN, 1, rectEnd, g_FVGPool[idx].low);
color midColor = (g_FVGPool[idx].status == FVG_USED) ? C'60,60,60' : clrSilver;
if (ObjectFind(0, midN) < 0)
ObjectCreate(0, midN, OBJ_TREND, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].mid, rectEnd, g_FVGPool[idx].mid);
ObjectSetInteger(0, midN, OBJPROP_COLOR, midColor); ObjectSetInteger(0, midN, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, midN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, midN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, midN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].mid);
ObjectMove(0, midN, 1, rectEnd, g_FVGPool[idx].mid);
string sym = (g_FVGPool[idx].direction == DIR_UP) ? "▲" : "▼";
string stTxt = "";
if (g_FVGPool[idx].status == FVG_TOUCHED) stTxt = " [T]";
else if (g_FVGPool[idx].usedCase == 2) stTxt = " [TRIG]";
else if (g_FVGPool[idx].usedCase == 1) stTxt = " [BRK]";
else if (g_FVGPool[idx].status == FVG_USED) stTxt = " [EXP]";
if (ObjectFind(0, lblN) < 0)
ObjectCreate(0, lblN, OBJ_TEXT, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high);
ObjectMove(0, lblN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high);
ObjectSetString(0, lblN, OBJPROP_TEXT, StringFormat("FVG#%d %s%s", g_FVGPool[idx].id, sym, stTxt));
ObjectSetInteger(0, lblN, OBJPROP_COLOR, fillColor); ObjectSetInteger(0, lblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, lblN, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
}
void DrawFVGPool()
{
if (!InpDebugDraw) return;
for (int i = 0; i < g_FVGCount; i++) DrawOneFVGRecord(i);
}
//----------------------------------------------------------------------
// DrawContextDebug top-left info panel
//----------------------------------------------------------------------
void DrawContextDebug()
{
if (!InpDebugDraw) return;
#define LBL(name,txt,y,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, y); \
ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 9); \
ObjectSetInteger(0,name,OBJPROP_COLOR, clr); \
ObjectSetString (0,name,OBJPROP_TEXT, txt);
LBL("DBG_HDR", "── ICT EA v4 ──", 10, clrSilver)
color cB = (g_Bias.bias==BIAS_UP)?clrLime:(g_Bias.bias==BIAS_DOWN)?clrTomato:(g_Bias.bias==BIAS_SIDEWAY)?clrOrange:clrGray;
LBL("DBG_BIAS", StringFormat("Bias : %s", EnumToString(g_Bias.bias)), 34, cB)
color cMT = (g_MiddleTrend.trend==DIR_UP)?clrLime:(g_MiddleTrend.trend==DIR_DOWN)?clrTomato:clrGray;
LBL("DBG_MT", StringFormat("H1 : %s KL=%.5f", EnumToString(g_MiddleTrend.trend), g_MiddleTrend.keyLevel), 58, cMT)
color cTT = (g_TriggerTrend.trend==DIR_UP)?clrLime:(g_TriggerTrend.trend==DIR_DOWN)?clrTomato:clrGray;
LBL("DBG_TT", StringFormat("M5 : %s KL=%.5f", EnumToString(g_TriggerTrend.trend), g_TriggerTrend.keyLevel), 82, cTT)
if (g_TriggerTrend.lastMssTime > 0)
{
color cM = (g_TriggerTrend.lastMssBreak==DIR_UP)?clrLime:clrTomato;
LBL("DBG_MSS", StringFormat("M5MSS: %s entry=%.5f SL=%.5f @ %s",
(g_TriggerTrend.lastMssBreak==DIR_UP)?"▲":"▼",
g_TriggerTrend.lastMssLevel, g_TriggerTrend.mssSLSwing,
TimeToString(g_TriggerTrend.lastMssTime, TIME_MINUTES)), 106, cM)
}
else ObjectDelete(0,"DBG_MSS");
double lostPct = g_DailyRisk.startBalance > 0
? (g_DailyRisk.startBalance - g_DailyRisk.currentBalance) / g_DailyRisk.startBalance * 100.0 : 0.0;
color cR = g_DailyRisk.limitHit?clrRed:(lostPct>InpMaxDailyLossPct*0.7?clrOrange:clrLime);
LBL("DBG_RISK", StringFormat("Risk : %.2f%% / %.2f%%", lostPct, InpMaxDailyLossPct), 130, cR)
color cS = (g_State==EA_IDLE)?clrSilver:(g_State==EA_WAIT_TOUCH)?clrOrange:(g_State==EA_WAIT_TRIGGER)?clrYellow:clrLime;
LBL("DBG_ST", StringFormat("State: %s", EnumToString(g_State)), 154, cS)
if (g_BlockReason != BLOCK_NONE)
{ LBL("DBG_BLK", StringFormat("Block: %s", EnumToString(g_BlockReason)), 178, clrTomato) }
else ObjectDelete(0,"DBG_BLK");
int nP=0,nT=0,nU=0;
for(int i=0;i<g_FVGCount;i++)
{ if(g_FVGPool[i].status==FVG_PENDING) nP++; else if(g_FVGPool[i].status==FVG_TOUCHED) nT++; else nU++; }
LBL("DBG_POOL", StringFormat("Pool : P=%d T=%d U=%d (%d/%d)", nP,nT,nU,g_FVGCount,MAX_FVG_POOL), 202, clrDodgerBlue)
if (g_ActiveFVGIdx >= 0 && g_ActiveFVGIdx < g_FVGCount)
{
int ai = g_ActiveFVGIdx;
LBL("DBG_ACT", StringFormat("Act : #%d %s [%.5f%.5f] %s",
g_FVGPool[ai].id, EnumToString(g_FVGPool[ai].direction),
g_FVGPool[ai].low, g_FVGPool[ai].high, EnumToString(g_FVGPool[ai].status)), 226, clrDeepSkyBlue)
}
else ObjectDelete(0,"DBG_ACT");
if (g_PendingTicket > 0 && g_OrderPlan.valid)
{
LBL("DBG_ORD", StringFormat("Order: %s #%llu @ %.5f SL=%.5f TP=%.5f",
(g_OrderPlan.direction>0)?"BUY":"SELL", g_PendingTicket,
g_OrderPlan.entry, g_OrderPlan.stopLoss, g_OrderPlan.takeProfit), 250, clrGold)
}
else ObjectDelete(0,"DBG_ORD");
#undef LBL
ChartRedraw(0);
}
//----------------------------------------------------------------------
// DrawVisuals master draw function
//----------------------------------------------------------------------
void DrawVisuals()
{
DrawMiddleSwingPoints(); // H1 swing H0/H1/L0/L1 + keyLevel dashed line
DrawTriggerSwingPoints(); // M5 swing tH0/tH1/tL0/tL1 + keyLevel dashed line
DrawMSSMarkers(); // MSS arrow + label + broken keyLevel line (cả H1 và M5)
DrawFVGPool(); // MiddleTF FVG rectangles (PENDING=dark, TOUCHED=bright, USED=status color)
DrawOrderVisualization(); // TradingView-style: TP zone (green) + SL zone (red) + entry/SL/TP lines
DrawContextDebug(); // Top-left info panel: bias, trend, MSS, risk, state, pool, order
}
//+------------------------------------------------------------------+
//| SECTION 12 EA LIFECYCLE |
//+------------------------------------------------------------------+
int OnInit()
{
ZeroMemory(g_Bias); ZeroMemory(g_MiddleTrend); ZeroMemory(g_TriggerTrend);
ZeroMemory(g_DailyRisk); ZeroMemory(g_OrderPlan);
for (int i = 0; i < MAX_FVG_POOL; i++) ZeroMemory(g_FVGPool[i]);
g_FVGCount = 0; g_NextFVGId = 0;
g_ActiveFVGIdx = -1; g_TradeBarIndex = -1; g_PendingTicket = 0;
g_State = EA_IDLE; g_BlockReason = BLOCK_NONE;
UpdateAllContexts();
ScanAndRegisterFVGs();
DrawVisuals();
PrintFormat("✅ ICT EA v4 | Bias=%s | H1=%s | M5=%s | Pool=%d",
EnumToString(g_Bias.bias), EnumToString(g_MiddleTrend.trend),
EnumToString(g_TriggerTrend.trend), g_FVGCount);
return INIT_SUCCEEDED;
}
void OnTick()
{
UpdateAllContexts(); // 1. Cập nhật D1 bias, H1/M5 swing+MSS
if (EvaluateGuards()) // 2. Check guards (session, risk, alignment)
RunStateMachine(); // Pass → chạy state machine
else if (g_State != EA_IDLE) // Fail + đang active → reset
ResetToIdle(EnumToString(g_BlockReason)); // Log lý do block rồi về IDLE
if (InpDebugDraw) DrawVisuals(); // 3. Vẽ chart objects (nếu enabled)
}
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, SW_PREFIX); // Xóa H1 swing objects (tạm thời)
ObjectsDeleteAll(0, TS_PREFIX); // Xóa M5 swing objects (tạm thời)
ObjectsDeleteAll(0, DBG_PREFIX); // Xóa debug panel (giữ lại: FVGP_, MSS_, ORD_)
ChartRedraw(0); // Force redraw chart
PrintFormat("ICT EA v4 deinit | reason=%d | pool=%d", reason, g_FVGCount);
}