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

1727 lines
40 KiB
Plaintext
Raw Normal View History

2025-12-20 05:28:49 +07:00
//+------------------------------------------------------------------+
//| ICT Early Trend Detection EA |
//| Structure + Early Key-Level Break (MSS-style) |
//| Clean Architecture Meaningful Naming |
//| Author: Bell CW |
//+------------------------------------------------------------------+
#property strict
2025-12-21 05:19:14 +07:00
//====================================================
// 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;
2025-12-24 05:37:38 +07:00
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
{
2025-12-26 04:54:55 +07:00
BLOCK_NONE,
BLOCK_NO_OB,
BLOCK_OB_NOT_TOUCHED,
BLOCK_BIAS_MISMATCH,
BLOCK_SESSION,
BLOCK_DAILY_LOSS
2025-12-24 05:37:38 +07:00
};
ENUM_BLOCK_REASON g_BlockReason = BLOCK_NONE;
2025-12-21 05:19:14 +07:00
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;
2025-12-26 04:54:55 +07:00
enum ENUM_EA_STATE
2025-12-21 05:19:14 +07:00
{
2025-12-26 04:54:55 +07:00
EA_STATE_IDLE,
EA_STATE_WAIT_OB,
EA_STATE_WAIT_TRIGGER,
EA_STATE_ORDER_SENT
2025-12-21 05:19:14 +07:00
};
2025-12-26 04:54:55 +07:00
ENUM_EA_STATE g_EAState = EA_STATE_IDLE;
2025-12-21 05:19:14 +07:00
int g_OrderSentBarIndex = -1;
2025-12-20 05:28:49 +07:00
//====================================================
// INPUT PARAMETERS
//====================================================
2025-12-21 05:19:14 +07:00
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
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
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
2025-12-20 05:28:49 +07:00
2025-12-20 13:11:49 +07:00
input int OBMaxAliveMinutes = 180; // OB sống tối đa 180 phút
//====================================================
// SESSION FILTER (SERVER TIME)
//====================================================
// London Session
2025-12-21 05:19:14 +07:00
input int LondonStartHour = 8;
input int LondonEndHour = 17;
input int LondonAvoidLastMin = 60; // tránh 60 phút cuối phiên
2025-12-20 13:11:49 +07:00
// New York Session
2025-12-21 05:19:14 +07:00
input int NewYorkStartHour = 13;
input int NewYorkEndHour = 22;
input int NewYorkAvoidLastMin = 60; // tránh 60 phút cuối phiên
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
input double RiskPercent = 1.0; // % vốn rủi ro mỗi lệnh
input double RiskReward = 2.0; // R:R = 1:2
2025-12-20 13:11:49 +07:00
2025-12-20 19:27:50 +07:00
input double MaxDailyLossPercent = 3.0; // Dừng lệnh nếu lỗ vượt 3% vốn trong ngày
2025-12-20 05:28:49 +07:00
//====================================================
// DATA STRUCTURE
//====================================================
struct TrendState
{
2025-12-21 05:19:14 +07:00
int trendDirection; // 1 = uptrend, -1 = downtrend, 0 = neutral
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
double latestSwingHigh; // Swing High mới nhất
double previousSwingHigh; // Swing High trước đó
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
double latestSwingLow; // Swing Low mới nhất
double previousSwingLow; // Swing Low trước đó
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
int latestSwingHighIndex;
int previousSwingHighIndex;
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
int latestSwingLowIndex;
int previousSwingLowIndex;
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
double currentKeyLevel; // Key level theo ICT structure
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
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ý
2025-12-20 05:28:49 +07:00
};
//====================================================
// GLOBAL STATE
//====================================================
TrendState g_TrendState;
2025-12-20 13:11:49 +07:00
enum ENUM_HTF_BIAS
{
2025-12-21 05:19:14 +07:00
HTF_BIAS_NONE = 0,
HTF_BIAS_UP,
HTF_BIAS_DOWN,
HTF_BIAS_SIDEWAY
2025-12-20 13:11:49 +07:00
};
struct HTFBiasState
{
2025-12-21 05:19:14 +07:00
ENUM_HTF_BIAS bias;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
double rangeHigh;
double rangeLow;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
datetime lastUpdateTime;
2025-12-20 13:11:49 +07:00
};
HTFBiasState g_HTFBias;
struct OBWatchState
{
2025-12-21 05:19:14 +07:00
int direction; // 1 = buy, -1 = sell
2025-12-24 05:37:38 +07:00
ENUM_OB_STATE state;
2025-12-21 05:19:14 +07:00
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;
2025-12-20 13:11:49 +07:00
};
OBWatchState g_OBWatch;
struct OrderPlan
{
2025-12-21 05:19:14 +07:00
bool valid;
int direction; // 1 = buy, -1 = sell
double entry;
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
double stopLoss;
double takeProfit;
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
double riskPoints;
double lot;
2025-12-20 13:11:49 +07:00
};
struct TriggerTFStructure
{
2025-12-21 05:19:14 +07:00
bool valid;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
int direction; // 1 = up, -1 = down
double lastSwingHigh;
double lastSwingLow;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
int lastSwingHighIndex;
int lastSwingLowIndex;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
double breakLevel; // level sẽ bị phá
double newKeyLevel; // 🔥 key level mới sau break
2025-12-20 13:11:49 +07:00
};
TriggerTFStructure g_TriggerTF;
2025-12-20 19:27:50 +07:00
struct DailyRiskState
{
2025-12-21 05:19:14 +07:00
double startBalance;
datetime dayStartTime;
bool lossLimitHit;
2025-12-20 19:27:50 +07:00
};
DailyRiskState g_DailyRisk;
2025-12-20 13:11:49 +07:00
void UpdateHTFBias(
2025-12-21 05:19:14 +07:00
ENUM_TIMEFRAMES biasTf,
HTFBiasState &state)
{
datetime tf0Time = iTime(_Symbol, biasTf, 0);
if (tf0Time == state.lastUpdateTime)
return;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
state.lastUpdateTime = tf0Time;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// --- 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);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
double b2High = iHigh(_Symbol, biasTf, 2);
double b2Low = iLow(_Symbol, biasTf, 2);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
state.rangeHigh = 0;
state.rangeLow = 0;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// ===== UP BIAS =====
if (b1Close > b2High)
{
state.bias = HTF_BIAS_UP;
return;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// ===== DOWN BIAS =====
if (b1Close < b2Low)
{
state.bias = HTF_BIAS_DOWN;
return;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// ===== SIDEWAY: inside bar =====
bool insideBar =
2025-12-20 13:11:49 +07:00
b1High <= b2High &&
2025-12-21 05:19:14 +07:00
b1Low >= b2Low;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (insideBar)
{
state.bias = HTF_BIAS_SIDEWAY;
state.rangeHigh = b2High;
state.rangeLow = b2Low;
return;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// ===== SIDEWAY: indecision =====
bool indecision =
2025-12-20 13:11:49 +07:00
b1Close < b2High &&
b1Close > b2Low;
2025-12-21 05:19:14 +07:00
if (indecision)
{
state.bias = HTF_BIAS_SIDEWAY;
state.rangeHigh = b1High;
state.rangeLow = b1Low;
return;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
state.bias = HTF_BIAS_NONE;
2025-12-20 13:11:49 +07:00
}
2025-12-21 05:19:14 +07:00
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;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 13:11:49 +07:00
}
2025-12-20 19:27:50 +07:00
bool IsDailyLossExceeded(
2025-12-21 05:19:14 +07:00
DailyRiskState &risk,
double maxLossPercent)
2025-12-20 19:27:50 +07:00
{
2025-12-21 05:19:14 +07:00
if (risk.lossLimitHit)
return true;
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
double lossPct =
(risk.startBalance - balance) / risk.startBalance * 100.0;
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
if (lossPct >= maxLossPercent)
{
risk.lossLimitHit = true;
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
PrintFormat(
"⛔ MAX DAILY LOSS HIT | Loss=%.2f%% (Limit=%.2f%%)",
lossPct,
maxLossPercent);
return true;
}
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 19:27:50 +07:00
}
void UpdateDailyRiskState(DailyRiskState &risk)
{
2025-12-21 05:19:14 +07:00
// Thời gian mở nến D1 hiện tại
datetime dailyBarTime = iTime(_Symbol, PERIOD_D1, 0);
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
if (dailyBarTime != risk.dayStartTime)
{
risk.dayStartTime = dailyBarTime;
risk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE);
risk.lossLimitHit = false;
}
2025-12-20 19:27:50 +07:00
}
2025-12-24 05:37:38 +07:00
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();
}
2025-12-20 05:28:49 +07:00
//====================================================
// INITIALIZATION
//====================================================
int OnInit()
{
2025-12-24 05:37:38 +07:00
// ===============================
// 0️⃣ RESET TOÀN BỘ STATE
// ===============================
2025-12-21 05:19:14 +07:00
ZeroMemory(g_TrendState);
2025-12-24 05:37:38 +07:00
ZeroMemory(g_HTFBias);
ZeroMemory(g_OBWatch);
ZeroMemory(g_TriggerTF);
ZeroMemory(g_DailyRisk);
2025-12-20 05:28:49 +07:00
2025-12-26 04:54:55 +07:00
g_EAState = EA_STATE_IDLE;
2025-12-20 13:11:49 +07:00
2025-12-24 05:37:38 +07:00
// ===============================
// 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)
{
2025-12-26 04:54:55 +07:00
g_EAState = EA_STATE_WAIT_OB;
2025-12-24 05:37:38 +07:00
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();
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return INIT_SUCCEEDED;
2025-12-20 05:28:49 +07:00
}
2025-12-24 05:37:38 +07:00
2025-12-20 05:28:49 +07:00
//====================================================
// BAR UTILITY
//====================================================
bool IsNewBarFormed(ENUM_TIMEFRAMES timeframe, datetime &lastBarTime)
{
2025-12-21 05:19:14 +07:00
datetime currentBarTime = iTime(_Symbol, timeframe, 0);
if (currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
2025-12-20 05:28:49 +07:00
}
//====================================================
// SWING DETECTION (GENERIC)
//====================================================
bool IsSwingHighAtBar(
2025-12-21 05:19:14 +07:00
ENUM_TIMEFRAMES timeframe,
int barIndex)
2025-12-20 05:28:49 +07:00
{
2025-12-21 05:19:14 +07:00
double highPrice = iHigh(_Symbol, timeframe, barIndex);
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
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;
2025-12-20 05:28:49 +07:00
}
bool IsSwingLowAtBar(
2025-12-21 05:19:14 +07:00
ENUM_TIMEFRAMES timeframe,
int barIndex)
2025-12-20 05:28:49 +07:00
{
2025-12-21 05:19:14 +07:00
double lowPrice = iLow(_Symbol, timeframe, barIndex);
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
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;
2025-12-20 05:28:49 +07:00
}
//====================================================
// STRUCTURE VALIDATION
//====================================================
bool HasConfirmedMarketStructure(const TrendState &state)
{
2025-12-21 05:19:14 +07:00
return (
state.latestSwingHigh > 0 &&
2025-12-20 05:28:49 +07:00
state.previousSwingHigh > 0 &&
2025-12-21 05:19:14 +07:00
state.latestSwingLow > 0 &&
state.previousSwingLow > 0);
2025-12-20 05:28:49 +07:00
}
//====================================================
// STRUCTURE UPDATE (TIMEFRAME AGNOSTIC)
//====================================================
void UpdateMarketStructure(
2025-12-21 05:19:14 +07:00
ENUM_TIMEFRAMES timeframe,
TrendState &state)
2025-12-20 05:28:49 +07:00
{
2025-12-21 05:19:14 +07:00
datetime currentStructureBarTime = iTime(_Symbol, timeframe, 0);
if (currentStructureBarTime == state.lastStructureUpdateTime)
return;
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
state.lastStructureUpdateTime = currentStructureBarTime;
state.earlyFlipUsedThisBar = false;
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
double detectedSwingHighs[2];
double detectedSwingLows[2];
int detectedSwingHighIndices[2];
int detectedSwingLowIndices[2];
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
int highCount = 0;
int lowCount = 0;
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
for (int barIndex = SwingDetectionRange + 1;
2025-12-20 05:28:49 +07:00
barIndex < StructureScanLookbackBars;
barIndex++)
2025-12-21 05:19:14 +07:00
{
if (highCount < 2 && IsSwingHighAtBar(timeframe, barIndex))
{
detectedSwingHighs[highCount] = iHigh(_Symbol, timeframe, barIndex);
detectedSwingHighIndices[highCount] = barIndex;
highCount++;
}
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
if (lowCount < 2 && IsSwingLowAtBar(timeframe, barIndex))
{
detectedSwingLows[lowCount] = iLow(_Symbol, timeframe, barIndex);
detectedSwingLowIndices[lowCount] = barIndex;
lowCount++;
}
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
if (highCount == 2 && lowCount == 2)
break;
}
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
if (highCount < 2 || lowCount < 2) {
if(Debug_Trend)
Print("⏳ Waiting for enough swing points...");
return;
}
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
state.latestSwingHigh = detectedSwingHighs[0];
state.previousSwingHigh = detectedSwingHighs[1];
state.latestSwingLow = detectedSwingLows[0];
state.previousSwingLow = detectedSwingLows[1];
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
state.latestSwingHighIndex = detectedSwingHighIndices[0];
state.previousSwingHighIndex = detectedSwingHighIndices[1];
state.latestSwingLowIndex = detectedSwingLowIndices[0];
state.previousSwingLowIndex = detectedSwingLowIndices[1];
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
// ===== 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;
}
2025-12-20 05:28:49 +07:00
}
//====================================================
// EARLY TREND FLIP (MSS-STYLE)
//====================================================
void CheckForEarlyTrendFlip(
2025-12-21 05:19:14 +07:00
TrendState &state,
double currentPrice)
2025-12-20 05:28:49 +07:00
{
2025-12-21 05:19:14 +07:00
if (!HasConfirmedMarketStructure(state))
return;
if (state.earlyFlipUsedThisBar)
return;
2025-12-20 05:28:49 +07:00
2025-12-21 05:19:14 +07:00
if (state.trendDirection == 1 &&
2025-12-20 05:28:49 +07:00
currentPrice < state.currentKeyLevel)
2025-12-21 05:19:14 +07:00
{
state.trendDirection = -1;
state.currentKeyLevel = state.latestSwingHigh;
state.earlyFlipUsedThisBar = true;
}
else if (state.trendDirection == -1 &&
2025-12-20 05:28:49 +07:00
currentPrice > state.currentKeyLevel)
2025-12-21 05:19:14 +07:00
{
state.trendDirection = 1;
state.currentKeyLevel = state.latestSwingLow;
state.earlyFlipUsedThisBar = true;
}
2025-12-20 05:28:49 +07:00
}
2025-12-20 13:11:49 +07:00
//====================================================
// SESSION GUARD
//====================================================
bool IsWithinSession(
2025-12-21 05:19:14 +07:00
int currentMinutes,
int sessionStartHour,
int sessionEndHour,
int avoidLastMinutes)
2025-12-20 13:11:49 +07:00
{
2025-12-21 05:19:14 +07:00
int sessionStartMin = sessionStartHour * 60;
int sessionEndMin = sessionEndHour * 60;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// Trong phiên
if (currentMinutes < sessionStartMin ||
2025-12-20 13:11:49 +07:00
currentMinutes >= sessionEndMin)
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// Tránh cuối phiên
if (currentMinutes >= (sessionEndMin - avoidLastMinutes))
return false;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return true;
2025-12-20 13:11:49 +07:00
}
bool IsTradingSessionAllowed()
{
2025-12-21 05:19:14 +07:00
datetime now = TimeCurrent();
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
MqlDateTime tm;
TimeToStruct(now, tm);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
int currentMinutes = tm.hour * 60 + tm.min;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (IsWithinSession(currentMinutes,
2025-12-20 13:11:49 +07:00
LondonStartHour,
LondonEndHour,
LondonAvoidLastMin))
2025-12-21 05:19:14 +07:00
return true;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (IsWithinSession(currentMinutes,
2025-12-20 13:11:49 +07:00
NewYorkStartHour,
NewYorkEndHour,
NewYorkAvoidLastMin))
2025-12-21 05:19:14 +07:00
return true;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 13:11:49 +07:00
}
bool HasPriceTouchedOB(
2025-12-21 05:19:14 +07:00
int direction,
double obHigh,
double obLow,
double price)
{
if (direction == 1) // buy
return price <= obHigh && price >= obLow;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (direction == -1) // sell
return price >= obLow && price <= obHigh;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 13:11:49 +07:00
}
bool FindTrendOrderBlock(
2025-12-21 05:19:14 +07:00
ENUM_TIMEFRAMES timeframe,
const TrendState &trend,
double &obHigh,
double &obLow,
int &obCandleIndex)
2025-12-20 13:11:49 +07:00
{
2025-12-24 05:37:38 +07:00
obHigh = 0;
obLow = 0;
obCandleIndex = -1;
2025-12-20 13:11:49 +07:00
2025-12-24 05:37:38 +07:00
// ===== XÁC ĐỊNH VÙNG QUÉT =====
int scanFrom, scanTo;
if (trend.trendDirection == 1)
2025-12-21 05:19:14 +07:00
{
2025-12-24 05:37:38 +07:00
// Uptrend: OB phải nằm giữa L1 -> L0
scanFrom = trend.latestSwingLowIndex - 1;
scanTo = trend.previousSwingLowIndex + 1;
2025-12-21 05:19:14 +07:00
}
2025-12-24 05:37:38 +07:00
else if (trend.trendDirection == -1)
2025-12-21 05:19:14 +07:00
{
2025-12-24 05:37:38 +07:00
// Downtrend: OB phải nằm giữa H1 -> H0
scanFrom = trend.latestSwingHighIndex - 1;
scanTo = trend.previousSwingHighIndex + 1;
2025-12-21 05:19:14 +07:00
}
2025-12-24 05:37:38 +07:00
else
return false;
2025-12-20 13:11:49 +07:00
2025-12-24 05:37:38 +07:00
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;
2025-12-20 13:11:49 +07:00
}
2025-12-26 04:54:55 +07:00
void ResetOBWithReason(ENUM_OB_RESET_REASON reason)
{
g_LastOBResetReason = reason;
ResetOBWatch();
ResetTriggerTFStructure();
g_EAState = EA_STATE_IDLE;
}
2025-12-20 13:11:49 +07:00
void ResetOBWatch()
{
2025-12-24 05:37:38 +07:00
g_OBWatch.state = OB_NONE;
2025-12-21 05:19:14 +07:00
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
);
2025-12-20 13:11:49 +07:00
}
bool IsOBInvalidated(
2025-12-21 05:19:14 +07:00
int direction,
double obHigh,
double obLow,
double price)
{
if (direction == 1) // buy
return price < obLow;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (direction == -1) // sell
return price > obHigh;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 13:11:49 +07:00
}
void ResetTriggerTFStructure()
{
2025-12-21 05:19:14 +07:00
ZeroMemory(g_TriggerTF);
2025-12-20 13:11:49 +07:00
}
bool IsTriggerTFStructureBreak(
2025-12-21 05:19:14 +07:00
const TriggerTFStructure &ts,
int trendDirection,
ENUM_TIMEFRAMES tf)
2025-12-20 13:11:49 +07:00
{
2025-12-21 05:19:14 +07:00
if (!ts.valid)
return false;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
double high = iHigh(_Symbol, tf, 1);
double low = iLow(_Symbol, tf, 1);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (trendDirection == 1)
return high > ts.breakLevel;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (trendDirection == -1)
return low < ts.breakLevel;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 13:11:49 +07:00
}
void UpdateTriggerTFStructure(
2025-12-21 05:19:14 +07:00
ENUM_TIMEFRAMES tf,
TriggerTFStructure &ts,
int startBar)
{
double sh = 0.0;
double sl = 0.0;
int shi = -1;
int sli = -1;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
bool foundHigh = false;
bool foundLow = false;
2025-12-20 13:11:49 +07:00
2025-12-26 04:54:55 +07:00
for (int i = startBar - SwingDetectionRange;
i >= MathMax(1, startBar - TriggerMaxScanBars);
i--)
2025-12-21 05:19:14 +07:00
{
if (!foundHigh && IsSwingHighAtBar(tf, i))
{
sh = iHigh(_Symbol, tf, i);
shi = i;
foundHigh = true;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (!foundLow && IsSwingLowAtBar(tf, i))
{
sl = iLow(_Symbol, tf, i);
sli = i;
foundLow = true;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (foundHigh && foundLow)
break;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (!foundHigh || !foundLow)
return;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
ts.lastSwingHigh = sh;
ts.lastSwingLow = sl;
ts.lastSwingHighIndex = shi;
ts.lastSwingLowIndex = sli;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
ts.direction = g_TrendState.trendDirection;
ts.valid = true;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (ts.direction == 1)
{
ts.breakLevel = ts.lastSwingHigh;
ts.newKeyLevel = ts.lastSwingLow;
}
else
{
ts.breakLevel = ts.lastSwingLow;
ts.newKeyLevel = ts.lastSwingHigh;
}
2025-12-20 13:11:49 +07:00
}
bool IsOBExpired(const OBWatchState &ob)
{
2025-12-24 05:37:38 +07:00
if (ob.createdTime <= 0)
return false;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
int aliveSec = (int)(TimeCurrent() - ob.createdTime);
return aliveSec > OBMaxAliveMinutes * 60;
2025-12-20 13:11:49 +07:00
}
void MarkOBAsUsed()
{
2025-12-24 05:37:38 +07:00
g_OBWatch.state = OB_USED;
2025-12-20 13:11:49 +07:00
}
2025-12-24 05:37:38 +07:00
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;
2025-12-26 04:54:55 +07:00
int shift = iBarShift(_Symbol, triggerTf, ob.touchTime);
ob.triggerStartBar = (shift > 0 ? shift : 1);
2025-12-24 05:37:38 +07:00
// Reset trigger TF structure
ResetTriggerTFStructure();
if (Debug_OB)
{
PrintFormat(
"🟦 OB TOUCHED | dir=%d | OB[%.2f - %.2f]",
ob.direction,
ob.obLow,
ob.obHigh
);
}
}
2025-12-20 13:11:49 +07:00
bool IsOBCloseBreak(
2025-12-21 05:19:14 +07:00
int direction,
double obHigh,
double obLow)
{
2025-12-24 05:37:38 +07:00
double close = iClose(_Symbol, TrendTimeframe, 1);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (direction == 1) // buy OB
return close < obLow;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (direction == -1) // sell OB
return close > obHigh;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return false;
2025-12-20 13:11:49 +07:00
}
OrderPlan BuildOrderPlanFromTriggerTF(
2025-12-21 05:19:14 +07:00
const TriggerTFStructure &ts,
ENUM_TIMEFRAMES tf)
2025-12-20 13:11:49 +07:00
{
2025-12-21 05:19:14 +07:00
OrderPlan plan;
ZeroMemory(plan);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
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
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
double riskPoints;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// ===== BUY =====
if (ts.direction == 1)
{
plan.stopLoss = MathMin(ts.newKeyLevel, g_OBWatch.obLow);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
riskPoints = plan.entry - plan.stopLoss;
if (riskPoints <= 0)
{
plan.valid = false;
return plan;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
plan.takeProfit = plan.entry + riskPoints * RiskReward;
}
// ===== SELL =====
else
{
plan.stopLoss = MathMax(ts.newKeyLevel, g_OBWatch.obHigh);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
riskPoints = plan.stopLoss - plan.entry;
if (riskPoints <= 0)
{
plan.valid = false;
return plan;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
plan.takeProfit = plan.entry - riskPoints * RiskReward;
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return plan;
2025-12-20 13:11:49 +07:00
}
double CalculateRiskLot(
2025-12-21 05:19:14 +07:00
double entryPrice,
double stopLossPrice)
2025-12-20 19:27:50 +07:00
{
2025-12-21 05:19:14 +07:00
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double riskMoney = equity * RiskPercent / 100.0;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
if (contractSize <= 0)
return 0;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
double slDistance = MathAbs(entryPrice - stopLossPrice);
if (slDistance <= _Point)
return 0;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// Cost cho 1 lot nếu SL hit
double costPerLot = slDistance * contractSize;
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
if (costPerLot <= 0)
return 0;
2025-12-20 19:27:50 +07:00
2025-12-21 05:19:14 +07:00
double rawLot = riskMoney / costPerLot;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
// ===== Normalize theo broker =====
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
rawLot = MathMax(minLot, MathMin(rawLot, maxLot));
rawLot = MathFloor(rawLot / lotStep) * lotStep;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
return NormalizeDouble(rawLot, 2);
2025-12-20 13:11:49 +07:00
}
2025-12-26 04:54:55 +07:00
bool ExecuteOrder(const OrderPlan &plan)
2025-12-20 13:11:49 +07:00
{
2025-12-26 04:54:55 +07:00
if (!plan.valid) return false;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
MqlTradeRequest req;
MqlTradeResult res;
ZeroMemory(req);
ZeroMemory(res);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
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;
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
req.sl = plan.stopLoss;
req.tp = plan.takeProfit;
req.deviation = 5;
req.magic = 202501;
req.comment = "ICT_OB_Trigger";
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
bool sent = OrderSend(req, res);
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
if (!sent || res.retcode != TRADE_RETCODE_DONE)
{
PrintFormat(
"❌ OrderSend failed | sent=%d | retcode=%d",
sent,
res.retcode);
2025-12-26 04:54:55 +07:00
return false;
} else {
2025-12-21 05:19:14 +07:00
PrintFormat(
"✅ ORDER SENT | lot=%.2f | entry=%.2f | SL=%.2f | TP=%.2f",
plan.lot,
plan.entry,
plan.stopLoss,
plan.takeProfit);
2025-12-26 04:54:55 +07:00
return true;
2025-12-21 05:19:14 +07:00
}
2025-12-20 13:11:49 +07:00
}
2025-12-26 04:54:55 +07:00
string GetOBRectName(const OBWatchState &ob)
2025-12-24 05:37:38 +07:00
{
2025-12-26 04:54:55 +07:00
return StringFormat("OB_RECT_%I64d", ob.createdTime);
}
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
string GetOBLabelName(const OBWatchState &ob)
{
return StringFormat("OB_LABEL_%I64d", ob.createdTime);
}
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
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);
}
2025-12-24 05:37:38 +07:00
}
2025-12-20 05:28:49 +07:00
void OnTick()
{
2025-12-26 04:54:55 +07:00
UpdateContext();
2025-12-20 19:27:50 +07:00
2025-12-26 04:54:55 +07:00
bool allowStateMachine = EvaluateTradePermission();
2025-12-20 19:27:50 +07:00
2025-12-26 04:54:55 +07:00
if (allowStateMachine)
RunStateMachine();
else
ResetOBIfBlocked();
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
DrawVisuals();
2025-12-20 05:28:49 +07:00
}
2025-12-20 13:11:49 +07:00
2025-12-21 05:19:14 +07:00
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
//====================================================
2025-12-26 04:54:55 +07:00
void DrawVisuals()
{
DrawMarketStructure(TrendTimeframe, g_TrendState);
DrawTrendSummaryLabel(g_TrendState);
DrawOrUpdateOBRectangle(g_OBWatch);
DrawOrUpdateOBLabel(g_OBWatch);
DrawEAStateOverlay();
DrawBlockReasonOverlay();
}
2025-12-21 05:19:14 +07:00
void DrawEAStateOverlay()
{
string stepText;
color stepColor = clrGray;
2025-12-26 04:54:55 +07:00
switch (g_EAState)
2025-12-21 05:19:14 +07:00
{
2025-12-26 04:54:55 +07:00
case EA_STATE_IDLE:
2025-12-21 05:19:14 +07:00
stepText = "STEP 0: IDLE / GUARD";
stepColor = clrSilver;
break;
2025-12-26 04:54:55 +07:00
case EA_STATE_WAIT_OB:
stepText = "STEP 1: WAIT OB TOUCH";
2025-12-21 05:19:14 +07:00
stepColor = clrOrange;
break;
2025-12-26 04:54:55 +07:00
case EA_STATE_WAIT_TRIGGER:
stepText = "STEP 2: WAIT TRIGGER";
2025-12-21 05:19:14 +07:00
stepColor = clrYellow;
break;
2025-12-26 04:54:55 +07:00
case EA_STATE_ORDER_SENT:
stepText = "STEP 3: ORDER SENT";
2025-12-21 05:19:14 +07:00
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);
2025-12-24 05:37:38 +07:00
if (g_OBWatch.state == OB_CANDIDATE)
2025-12-21 05:19:14 +07:00
{
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)
{
2025-12-24 05:37:38 +07:00
if (barIndex < 0 || price <= 0)
2025-12-21 05:19:14 +07:00
return;
datetime barTime = iTime(_Symbol, timeframe, barIndex);
ObjectDelete(0, objectName);
ObjectCreate(0, objectName, OBJ_TEXT, 0, barTime, price);
2025-12-24 05:37:38 +07:00
2025-12-21 05:19:14 +07:00
ObjectSetInteger(0, objectName, OBJPROP_COLOR, labelColor);
2025-12-24 05:37:38 +07:00
ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 9);
2025-12-21 05:19:14 +07:00
ObjectSetString(0, objectName, OBJPROP_TEXT, labelText);
2025-12-24 05:37:38 +07:00
// ===== 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);
}
2025-12-21 05:19:14 +07:00
}
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);
}
2025-12-24 05:37:38 +07:00
void DrawOrUpdateOBRectangle(const OBWatchState &ob)
{
2025-12-26 04:54:55 +07:00
string rectName = GetOBRectName(ob);
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
if (ob.state == OB_NONE)
return;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
datetime endTime;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
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
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
color clr;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
switch (ob.state)
{
case OB_CANDIDATE: clr = clrDodgerBlue; break;
case OB_TOUCHED: clr = clrDeepSkyBlue; break;
case OB_USED: clr = clrGray; break;
default: return;
}
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
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);
}
2025-12-24 05:37:38 +07:00
}
void DrawOrUpdateOBLabel(const OBWatchState &ob)
{
2025-12-26 04:54:55 +07:00
string labelName = GetOBLabelName(ob);
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
if (ob.state == OB_NONE)
return;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
string text;
color clr;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
switch (ob.state)
{
case OB_CANDIDATE:
text = "OB: CANDIDATE";
clr = clrDodgerBlue;
break;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
case OB_TOUCHED:
text = "OB: TOUCHED";
clr = clrDeepSkyBlue;
break;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
case OB_USED:
text = "OB: USED";
clr = clrGray;
break;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
default:
return;
}
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
datetime labelTime = ob.createdTime;
double labelPrice = ob.obHigh;
2025-12-24 05:37:38 +07:00
2025-12-26 04:54:55 +07:00
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);
2025-12-24 05:37:38 +07:00
}