1109 lines
27 KiB
Plaintext
1109 lines
27 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| ICT Early Trend Detection EA |
|
||
//| Structure + Early Key-Level Break (MSS-style) |
|
||
//| Clean Architecture – Meaningful Naming |
|
||
//| Author: Bell CW |
|
||
//+------------------------------------------------------------------+
|
||
#property strict
|
||
|
||
//====================================================
|
||
// 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 = 150; // 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;
|
||
datetime g_LastTriggerBarTime = 0;
|
||
|
||
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
|
||
{
|
||
bool active; // đang theo dõi OB
|
||
int direction; // 1 = buy, -1 = sell
|
||
bool used; // OB đã được sử dụng (entry xong)
|
||
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;
|
||
}
|
||
}
|
||
|
||
//====================================================
|
||
// INITIALIZATION
|
||
//====================================================
|
||
int OnInit()
|
||
{
|
||
ZeroMemory(g_TrendState);
|
||
g_TrendState.trendDirection = 0;
|
||
g_TrendState.lastStructureUpdateTime = 0;
|
||
|
||
g_HTFBias.bias = HTF_BIAS_NONE;
|
||
g_HTFBias.rangeHigh = 0;
|
||
g_HTFBias.rangeLow = 0;
|
||
g_HTFBias.lastUpdateTime = 0;
|
||
|
||
g_OBWatch.active = false;
|
||
|
||
Print("ICT Early Trend Detection EA initialized successfully");
|
||
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)
|
||
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;
|
||
}
|
||
}
|
||
|
||
//====================================================
|
||
// DRAWING UTILITIES
|
||
//====================================================
|
||
|
||
void DrawSwingLabel(
|
||
string objectName,
|
||
string labelText,
|
||
ENUM_TIMEFRAMES timeframe,
|
||
int barIndex,
|
||
double price,
|
||
color labelColor
|
||
)
|
||
{
|
||
if(barIndex < 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, 8);
|
||
ObjectSetString (0, objectName, OBJPROP_TEXT, labelText);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
//====================================================
|
||
// MAIN LOOP
|
||
//====================================================
|
||
|
||
//====================================================
|
||
// 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;
|
||
|
||
// ===== UP TREND =====
|
||
if(trend.trendDirection == 1)
|
||
{
|
||
// Bắt đầu từ swing low gần nhất (key level)
|
||
for(int i = trend.latestSwingLowIndex - 1; i >= 1; i--)
|
||
{
|
||
double open = iOpen (_Symbol, timeframe, i);
|
||
double close = iClose(_Symbol, timeframe, i);
|
||
|
||
// Nến giảm
|
||
if(close < open)
|
||
{
|
||
obHigh = iHigh(_Symbol, timeframe, i);
|
||
obLow = iLow (_Symbol, timeframe, i);
|
||
obCandleIndex = i;
|
||
|
||
double range = MathAbs(obHigh - obLow);
|
||
double minRange = 10 * _Point;
|
||
|
||
if(range < minRange)
|
||
continue;
|
||
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== DOWN TREND =====
|
||
if(trend.trendDirection == -1)
|
||
{
|
||
// Bắt đầu từ swing high gần nhất (key level)
|
||
for(int i = trend.latestSwingHighIndex - 1; i >= 1; i--)
|
||
{
|
||
double open = iOpen (_Symbol, timeframe, i);
|
||
double close = iClose(_Symbol, timeframe, i);
|
||
|
||
// Nến tăng
|
||
if(close > open)
|
||
{
|
||
obHigh = iHigh(_Symbol, timeframe, i);
|
||
obLow = iLow (_Symbol, timeframe, i);
|
||
obCandleIndex = i;
|
||
|
||
double range = MathAbs(obHigh - obLow);
|
||
double minRange = 10 * _Point;
|
||
|
||
if(range < minRange)
|
||
continue;
|
||
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
void ResetOBWatch()
|
||
{
|
||
g_OBWatch.active = false;
|
||
g_OBWatch.used = false;
|
||
g_OBWatch.direction = 0;
|
||
g_OBWatch.obHigh = 0;
|
||
g_OBWatch.obLow = 0;
|
||
g_OBWatch.createdTime = 0;
|
||
}
|
||
|
||
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 < startBar + StructureScanLookbackBars;
|
||
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;
|
||
}
|
||
}
|
||
|
||
void TryActivateOBWatch(
|
||
const TrendState &trend,
|
||
double currentPrice
|
||
) {
|
||
if(g_OBWatch.used) return;
|
||
double obHigh, obLow;
|
||
int obIndex;
|
||
|
||
if(!FindTrendOrderBlock(TrendTimeframe,
|
||
trend,
|
||
obHigh,
|
||
obLow,
|
||
obIndex))
|
||
return;
|
||
|
||
if(!HasPriceTouchedOB(trend.trendDirection,
|
||
obHigh,
|
||
obLow,
|
||
currentPrice))
|
||
return;
|
||
|
||
g_OBWatch.active = true;
|
||
ResetTriggerTFStructure();
|
||
g_OBWatch.direction = trend.trendDirection;
|
||
g_OBWatch.obHigh = obHigh;
|
||
g_OBWatch.obLow = obLow;
|
||
g_OBWatch.createdTime = TimeCurrent();
|
||
g_OBWatch.barsAlive = 0;
|
||
g_OBWatch.triggerStartBar = 1;
|
||
|
||
// Debug (optional)
|
||
PrintFormat(
|
||
"OB WATCH ACTIVATED | dir=%d | OB[%.2f - %.2f]",
|
||
g_OBWatch.direction,
|
||
g_OBWatch.obLow,
|
||
g_OBWatch.obHigh
|
||
);
|
||
}
|
||
|
||
void HandleOBWatching(
|
||
const TrendState &trend,
|
||
double currentPrice
|
||
) {
|
||
g_OBWatch.barsAlive++;
|
||
|
||
if(g_OBWatch.barsAlive > 150) // ~30 bar M5 = ~150 phút
|
||
{
|
||
ResetOBWatch();
|
||
return;
|
||
}
|
||
|
||
// 1️⃣ Bias hoặc trend không còn hợp lệ
|
||
if(!IsHTFBiasAligned(g_HTFBias.bias, trend.trendDirection))
|
||
{
|
||
ResetOBWatch();
|
||
return;
|
||
}
|
||
|
||
// 2️⃣ OB quá hạn
|
||
if(IsOBExpired(g_OBWatch))
|
||
{
|
||
ResetOBWatch();
|
||
return;
|
||
}
|
||
|
||
// 3️⃣ Giá đóng nến xuyên qua OB
|
||
if(IsOBCloseBreak(g_OBWatch.direction,
|
||
g_OBWatch.obHigh,
|
||
g_OBWatch.obLow))
|
||
{
|
||
ResetOBWatch();
|
||
return;
|
||
}
|
||
|
||
// 4️⃣ OB bị phá sâu (tick safety)
|
||
if(IsOBInvalidated(g_OBWatch.direction,
|
||
g_OBWatch.obHigh,
|
||
g_OBWatch.obLow,
|
||
currentPrice))
|
||
{
|
||
ResetOBWatch();
|
||
return;
|
||
}
|
||
|
||
// =================================================
|
||
// 🔥 TRIGGER TF STRUCTURE LOGIC (CHỖ QUAN TRỌNG)
|
||
// =================================================
|
||
UpdateTriggerTFStructure(
|
||
TriggerTimeframe,
|
||
g_TriggerTF,
|
||
g_OBWatch.triggerStartBar
|
||
);
|
||
|
||
if(IsTriggerTFStructureBreak(g_TriggerTF,
|
||
trend.trendDirection,
|
||
TriggerTimeframe))
|
||
{
|
||
OrderPlan plan =
|
||
BuildOrderPlanFromTriggerTF(g_TriggerTF,
|
||
TriggerTimeframe);
|
||
|
||
if(plan.valid)
|
||
{
|
||
plan.lot = CalculateRiskLot(plan.entry, plan.stopLoss);
|
||
|
||
if(plan.lot <= 0)
|
||
return;
|
||
|
||
ExecuteOrder(plan);
|
||
|
||
PrintFormat(
|
||
"🎯 ENTRY CONFIRMED | dir=%d | entry=%.2f | SL=%.2f | TP=%.2f | RR=1:%.1f",
|
||
plan.direction,
|
||
plan.entry,
|
||
plan.stopLoss,
|
||
plan.takeProfit,
|
||
RiskReward
|
||
);
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
bool IsOBExpired(const OBWatchState &ob)
|
||
{
|
||
if(!ob.active)
|
||
return true;
|
||
|
||
int aliveSec = (int)(TimeCurrent() - ob.createdTime);
|
||
return aliveSec > OBMaxAliveMinutes * 60;
|
||
}
|
||
|
||
void MarkOBAsUsed()
|
||
{
|
||
g_OBWatch.used = true;
|
||
g_OBWatch.active = false;
|
||
}
|
||
|
||
bool IsOBCloseBreak(
|
||
int direction,
|
||
double obHigh,
|
||
double obLow
|
||
) {
|
||
double close = iClose(_Symbol, TriggerTimeframe, 1);
|
||
|
||
if(direction == 1) // buy OB
|
||
return close < obLow;
|
||
|
||
if(direction == -1) // sell OB
|
||
return close > obHigh;
|
||
|
||
return false;
|
||
}
|
||
|
||
bool IsTriggerKeyLevelBreak(
|
||
const TrendState &trend,
|
||
ENUM_TIMEFRAMES triggerTf
|
||
) {
|
||
double close = iClose(_Symbol, triggerTf, 1);
|
||
|
||
if(trend.trendDirection == 1)
|
||
return close > trend.currentKeyLevel;
|
||
|
||
if(trend.trendDirection == -1)
|
||
return close < trend.currentKeyLevel;
|
||
|
||
return false;
|
||
}
|
||
|
||
OrderPlan BuildOrderPlanFromTriggerTF(
|
||
const TriggerTFStructure &ts,
|
||
ENUM_TIMEFRAMES tf
|
||
)
|
||
{
|
||
OrderPlan plan;
|
||
ZeroMemory(plan);
|
||
|
||
double entryPrice = iClose(_Symbol, tf, 1);
|
||
|
||
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);
|
||
}
|
||
|
||
void ExecuteOrder(const OrderPlan &plan)
|
||
{
|
||
if(!plan.valid) return;
|
||
|
||
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;
|
||
}
|
||
else
|
||
{
|
||
PrintFormat(
|
||
"✅ ORDER SENT | lot=%.2f | entry=%.2f | SL=%.2f | TP=%.2f",
|
||
plan.lot,
|
||
plan.entry,
|
||
plan.stopLoss,
|
||
plan.takeProfit
|
||
);
|
||
MarkOBAsUsed();
|
||
}
|
||
}
|
||
|
||
void OnTick()
|
||
{
|
||
UpdateDailyRiskState(g_DailyRisk);
|
||
|
||
if(IsDailyLossExceeded(g_DailyRisk, MaxDailyLossPercent))
|
||
return;
|
||
|
||
if(!IsNewBarFormed(TriggerTimeframe, g_LastTriggerBarTime))
|
||
return;
|
||
|
||
if(!IsTradingSessionAllowed())
|
||
return;
|
||
|
||
// ===== HTF BIAS =====
|
||
UpdateHTFBias(BiasTimeframe, g_HTFBias);
|
||
|
||
// ===== TREND STRUCTURE =====
|
||
UpdateMarketStructure(TrendTimeframe, g_TrendState);
|
||
|
||
double currentBidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||
CheckForEarlyTrendFlip(g_TrendState, currentBidPrice);
|
||
|
||
DrawMarketStructure(TrendTimeframe, g_TrendState);
|
||
DrawTrendSummaryLabel(g_TrendState);
|
||
|
||
// ===== ENTRY FILTER =====
|
||
if(!IsHTFBiasAligned(g_HTFBias.bias,
|
||
g_TrendState.trendDirection))
|
||
return;
|
||
|
||
// ===== ORDER BLOCK FLOW =====
|
||
if(!g_OBWatch.active) {
|
||
TryActivateOBWatch(g_TrendState, currentBidPrice);
|
||
} else {
|
||
HandleOBWatching(g_TrendState, currentBidPrice);
|
||
}
|
||
}
|
||
|
||
//+------------------------------------------------------------------+
|