2026-03-10 13:47:42 +07:00
|
|
|
|
#ifndef EA_ICT_CL__SIGNALS_BOS_FVG_OB_MQH
|
2026-03-10 20:39:33 +07:00
|
|
|
|
#define EA_ICT_CL__SIGNALS_BOS_FVG_OB_MQH
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
/** True if candle at bar index has body % of range >= InpFVGMinBodyPct. */
|
|
|
|
|
|
inline bool IsCandleStrong(ENUM_TIMEFRAMES tf, int barIndex)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
double high = iHigh(_Symbol, tf, barIndex);
|
|
|
|
|
|
double low = iLow(_Symbol, tf, barIndex);
|
|
|
|
|
|
double open = iOpen(_Symbol, tf, barIndex);
|
|
|
|
|
|
double close = iClose(_Symbol, tf, barIndex);
|
|
|
|
|
|
double range = high - low;
|
|
|
|
|
|
if (range < _Point) return false;
|
|
|
|
|
|
return (MathAbs(close - open) / range * 100.0) >= InpFVGMinBodyPct;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
/** True if an FVG with given created time already exists in pool. */
|
|
|
|
|
|
inline bool IsFVGInPool(datetime createdTime)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
|
|
|
|
|
for (int j = 0; j < g_FVGCount; j++)
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (g_FVGPool[j].createdTime == createdTime) return true;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
/** Scans middle TF for FVGs and registers them into g_FVGPool (once per new bar). */
|
2026-03-10 13:47:42 +07:00
|
|
|
|
inline void ScanAndRegisterFVGs()
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
static datetime lastScanBarTime = 0;
|
|
|
|
|
|
datetime currentBarTime = iTime(_Symbol, InpMiddleTF, 0);
|
|
|
|
|
|
if (currentBarTime == lastScanBarTime) return;
|
|
|
|
|
|
lastScanBarTime = currentBarTime;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
MarketDir trendDir = g_MiddleTrend.trend;
|
|
|
|
|
|
if (trendDir == DIR_NONE) return;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
int maxBarIndex = MathMin(InpFVGScanBars, Bars(_Symbol, InpMiddleTF) - 2);
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
for (int i = 2; i <= maxBarIndex; i++)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
double leftBarHigh = iHigh (_Symbol, InpMiddleTF, i + 1);
|
|
|
|
|
|
double leftBarLow = iLow (_Symbol, InpMiddleTF, i + 1);
|
|
|
|
|
|
double rightBarHigh = iHigh (_Symbol, InpMiddleTF, i - 1);
|
|
|
|
|
|
double rightBarLow = iLow (_Symbol, InpMiddleTF, i - 1);
|
|
|
|
|
|
double midOpen = iOpen (_Symbol, InpMiddleTF, i);
|
|
|
|
|
|
double midClose = iClose(_Symbol, InpMiddleTF, i);
|
|
|
|
|
|
double gapHigh = 0, gapLow = 0;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (trendDir == DIR_UP)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (leftBarHigh >= rightBarLow) continue;
|
|
|
|
|
|
if (midClose <= midOpen) continue;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
if (!IsCandleStrong(InpMiddleTF, i)) continue;
|
2026-03-10 20:39:33 +07:00
|
|
|
|
gapLow = leftBarHigh;
|
|
|
|
|
|
gapHigh = rightBarLow;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (leftBarLow <= rightBarHigh) continue;
|
|
|
|
|
|
if (midClose >= midOpen) continue;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
if (!IsCandleStrong(InpMiddleTF, i)) continue;
|
2026-03-10 20:39:33 +07:00
|
|
|
|
gapHigh = leftBarLow;
|
|
|
|
|
|
gapLow = rightBarHigh;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
datetime createdTime = iTime(_Symbol, InpMiddleTF, i - 1);
|
|
|
|
|
|
if (IsFVGInPool(createdTime)) continue;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
|
|
|
|
|
if (g_FVGCount >= MAX_FVG_POOL)
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
int evictIndex = -1;
|
|
|
|
|
|
datetime oldestTime = TimeCurrent();
|
2026-03-10 13:47:42 +07:00
|
|
|
|
for (int j = 0; j < g_FVGCount; j++)
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (g_FVGPool[j].status == FVG_USED && g_FVGPool[j].createdTime < oldestTime)
|
|
|
|
|
|
{ oldestTime = g_FVGPool[j].createdTime; evictIndex = j; }
|
|
|
|
|
|
if (evictIndex < 0) { if (InpDebugLog) Print("[FVG POOL] Full"); break; }
|
|
|
|
|
|
for (int j = evictIndex; j < g_FVGCount - 1; j++) g_FVGPool[j] = g_FVGPool[j + 1];
|
2026-03-10 13:47:42 +07:00
|
|
|
|
g_FVGCount--;
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (g_ActiveFVGIdx > evictIndex) g_ActiveFVGIdx--;
|
|
|
|
|
|
else if (g_ActiveFVGIdx == evictIndex) g_ActiveFVGIdx = -1;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
FVGRecord record;
|
|
|
|
|
|
ZeroMemory(record);
|
|
|
|
|
|
record.id = g_NextFVGId++;
|
|
|
|
|
|
record.direction = trendDir;
|
|
|
|
|
|
record.high = gapHigh;
|
|
|
|
|
|
record.low = gapLow;
|
|
|
|
|
|
record.mid = (gapHigh + gapLow) / 2.0;
|
|
|
|
|
|
record.createdTime = createdTime;
|
|
|
|
|
|
int rightBarIndex = i - 1;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
bool isBrokenByClose = false;
|
|
|
|
|
|
datetime breakTime = 0;
|
|
|
|
|
|
for (int j = rightBarIndex - 1; j >= 1; j--)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
double closeAtJ = iClose(_Symbol, InpMiddleTF, j);
|
|
|
|
|
|
if ((record.direction == DIR_UP && closeAtJ < record.low) ||
|
|
|
|
|
|
(record.direction == DIR_DOWN && closeAtJ > record.high))
|
|
|
|
|
|
{ isBrokenByClose = true; breakTime = iTime(_Symbol, InpMiddleTF, j); break; }
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (isBrokenByClose) { record.status = FVG_USED; record.usedCase = 1; record.usedTime = breakTime; }
|
2026-03-10 13:47:42 +07:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-03-10 23:43:41 +07:00
|
|
|
|
record.status = FVG_PENDING;
|
|
|
|
|
|
if ((int)(TimeCurrent() - record.createdTime) > InpFVGMaxAliveMin * 60)
|
|
|
|
|
|
{ record.status = FVG_USED; record.usedCase = 0; record.usedTime = TimeCurrent(); }
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
g_FVGPool[g_FVGCount] = record;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
g_FVGCount++;
|
|
|
|
|
|
|
|
|
|
|
|
if (InpDebugLog)
|
|
|
|
|
|
PrintFormat("[FVG +] #%d %s [%.5f–%.5f] %s | %s",
|
2026-03-10 20:39:33 +07:00
|
|
|
|
record.id, EnumToString(record.direction), record.low, record.high,
|
|
|
|
|
|
EnumToString(record.status), TimeToString(record.createdTime));
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 22:21:59 +07:00
|
|
|
|
/** Updates status of each FVG in pool (broken, touched, MSS-triggered). TOUCHED chỉ chuyển USED khi broke hoặc MSS, không expire. */
|
2026-03-10 13:47:42 +07:00
|
|
|
|
inline void UpdateFVGStatuses()
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
|
|
|
|
double lastBarClose = iClose(_Symbol, InpMiddleTF, 1);
|
|
|
|
|
|
datetime lastBarTime = iTime (_Symbol, InpMiddleTF, 1);
|
2026-03-10 13:47:42 +07:00
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < g_FVGCount; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (g_FVGPool[i].status == FVG_USED) continue;
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
bool isBroken = (g_FVGPool[i].direction == DIR_UP && lastBarClose < g_FVGPool[i].low) ||
|
|
|
|
|
|
(g_FVGPool[i].direction == DIR_DOWN && lastBarClose > g_FVGPool[i].high);
|
|
|
|
|
|
if (isBroken)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
|
|
|
|
|
g_FVGPool[i].status = FVG_USED;
|
|
|
|
|
|
g_FVGPool[i].usedCase = 1;
|
2026-03-10 20:39:33 +07:00
|
|
|
|
g_FVGPool[i].usedTime = lastBarTime;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
if (InpDebugLog)
|
2026-03-10 20:39:33 +07:00
|
|
|
|
PrintFormat("[FVG #%d] BROKEN | close=%.5f", g_FVGPool[i].id, lastBarClose);
|
2026-03-10 13:47:42 +07:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (g_FVGPool[i].status == FVG_PENDING)
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
int ageSeconds = (int)(TimeCurrent() - g_FVGPool[i].createdTime);
|
|
|
|
|
|
if (ageSeconds > InpFVGMaxAliveMin * 60)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
g_FVGPool[i].status = FVG_USED;
|
|
|
|
|
|
g_FVGPool[i].usedCase = 0;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
g_FVGPool[i].usedTime = TimeCurrent();
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 23:43:41 +07:00
|
|
|
|
double fvgRange = g_FVGPool[i].high - g_FVGPool[i].low;
|
|
|
|
|
|
double touchDepth = fvgRange * InpFVGTouchPct / 100.0;
|
|
|
|
|
|
bool isTouched = (g_FVGPool[i].direction == DIR_UP && bid <= g_FVGPool[i].high - touchDepth) ||
|
|
|
|
|
|
(g_FVGPool[i].direction == DIR_DOWN && bid >= g_FVGPool[i].low + touchDepth);
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (isTouched)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
|
|
|
|
|
g_FVGPool[i].status = FVG_TOUCHED;
|
|
|
|
|
|
g_FVGPool[i].touchTime = TimeCurrent();
|
|
|
|
|
|
g_FVGPool[i].triggerTrendAtTouch = g_TriggerTrend.trend;
|
|
|
|
|
|
if (InpDebugLog)
|
|
|
|
|
|
PrintFormat("[FVG #%d] TOUCHED | bid=%.5f [%.5f–%.5f]",
|
|
|
|
|
|
g_FVGPool[i].id, bid, g_FVGPool[i].low, g_FVGPool[i].high);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (g_FVGPool[i].status == FVG_TOUCHED)
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
bool hasMssAfterTouch =
|
2026-03-10 13:47:42 +07:00
|
|
|
|
g_TriggerTrend.lastMssTime > g_FVGPool[i].touchTime &&
|
|
|
|
|
|
g_TriggerTrend.lastMssBreak == g_FVGPool[i].direction;
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (hasMssAfterTouch)
|
2026-03-10 13:47:42 +07:00
|
|
|
|
{
|
|
|
|
|
|
g_FVGPool[i].status = FVG_USED;
|
|
|
|
|
|
g_FVGPool[i].usedCase = 2;
|
|
|
|
|
|
g_FVGPool[i].usedTime = TimeCurrent();
|
|
|
|
|
|
g_FVGPool[i].mssTime = g_TriggerTrend.lastMssTime;
|
|
|
|
|
|
g_FVGPool[i].mssEntry = g_TriggerTrend.lastMssLevel;
|
|
|
|
|
|
g_FVGPool[i].mssSL = g_TriggerTrend.mssSLSwing;
|
|
|
|
|
|
|
|
|
|
|
|
if (InpDebugLog)
|
|
|
|
|
|
PrintFormat("[FVG #%d] TRIGGERED | MSS %s entry=%.5f SL=%.5f @ %s",
|
|
|
|
|
|
g_FVGPool[i].id,
|
|
|
|
|
|
(g_TriggerTrend.lastMssBreak == DIR_UP) ? "▲" : "▼",
|
|
|
|
|
|
g_FVGPool[i].mssEntry, g_FVGPool[i].mssSL,
|
|
|
|
|
|
TimeToString(g_FVGPool[i].mssTime, TIME_MINUTES));
|
|
|
|
|
|
}
|
2026-03-10 22:21:59 +07:00
|
|
|
|
// TOUCHED chỉ kết thúc khi BROKE (usedCase 1) hoặc MSS (usedCase 2); không expire theo thời gian.
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
/** Returns index of best active FVG (prefer TOUCHED, then newest by created time). */
|
2026-03-10 13:47:42 +07:00
|
|
|
|
inline int GetBestActiveFVGIdx()
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
int bestIndex = -1;
|
|
|
|
|
|
datetime bestCreatedTime = 0;
|
|
|
|
|
|
bool foundTouched = false;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
for (int i = 0; i < g_FVGCount; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (g_FVGPool[i].status == FVG_USED) continue;
|
|
|
|
|
|
if (g_FVGPool[i].status == FVG_TOUCHED)
|
|
|
|
|
|
{
|
2026-03-10 20:39:33 +07:00
|
|
|
|
if (!foundTouched || g_FVGPool[i].createdTime > bestCreatedTime)
|
|
|
|
|
|
{ foundTouched = true; bestIndex = i; bestCreatedTime = g_FVGPool[i].createdTime; }
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
2026-03-10 20:39:33 +07:00
|
|
|
|
else if (!foundTouched && g_FVGPool[i].createdTime > bestCreatedTime)
|
|
|
|
|
|
{ bestIndex = i; bestCreatedTime = g_FVGPool[i].createdTime; }
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
2026-03-10 20:39:33 +07:00
|
|
|
|
return bestIndex;
|
2026-03-10 13:47:42 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:39:33 +07:00
|
|
|
|
#endif
|