Files
Bell-PriceActionWithEma-EA/Experts/XAU_H1_M5_Pullback.mq5
T
2025-12-26 04:54:55 +07:00

1727 lines
40 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 Early Trend Detection EA |
//| Structure + Early Key-Level Break (MSS-style) |
//| Clean Architecture Meaningful Naming |
//| Author: Bell CW |
//+------------------------------------------------------------------+
#property strict
//====================================================
// DEBUG CONFIG
//====================================================
input bool Debug_Session = false;
input bool Debug_HTFBias = false;
input bool Debug_Trend = false;
input bool Debug_OB = false;
input bool Debug_TriggerTF = false;
input bool Debug_Order = true;
input bool Debug_DrawOnChart = true;
input int TriggerMaxScanBars = 30;
enum ENUM_OB_STATE
{
OB_NONE = 0, // chưa có OB nào
OB_CANDIDATE, // tìm được OB hợp lệ, CHƯA CHẠM
OB_TOUCHED, // đã chạm (mitigated), chờ trigger
OB_USED, // đã trade HOẶC bị invalidate (KẾT THÚC)
};
enum ENUM_BLOCK_REASON
{
BLOCK_NONE,
BLOCK_NO_OB,
BLOCK_OB_NOT_TOUCHED,
BLOCK_BIAS_MISMATCH,
BLOCK_SESSION,
BLOCK_DAILY_LOSS
};
ENUM_BLOCK_REASON g_BlockReason = BLOCK_NONE;
enum ENUM_OB_RESET_REASON
{
OB_RESET_NONE,
OB_RESET_BIAS_MISMATCH,
OB_RESET_EXPIRED,
OB_RESET_BAR_TIMEOUT,
OB_RESET_CLOSE_BREAK,
OB_RESET_INVALIDATED
};
ENUM_OB_RESET_REASON g_LastOBResetReason;
enum ENUM_EA_STATE
{
EA_STATE_IDLE,
EA_STATE_WAIT_OB,
EA_STATE_WAIT_TRIGGER,
EA_STATE_ORDER_SENT
};
ENUM_EA_STATE g_EAState = EA_STATE_IDLE;
int g_OrderSentBarIndex = -1;
//====================================================
// INPUT PARAMETERS
//====================================================
input ENUM_TIMEFRAMES BiasTimeframe = PERIOD_D1; // TF bias cao hơn Trend TF
input ENUM_TIMEFRAMES TrendTimeframe = PERIOD_H1; // Timeframe xác định trend
input ENUM_TIMEFRAMES TriggerTimeframe = PERIOD_M5; // Timeframe kích hoạt logic
input int SwingDetectionRange = 2; // Số nến trái/phải xác định swing
input int StructureScanLookbackBars = 50; // Số nến quét structure
input int OBMaxAliveMinutes = 180; // OB sống tối đa 180 phút
//====================================================
// SESSION FILTER (SERVER TIME)
//====================================================
// London Session
input int LondonStartHour = 8;
input int LondonEndHour = 17;
input int LondonAvoidLastMin = 60; // tránh 60 phút cuối phiên
// New York Session
input int NewYorkStartHour = 13;
input int NewYorkEndHour = 22;
input int NewYorkAvoidLastMin = 60; // tránh 60 phút cuối phiên
input double RiskPercent = 1.0; // % vốn rủi ro mỗi lệnh
input double RiskReward = 2.0; // R:R = 1:2
input double MaxDailyLossPercent = 3.0; // Dừng lệnh nếu lỗ vượt 3% vốn trong ngày
//====================================================
// DATA STRUCTURE
//====================================================
struct TrendState
{
int trendDirection; // 1 = uptrend, -1 = downtrend, 0 = neutral
double latestSwingHigh; // Swing High mới nhất
double previousSwingHigh; // Swing High trước đó
double latestSwingLow; // Swing Low mới nhất
double previousSwingLow; // Swing Low trước đó
int latestSwingHighIndex;
int previousSwingHighIndex;
int latestSwingLowIndex;
int previousSwingLowIndex;
double currentKeyLevel; // Key level theo ICT structure
bool earlyFlipUsedThisBar; // Đã dùng early flip trong HTF bar này chưa
datetime lastStructureUpdateTime; // Thời gian HTF bar cuối cùng đã xử lý
};
//====================================================
// GLOBAL STATE
//====================================================
TrendState g_TrendState;
enum ENUM_HTF_BIAS
{
HTF_BIAS_NONE = 0,
HTF_BIAS_UP,
HTF_BIAS_DOWN,
HTF_BIAS_SIDEWAY
};
struct HTFBiasState
{
ENUM_HTF_BIAS bias;
double rangeHigh;
double rangeLow;
datetime lastUpdateTime;
};
HTFBiasState g_HTFBias;
struct OBWatchState
{
int direction; // 1 = buy, -1 = sell
ENUM_OB_STATE state;
datetime touchTime; // 🔥 thời điểm chạm đầu tiên
double obHigh;
double obLow;
datetime createdTime;
int barsAlive; // số bar trigger TF đã trôi qua
int triggerStartBar;
};
OBWatchState g_OBWatch;
struct OrderPlan
{
bool valid;
int direction; // 1 = buy, -1 = sell
double entry;
double stopLoss;
double takeProfit;
double riskPoints;
double lot;
};
struct TriggerTFStructure
{
bool valid;
int direction; // 1 = up, -1 = down
double lastSwingHigh;
double lastSwingLow;
int lastSwingHighIndex;
int lastSwingLowIndex;
double breakLevel; // level sẽ bị phá
double newKeyLevel; // 🔥 key level mới sau break
};
TriggerTFStructure g_TriggerTF;
struct DailyRiskState
{
double startBalance;
datetime dayStartTime;
bool lossLimitHit;
};
DailyRiskState g_DailyRisk;
void UpdateHTFBias(
ENUM_TIMEFRAMES biasTf,
HTFBiasState &state)
{
datetime tf0Time = iTime(_Symbol, biasTf, 0);
if (tf0Time == state.lastUpdateTime)
return;
state.lastUpdateTime = tf0Time;
// --- Bar 1 và Bar 2 đã đóng
double b1Open = iOpen(_Symbol, biasTf, 1);
double b1Close = iClose(_Symbol, biasTf, 1);
double b1High = iHigh(_Symbol, biasTf, 1);
double b1Low = iLow(_Symbol, biasTf, 1);
double b2High = iHigh(_Symbol, biasTf, 2);
double b2Low = iLow(_Symbol, biasTf, 2);
state.rangeHigh = 0;
state.rangeLow = 0;
// ===== UP BIAS =====
if (b1Close > b2High)
{
state.bias = HTF_BIAS_UP;
return;
}
// ===== DOWN BIAS =====
if (b1Close < b2Low)
{
state.bias = HTF_BIAS_DOWN;
return;
}
// ===== SIDEWAY: inside bar =====
bool insideBar =
b1High <= b2High &&
b1Low >= b2Low;
if (insideBar)
{
state.bias = HTF_BIAS_SIDEWAY;
state.rangeHigh = b2High;
state.rangeLow = b2Low;
return;
}
// ===== SIDEWAY: indecision =====
bool indecision =
b1Close < b2High &&
b1Close > b2Low;
if (indecision)
{
state.bias = HTF_BIAS_SIDEWAY;
state.rangeHigh = b1High;
state.rangeLow = b1Low;
return;
}
state.bias = HTF_BIAS_NONE;
}
bool IsHTFBiasAligned(ENUM_HTF_BIAS bias, int trendDirection)
{
if (bias == HTF_BIAS_UP && trendDirection == 1)
return true;
if (bias == HTF_BIAS_DOWN && trendDirection == -1)
return true;
return false;
}
bool IsDailyLossExceeded(
DailyRiskState &risk,
double maxLossPercent)
{
if (risk.lossLimitHit)
return true;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double lossPct =
(risk.startBalance - balance) / risk.startBalance * 100.0;
if (lossPct >= maxLossPercent)
{
risk.lossLimitHit = true;
PrintFormat(
"⛔ MAX DAILY LOSS HIT | Loss=%.2f%% (Limit=%.2f%%)",
lossPct,
maxLossPercent);
return true;
}
return false;
}
void UpdateDailyRiskState(DailyRiskState &risk)
{
// Thời gian mở nến D1 hiện tại
datetime dailyBarTime = iTime(_Symbol, PERIOD_D1, 0);
if (dailyBarTime != risk.dayStartTime)
{
risk.dayStartTime = dailyBarTime;
risk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE);
risk.lossLimitHit = false;
}
}
void BootstrapHTFBias(
ENUM_TIMEFRAMES biasTf,
HTFBiasState &state)
{
// ép reset
state.lastUpdateTime = 0;
// đảm bảo đủ data
if (Bars(_Symbol, biasTf) < 5)
return;
UpdateHTFBias(biasTf, state);
}
void BootstrapMarketStructure(
ENUM_TIMEFRAMES tf,
TrendState &state)
{
ZeroMemory(state);
state.trendDirection = 0;
state.lastStructureUpdateTime = 0;
int maxBar =
MathMin(StructureScanLookbackBars,
Bars(_Symbol, tf) - SwingDetectionRange - 2);
double highs[2], lows[2];
int hiIdx[2], loIdx[2];
int hc = 0, lc = 0;
for (int i = SwingDetectionRange + 1; i <= maxBar; i++)
{
if (hc < 2 && IsSwingHighAtBar(tf, i))
{
highs[hc] = iHigh(_Symbol, tf, i);
hiIdx[hc] = i;
hc++;
}
if (lc < 2 && IsSwingLowAtBar(tf, i))
{
lows[lc] = iLow(_Symbol, tf, i);
loIdx[lc] = i;
lc++;
}
if (hc == 2 && lc == 2)
break;
}
if (hc < 2 || lc < 2)
return;
state.latestSwingHigh = highs[0];
state.previousSwingHigh = highs[1];
state.latestSwingLow = lows[0];
state.previousSwingLow = lows[1];
state.latestSwingHighIndex = hiIdx[0];
state.previousSwingHighIndex = hiIdx[1];
state.latestSwingLowIndex = loIdx[0];
state.previousSwingLowIndex = loIdx[1];
// interpret structure
if (state.previousSwingHigh < state.latestSwingHigh &&
state.previousSwingLow < state.latestSwingLow)
{
state.trendDirection = 1;
state.currentKeyLevel = state.latestSwingLow;
}
else if (state.previousSwingHigh > state.latestSwingHigh &&
state.previousSwingLow > state.latestSwingLow)
{
state.trendDirection = -1;
state.currentKeyLevel = state.latestSwingHigh;
}
}
bool BootstrapOBCandidate(
ENUM_TIMEFRAMES tf,
const TrendState &trend,
OBWatchState &ob)
{
double h, l;
int idx;
if (!FindTrendOrderBlock(tf, trend, h, l, idx))
return false;
ob.state = OB_CANDIDATE; // candidate
ob.direction = trend.trendDirection;
ob.obHigh = h;
ob.obLow = l;
ob.createdTime = iTime(_Symbol, tf, idx);
ob.barsAlive = 0;
ob.triggerStartBar = 1;
return true;
}
void DrawInitialContext()
{
DrawMarketStructure(TrendTimeframe, g_TrendState);
DrawTrendSummaryLabel(g_TrendState);
DrawEAStateOverlay();
}
//====================================================
// INITIALIZATION
//====================================================
int OnInit()
{
// ===============================
// 0️⃣ RESET TOÀN BỘ STATE
// ===============================
ZeroMemory(g_TrendState);
ZeroMemory(g_HTFBias);
ZeroMemory(g_OBWatch);
ZeroMemory(g_TriggerTF);
ZeroMemory(g_DailyRisk);
g_EAState = EA_STATE_IDLE;
// ===============================
// 1️⃣ ENSURE HISTORY READY
// ===============================
if (Bars(_Symbol, TrendTimeframe) < StructureScanLookbackBars + 10 ||
Bars(_Symbol, BiasTimeframe) < 5)
{
Print("⏳ Not enough historical data to initialize EA");
return INIT_FAILED;
}
// ===============================
// 2️⃣ BOOTSTRAP HTF BIAS
// ===============================
BootstrapHTFBias(BiasTimeframe, g_HTFBias);
if (Debug_HTFBias)
PrintFormat("INIT | HTF Bias = %d", g_HTFBias.bias);
// ===============================
// 3️⃣ BOOTSTRAP TREND STRUCTURE
// ===============================
BootstrapMarketStructure(TrendTimeframe, g_TrendState);
if (!HasConfirmedMarketStructure(g_TrendState))
{
Print("⚠ INIT | Market structure not confirmed yet");
return INIT_SUCCEEDED; // vẫn cho EA chạy, nhưng không trade
}
if (Debug_Trend)
{
PrintFormat(
"INIT | Trend=%d | H0=%.2f | H1=%.2f | L0=%.2f | L1=%.2f",
g_TrendState.trendDirection,
g_TrendState.latestSwingHigh,
g_TrendState.previousSwingHigh,
g_TrendState.latestSwingLow,
g_TrendState.previousSwingLow
);
}
// ===============================
// 4️⃣ BOOTSTRAP OB CANDIDATE
// ===============================
if (IsHTFBiasAligned(g_HTFBias.bias, g_TrendState.trendDirection))
{
bool obFound = BootstrapOBCandidate(
TrendTimeframe,
g_TrendState,
g_OBWatch);
if (obFound)
{
g_EAState = EA_STATE_WAIT_OB;
if (Debug_OB)
PrintFormat(
"INIT | OB Candidate [%0.2f - %0.2f]",
g_OBWatch.obLow,
g_OBWatch.obHigh
);
}
else
{
if (Debug_OB)
Print("INIT | No valid OB candidate found");
}
}
else
{
if (Debug_HTFBias)
Print("INIT | Bias not aligned with trend → skip OB bootstrap");
}
Print("✅ ICT Early Trend Detection EA initialized (BOOTSTRAPPED)");
DrawInitialContext();
return INIT_SUCCEEDED;
}
//====================================================
// BAR UTILITY
//====================================================
bool IsNewBarFormed(ENUM_TIMEFRAMES timeframe, datetime &lastBarTime)
{
datetime currentBarTime = iTime(_Symbol, timeframe, 0);
if (currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
//====================================================
// SWING DETECTION (GENERIC)
//====================================================
bool IsSwingHighAtBar(
ENUM_TIMEFRAMES timeframe,
int barIndex)
{
double highPrice = iHigh(_Symbol, timeframe, barIndex);
for (int offset = 1; offset <= SwingDetectionRange; offset++)
{
if (iHigh(_Symbol, timeframe, barIndex - offset) >= highPrice)
return false;
if (iHigh(_Symbol, timeframe, barIndex + offset) >= highPrice)
return false;
}
return true;
}
bool IsSwingLowAtBar(
ENUM_TIMEFRAMES timeframe,
int barIndex)
{
double lowPrice = iLow(_Symbol, timeframe, barIndex);
for (int offset = 1; offset <= SwingDetectionRange; offset++)
{
if (iLow(_Symbol, timeframe, barIndex - offset) <= lowPrice)
return false;
if (iLow(_Symbol, timeframe, barIndex + offset) <= lowPrice)
return false;
}
return true;
}
//====================================================
// STRUCTURE VALIDATION
//====================================================
bool HasConfirmedMarketStructure(const TrendState &state)
{
return (
state.latestSwingHigh > 0 &&
state.previousSwingHigh > 0 &&
state.latestSwingLow > 0 &&
state.previousSwingLow > 0);
}
//====================================================
// STRUCTURE UPDATE (TIMEFRAME AGNOSTIC)
//====================================================
void UpdateMarketStructure(
ENUM_TIMEFRAMES timeframe,
TrendState &state)
{
datetime currentStructureBarTime = iTime(_Symbol, timeframe, 0);
if (currentStructureBarTime == state.lastStructureUpdateTime)
return;
state.lastStructureUpdateTime = currentStructureBarTime;
state.earlyFlipUsedThisBar = false;
double detectedSwingHighs[2];
double detectedSwingLows[2];
int detectedSwingHighIndices[2];
int detectedSwingLowIndices[2];
int highCount = 0;
int lowCount = 0;
for (int barIndex = SwingDetectionRange + 1;
barIndex < StructureScanLookbackBars;
barIndex++)
{
if (highCount < 2 && IsSwingHighAtBar(timeframe, barIndex))
{
detectedSwingHighs[highCount] = iHigh(_Symbol, timeframe, barIndex);
detectedSwingHighIndices[highCount] = barIndex;
highCount++;
}
if (lowCount < 2 && IsSwingLowAtBar(timeframe, barIndex))
{
detectedSwingLows[lowCount] = iLow(_Symbol, timeframe, barIndex);
detectedSwingLowIndices[lowCount] = barIndex;
lowCount++;
}
if (highCount == 2 && lowCount == 2)
break;
}
if (highCount < 2 || lowCount < 2) {
if(Debug_Trend)
Print("⏳ Waiting for enough swing points...");
return;
}
state.latestSwingHigh = detectedSwingHighs[0];
state.previousSwingHigh = detectedSwingHighs[1];
state.latestSwingLow = detectedSwingLows[0];
state.previousSwingLow = detectedSwingLows[1];
state.latestSwingHighIndex = detectedSwingHighIndices[0];
state.previousSwingHighIndex = detectedSwingHighIndices[1];
state.latestSwingLowIndex = detectedSwingLowIndices[0];
state.previousSwingLowIndex = detectedSwingLowIndices[1];
// ===== STRUCTURE INTERPRETATION =====
if (state.previousSwingHigh < state.latestSwingHigh &&
state.previousSwingLow < state.latestSwingLow)
{
state.trendDirection = 1;
state.currentKeyLevel = state.latestSwingLow;
}
else if (state.previousSwingHigh > state.latestSwingHigh &&
state.previousSwingLow > state.latestSwingLow)
{
state.trendDirection = -1;
state.currentKeyLevel = state.latestSwingHigh;
}
}
//====================================================
// EARLY TREND FLIP (MSS-STYLE)
//====================================================
void CheckForEarlyTrendFlip(
TrendState &state,
double currentPrice)
{
if (!HasConfirmedMarketStructure(state))
return;
if (state.earlyFlipUsedThisBar)
return;
if (state.trendDirection == 1 &&
currentPrice < state.currentKeyLevel)
{
state.trendDirection = -1;
state.currentKeyLevel = state.latestSwingHigh;
state.earlyFlipUsedThisBar = true;
}
else if (state.trendDirection == -1 &&
currentPrice > state.currentKeyLevel)
{
state.trendDirection = 1;
state.currentKeyLevel = state.latestSwingLow;
state.earlyFlipUsedThisBar = true;
}
}
//====================================================
// SESSION GUARD
//====================================================
bool IsWithinSession(
int currentMinutes,
int sessionStartHour,
int sessionEndHour,
int avoidLastMinutes)
{
int sessionStartMin = sessionStartHour * 60;
int sessionEndMin = sessionEndHour * 60;
// Trong phiên
if (currentMinutes < sessionStartMin ||
currentMinutes >= sessionEndMin)
return false;
// Tránh cuối phiên
if (currentMinutes >= (sessionEndMin - avoidLastMinutes))
return false;
return true;
}
bool IsTradingSessionAllowed()
{
datetime now = TimeCurrent();
MqlDateTime tm;
TimeToStruct(now, tm);
int currentMinutes = tm.hour * 60 + tm.min;
if (IsWithinSession(currentMinutes,
LondonStartHour,
LondonEndHour,
LondonAvoidLastMin))
return true;
if (IsWithinSession(currentMinutes,
NewYorkStartHour,
NewYorkEndHour,
NewYorkAvoidLastMin))
return true;
return false;
}
bool HasPriceTouchedOB(
int direction,
double obHigh,
double obLow,
double price)
{
if (direction == 1) // buy
return price <= obHigh && price >= obLow;
if (direction == -1) // sell
return price >= obLow && price <= obHigh;
return false;
}
bool FindTrendOrderBlock(
ENUM_TIMEFRAMES timeframe,
const TrendState &trend,
double &obHigh,
double &obLow,
int &obCandleIndex)
{
obHigh = 0;
obLow = 0;
obCandleIndex = -1;
// ===== XÁC ĐỊNH VÙNG QUÉT =====
int scanFrom, scanTo;
if (trend.trendDirection == 1)
{
// Uptrend: OB phải nằm giữa L1 -> L0
scanFrom = trend.latestSwingLowIndex - 1;
scanTo = trend.previousSwingLowIndex + 1;
}
else if (trend.trendDirection == -1)
{
// Downtrend: OB phải nằm giữa H1 -> H0
scanFrom = trend.latestSwingHighIndex - 1;
scanTo = trend.previousSwingHighIndex + 1;
}
else
return false;
scanFrom = MathMax(scanFrom, 1);
scanTo = MathMax(scanTo, 1);
// ===== QUÉT OB =====
for (int i = scanFrom; i >= scanTo; i--)
{
double open = iOpen(_Symbol, timeframe, i);
double close = iClose(_Symbol, timeframe, i);
// ===== NẾN NGƯỢC TREND =====
bool isBear = close < open;
bool isBull = close > open;
if ((trend.trendDirection == 1 && !isBear) ||
(trend.trendDirection == -1 && !isBull))
continue;
double h = iHigh(_Symbol, timeframe, i);
double l = iLow(_Symbol, timeframe, i);
// ===== RANGE FILTER =====
if (MathAbs(h - l) < 10 * _Point)
continue;
// ===== FILTER 1: OB PHẢI NẰM TRONG CON SÓNG =====
if (trend.trendDirection == 1 && l < trend.latestSwingLow)
continue;
if (trend.trendDirection == -1 && h > trend.latestSwingHigh)
continue;
// ===== FILTER 2: CHƯA BỊ MITIGATION =====
bool mitigated = false;
for (int j = i - 1; j >= 1; j--)
{
double price = iClose(_Symbol, timeframe, j);
if (HasPriceTouchedOB(trend.trendDirection, h, l, price))
{
mitigated = true;
break;
}
}
if (mitigated)
continue;
// ===== FILTER 3: CHƯA BỊ CLOSE BREAK (HTF) =====
if (IsOBCloseBreak(trend.trendDirection, h, l))
continue;
// ===== OK → OB HỢP LỆ =====
obHigh = h;
obLow = l;
obCandleIndex = i;
return true;
}
return false;
}
void ResetOBWithReason(ENUM_OB_RESET_REASON reason)
{
g_LastOBResetReason = reason;
ResetOBWatch();
ResetTriggerTFStructure();
g_EAState = EA_STATE_IDLE;
}
void ResetOBWatch()
{
g_OBWatch.state = OB_NONE;
g_OBWatch.touchTime = 0;
g_OBWatch.direction = 0;
g_OBWatch.obHigh = 0;
g_OBWatch.obLow = 0;
g_OBWatch.createdTime = 0;
DebugLabel("EA_OB_RESET",
StringFormat("OB Reset Reason=%d", g_LastOBResetReason),
130,
clrRed
);
}
bool IsOBInvalidated(
int direction,
double obHigh,
double obLow,
double price)
{
if (direction == 1) // buy
return price < obLow;
if (direction == -1) // sell
return price > obHigh;
return false;
}
void ResetTriggerTFStructure()
{
ZeroMemory(g_TriggerTF);
}
bool IsTriggerTFStructureBreak(
const TriggerTFStructure &ts,
int trendDirection,
ENUM_TIMEFRAMES tf)
{
if (!ts.valid)
return false;
double high = iHigh(_Symbol, tf, 1);
double low = iLow(_Symbol, tf, 1);
if (trendDirection == 1)
return high > ts.breakLevel;
if (trendDirection == -1)
return low < ts.breakLevel;
return false;
}
void UpdateTriggerTFStructure(
ENUM_TIMEFRAMES tf,
TriggerTFStructure &ts,
int startBar)
{
double sh = 0.0;
double sl = 0.0;
int shi = -1;
int sli = -1;
bool foundHigh = false;
bool foundLow = false;
for (int i = startBar - SwingDetectionRange;
i >= MathMax(1, startBar - TriggerMaxScanBars);
i--)
{
if (!foundHigh && IsSwingHighAtBar(tf, i))
{
sh = iHigh(_Symbol, tf, i);
shi = i;
foundHigh = true;
}
if (!foundLow && IsSwingLowAtBar(tf, i))
{
sl = iLow(_Symbol, tf, i);
sli = i;
foundLow = true;
}
if (foundHigh && foundLow)
break;
}
if (!foundHigh || !foundLow)
return;
ts.lastSwingHigh = sh;
ts.lastSwingLow = sl;
ts.lastSwingHighIndex = shi;
ts.lastSwingLowIndex = sli;
ts.direction = g_TrendState.trendDirection;
ts.valid = true;
if (ts.direction == 1)
{
ts.breakLevel = ts.lastSwingHigh;
ts.newKeyLevel = ts.lastSwingLow;
}
else
{
ts.breakLevel = ts.lastSwingLow;
ts.newKeyLevel = ts.lastSwingHigh;
}
}
bool IsOBExpired(const OBWatchState &ob)
{
if (ob.createdTime <= 0)
return false;
int aliveSec = (int)(TimeCurrent() - ob.createdTime);
return aliveSec > OBMaxAliveMinutes * 60;
}
void MarkOBAsUsed()
{
g_OBWatch.state = OB_USED;
}
void MarkOBAsTouched(
OBWatchState &ob,
ENUM_TIMEFRAMES triggerTf)
{
if (ob.state != OB_CANDIDATE) return;
ob.state = OB_TOUCHED;
// Thời điểm chạm đầu tiên (bar đã đóng)
ob.touchTime = iTime(_Symbol, triggerTf, 1);
// Reset trigger waiting
ob.barsAlive = 0;
int shift = iBarShift(_Symbol, triggerTf, ob.touchTime);
ob.triggerStartBar = (shift > 0 ? shift : 1);
// Reset trigger TF structure
ResetTriggerTFStructure();
if (Debug_OB)
{
PrintFormat(
"🟦 OB TOUCHED | dir=%d | OB[%.2f - %.2f]",
ob.direction,
ob.obLow,
ob.obHigh
);
}
}
bool IsOBCloseBreak(
int direction,
double obHigh,
double obLow)
{
double close = iClose(_Symbol, TrendTimeframe, 1);
if (direction == 1) // buy OB
return close < obLow;
if (direction == -1) // sell OB
return close > obHigh;
return false;
}
OrderPlan BuildOrderPlanFromTriggerTF(
const TriggerTFStructure &ts,
ENUM_TIMEFRAMES tf)
{
OrderPlan plan;
ZeroMemory(plan);
plan.valid = true;
plan.direction = ts.direction;
plan.entry = (ts.direction == 1)
? g_OBWatch.obHigh // buy tại đỉnh OB
: g_OBWatch.obLow; // sell tại đáy OB
double riskPoints;
// ===== BUY =====
if (ts.direction == 1)
{
plan.stopLoss = MathMin(ts.newKeyLevel, g_OBWatch.obLow);
riskPoints = plan.entry - plan.stopLoss;
if (riskPoints <= 0)
{
plan.valid = false;
return plan;
}
plan.takeProfit = plan.entry + riskPoints * RiskReward;
}
// ===== SELL =====
else
{
plan.stopLoss = MathMax(ts.newKeyLevel, g_OBWatch.obHigh);
riskPoints = plan.stopLoss - plan.entry;
if (riskPoints <= 0)
{
plan.valid = false;
return plan;
}
plan.takeProfit = plan.entry - riskPoints * RiskReward;
}
return plan;
}
double CalculateRiskLot(
double entryPrice,
double stopLossPrice)
{
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double riskMoney = equity * RiskPercent / 100.0;
double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
if (contractSize <= 0)
return 0;
double slDistance = MathAbs(entryPrice - stopLossPrice);
if (slDistance <= _Point)
return 0;
// Cost cho 1 lot nếu SL hit
double costPerLot = slDistance * contractSize;
if (costPerLot <= 0)
return 0;
double rawLot = riskMoney / costPerLot;
// ===== Normalize theo broker =====
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
rawLot = MathMax(minLot, MathMin(rawLot, maxLot));
rawLot = MathFloor(rawLot / lotStep) * lotStep;
return NormalizeDouble(rawLot, 2);
}
bool ExecuteOrder(const OrderPlan &plan)
{
if (!plan.valid) return false;
MqlTradeRequest req;
MqlTradeResult res;
ZeroMemory(req);
ZeroMemory(res);
req.action = TRADE_ACTION_PENDING;
req.symbol = _Symbol;
req.volume = plan.lot;
req.type = plan.direction == 1 ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT;
req.price = plan.entry;
req.sl = plan.stopLoss;
req.tp = plan.takeProfit;
req.deviation = 5;
req.magic = 202501;
req.comment = "ICT_OB_Trigger";
bool sent = OrderSend(req, res);
if (!sent || res.retcode != TRADE_RETCODE_DONE)
{
PrintFormat(
"❌ OrderSend failed | sent=%d | retcode=%d",
sent,
res.retcode);
return false;
} else {
PrintFormat(
"✅ ORDER SENT | lot=%.2f | entry=%.2f | SL=%.2f | TP=%.2f",
plan.lot,
plan.entry,
plan.stopLoss,
plan.takeProfit);
return true;
}
}
string GetOBRectName(const OBWatchState &ob)
{
return StringFormat("OB_RECT_%I64d", ob.createdTime);
}
string GetOBLabelName(const OBWatchState &ob)
{
return StringFormat("OB_LABEL_%I64d", ob.createdTime);
}
bool IsReadyToTrade()
{
if (g_OBWatch.state != OB_TOUCHED)
return false;
if (!g_TriggerTF.valid)
return false;
if (!IsTriggerTFStructureBreak(
g_TriggerTF,
g_TrendState.trendDirection,
TriggerTimeframe))
return false;
return true;
}
bool TryExecuteTradeIfReady()
{
// =====================================
// 0️⃣ CHƯA SẴN SÀNG
// =====================================
if (!IsReadyToTrade())
return false;
// =====================================
// 1️⃣ BUILD ORDER PLAN
// =====================================
OrderPlan plan =
BuildOrderPlanFromTriggerTF(
g_TriggerTF,
TriggerTimeframe);
if (!plan.valid)
return false;
plan.lot = CalculateRiskLot(
plan.entry,
plan.stopLoss);
if (plan.lot <= 0)
return false;
// =====================================
// 2️⃣ EXECUTE ORDER (DUY NHẤT Ở ĐÂY)
// =====================================
bool isExecuted = ExecuteOrder(plan);
if (Debug_Order)
{
PrintFormat(
"🚀 ORDER EXECUTED | dir=%d | entry=%.2f | SL=%.2f | TP=%.2f",
plan.direction,
plan.entry,
plan.stopLoss,
plan.takeProfit);
}
return isExecuted;
}
// Kiểm tra có đủ điều kiện tạo OB không
void StateIdle()
{
if (!HasConfirmedMarketStructure(g_TrendState))
return;
if (!IsHTFBiasAligned(g_HTFBias.bias, g_TrendState.trendDirection))
return;
if (BootstrapOBCandidate(TrendTimeframe, g_TrendState, g_OBWatch))
{
g_EAState = EA_STATE_WAIT_OB;
}
}
// Theo dõi OB chờ chạm
void StateWaitOB()
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// invalidate / expire
if (IsOBCloseBreak(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow) ||
IsOBInvalidated(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow, price) ||
IsOBExpired(g_OBWatch))
{
ResetOBWithReason(OB_RESET_INVALIDATED);
return;
}
// touch
if (HasPriceTouchedOB(
g_OBWatch.direction,
g_OBWatch.obHigh,
g_OBWatch.obLow,
price))
{
MarkOBAsTouched(g_OBWatch, TriggerTimeframe);
g_EAState = EA_STATE_WAIT_TRIGGER;
}
}
// Theo dõi trigger timeframe chờ tín hiệu vào lệnh
void StateWaitTrigger()
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// OB bị phá
if (IsOBCloseBreak(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow) ||
IsOBInvalidated(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow, price))
{
ResetOBWithReason(OB_RESET_INVALIDATED);
return;
}
static datetime lastTriggerBarForAlive = 0;
datetime currentTriggerBar = iTime(_Symbol, TriggerTimeframe, 0);
if (currentTriggerBar != lastTriggerBarForAlive)
{
lastTriggerBarForAlive = currentTriggerBar;
if (g_OBWatch.state == OB_TOUCHED)
g_OBWatch.barsAlive++;
}
if (g_OBWatch.barsAlive > TriggerMaxScanBars)
{
ResetOBWithReason(OB_RESET_EXPIRED);
return;
}
static datetime lastTriggerBar = 0;
datetime currentBar = iTime(_Symbol, TriggerTimeframe, 0);
if (currentBar != lastTriggerBar)
{
lastTriggerBar = currentBar;
UpdateTriggerTFStructure(TriggerTimeframe, g_TriggerTF, g_OBWatch.triggerStartBar);
}
if (IsReadyToTrade())
{
bool isExecutedSuccess = TryExecuteTradeIfReady();
if (isExecutedSuccess) {
MarkOBAsUsed();
g_EAState = EA_STATE_ORDER_SENT;
g_OrderSentBarIndex = Bars(_Symbol, TriggerTimeframe); // lưu lại bar index khi lệnh được gửi
}
}
}
void StateOrderSent()
{
int currentBar = Bars(_Symbol, TriggerTimeframe);
// ❌ Không cho reset trong cùng bar vừa gửi lệnh
if (currentBar <= g_OrderSentBarIndex)
return;
if (!PositionSelect(_Symbol) && OrdersTotal() == 0)
{
g_EAState = EA_STATE_IDLE;
}
}
void UpdateContext()
{
UpdateDailyRiskState(g_DailyRisk);
UpdateHTFBias(BiasTimeframe, g_HTFBias);
UpdateMarketStructure(TrendTimeframe, g_TrendState);
static datetime lastTrendBarTime = 0;
datetime currentTrendBarTime = iTime(_Symbol, TrendTimeframe, 0);
if (currentTrendBarTime != lastTrendBarTime)
{
lastTrendBarTime = currentTrendBarTime;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
CheckForEarlyTrendFlip(g_TrendState, bid);
}
}
bool EvaluateTradePermission()
{
g_BlockReason = BLOCK_NONE;
if (!IsTradingSessionAllowed())
{
g_BlockReason = BLOCK_SESSION;
return false;
}
if (IsDailyLossExceeded(g_DailyRisk, MaxDailyLossPercent))
{
g_BlockReason = BLOCK_DAILY_LOSS;
return false;
}
if (!IsHTFBiasAligned(g_HTFBias.bias, g_TrendState.trendDirection))
{
g_BlockReason = BLOCK_BIAS_MISMATCH;
return false;
}
return true;
}
void RunStateMachine()
{
switch (g_EAState)
{
case EA_STATE_IDLE:
StateIdle();
break;
case EA_STATE_WAIT_OB:
StateWaitOB();
break;
case EA_STATE_WAIT_TRIGGER:
StateWaitTrigger();
break;
case EA_STATE_ORDER_SENT:
StateOrderSent();
break;
}
}
void ResetOBIfBlocked()
{
if (g_EAState == EA_STATE_WAIT_OB ||
g_EAState == EA_STATE_WAIT_TRIGGER)
{
ResetOBWithReason(OB_RESET_BIAS_MISMATCH);
}
}
void OnTick()
{
UpdateContext();
bool allowStateMachine = EvaluateTradePermission();
if (allowStateMachine)
RunStateMachine();
else
ResetOBIfBlocked();
DrawVisuals();
}
void DebugPrint(bool enabled, string message)
{
if (!enabled)
return;
Print(message);
}
//----------------------------------------------------
void DebugLabel(string name, string text, int y, color clr)
{
if (!Debug_DrawOnChart)
return;
ObjectDelete(0, name);
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_LOWER);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 8);
ObjectSetString(0, name, OBJPROP_TEXT, text);
}
//====================================================
// DRAWING UTILITIES
//====================================================
void DrawVisuals()
{
DrawMarketStructure(TrendTimeframe, g_TrendState);
DrawTrendSummaryLabel(g_TrendState);
DrawOrUpdateOBRectangle(g_OBWatch);
DrawOrUpdateOBLabel(g_OBWatch);
DrawEAStateOverlay();
DrawBlockReasonOverlay();
}
void DrawEAStateOverlay()
{
string stepText;
color stepColor = clrGray;
switch (g_EAState)
{
case EA_STATE_IDLE:
stepText = "STEP 0: IDLE / GUARD";
stepColor = clrSilver;
break;
case EA_STATE_WAIT_OB:
stepText = "STEP 1: WAIT OB TOUCH";
stepColor = clrOrange;
break;
case EA_STATE_WAIT_TRIGGER:
stepText = "STEP 2: WAIT TRIGGER";
stepColor = clrYellow;
break;
case EA_STATE_ORDER_SENT:
stepText = "STEP 3: ORDER SENT";
stepColor = clrLime;
break;
}
DebugLabel("EA_STATE", stepText, 40, stepColor);
DebugLabel("EA_TREND",
StringFormat("Trend=%d | Bias=%d",
g_TrendState.trendDirection,
g_HTFBias.bias),
70, clrWhite);
if (g_OBWatch.state == OB_CANDIDATE)
{
DebugLabel("EA_OB",
StringFormat("OB [%0.2f - %0.2f] alive=%d",
g_OBWatch.obLow,
g_OBWatch.obHigh,
g_OBWatch.barsAlive),
100, clrOrange);
}
}
void DrawSwingLabel(
string objectName,
string labelText,
ENUM_TIMEFRAMES timeframe,
int barIndex,
double price,
color labelColor)
{
if (barIndex < 0 || price <= 0)
return;
datetime barTime = iTime(_Symbol, timeframe, barIndex);
ObjectDelete(0, objectName);
ObjectCreate(0, objectName, OBJ_TEXT, 0, barTime, price);
ObjectSetInteger(0, objectName, OBJPROP_COLOR, labelColor);
ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 9);
ObjectSetString(0, objectName, OBJPROP_TEXT, labelText);
// ===== PIXEL OFFSET =====
int SwingLabelYOffsetPx = 20;
if (labelText == "H0" || labelText == "H1")
{
ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_BOTTOM);
ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, SwingLabelYOffsetPx);
}
else if (labelText == "L0" || labelText == "L1")
{
ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_TOP);
ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, SwingLabelYOffsetPx);
}
}
void DrawMarketStructure(
ENUM_TIMEFRAMES timeframe,
const TrendState &state)
{
DrawSwingLabel("SWING_HIGH_LATEST", "H0", timeframe,
state.latestSwingHighIndex,
state.latestSwingHigh, clrLime);
DrawSwingLabel("SWING_HIGH_PREVIOUS", "H1", timeframe,
state.previousSwingHighIndex,
state.previousSwingHigh, clrGreen);
DrawSwingLabel("SWING_LOW_LATEST", "L0", timeframe,
state.latestSwingLowIndex,
state.latestSwingLow, clrRed);
DrawSwingLabel("SWING_LOW_PREVIOUS", "L1", timeframe,
state.previousSwingLowIndex,
state.previousSwingLow, clrMaroon);
}
void DrawTrendSummaryLabel(const TrendState &state)
{
string labelName = "TREND_SUMMARY";
ObjectDelete(0, labelName);
string labelText = "TREND: NEUTRAL";
color labelColor = clrGray;
if (state.trendDirection == 1)
{
labelText = StringFormat(
"UPTREND - Key Level: %.2f%s",
state.currentKeyLevel,
state.earlyFlipUsedThisBar ? " -> EARLY FLIP" : "");
labelColor = state.earlyFlipUsedThisBar ? clrOrange : clrLime;
}
else if (state.trendDirection == -1)
{
labelText = StringFormat(
"DOWNTREND - Key Level: %.2f%s",
state.currentKeyLevel,
state.earlyFlipUsedThisBar ? " -> EARLY FLIP" : "");
labelColor = state.earlyFlipUsedThisBar ? clrOrange : clrRed;
}
ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, labelName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, 20);
ObjectSetInteger(0, labelName, OBJPROP_COLOR, labelColor);
ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 9);
ObjectSetString(0, labelName, OBJPROP_TEXT, labelText);
}
void DrawOrUpdateOBRectangle(const OBWatchState &ob)
{
string rectName = GetOBRectName(ob);
if (ob.state == OB_NONE)
return;
datetime endTime;
if (ob.state == OB_CANDIDATE)
endTime = iTime(_Symbol, TriggerTimeframe, 0); // kéo tới hiện tại
else
endTime = ob.touchTime; // cố định khi touched / used
color clr;
switch (ob.state)
{
case OB_CANDIDATE: clr = clrDodgerBlue; break;
case OB_TOUCHED: clr = clrDeepSkyBlue; break;
case OB_USED: clr = clrGray; break;
default: return;
}
if (!ObjectFind(0, rectName))
{
ObjectCreate(0, rectName, OBJ_RECTANGLE, 0,
ob.createdTime, ob.obHigh,
endTime, ob.obLow);
ObjectSetInteger(0, rectName, OBJPROP_BACK, true);
ObjectSetInteger(0, rectName, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, rectName, OBJPROP_FILL, true);
}
ObjectSetInteger(0, rectName, OBJPROP_COLOR, clr);
if (ob.state == OB_CANDIDATE)
{
ObjectMove(0, rectName, 1, endTime, ob.obLow);
}
}
void DrawOrUpdateOBLabel(const OBWatchState &ob)
{
string labelName = GetOBLabelName(ob);
if (ob.state == OB_NONE)
return;
string text;
color clr;
switch (ob.state)
{
case OB_CANDIDATE:
text = "OB: CANDIDATE";
clr = clrDodgerBlue;
break;
case OB_TOUCHED:
text = "OB: TOUCHED";
clr = clrDeepSkyBlue;
break;
case OB_USED:
text = "OB: USED";
clr = clrGray;
break;
default:
return;
}
datetime labelTime = ob.createdTime;
double labelPrice = ob.obHigh;
if (!ObjectFind(0, labelName))
{
ObjectCreate(0, labelName, OBJ_TEXT, 0,
labelTime, labelPrice);
}
ObjectSetString(0, labelName, OBJPROP_TEXT, text);
ObjectSetInteger(0, labelName, OBJPROP_COLOR, clr);
ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 9);
}
string BlockReasonToText(ENUM_BLOCK_REASON reason)
{
switch (reason)
{
case BLOCK_SESSION: return "BLOCK: OUT OF SESSION";
case BLOCK_DAILY_LOSS: return "BLOCK: MAX DAILY LOSS";
case BLOCK_BIAS_MISMATCH: return "BLOCK: BIAS MISMATCH";
case BLOCK_NO_OB: return "BLOCK: NO VALID OB";
case BLOCK_OB_NOT_TOUCHED: return "BLOCK: OB NOT TOUCHED";
default: return "";
}
}
color BlockReasonToColor(ENUM_BLOCK_REASON reason)
{
switch (reason)
{
case BLOCK_SESSION: return clrOrangeRed;
case BLOCK_DAILY_LOSS: return clrRed;
case BLOCK_BIAS_MISMATCH: return clrOrange;
case BLOCK_NO_OB: return clrGray;
case BLOCK_OB_NOT_TOUCHED: return clrDodgerBlue;
default: return clrNONE;
}
}
void DrawBlockReasonOverlay()
{
if (!Debug_DrawOnChart)
return;
if (g_BlockReason == BLOCK_NONE)
{
ObjectDelete(0, "EA_BLOCK_REASON");
return;
}
string text = BlockReasonToText(g_BlockReason);
color clr = BlockReasonToColor(g_BlockReason);
ObjectDelete(0, "EA_BLOCK_REASON");
ObjectCreate(0, "EA_BLOCK_REASON", OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_CORNER, CORNER_LEFT_LOWER);
ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_YDISTANCE, 160);
ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_COLOR, clr);
ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_FONTSIZE, 9);
ObjectSetString(0, "EA_BLOCK_REASON", OBJPROP_TEXT, text);
}