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

1926 lines
79 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//+------------------------------------------------------------------+
//| ICT EA FVG Edition (MQL5) v3.0 |
//| Architecture : BiasTF(D1) + MiddleTF(H1) + TriggerTF(M5) |
//| State Machine: IDLE → WAIT_TOUCH → WAIT_TRIGGER → IN_TRADE |
//| |
//| FVG lifecycle: |
//| PENDING → hình thành, chưa touch |
//| TOUCHED → giá chạm vào gap (bid inside) |
//| USED → case0=expired / case1=broken / case2=triggered |
//| |
//| MSS (Market Structure Shift) v3 definition: |
//| MSS hợp lệ = Liquidity Sweep + Structure Break |
//| |
//| Bullish MSS (MiddleTF UP): |
//| 1. Sweep: wick < L0, close > L0 (quét sell-side liq) |
//| 2. Break: close > H0 (phá vỡ swing high → đảo chiều lên) |
//| |
//| Bearish MSS (MiddleTF DOWN): |
//| 1. Sweep: wick > H0, close < H0 (quét buy-side liq) |
//| 2. Break: close < L0 (phá vỡ swing low → đảo chiều xuống) |
//| |
//| Không có sweep → MSS không hợp lệ, bỏ qua |
//| |
//| TriggerTF MSS chỉ nhận thuận chiều MiddleTF trend |
//| |
//| Drawing: objects KHÔNG BAO GIỜ bị xóa, chỉ update |
//+------------------------------------------------------------------+
#property copyright "Bell's ICT EA"
#property version "3.00"
#define MAX_FVG_POOL 30
//====================================================
// INPUTS
//====================================================
input ENUM_TIMEFRAMES InpBiasTF = PERIOD_D1; // Bias TF (HTF)
input ENUM_TIMEFRAMES InpMiddleTF = PERIOD_H1; // FVG + trend TF (MTF)
input ENUM_TIMEFRAMES InpTriggerTF = PERIOD_M5; // Entry confirmation (LTF)
input double InpRiskPercent = 1.0; // Risk % per trade
input double InpRiskReward = 2.0; // TP/SL ratio
input double InpMaxDailyLossPct = 3.0; // Max daily loss %
input int InpLondonStartHour = 8; // London open (UTC)
input int InpLondonEndHour = 17; // London close (UTC)
input int InpNYStartHour = 13; // NY open (UTC)
input int InpNYEndHour = 22; // NY close (UTC)
input int InpSwingRange = 2; // Bars each side for swing confirm
input int InpSwingLookback = 50; // MiddleTF swing scan bars
input int InpTriggerSwingLookback = 30; // TriggerTF swing scan bars
input int InpFVGMaxAliveMin = 4320; // Max PENDING lifetime (min) = 72h
input int InpFVGScanBars = 50; // MiddleTF bars to scan for FVGs
input double InpFVGMinBodyPct = 60.0; // Mid-candle min body %
input int InpTriggerMaxBars = 30; // Trigger timeout (TriggerTF bars)
input long InpMagicNumber = 20250308; // EA magic number
input int InpSlippage = 5; // Max slippage (points)
input bool InpDebugLog = true; // Journal logging
input bool InpDebugDraw = true; // Chart drawing
//====================================================
// ENUMS
//====================================================
enum EAState
{
EA_IDLE, // Looking for best FVG
EA_WAIT_TOUCH, // Active FVG = PENDING, waiting price entry
EA_WAIT_TRIGGER, // Active FVG = TOUCHED, waiting TriggerTF MSS
EA_IN_TRADE // Position open
};
enum HTFBias { BIAS_NONE=0, BIAS_UP=1, BIAS_DOWN=-1, BIAS_SIDEWAY=2 };
enum MarketDir { DIR_NONE=0, DIR_UP=1, DIR_DOWN=-1 };
enum BlockReason
{
BLOCK_NONE, BLOCK_SESSION, BLOCK_DAILY_LOSS,
BLOCK_BIAS_MISMATCH, BLOCK_NO_BIAS
};
enum FVGStatus
{
FVG_PENDING, // Formed, not yet touched
FVG_TOUCHED, // Price entered gap zone
FVG_USED // Consumed: 0=expired 1=broken 2=triggered
};
//====================================================
// STRUCTS
//====================================================
struct BiasContext
{
HTFBias bias;
double rangeHigh, rangeLow;
datetime lastBarTime;
};
//----------------------------------------------------------------------
// TFTrendContext Swing structure + MSS + associated sweep
//
// v3: MSS hợp lệ yêu cầu có Liquidity Sweep trước đó.
// Khi MSS được ghi nhận, sweep info cũng được lưu kèm
// để vẽ cả 2 thành 1 pattern hoàn chỉnh.
//----------------------------------------------------------------------
struct TFTrendContext
{
// ── Swing structure ────────────────────────────────────────────
MarketDir trend; // DIR_UP / DIR_DOWN / DIR_NONE
double h0, h1, l0, l1; // 2 swing highs + 2 swing lows gần nhất
int idxH0, idxH1, idxL0, idxL1; // Bar indices tương ứng
double keyLevel; // L0 (uptrend) hoặc H0 (downtrend)
datetime lastBarTime; // Guard: chỉ recalc trên bar mới
// ── MSS tracking ───────────────────────────────────────────────
datetime lastMssTime; // Open time cây nến break structure
double lastMssLevel; // Key level bị phá (giá break qua)
MarketDir lastMssBreak; // DIR_UP = bull MSS, DIR_DOWN = bear MSS
// ── v3: Sweep đi kèm MSS (chỉ có giá trị khi lastMssTime > 0) ─
datetime mssSweepTime; // Thời gian cây nến sweep
double mssSweepLevel; // Swing level bị quét (L0 hoặc H0)
double mssSweepWick; // Wick extreme (low cho bull, high cho bear)
};
struct FVGRecord
{
int id;
FVGStatus status;
int usedCase; // 0=expired 1=broken 2=triggered
MarketDir direction;
double high, low, mid;
datetime createdTime;
datetime touchTime;
datetime usedTime;
MarketDir triggerTrendAtTouch; // M5 trend tại thời điểm touch
};
struct TriggerFVGRecord
{
bool valid;
MarketDir direction;
double high, low, mid;
datetime createdTime;
};
struct TriggerContext
{
bool valid;
MarketDir direction;
double swingHigh, swingLow;
int idxHigh, idxLow;
double breakLevel;
};
struct OrderPlan
{
bool valid;
int direction; // +1 buy, -1 sell
double entry, stopLoss, takeProfit, lot;
int parentFVGId;
};
struct DailyRiskContext
{
double startBalance, currentBalance;
datetime dayStartTime;
bool limitHit;
};
//----------------------------------------------------------------------
// v3: Liquidity Sweep event (internal tracking, KHÔNG vẽ riêng)
//
// Sweep = wick quét qua swing rồi close lại phía trong:
// Bullish: low < L0 && close > L0 → quét sell-side liquidity
// Bearish: high > H0 && close < H0 → quét buy-side liquidity
//
// Sweep chỉ dùng làm prerequisite cho valid MSS.
// Khi MSS xảy ra, sweep info được copy vào TFTrendContext.
//----------------------------------------------------------------------
struct LiqSweepEvent
{
bool valid;
MarketDir direction; // DIR_UP = bull sweep, DIR_DOWN = bear
double sweptLevel; // Swing level bị quét
double extremeWick; // Wick thực tế (low cho bull, high cho bear)
datetime time; // Open time cây nến sweep
};
//====================================================
// GLOBALS
//====================================================
EAState g_State = EA_IDLE;
BlockReason g_BlockReason = BLOCK_NONE;
BiasContext g_Bias;
TFTrendContext g_MiddleTrend;
TFTrendContext g_TriggerTrend;
TriggerContext g_Trigger;
DailyRiskContext g_DailyRisk;
FVGRecord g_FVGPool[MAX_FVG_POOL];
int g_FVGCount = 0;
int g_NextFVGId = 0;
int g_ActiveFVGIdx = -1;
int g_TradeBarIndex = -1;
TriggerFVGRecord g_TrigFVG;
OrderPlan g_OrderPlan;
ulong g_PendingTicket = 0;
// v3: Sweep tracking (internal, prerequisite cho MSS)
LiqSweepEvent g_LastLiqSweep;
// Chart object name prefixes
const string SW_PREFIX = "SW_"; // MiddleTF swing arrows/labels
const string TS_PREFIX = "TS_"; // TriggerTF swing arrows/labels
const string MSS_PREFIX = "MSS_"; // MSS markers (cả sweep + break)
const string FVGP_PREFIX = "FVGP_"; // MiddleTF FVG rectangles
const string TFVG_PREFIX = "TFVG_"; // TriggerTF FVG rectangles
const string ORD_PREFIX = "ORD_"; // v3: Order visualization
const string DBG_PREFIX = "DBG_"; // Debug panel labels
//+------------------------------------------------------------------+
//| MQL5 HELPER: iBarShift replacement |
//+------------------------------------------------------------------+
int MyBarShift(string symbol, ENUM_TIMEFRAMES tf, datetime time, bool exact = false)
{
datetime arr[];
int maxCopy = MathMin(Bars(symbol, tf), 5000); // Giới hạn 5000 bars tránh quá tải
int copied = CopyTime(symbol, tf, 0, maxCopy, arr);
if (copied <= 0) return -1;
for (int i = copied - 1; i >= 0; i--) // Duyệt từ mới → cũ
{
if (arr[i] <= time)
return copied - 1 - i; // Chuyển array index → bar shift
}
return exact ? -1 : copied - 1;
}
//+------------------------------------------------------------------+
//| SECTION 1 SWING HELPERS |
//+------------------------------------------------------------------+
bool IsSwingHighAt(ENUM_TIMEFRAMES tf, int i)
{
double p = iHigh(_Symbol, tf, i);
for (int k = 1; k <= InpSwingRange; k++)
if (iHigh(_Symbol, tf, i-k) >= p || iHigh(_Symbol, tf, i+k) >= p) return false;
return true;
}
bool IsSwingLowAt(ENUM_TIMEFRAMES tf, int i)
{
double p = iLow(_Symbol, tf, i);
for (int k = 1; k <= InpSwingRange; k++)
if (iLow(_Symbol, tf, i-k) <= p || iLow(_Symbol, tf, i+k) <= p) return false;
return true;
}
bool ScanSwingStructure(
ENUM_TIMEFRAMES tf, int lookback,
double &h0, double &h1, int &idxH0, int &idxH1,
double &l0, double &l1, int &idxL0, int &idxL1)
{
int maxBar = MathMin(lookback, Bars(_Symbol, tf) - InpSwingRange - 2);
double highs[2]; int hiIdx[2]; int hc = 0;
double lows [2]; int loIdx[2]; int lc = 0;
for (int i = InpSwingRange + 1; i <= maxBar; i++)
{
if (hc < 2 && IsSwingHighAt(tf, i)) { highs[hc] = iHigh(_Symbol, tf, i); hiIdx[hc] = i; hc++; }
if (lc < 2 && IsSwingLowAt (tf, i)) { lows [lc] = iLow (_Symbol, tf, i); loIdx[lc] = i; lc++; }
if (hc == 2 && lc == 2) break;
}
if (hc < 2 || lc < 2) return false;
h0 = highs[0]; idxH0 = hiIdx[0];
h1 = highs[1]; idxH1 = hiIdx[1];
l0 = lows [0]; idxL0 = loIdx[0];
l1 = lows [1]; idxL1 = loIdx[1];
return true;
}
void ResolveTrendFromSwings(
ENUM_TIMEFRAMES tf,
double h0, double h1, double l0, double l1,
MarketDir &trend, double &keyLevel)
{
double c1 = iClose(_Symbol, tf, 1);
if (h0 > h1 && l0 > l1 && c1 > l0) { trend = DIR_UP; keyLevel = l0; }
else if (h0 < h1 && l0 < l1 && c1 < h0) { trend = DIR_DOWN; keyLevel = h0; }
else { trend = DIR_NONE; keyLevel = 0; }
}
//+------------------------------------------------------------------+
//| SECTION 2 CONTEXT UPDATERS |
//+------------------------------------------------------------------+
HTFBias ResolveBias(double b1H, double b1L, double b1C, double b2H, double b2L)
{
if (b1C > b2H) return BIAS_UP;
if (b1C < b2L) return BIAS_DOWN;
if (b1H > b2H && b1C < b2H) return BIAS_DOWN;
if (b1L < b2L && b1C > b2L) return BIAS_UP;
return BIAS_SIDEWAY;
}
void UpdateBiasContext()
{
datetime t0 = iTime(_Symbol, InpBiasTF, 0);
if (t0 == g_Bias.lastBarTime) return;
g_Bias.lastBarTime = t0;
if (Bars(_Symbol, InpBiasTF) < 4) { g_Bias.bias = BIAS_NONE; return; }
double b1H = iHigh (_Symbol, InpBiasTF, 1);
double b1L = iLow (_Symbol, InpBiasTF, 1);
double b1C = iClose(_Symbol, InpBiasTF, 1);
double b2H = iHigh (_Symbol, InpBiasTF, 2);
double b2L = iLow (_Symbol, InpBiasTF, 2);
HTFBias prev = g_Bias.bias;
g_Bias.bias = ResolveBias(b1H, b1L, b1C, b2H, b2L);
g_Bias.rangeHigh = (g_Bias.bias == BIAS_SIDEWAY) ? b2H : 0;
g_Bias.rangeLow = (g_Bias.bias == BIAS_SIDEWAY) ? b2L : 0;
if (InpDebugLog && g_Bias.bias != prev)
PrintFormat("[BIAS] %s → %s | b1[H=%.5f L=%.5f C=%.5f] b2[H=%.5f L=%.5f]",
EnumToString(prev), EnumToString(g_Bias.bias), b1H, b1L, b1C, b2H, b2L);
}
//----------------------------------------------------------------------
// UpdateTFTrendContext
//
// Gọi mỗi bar mới trên tf. Thực hiện 3 việc TRƯỚC khi re-scan swing:
//
// 1a. TriggerTF: Detect Liquidity Sweep (wick quét swing)
// 1b. MSS check: close phá keyLevel
// - MiddleTF: ghi nhận mọi MSS
// - TriggerTF v3: CHỈ ghi nhận nếu:
// (a) thuận chiều MiddleTF trend
// (b) có Sweep trước đó (g_LastLiqSweep hợp lệ)
// 2. Re-scan swing structure → update trend/keyLevel
//
// Tất cả detection dùng OLD swing levels (trước re-scan).
//----------------------------------------------------------------------
void UpdateTFTrendContext(ENUM_TIMEFRAMES tf, int lookback, TFTrendContext &ctx)
{
datetime t0 = iTime(_Symbol, tf, 0);
if (t0 == ctx.lastBarTime) return; // Chỉ chạy 1 lần/bar
ctx.lastBarTime = t0;
double bar1C = iClose(_Symbol, tf, 1); // Bar vừa đóng: close
double bar1H = iHigh (_Symbol, tf, 1); // high
double bar1L = iLow (_Symbol, tf, 1); // low
double bar1O = iOpen (_Symbol, tf, 1); // open
datetime bar1T = iTime (_Symbol, tf, 1); // time
// ══════════════════════════════════════════════════════════════════
// 1a. TriggerTF: Liquidity Sweep detection (TRƯỚC MSS check)
//
// Bullish sweep (H1 UP): wick < L0 && close > L0
// → Giá quét dưới swing low, hút stop-loss bên sell
// → Rồi đóng lại phía trên → reversal signal
//
// Bearish sweep (H1 DOWN): wick > H0 && close < H0
// → Giá quét trên swing high, hút stop-loss bên buy
// → Rồi đóng lại phía dưới → reversal signal
//
// Sweep KHÔNG được vẽ riêng. Chỉ dùng làm prerequisite cho MSS.
// ══════════════════════════════════════════════════════════════════
if (tf == InpTriggerTF && g_MiddleTrend.trend != DIR_NONE)
{
if (g_MiddleTrend.trend == DIR_UP && ctx.l0 > 0) // Bullish setup
{
if (bar1L < ctx.l0 // Wick quét dưới L0
&& bar1C > ctx.l0 // Close đóng lại trên L0
&& bar1T != g_LastLiqSweep.time) // Chưa ghi nhận cây này
{
g_LastLiqSweep.valid = true;
g_LastLiqSweep.direction = DIR_UP; // Bull sweep = quét sell-side
g_LastLiqSweep.sweptLevel = ctx.l0; // Mức swing bị quét
g_LastLiqSweep.extremeWick = bar1L; // Wick thấp nhất
g_LastLiqSweep.time = bar1T;
if (InpDebugLog)
PrintFormat("[SWEEP ▲] below L0=%.5f | wick=%.5f close=%.5f | %s",
ctx.l0, bar1L, bar1C, TimeToString(bar1T));
}
}
else if (g_MiddleTrend.trend == DIR_DOWN && ctx.h0 > 0) // Bearish setup
{
if (bar1H > ctx.h0 // Wick quét trên H0
&& bar1C < ctx.h0 // Close đóng lại dưới H0
&& bar1T != g_LastLiqSweep.time)
{
g_LastLiqSweep.valid = true;
g_LastLiqSweep.direction = DIR_DOWN; // Bear sweep = quét buy-side
g_LastLiqSweep.sweptLevel = ctx.h0;
g_LastLiqSweep.extremeWick = bar1H; // Wick cao nhất
g_LastLiqSweep.time = bar1T;
if (InpDebugLog)
PrintFormat("[SWEEP ▼] above H0=%.5f | wick=%.5f close=%.5f | %s",
ctx.h0, bar1H, bar1C, TimeToString(bar1T));
}
}
}
// ══════════════════════════════════════════════════════════════════
// 1b. MSS check (dùng OLD keyLevel, trước re-scan)
//
// Uptrend: keyLevel = L0 → MSS khi close < L0 (bearish break)
// Downtrend: keyLevel = H0 → MSS khi close > H0 (bullish break)
//
// v3 TriggerTF: MSS chỉ hợp lệ khi:
// (a) breakDir thuận chiều MiddleTF (VD: H1 UP → chỉ nhận bull MSS)
// (b) Có Liquidity Sweep trước đó cùng hướng
// (bull MSS cần bull sweep, bear MSS cần bear sweep)
// (c) Sweep xảy ra TRƯỚC cây MSS (sweep.time < bar1T)
//
// MiddleTF: ghi nhận mọi MSS (không cần sweep filter)
// ══════════════════════════════════════════════════════════════════
if (ctx.trend != DIR_NONE && ctx.keyLevel > 0)
{
bool mssHit =
(ctx.trend == DIR_UP && bar1C < ctx.keyLevel) || // Bull→Bear: close < L0
(ctx.trend == DIR_DOWN && bar1C > ctx.keyLevel); // Bear→Bull: close > H0
if (mssHit && bar1T != ctx.lastMssTime)
{
MarketDir breakDir = (ctx.trend == DIR_UP) ? DIR_DOWN : DIR_UP;
// ── Determine if this MSS should be recorded ───────────────
bool recordMss = true;
string rejectReason = "";
if (tf == InpTriggerTF)
{
// Filter (a): thuận chiều MiddleTF
if (g_MiddleTrend.trend != DIR_NONE && breakDir != (MarketDir)g_MiddleTrend.trend)
{
recordMss = false;
rejectReason = "ngược chiều H1";
}
// Filter (b): phải có sweep trước đó, cùng hướng
if (recordMss)
{
bool hasSweep =
g_LastLiqSweep.valid && // Sweep tồn tại
g_LastLiqSweep.direction == breakDir && // Cùng hướng với MSS
g_LastLiqSweep.time < bar1T; // Sweep xảy ra TRƯỚC MSS
if (!hasSweep)
{
recordMss = false;
rejectReason = "chưa có sweep";
}
}
}
// ── Record hoặc skip MSS ──────────────────────────────────
if (recordMss)
{
ctx.lastMssTime = bar1T; // Thời gian cây nến MSS
ctx.lastMssLevel = ctx.keyLevel; // Key level bị phá
ctx.lastMssBreak = breakDir; // Hướng break
// v3: Lưu sweep info kèm MSS (cho TriggerTF)
if (tf == InpTriggerTF && g_LastLiqSweep.valid)
{
ctx.mssSweepTime = g_LastLiqSweep.time; // Copy sweep → MSS context
ctx.mssSweepLevel = g_LastLiqSweep.sweptLevel;
ctx.mssSweepWick = g_LastLiqSweep.extremeWick;
}
if (InpDebugLog)
PrintFormat("[%s MSS ✓] %s | KL=%.5f | close=%.5f | sweep@%s | bar=%s",
EnumToString(tf),
(breakDir == DIR_UP) ? "Bear→Bull ▲" : "Bull→Bear ▼",
ctx.lastMssLevel, bar1C,
(tf == InpTriggerTF) ? TimeToString(g_LastLiqSweep.time, TIME_MINUTES) : "n/a",
TimeToString(bar1T));
}
else if (InpDebugLog)
{
PrintFormat("[%s MSS ✗] %s bị skip: %s | KL=%.5f | close=%.5f",
EnumToString(tf),
(breakDir == DIR_UP) ? "▲" : "▼",
rejectReason, ctx.keyLevel, bar1C);
}
}
}
// ══════════════════════════════════════════════════════════════════
// 2. Re-scan swing structure
// ══════════════════════════════════════════════════════════════════
double h0, h1, l0, l1;
int idxH0, idxH1, idxL0, idxL1;
if (!ScanSwingStructure(tf, lookback, h0, h1, idxH0, idxH1, l0, l1, idxL0, idxL1))
{ ctx.trend = DIR_NONE; return; }
ctx.h0 = h0; ctx.idxH0 = idxH0;
ctx.h1 = h1; ctx.idxH1 = idxH1;
ctx.l0 = l0; ctx.idxL0 = idxL0;
ctx.l1 = l1; ctx.idxL1 = idxL1;
MarketDir prev = ctx.trend;
ResolveTrendFromSwings(tf, h0, h1, l0, l1, ctx.trend, ctx.keyLevel);
if (InpDebugLog && ctx.trend != prev)
PrintFormat("[%s TREND] %s → %s | H0=%.5f H1=%.5f L0=%.5f L1=%.5f | KL=%.5f",
EnumToString(tf), EnumToString(prev), EnumToString(ctx.trend),
h0, h1, l0, l1, ctx.keyLevel);
}
void UpdateDailyRiskContext()
{
datetime today = iTime(_Symbol, PERIOD_D1, 0);
if (today != g_DailyRisk.dayStartTime)
{
g_DailyRisk.dayStartTime = today;
g_DailyRisk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE);
g_DailyRisk.limitHit = false;
if (InpDebugLog) PrintFormat("[DAILY RISK] New day | start=%.2f", g_DailyRisk.startBalance);
}
if (g_DailyRisk.limitHit) return;
g_DailyRisk.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double lostPct = (g_DailyRisk.startBalance - g_DailyRisk.currentBalance)
/ g_DailyRisk.startBalance * 100.0;
if (lostPct >= InpMaxDailyLossPct)
{
g_DailyRisk.limitHit = true;
PrintFormat("[DAILY RISK] ⛔ Limit hit | lost=%.2f%% | bal=%.2f",
lostPct, g_DailyRisk.currentBalance);
}
}
void UpdateAllContexts()
{
UpdateDailyRiskContext();
UpdateBiasContext();
UpdateTFTrendContext(InpMiddleTF, InpSwingLookback, g_MiddleTrend);
UpdateTFTrendContext(InpTriggerTF, InpTriggerSwingLookback, g_TriggerTrend);
}
//+------------------------------------------------------------------+
//| SECTION 3 GUARDS |
//+------------------------------------------------------------------+
bool IsSessionAllowed() { return true; /* TODO: London/NY filter */ }
bool IsDailyLossOK() { return !g_DailyRisk.limitHit; }
bool IsBiasValid() { return g_Bias.bias == BIAS_UP || g_Bias.bias == BIAS_DOWN; }
bool IsMiddleTrendAligned()
{
if (g_MiddleTrend.trend == DIR_NONE) return false;
return (g_Bias.bias == BIAS_UP && g_MiddleTrend.trend == DIR_UP) ||
(g_Bias.bias == BIAS_DOWN && g_MiddleTrend.trend == DIR_DOWN);
}
bool EvaluateGuards()
{
g_BlockReason = BLOCK_NONE;
if (!IsSessionAllowed()) { g_BlockReason = BLOCK_SESSION; return false; }
if (!IsDailyLossOK()) { g_BlockReason = BLOCK_DAILY_LOSS; return false; }
if (!IsBiasValid()) { g_BlockReason = BLOCK_NO_BIAS; return false; }
if (!IsMiddleTrendAligned()) { g_BlockReason = BLOCK_BIAS_MISMATCH; return false; }
return true;
}
//+------------------------------------------------------------------+
//| SECTION 4 STATE MACHINE HELPERS |
//+------------------------------------------------------------------+
void TransitionTo(EAState next)
{
if (InpDebugLog)
PrintFormat("[STATE] %s → %s", EnumToString(g_State), EnumToString(next));
g_State = next;
}
void ResetToIdle(string reason = "")
{
if (InpDebugLog && reason != "")
PrintFormat("[RESET→IDLE] %s", reason);
g_ActiveFVGIdx = -1;
g_TradeBarIndex = -1;
g_PendingTicket = 0;
ZeroMemory(g_Trigger);
ZeroMemory(g_TrigFVG);
ZeroMemory(g_OrderPlan);
ZeroMemory(g_LastLiqSweep); // v3: reset sweep khi cycle mới
TransitionTo(EA_IDLE);
}
//+------------------------------------------------------------------+
//| SECTION 5 FVG HELPERS |
//+------------------------------------------------------------------+
bool IsCandleStrong(ENUM_TIMEFRAMES tf, int i)
{
double h = iHigh(_Symbol, tf, i), l = iLow(_Symbol, tf, i);
double o = iOpen(_Symbol, tf, i), c = iClose(_Symbol, tf, i);
double range = h - l;
if (range < _Point) return false;
return (MathAbs(c - o) / range * 100.0) >= InpFVGMinBodyPct;
}
bool IsFVGInPool(datetime created)
{
for (int j = 0; j < g_FVGCount; j++)
if (g_FVGPool[j].createdTime == created) return true;
return false;
}
//+------------------------------------------------------------------+
//| SECTION 6 FVG POOL: SCAN & REGISTER |
//+------------------------------------------------------------------+
void ScanAndRegisterFVGs()
{
static datetime s_lastScan = 0;
datetime t0 = iTime(_Symbol, InpMiddleTF, 0);
if (t0 == s_lastScan) return;
s_lastScan = t0;
MarketDir dir = g_MiddleTrend.trend;
if (dir == DIR_NONE) return;
int maxBar = MathMin(InpFVGScanBars, Bars(_Symbol, InpMiddleTF) - 2);
for (int i = 2; i <= maxBar; i++)
{
double leftH = iHigh (_Symbol, InpMiddleTF, i + 1);
double leftL = iLow (_Symbol, InpMiddleTF, i + 1);
double rightH = iHigh (_Symbol, InpMiddleTF, i - 1);
double rightL = iLow (_Symbol, InpMiddleTF, i - 1);
double midO = iOpen (_Symbol, InpMiddleTF, i);
double midC = iClose(_Symbol, InpMiddleTF, i);
double gH = 0, gL = 0;
if (dir == DIR_UP)
{
if (leftH >= rightL || midC <= midO || !IsCandleStrong(InpMiddleTF, i)) continue;
gL = leftH; gH = rightL;
}
else
{
if (leftL <= rightH || midC >= midO || !IsCandleStrong(InpMiddleTF, i)) continue;
gH = leftL; gL = rightH;
}
datetime created = iTime(_Symbol, InpMiddleTF, i - 1);
if (IsFVGInPool(created)) continue;
// ── Evict oldest USED if pool full ──────────────────────────────
if (g_FVGCount >= MAX_FVG_POOL)
{
int evict = -1; datetime oldest = TimeCurrent();
for (int j = 0; j < g_FVGCount; j++)
if (g_FVGPool[j].status == FVG_USED && g_FVGPool[j].createdTime < oldest)
{ oldest = g_FVGPool[j].createdTime; evict = j; }
if (evict < 0) { if (InpDebugLog) Print("[FVG POOL] Full no USED slot"); break; }
for (int j = evict; j < g_FVGCount - 1; j++) g_FVGPool[j] = g_FVGPool[j + 1];
g_FVGCount--;
if (g_ActiveFVGIdx > evict) g_ActiveFVGIdx--;
else if (g_ActiveFVGIdx == evict) g_ActiveFVGIdx = -1;
}
// ── Build record ─────────────────────────────────────────────────
FVGRecord rec;
ZeroMemory(rec);
rec.id = g_NextFVGId++;
rec.direction = dir;
rec.high = gH; rec.low = gL; rec.mid = (gH + gL) / 2.0;
rec.createdTime = created;
int rightBar = i - 1;
// P1: case1 close punched through gap
bool c1Hit = false; datetime c1T = 0;
for (int j = rightBar - 1; j >= 1; j--)
{
double cl = iClose(_Symbol, InpMiddleTF, j);
if ((rec.direction == DIR_UP && cl < rec.low) ||
(rec.direction == DIR_DOWN && cl > rec.high))
{ c1Hit = true; c1T = iTime(_Symbol, InpMiddleTF, j); break; }
}
if (c1Hit) { rec.status = FVG_USED; rec.usedCase = 1; rec.usedTime = c1T; }
else
{
// P2: touch wick entered gap
bool tdHit = false; datetime tdT = 0;
for (int j = rightBar - 1; j >= 1; j--)
{
bool inGap = (rec.direction == DIR_UP && iLow (_Symbol, InpMiddleTF, j) <= rec.high) ||
(rec.direction == DIR_DOWN && iHigh(_Symbol, InpMiddleTF, j) >= rec.low);
if (inGap) { tdHit = true; tdT = iTime(_Symbol, InpMiddleTF, j); break; }
}
if (tdHit) { rec.status = FVG_TOUCHED; rec.touchTime = tdT; rec.triggerTrendAtTouch = g_TriggerTrend.trend; }
else
{
rec.status = FVG_PENDING;
if ((int)(TimeCurrent() - rec.createdTime) > InpFVGMaxAliveMin * 60)
{ rec.status = FVG_USED; rec.usedCase = 0; rec.usedTime = TimeCurrent(); }
}
}
g_FVGPool[g_FVGCount] = rec;
g_FVGCount++;
if (InpDebugLog)
PrintFormat("[FVG +] #%d %s [%.5f%.5f] %s | %s",
rec.id, EnumToString(rec.direction), rec.low, rec.high,
EnumToString(rec.status), TimeToString(rec.createdTime));
}
}
//+------------------------------------------------------------------+
//| SECTION 7 FVG POOL: UPDATE STATUSES (every tick) |
//+------------------------------------------------------------------+
void UpdateFVGStatuses()
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double midC1 = iClose(_Symbol, InpMiddleTF, 1);
datetime midT1 = iTime (_Symbol, InpMiddleTF, 1);
for (int i = 0; i < g_FVGCount; i++)
{
if (g_FVGPool[i].status == FVG_USED) continue;
// ── 1. Case1: H1 close phá xuyên qua FVG → broken ──────────────
bool c1 = (g_FVGPool[i].direction == DIR_UP && midC1 < g_FVGPool[i].low) ||
(g_FVGPool[i].direction == DIR_DOWN && midC1 > g_FVGPool[i].high);
if (c1)
{
g_FVGPool[i].status = FVG_USED;
g_FVGPool[i].usedCase = 1;
g_FVGPool[i].usedTime = midT1;
if (InpDebugLog)
PrintFormat("[FVG #%d] USED(case1=broken) | close=%.5f vs [%.5f%.5f]",
g_FVGPool[i].id, midC1, g_FVGPool[i].low, g_FVGPool[i].high);
continue;
}
if (g_FVGPool[i].status == FVG_PENDING)
{
// ── 2. Expire: quá thời hạn ─────────────────────────────────────
int age = (int)(TimeCurrent() - g_FVGPool[i].createdTime);
if (age > InpFVGMaxAliveMin * 60)
{
g_FVGPool[i].status = FVG_USED;
g_FVGPool[i].usedCase = 0;
g_FVGPool[i].usedTime = TimeCurrent();
if (InpDebugLog)
PrintFormat("[FVG #%d] USED(expired) | age=%dmin", g_FVGPool[i].id, age / 60);
continue;
}
// ── 3. Touch: bid vào vùng gap ───────────────────────────────────
bool touched = (g_FVGPool[i].direction == DIR_UP && bid <= g_FVGPool[i].high) ||
(g_FVGPool[i].direction == DIR_DOWN && bid >= g_FVGPool[i].low);
if (touched)
{
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] TrigTrend=%s",
g_FVGPool[i].id, bid, g_FVGPool[i].low, g_FVGPool[i].high,
EnumToString(g_FVGPool[i].triggerTrendAtTouch));
}
}
else if (g_FVGPool[i].status == FVG_TOUCHED)
{
// ── 4. Case2: TriggerTF MSS confirmed → entry signal ────────────
bool wasOpposing, nowAligned;
if (g_FVGPool[i].direction == DIR_UP)
{ wasOpposing = (g_FVGPool[i].triggerTrendAtTouch != DIR_UP); nowAligned = (g_TriggerTrend.trend == DIR_UP); }
else
{ wasOpposing = (g_FVGPool[i].triggerTrendAtTouch != DIR_DOWN); nowAligned = (g_TriggerTrend.trend == DIR_DOWN); }
if (wasOpposing && nowAligned)
{
g_FVGPool[i].status = FVG_USED;
g_FVGPool[i].usedCase = 2;
g_FVGPool[i].usedTime = TimeCurrent();
if (InpDebugLog)
PrintFormat("[FVG #%d] USED(case2=triggered) | TrigTrend %s→%s [%.5f%.5f] %s",
g_FVGPool[i].id,
EnumToString(g_FVGPool[i].triggerTrendAtTouch),
EnumToString(g_TriggerTrend.trend),
g_FVGPool[i].low, g_FVGPool[i].high,
EnumToString(g_FVGPool[i].direction));
continue;
}
// ── 5. Trigger timeout ────────────────────────────────────────────
int bars = (int)((TimeCurrent() - g_FVGPool[i].touchTime) / PeriodSeconds(InpTriggerTF));
if (bars > InpTriggerMaxBars
&& g_ActiveFVGIdx >= 0
&& g_FVGPool[g_ActiveFVGIdx].id == g_FVGPool[i].id)
{
if (InpDebugLog)
PrintFormat("[FVG #%d] Trigger timeout (%d bars) release active", g_FVGPool[i].id, bars);
g_ActiveFVGIdx = -1;
}
}
}
}
//+------------------------------------------------------------------+
//| SECTION 7B TRIGGER TF FVG SCANNER |
//+------------------------------------------------------------------+
bool ScanTriggerTFFVG(datetime touchTime, datetime mssTime, MarketDir dir)
{
ZeroMemory(g_TrigFVG);
int barTouch = MyBarShift(_Symbol, InpTriggerTF, touchTime);
int barMss = MyBarShift(_Symbol, InpTriggerTF, mssTime);
if (barTouch < 0 || barMss < 0) return false;
if (barTouch <= barMss + 2) return false; // Cần ít nhất 3 bars giữa 2 mốc
if (InpDebugLog)
PrintFormat("[TFVG SCAN] touchBar=%d mssBar=%d dir=%s | scanning %d bars on %s",
barTouch, barMss, EnumToString(dir), barTouch - barMss, EnumToString(InpTriggerTF));
for (int i = barMss + 2; i < barTouch; i++) // Scan từ gần MSS về phía touch
{
double leftH = iHigh (_Symbol, InpTriggerTF, i + 1);
double leftL = iLow (_Symbol, InpTriggerTF, i + 1);
double rightH = iHigh (_Symbol, InpTriggerTF, i - 1);
double rightL = iLow (_Symbol, InpTriggerTF, i - 1);
double midO = iOpen (_Symbol, InpTriggerTF, i);
double midC = iClose(_Symbol, InpTriggerTF, i);
double gH = 0, gL = 0;
if (dir == DIR_UP)
{
if (leftH >= rightL) continue; // Không có gap
if (midC <= midO) continue; // Mid candle phải bullish
gL = leftH; gH = rightL;
}
else
{
if (leftL <= rightH) continue;
if (midC >= midO) continue; // Mid candle phải bearish
gH = leftL; gL = rightH;
}
if (gH - gL < _Point) continue; // Gap quá nhỏ
g_TrigFVG.valid = true;
g_TrigFVG.direction = dir;
g_TrigFVG.high = gH;
g_TrigFVG.low = gL;
g_TrigFVG.mid = (gH + gL) / 2.0;
g_TrigFVG.createdTime = iTime(_Symbol, InpTriggerTF, i - 1);
if (InpDebugLog)
PrintFormat("[TFVG FOUND] %s [%.5f%.5f] mid=%.5f | bar=%d %s",
EnumToString(dir), gL, gH, g_TrigFVG.mid, i,
TimeToString(g_TrigFVG.createdTime));
return true;
}
if (InpDebugLog)
Print("[TFVG SCAN] No TriggerTF FVG found between touch and MSS");
return false;
}
//+------------------------------------------------------------------+
//| SECTION 7C ORDER PLAN & EXECUTION (MQL5 native) |
//+------------------------------------------------------------------+
double CalcLotFromRisk(double entry, double sl)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskMoney = balance * InpRiskPercent / 100.0;
double slPips = MathAbs(entry - sl) / _Point;
if (slPips < 1) return 0;
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if (tickValue <= 0 || tickSize <= 0) return 0;
double pipValue = tickValue * (_Point / tickSize);
double rawLot = riskMoney / (slPips * pipValue);
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if (lotStep <= 0) lotStep = 0.01;
rawLot = MathFloor(rawLot / lotStep) * lotStep;
rawLot = MathMax(minLot, MathMin(maxLot, rawLot));
return NormalizeDouble(rawLot, 2);
}
bool BuildOrderPlan(int fvgId, MarketDir dir)
{
ZeroMemory(g_OrderPlan);
if (!g_TrigFVG.valid) return false;
double entry = NormalizeDouble(g_TrigFVG.mid, _Digits); // Entry = mid TriggerTF FVG
double sl, tp;
if (dir == DIR_UP)
{
sl = g_TriggerTrend.l0; // SL dưới TriggerTF swing low
if (sl <= 0 || sl >= entry) return false;
sl = NormalizeDouble(sl - 2 * _Point, _Digits); // Buffer 2 points
double riskDist = entry - sl;
tp = NormalizeDouble(entry + InpRiskReward * riskDist, _Digits); // TP = entry + 2R
}
else
{
sl = g_TriggerTrend.h0; // SL trên TriggerTF swing high
if (sl <= 0 || sl <= entry) return false;
sl = NormalizeDouble(sl + 2 * _Point, _Digits);
double riskDist = sl - entry;
tp = NormalizeDouble(entry - InpRiskReward * riskDist, _Digits);
}
double lot = CalcLotFromRisk(entry, sl);
if (lot <= 0) return false;
g_OrderPlan.valid = true;
g_OrderPlan.direction = (dir == DIR_UP) ? 1 : -1;
g_OrderPlan.entry = entry;
g_OrderPlan.stopLoss = sl;
g_OrderPlan.takeProfit = tp;
g_OrderPlan.lot = lot;
g_OrderPlan.parentFVGId = fvgId;
if (InpDebugLog)
PrintFormat("[ORDER PLAN] %s | entry=%.5f SL=%.5f TP=%.5f lot=%.2f | R=%.1f%% RR=%.1f | FVG#%d",
(dir == DIR_UP) ? "BUY LIMIT" : "SELL LIMIT",
entry, sl, tp, lot, InpRiskPercent, InpRiskReward, fvgId);
return true;
}
ulong ExecuteLimitOrder()
{
if (!g_OrderPlan.valid) return 0;
ENUM_ORDER_TYPE cmd = (g_OrderPlan.direction > 0)
? ORDER_TYPE_BUY_LIMIT
: ORDER_TYPE_SELL_LIMIT;
string comment = StringFormat("ICT_FVG#%d", g_OrderPlan.parentFVGId);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if (cmd == ORDER_TYPE_BUY_LIMIT && g_OrderPlan.entry >= ask)
{
if (InpDebugLog)
PrintFormat("[ORDER] BUY LIMIT rejected: entry %.5f >= ask %.5f → skip",
g_OrderPlan.entry, ask);
return 0;
}
if (cmd == ORDER_TYPE_SELL_LIMIT && g_OrderPlan.entry <= bid)
{
if (InpDebugLog)
PrintFormat("[ORDER] SELL LIMIT rejected: entry %.5f <= bid %.5f → skip",
g_OrderPlan.entry, bid);
return 0;
}
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
request.action = TRADE_ACTION_PENDING;
request.symbol = _Symbol;
request.volume = g_OrderPlan.lot;
request.type = cmd;
request.price = g_OrderPlan.entry;
request.sl = g_OrderPlan.stopLoss;
request.tp = g_OrderPlan.takeProfit;
request.deviation = (ulong)InpSlippage;
request.magic = InpMagicNumber;
request.comment = comment;
request.type_filling = ORDER_FILLING_RETURN;
request.type_time = ORDER_TIME_GTC;
if (!OrderSend(request, result))
{
PrintFormat("[ORDER] ❌ OrderSend failed | retcode=%u | %s %.2f @ %.5f SL=%.5f TP=%.5f",
result.retcode,
(cmd == ORDER_TYPE_BUY_LIMIT) ? "BUY_LIMIT" : "SELL_LIMIT",
g_OrderPlan.lot, g_OrderPlan.entry,
g_OrderPlan.stopLoss, g_OrderPlan.takeProfit);
return 0;
}
if (result.retcode != TRADE_RETCODE_DONE && result.retcode != TRADE_RETCODE_PLACED)
{
PrintFormat("[ORDER] ❌ Server rejected | retcode=%u comment=%s",
result.retcode, result.comment);
return 0;
}
PrintFormat("[ORDER] ✅ %s ticket=%llu | %.2f @ %.5f SL=%.5f TP=%.5f | %s",
(cmd == ORDER_TYPE_BUY_LIMIT) ? "BUY_LIMIT" : "SELL_LIMIT",
result.order, g_OrderPlan.lot, g_OrderPlan.entry,
g_OrderPlan.stopLoss, g_OrderPlan.takeProfit, comment);
return result.order;
}
//+------------------------------------------------------------------+
//| SECTION 8 BEST FVG SELECTOR |
//| Priority: TOUCHED newest > PENDING newest |
//+------------------------------------------------------------------+
int GetBestActiveFVGIdx()
{
int bestIdx = -1; datetime bestTime = 0; bool foundTouch = false;
for (int i = 0; i < g_FVGCount; i++)
{
if (g_FVGPool[i].status == FVG_USED) continue;
if (g_FVGPool[i].status == FVG_TOUCHED)
{
if (!foundTouch || g_FVGPool[i].createdTime > bestTime)
{ foundTouch = true; bestIdx = i; bestTime = g_FVGPool[i].createdTime; }
}
else if (!foundTouch && g_FVGPool[i].createdTime > bestTime)
{ bestIdx = i; bestTime = g_FVGPool[i].createdTime; }
}
return bestIdx;
}
//+------------------------------------------------------------------+
//| SECTION 9 STATE HANDLERS |
//+------------------------------------------------------------------+
void OnStateIdle()
{
int idx = GetBestActiveFVGIdx();
if (idx < 0) return;
g_ActiveFVGIdx = idx;
if (InpDebugLog)
PrintFormat("[ACTIVE FVG] #%d %s [%.5f%.5f] %s",
g_FVGPool[idx].id, EnumToString(g_FVGPool[idx].direction),
g_FVGPool[idx].low, g_FVGPool[idx].high, EnumToString(g_FVGPool[idx].status));
TransitionTo(g_FVGPool[idx].status == FVG_TOUCHED ? EA_WAIT_TRIGGER : EA_WAIT_TOUCH);
}
void OnStateWaitTouch()
{
if (g_ActiveFVGIdx < 0) { ResetToIdle("active lost"); return; }
int ai = g_ActiveFVGIdx;
if (g_FVGPool[ai].status == FVG_USED)
{ ResetToIdle(StringFormat("FVG #%d used(case%d) before touch", g_FVGPool[ai].id, g_FVGPool[ai].usedCase)); return; }
if (g_FVGPool[ai].status == FVG_TOUCHED)
{ TransitionTo(EA_WAIT_TRIGGER); return; }
int better = GetBestActiveFVGIdx();
if (better >= 0 && better != ai)
{
bool bTouch = (g_FVGPool[better].status == FVG_TOUCHED);
bool bNewer = (g_FVGPool[better].createdTime > g_FVGPool[ai].createdTime);
if (bTouch || bNewer)
{
if (InpDebugLog)
PrintFormat("[SWITCH] FVG #%d → #%d", g_FVGPool[ai].id, g_FVGPool[better].id);
g_ActiveFVGIdx = better;
if (bTouch) TransitionTo(EA_WAIT_TRIGGER);
}
}
}
void OnStateWaitTrigger()
{
if (g_ActiveFVGIdx < 0) { ResetToIdle("trigger timeout / active lost"); return; }
int ai = g_ActiveFVGIdx;
if (g_FVGPool[ai].status == FVG_USED)
{
if (g_FVGPool[ai].usedCase == 2)
{
if (InpDebugLog)
PrintFormat("[ENTRY SIGNAL] FVG #%d %s [%.5f%.5f]",
g_FVGPool[ai].id, EnumToString(g_FVGPool[ai].direction),
g_FVGPool[ai].low, g_FVGPool[ai].high);
bool hasTFVG = ScanTriggerTFFVG(
g_FVGPool[ai].touchTime,
g_TriggerTrend.lastMssTime,
g_FVGPool[ai].direction);
if (hasTFVG && BuildOrderPlan(g_FVGPool[ai].id, g_FVGPool[ai].direction))
{
ulong ticket = ExecuteLimitOrder();
if (ticket > 0)
{
g_PendingTicket = ticket;
g_TradeBarIndex = Bars(_Symbol, InpTriggerTF);
TransitionTo(EA_IN_TRADE);
}
else
ResetToIdle(StringFormat("FVG #%d order failed", g_FVGPool[ai].id));
}
else
ResetToIdle(StringFormat("FVG #%d no valid TriggerTF FVG or order plan", g_FVGPool[ai].id));
}
else
ResetToIdle(StringFormat("FVG #%d used(case%d) during trigger", g_FVGPool[ai].id, g_FVGPool[ai].usedCase));
return;
}
if (g_FVGPool[ai].status == FVG_PENDING) { TransitionTo(EA_WAIT_TOUCH); return; }
}
void OnStateInTrade()
{
// ── 1. Kiểm tra pending order còn active không ─────────────────
if (g_PendingTicket > 0)
{
bool foundPending = false;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if (ticket == g_PendingTicket)
{
foundPending = true;
break;
}
}
if (!foundPending)
{
// Tìm position tương ứng (limit đã fill)
bool posFound = false;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong posTicket = PositionGetTicket(i);
if (posTicket == 0) continue;
if (PositionGetInteger(POSITION_MAGIC) == InpMagicNumber &&
PositionGetString(POSITION_SYMBOL) == _Symbol)
{
posFound = true;
g_PendingTicket = 0; // Filled → track position
if (InpDebugLog)
PrintFormat("[TRADE] Limit filled → position ticket=%llu", posTicket);
break;
}
}
if (!posFound)
{
HistorySelect(TimeCurrent() - 86400, TimeCurrent());
// Check: lệnh bị hủy?
bool wasCancelled = false;
for (int i = HistoryOrdersTotal() - 1; i >= 0; i--)
{
ulong hTicket = HistoryOrderGetTicket(i);
if (hTicket == g_PendingTicket)
{
long state = HistoryOrderGetInteger(hTicket, ORDER_STATE);
if (state == ORDER_STATE_CANCELED || state == ORDER_STATE_EXPIRED ||
state == ORDER_STATE_REJECTED)
wasCancelled = true;
break;
}
}
if (wasCancelled) { ResetToIdle("pending order cancelled/expired"); return; }
// Check: position đã đóng?
bool dealFound = false;
for (int i = HistoryDealsTotal() - 1; i >= 0; i--)
{
ulong dTicket = HistoryDealGetTicket(i);
if (HistoryDealGetInteger(dTicket, DEAL_MAGIC) == InpMagicNumber &&
HistoryDealGetString(dTicket, DEAL_SYMBOL) == _Symbol &&
HistoryDealGetInteger(dTicket, DEAL_ENTRY) == DEAL_ENTRY_OUT)
{
double profit = HistoryDealGetDouble(dTicket, DEAL_PROFIT);
if (InpDebugLog)
PrintFormat("[TRADE] Position closed | deal=%llu profit=%.2f", dTicket, profit);
dealFound = true;
break;
}
}
if (dealFound) ResetToIdle("trade closed");
else ResetToIdle("pending order lost");
return;
}
}
return; // Pending still alive → wait
}
// ── 2. Track open position (pending đã fill) ────────────────────
bool hasPosition = false;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong posTicket = PositionGetTicket(i);
if (posTicket == 0) continue;
if (PositionGetInteger(POSITION_MAGIC) == InpMagicNumber &&
PositionGetString(POSITION_SYMBOL) == _Symbol)
{
hasPosition = true;
break;
}
}
if (!hasPosition)
ResetToIdle("trade closed no position");
}
//+------------------------------------------------------------------+
//| SECTION 10 STATE MACHINE RUNNER |
//+------------------------------------------------------------------+
void RunStateMachine()
{
UpdateFVGStatuses();
ScanAndRegisterFVGs();
switch (g_State)
{
case EA_IDLE: OnStateIdle(); break;
case EA_WAIT_TOUCH: OnStateWaitTouch(); break;
case EA_WAIT_TRIGGER: OnStateWaitTrigger(); break;
case EA_IN_TRADE: OnStateInTrade(); break;
}
}
//+------------------------------------------------------------------+
//| SECTION 11 DRAWING |
//+------------------------------------------------------------------+
//----------------------------------------------------------------------
// DrawOneSwingPoint generic swing-point drawer
//----------------------------------------------------------------------
void DrawOneSwingPoint(
string prefix, ENUM_TIMEFRAMES tf,
string tag, bool isHigh, int barIdx, double price,
color clr, bool isKL, int arrowSz = 2, int fontSize = 8)
{
string arrN = prefix + "ARR_" + tag;
string txtN = prefix + "TXT_" + tag;
string klN = prefix + "KL_" + tag;
datetime t = iTime(_Symbol, tf, barIdx);
if (ObjectFind(0, arrN) < 0) ObjectCreate(0, arrN, OBJ_ARROW, 0, t, price);
ObjectSetInteger(0, arrN, OBJPROP_ARROWCODE, isHigh ? 234 : 233);
ObjectSetInteger(0, arrN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, arrN, OBJPROP_WIDTH, arrowSz);
ObjectSetInteger(0, arrN, OBJPROP_ANCHOR, isHigh ? ANCHOR_BOTTOM : ANCHOR_TOP);
ObjectMove(0, arrN, 0, t, price);
double rng = iHigh(_Symbol, tf, barIdx) - iLow(_Symbol, tf, barIdx);
double txtY = isHigh ? price + rng * 0.3 : price - rng * 0.3;
if (ObjectFind(0, txtN) < 0) ObjectCreate(0, txtN, OBJ_TEXT, 0, t, txtY);
ObjectMove(0, txtN, 0, t, txtY);
ObjectSetString (0, txtN, OBJPROP_TEXT, tag);
ObjectSetInteger(0, txtN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, txtN, OBJPROP_FONTSIZE, fontSize);
ObjectSetInteger(0, txtN, OBJPROP_ANCHOR, isHigh ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
if (isKL)
{
datetime tEnd = iTime(_Symbol, tf, 0);
if (ObjectFind(0, klN) < 0) ObjectCreate(0, klN, OBJ_TREND, 0, t, price, tEnd, price);
ObjectSetInteger(0, klN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, klN, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, klN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, klN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, klN, 0, t, price);
ObjectMove(0, klN, 1, tEnd, price);
}
else ObjectDelete(0, klN);
}
void DrawMiddleSwingPoints()
{
if (!InpDebugDraw) return;
if (g_MiddleTrend.idxH0 <= 0 || g_MiddleTrend.idxH1 <= 0 ||
g_MiddleTrend.idxL0 <= 0 || g_MiddleTrend.idxL1 <= 0)
{ ObjectsDeleteAll(0, SW_PREFIX); return; }
bool isUp = (g_MiddleTrend.trend == DIR_UP);
bool isDown = (g_MiddleTrend.trend == DIR_DOWN);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "H0", true, g_MiddleTrend.idxH0, g_MiddleTrend.h0, clrAqua, isDown, 2, 8);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "H1", true, g_MiddleTrend.idxH1, g_MiddleTrend.h1, C'0,140,160', false, 2, 8);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "L0", false, g_MiddleTrend.idxL0, g_MiddleTrend.l0, clrYellow, isUp, 2, 8);
DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "L1", false, g_MiddleTrend.idxL1, g_MiddleTrend.l1, C'160,140,0', false, 2, 8);
}
void DrawTriggerSwingPoints()
{
if (!InpDebugDraw) return;
if (g_TriggerTrend.idxH0 <= 0 || g_TriggerTrend.idxH1 <= 0 ||
g_TriggerTrend.idxL0 <= 0 || g_TriggerTrend.idxL1 <= 0)
{ ObjectsDeleteAll(0, TS_PREFIX); return; }
bool isUp = (g_TriggerTrend.trend == DIR_UP);
bool isDown = (g_TriggerTrend.trend == DIR_DOWN);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tH0", true, g_TriggerTrend.idxH0, g_TriggerTrend.h0, C'180,100,255', isDown, 1, 7);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tH1", true, g_TriggerTrend.idxH1, g_TriggerTrend.h1, C'100,60,160', false, 1, 7);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tL0", false, g_TriggerTrend.idxL0, g_TriggerTrend.l0, C'255,160,40', isUp, 1, 7);
DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tL1", false, g_TriggerTrend.idxL1, g_TriggerTrend.l1, C'160,100,20', false, 1, 7);
}
//----------------------------------------------------------------------
// DrawMSSMarker v3: Vẽ MSS kèm sweep marker
//
// Khi MSS hợp lệ trên TriggerTF, vẽ CẢ HAI:
// 1. Sweep: ◆ diamond tại wick extreme + đường ngang tại swept level
// 2. Break: ▲/▼ arrow tại close MSS + đường ngang tại broken key level
//
// Cả 2 tạo thành 1 pattern hoàn chỉnh trên chart.
//----------------------------------------------------------------------
void DrawMSSMarker(
string mssId, ENUM_TIMEFRAMES tf,
datetime mssTime, double mssLevel, MarketDir mssBreak,
datetime sweepTime, double sweepLevel, double sweepWick)
{
if (!InpDebugDraw || mssTime == 0) return;
// ── MSS break: arrow + label + key level line ─────────────────
string arrN = MSS_PREFIX + mssId + "_ARR";
string lblN = MSS_PREFIX + mssId + "_LBL";
string klN = MSS_PREFIX + mssId + "_KL";
bool isBull = (mssBreak == DIR_UP);
color clr = isBull ? clrLime : clrTomato;
int shift = MyBarShift(_Symbol, tf, mssTime);
if (shift < 0) return;
double closeAtMss = iClose(_Symbol, tf, shift);
// Arrow tại close price cây nến MSS
if (ObjectFind(0, arrN) < 0)
ObjectCreate(0, arrN, OBJ_ARROW, 0, mssTime, closeAtMss);
ObjectSetInteger(0, arrN, OBJPROP_ARROWCODE, isBull ? 233 : 234);
ObjectSetInteger(0, arrN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, arrN, OBJPROP_WIDTH, 2);
ObjectSetInteger(0, arrN, OBJPROP_ANCHOR, isBull ? ANCHOR_TOP : ANCHOR_BOTTOM);
ObjectMove(0, arrN, 0, mssTime, closeAtMss);
// Label "▲MSS" / "▼MSS"
double rng = iHigh(_Symbol, tf, shift) - iLow(_Symbol, tf, shift);
double lblY = isBull ? closeAtMss - rng * 0.4 : closeAtMss + rng * 0.4;
if (ObjectFind(0, lblN) < 0)
ObjectCreate(0, lblN, OBJ_TEXT, 0, mssTime, lblY);
ObjectMove(0, lblN, 0, mssTime, lblY);
ObjectSetString (0, lblN, OBJPROP_TEXT, isBull ? "▲MSS" : "▼MSS");
ObjectSetInteger(0, lblN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, lblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, lblN, OBJPROP_ANCHOR, isBull ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER);
// Dotted line tại broken key level
datetime tEnd = iTime(_Symbol, tf, 0);
if (ObjectFind(0, klN) < 0)
ObjectCreate(0, klN, OBJ_TREND, 0, mssTime, mssLevel, tEnd, mssLevel);
ObjectSetInteger(0, klN, OBJPROP_COLOR, clr);
ObjectSetInteger(0, klN, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, klN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, klN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, klN, 0, mssTime, mssLevel);
ObjectMove(0, klN, 1, tEnd, mssLevel);
// ── v3: Sweep marker (chỉ vẽ nếu sweep data hợp lệ) ──────────
if (sweepTime > 0 && sweepLevel > 0)
{
string swpArrN = MSS_PREFIX + mssId + "_SWP_ARR"; // ◆ diamond tại wick
string swpLblN = MSS_PREFIX + mssId + "_SWP_LBL"; // Label "SWEEP"
string swpLvlN = MSS_PREFIX + mssId + "_SWP_LVL"; // Đường ngang tại swept level
string swpWkN = MSS_PREFIX + mssId + "_SWP_WK"; // Vertical wick line
color swpClr = isBull ? C'0,210,230' : C'230,80,210'; // Cyan / Magenta
// ◆ Diamond tại wick extreme
if (ObjectFind(0, swpArrN) < 0)
ObjectCreate(0, swpArrN, OBJ_ARROW, 0, sweepTime, sweepWick);
ObjectSetInteger(0, swpArrN, OBJPROP_ARROWCODE, 4); // 4 = diamond
ObjectSetInteger(0, swpArrN, OBJPROP_COLOR, swpClr);
ObjectSetInteger(0, swpArrN, OBJPROP_WIDTH, 2);
ObjectMove(0, swpArrN, 0, sweepTime, sweepWick);
// Label "SWEEP"
int swShift = MyBarShift(_Symbol, tf, sweepTime);
double swRng = (swShift >= 0) ? iHigh(_Symbol, tf, swShift) - iLow(_Symbol, tf, swShift) : 10 * _Point;
double swLblY = isBull ? sweepWick - swRng * 0.7 : sweepWick + swRng * 0.7;
if (ObjectFind(0, swpLblN) < 0)
ObjectCreate(0, swpLblN, OBJ_TEXT, 0, sweepTime, swLblY);
ObjectMove(0, swpLblN, 0, sweepTime, swLblY);
ObjectSetString (0, swpLblN, OBJPROP_TEXT, "SWEEP");
ObjectSetInteger(0, swpLblN, OBJPROP_COLOR, swpClr);
ObjectSetInteger(0, swpLblN, OBJPROP_FONTSIZE, 7);
ObjectSetInteger(0, swpLblN, OBJPROP_ANCHOR, isBull ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER);
// Đường ngang đứt tại swept swing level (từ sweep đến MSS)
if (ObjectFind(0, swpLvlN) < 0)
ObjectCreate(0, swpLvlN, OBJ_TREND, 0, sweepTime, sweepLevel, mssTime, sweepLevel);
ObjectSetInteger(0, swpLvlN, OBJPROP_COLOR, swpClr);
ObjectSetInteger(0, swpLvlN, OBJPROP_STYLE, STYLE_DASHDOTDOT);
ObjectSetInteger(0, swpLvlN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, swpLvlN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, swpLvlN, 0, sweepTime, sweepLevel);
ObjectMove(0, swpLvlN, 1, mssTime, sweepLevel);
// Vertical wick line: swept level → wick extreme
if (ObjectFind(0, swpWkN) < 0)
ObjectCreate(0, swpWkN, OBJ_TREND, 0, sweepTime, sweepLevel, sweepTime, sweepWick);
ObjectSetInteger(0, swpWkN, OBJPROP_COLOR, swpClr);
ObjectSetInteger(0, swpWkN, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, swpWkN, OBJPROP_WIDTH, 2);
ObjectMove(0, swpWkN, 0, sweepTime, sweepLevel);
ObjectMove(0, swpWkN, 1, sweepTime, sweepWick);
}
}
void DrawMSSMarkers()
{
if (!InpDebugDraw) return;
// MiddleTF MSS vẽ KHÔNG kèm sweep (MiddleTF không cần sweep)
if (g_MiddleTrend.lastMssTime > 0)
{
string mid = "M_" + IntegerToString((long)g_MiddleTrend.lastMssTime);
DrawMSSMarker(mid, InpMiddleTF,
g_MiddleTrend.lastMssTime, g_MiddleTrend.lastMssLevel, g_MiddleTrend.lastMssBreak,
0, 0, 0); // Không có sweep data
}
// TriggerTF MSS vẽ KÈM sweep (đã validate trong UpdateTFTrendContext)
if (g_TriggerTrend.lastMssTime > 0)
{
string tid = "T_" + IntegerToString((long)g_TriggerTrend.lastMssTime);
DrawMSSMarker(tid, InpTriggerTF,
g_TriggerTrend.lastMssTime, g_TriggerTrend.lastMssLevel, g_TriggerTrend.lastMssBreak,
g_TriggerTrend.mssSweepTime, // Sweep time đi kèm MSS
g_TriggerTrend.mssSweepLevel, // Swept swing level
g_TriggerTrend.mssSweepWick); // Wick extreme
}
}
//----------------------------------------------------------------------
// v3: DrawOrderVisualization TradingView-style Entry/SL/TP
//
// Layout:
// ┌───────────────────────────┐ ◎ TP 1.23568 +112p (2.0R)
// │ TP zone (dark green) │
// ├───────────────────────────┤ ▶ BUY LIM 1.23456 | 0.05 lot
// │ SL zone (dark red) │
// └───────────────────────────┘ ✕ SL 1.23400 -56p
//----------------------------------------------------------------------
void DrawOrderVisualization()
{
if (!InpDebugDraw) return;
string tpZoneN = ORD_PREFIX + "TP_ZONE";
string slZoneN = ORD_PREFIX + "SL_ZONE";
string entLineN = ORD_PREFIX + "ENTRY_LINE";
string slLineN = ORD_PREFIX + "SL_LINE";
string tpLineN = ORD_PREFIX + "TP_LINE";
string entLblN = ORD_PREFIX + "ENTRY_LBL";
string slLblN = ORD_PREFIX + "SL_LBL";
string tpLblN = ORD_PREFIX + "TP_LBL";
string infoLblN = ORD_PREFIX + "INFO_LBL";
// Nếu không có order plan → xóa tất cả ORD_ objects
if (!g_OrderPlan.valid || g_State == EA_IDLE)
{
ObjectDelete(0, tpZoneN); ObjectDelete(0, slZoneN);
ObjectDelete(0, entLineN); ObjectDelete(0, slLineN); ObjectDelete(0, tpLineN);
ObjectDelete(0, entLblN); ObjectDelete(0, slLblN); ObjectDelete(0, tpLblN);
ObjectDelete(0, infoLblN);
return;
}
bool isBuy = (g_OrderPlan.direction > 0);
double entry = g_OrderPlan.entry;
double sl = g_OrderPlan.stopLoss;
double tp = g_OrderPlan.takeProfit;
double lot = g_OrderPlan.lot;
// Time range cho rectangles
datetime tStart = (g_TrigFVG.valid) ? g_TrigFVG.createdTime : iTime(_Symbol, InpTriggerTF, 20);
datetime tEnd = iTime(_Symbol, InpTriggerTF, 0) + PeriodSeconds(InpTriggerTF) * 25;
// Tính pips & R:R
double slPips = MathAbs(entry - sl) / _Point;
double tpPips = MathAbs(tp - entry) / _Point;
double rr = (slPips > 0) ? tpPips / slPips : 0;
// Colors
color entryClr = isBuy ? C'33,150,243' : C'255,152,0'; // Blue / Orange
color tpFill = C'15,65,35'; // Dark green fill
color slFill = C'85,15,15'; // Dark red fill
color tpLine = C'38,166,91'; // Green line
color slLine = C'229,57,53'; // Red line
// ── TP Zone rectangle ──────────────────────────────────────────
double tpTop = (isBuy) ? tp : entry;
double tpBot = (isBuy) ? entry : tp;
if (ObjectFind(0, tpZoneN) < 0)
ObjectCreate(0, tpZoneN, OBJ_RECTANGLE, 0, tStart, tpTop, tEnd, tpBot);
ObjectSetInteger(0, tpZoneN, OBJPROP_COLOR, tpFill);
ObjectSetInteger(0, tpZoneN, OBJPROP_FILL, true);
ObjectSetInteger(0, tpZoneN, OBJPROP_BACK, true);
ObjectMove(0, tpZoneN, 0, tStart, tpTop);
ObjectMove(0, tpZoneN, 1, tEnd, tpBot);
// ── SL Zone rectangle ──────────────────────────────────────────
double slTop = (isBuy) ? entry : sl;
double slBot = (isBuy) ? sl : entry;
if (ObjectFind(0, slZoneN) < 0)
ObjectCreate(0, slZoneN, OBJ_RECTANGLE, 0, tStart, slTop, tEnd, slBot);
ObjectSetInteger(0, slZoneN, OBJPROP_COLOR, slFill);
ObjectSetInteger(0, slZoneN, OBJPROP_FILL, true);
ObjectSetInteger(0, slZoneN, OBJPROP_BACK, true);
ObjectMove(0, slZoneN, 0, tStart, slTop);
ObjectMove(0, slZoneN, 1, tEnd, slBot);
// ── Entry line (solid 2px, ray right) ──────────────────────────
if (ObjectFind(0, entLineN) < 0)
ObjectCreate(0, entLineN, OBJ_TREND, 0, tStart, entry, tEnd, entry);
ObjectSetInteger(0, entLineN, OBJPROP_COLOR, entryClr);
ObjectSetInteger(0, entLineN, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, entLineN, OBJPROP_WIDTH, 2);
ObjectSetInteger(0, entLineN, OBJPROP_RAY_RIGHT, true);
ObjectMove(0, entLineN, 0, tStart, entry);
ObjectMove(0, entLineN, 1, tEnd, entry);
// ── TP line (dashed, ray right) ────────────────────────────────
if (ObjectFind(0, tpLineN) < 0)
ObjectCreate(0, tpLineN, OBJ_TREND, 0, tStart, tp, tEnd, tp);
ObjectSetInteger(0, tpLineN, OBJPROP_COLOR, tpLine);
ObjectSetInteger(0, tpLineN, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, tpLineN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, tpLineN, OBJPROP_RAY_RIGHT, true);
ObjectMove(0, tpLineN, 0, tStart, tp);
ObjectMove(0, tpLineN, 1, tEnd, tp);
// ── SL line (dashed, ray right) ────────────────────────────────
if (ObjectFind(0, slLineN) < 0)
ObjectCreate(0, slLineN, OBJ_TREND, 0, tStart, sl, tEnd, sl);
ObjectSetInteger(0, slLineN, OBJPROP_COLOR, slLine);
ObjectSetInteger(0, slLineN, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, slLineN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, slLineN, OBJPROP_RAY_RIGHT, true);
ObjectMove(0, slLineN, 0, tStart, sl);
ObjectMove(0, slLineN, 1, tEnd, sl);
// ── Entry label ────────────────────────────────────────────────
datetime lblTime = tEnd;
string entTxt = StringFormat("%s %s | %.2f lot",
isBuy ? "▶ BUY LIM" : "▶ SELL LIM",
DoubleToString(entry, _Digits), lot);
if (ObjectFind(0, entLblN) < 0)
ObjectCreate(0, entLblN, OBJ_TEXT, 0, lblTime, entry);
ObjectMove(0, entLblN, 0, lblTime, entry);
ObjectSetString (0, entLblN, OBJPROP_TEXT, entTxt);
ObjectSetInteger(0, entLblN, OBJPROP_COLOR, entryClr);
ObjectSetInteger(0, entLblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, entLblN, OBJPROP_ANCHOR, ANCHOR_LEFT);
// ── TP label ───────────────────────────────────────────────────
string tpTxt = StringFormat("◎ TP %s +%.0fp (%.1fR)",
DoubleToString(tp, _Digits), tpPips, rr);
if (ObjectFind(0, tpLblN) < 0)
ObjectCreate(0, tpLblN, OBJ_TEXT, 0, lblTime, tp);
ObjectMove(0, tpLblN, 0, lblTime, tp);
ObjectSetString (0, tpLblN, OBJPROP_TEXT, tpTxt);
ObjectSetInteger(0, tpLblN, OBJPROP_COLOR, tpLine);
ObjectSetInteger(0, tpLblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, tpLblN, OBJPROP_ANCHOR, isBuy ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
// ── SL label ───────────────────────────────────────────────────
string slTxt = StringFormat("✕ SL %s -%.0fp",
DoubleToString(sl, _Digits), slPips);
if (ObjectFind(0, slLblN) < 0)
ObjectCreate(0, slLblN, OBJ_TEXT, 0, lblTime, sl);
ObjectMove(0, slLblN, 0, lblTime, sl);
ObjectSetString (0, slLblN, OBJPROP_TEXT, slTxt);
ObjectSetInteger(0, slLblN, OBJPROP_COLOR, slLine);
ObjectSetInteger(0, slLblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, slLblN, OBJPROP_ANCHOR, isBuy ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER);
// ── Risk info label (giữa entry và SL) ─────────────────────────
double infoY = (entry + sl) / 2.0;
string infoTxt = StringFormat("Risk %.1f%% | SL %.0fp | TP %.0fp | %.1fR",
InpRiskPercent, slPips, tpPips, rr);
if (ObjectFind(0, infoLblN) < 0)
ObjectCreate(0, infoLblN, OBJ_TEXT, 0, lblTime, infoY);
ObjectMove(0, infoLblN, 0, lblTime, infoY);
ObjectSetString (0, infoLblN, OBJPROP_TEXT, infoTxt);
ObjectSetInteger(0, infoLblN, OBJPROP_COLOR, C'160,160,160');
ObjectSetInteger(0, infoLblN, OBJPROP_FONTSIZE, 7);
ObjectSetInteger(0, infoLblN, OBJPROP_ANCHOR, ANCHOR_LEFT);
}
//----------------------------------------------------------------------
// DrawTriggerFVG TriggerTF FVG rectangle + midline + label
//----------------------------------------------------------------------
void DrawTriggerFVG()
{
if (!InpDebugDraw) return;
if (!g_TrigFVG.valid) return;
string rectN = TFVG_PREFIX + "RECT";
string midN = TFVG_PREFIX + "MID";
string lblN = TFVG_PREFIX + "LBL";
string entN = TFVG_PREFIX + "ENTRY";
datetime tStart = g_TrigFVG.createdTime;
datetime tEnd = iTime(_Symbol, InpTriggerTF, 0);
if (tEnd <= tStart) tEnd = tStart + PeriodSeconds(InpTriggerTF) * 10;
color fillColor = (g_TrigFVG.direction == DIR_UP) ? C'0,180,100' : C'200,50,80';
if (ObjectFind(0, rectN) < 0)
ObjectCreate(0, rectN, OBJ_RECTANGLE, 0, tStart, g_TrigFVG.high, tEnd, g_TrigFVG.low);
ObjectSetInteger(0, rectN, OBJPROP_COLOR, fillColor);
ObjectSetInteger(0, rectN, OBJPROP_FILL, true);
ObjectSetInteger(0, rectN, OBJPROP_BACK, true);
ObjectSetInteger(0, rectN, OBJPROP_WIDTH, 1);
ObjectMove(0, rectN, 0, tStart, g_TrigFVG.high);
ObjectMove(0, rectN, 1, tEnd, g_TrigFVG.low);
if (ObjectFind(0, midN) < 0)
ObjectCreate(0, midN, OBJ_TREND, 0, tStart, g_TrigFVG.mid, tEnd, g_TrigFVG.mid);
ObjectSetInteger(0, midN, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, midN, OBJPROP_STYLE, STYLE_DASHDOT);
ObjectSetInteger(0, midN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, midN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, midN, 0, tStart, g_TrigFVG.mid);
ObjectMove(0, midN, 1, tEnd, g_TrigFVG.mid);
if (ObjectFind(0, lblN) < 0)
ObjectCreate(0, lblN, OBJ_TEXT, 0, tStart, g_TrigFVG.high);
ObjectMove(0, lblN, 0, tStart, g_TrigFVG.high);
string sym = (g_TrigFVG.direction == DIR_UP) ? "▲" : "▼";
ObjectSetString (0, lblN, OBJPROP_TEXT, StringFormat("tFVG %s [%.5f]", sym, g_TrigFVG.mid));
ObjectSetInteger(0, lblN, OBJPROP_COLOR, fillColor);
ObjectSetInteger(0, lblN, OBJPROP_FONTSIZE, 7);
ObjectSetInteger(0, lblN, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
if (g_OrderPlan.valid)
{
if (ObjectFind(0, entN) < 0)
ObjectCreate(0, entN, OBJ_TREND, 0, tStart, g_OrderPlan.entry, tEnd, g_OrderPlan.entry);
ObjectSetInteger(0, entN, OBJPROP_COLOR, clrGold);
ObjectSetInteger(0, entN, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, entN, OBJPROP_WIDTH, 2);
ObjectSetInteger(0, entN, OBJPROP_RAY_RIGHT, true);
ObjectMove(0, entN, 0, tStart, g_OrderPlan.entry);
ObjectMove(0, entN, 1, tEnd, g_OrderPlan.entry);
}
}
//----------------------------------------------------------------------
// DrawOneFVGRecord MiddleTF FVG rectangle + midline + label
//----------------------------------------------------------------------
void DrawOneFVGRecord(int idx)
{
if (!InpDebugDraw || idx < 0 || idx >= g_FVGCount) return;
string sid = IntegerToString(g_FVGPool[idx].id);
string rectN = FVGP_PREFIX + "RECT_" + sid;
string midN = FVGP_PREFIX + "MID_" + sid;
string lblN = FVGP_PREFIX + "LBL_" + sid;
datetime rectEnd;
if (g_FVGPool[idx].status == FVG_PENDING)
rectEnd = iTime(_Symbol, InpMiddleTF, 0);
else if (g_FVGPool[idx].touchTime > 0)
{
int shift = MyBarShift(_Symbol, InpTriggerTF, g_FVGPool[idx].touchTime);
rectEnd = (shift >= 0) ? iTime(_Symbol, InpTriggerTF, shift) : iTime(_Symbol, InpMiddleTF, 0);
}
else
{
int shift = MyBarShift(_Symbol, InpMiddleTF, g_FVGPool[idx].usedTime);
rectEnd = (shift >= 0) ? iTime(_Symbol, InpMiddleTF, shift) : iTime(_Symbol, InpMiddleTF, 0);
}
if (rectEnd <= g_FVGPool[idx].createdTime) rectEnd = iTime(_Symbol, InpMiddleTF, 0);
color fillColor;
if (g_FVGPool[idx].status == FVG_PENDING) fillColor = (g_FVGPool[idx].direction == DIR_UP) ? C'0,50,110' : C'90,25,0';
else if (g_FVGPool[idx].status == FVG_TOUCHED) fillColor = (g_FVGPool[idx].direction == DIR_UP) ? C'0,120,220' : C'220,75,0';
else if (g_FVGPool[idx].usedCase == 2) fillColor = C'0,100,0';
else if (g_FVGPool[idx].usedCase == 1) fillColor = C'70,0,0';
else fillColor = C'50,50,50';
if (ObjectFind(0, rectN) < 0)
ObjectCreate(0, rectN, OBJ_RECTANGLE, 0,
g_FVGPool[idx].createdTime, g_FVGPool[idx].high, rectEnd, g_FVGPool[idx].low);
ObjectSetInteger(0, rectN, OBJPROP_COLOR, fillColor);
ObjectSetInteger(0, rectN, OBJPROP_FILL, true);
ObjectSetInteger(0, rectN, OBJPROP_BACK, true);
ObjectSetInteger(0, rectN, OBJPROP_WIDTH, 1);
ObjectMove(0, rectN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high);
ObjectMove(0, rectN, 1, rectEnd, g_FVGPool[idx].low);
color midColor = (g_FVGPool[idx].status == FVG_USED) ? C'60,60,60' : clrSilver;
if (ObjectFind(0, midN) < 0)
ObjectCreate(0, midN, OBJ_TREND, 0,
g_FVGPool[idx].createdTime, g_FVGPool[idx].mid, rectEnd, g_FVGPool[idx].mid);
ObjectSetInteger(0, midN, OBJPROP_COLOR, midColor);
ObjectSetInteger(0, midN, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, midN, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, midN, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, midN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].mid);
ObjectMove(0, midN, 1, rectEnd, g_FVGPool[idx].mid);
string sym = (g_FVGPool[idx].direction == DIR_UP) ? "▲" : "▼";
string stTxt = "";
if (g_FVGPool[idx].status == FVG_TOUCHED) stTxt = " [TOUCHED]";
else if (g_FVGPool[idx].usedCase == 2) stTxt = " [TRIGGERED]";
else if (g_FVGPool[idx].usedCase == 1) stTxt = " [BROKEN]";
else if (g_FVGPool[idx].status == FVG_USED) stTxt = " [EXPIRED]";
if (ObjectFind(0, lblN) < 0)
ObjectCreate(0, lblN, OBJ_TEXT, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high);
ObjectMove(0, lblN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high);
ObjectSetString (0, lblN, OBJPROP_TEXT,
StringFormat("FVG#%d %s%s", g_FVGPool[idx].id, sym, stTxt));
ObjectSetInteger(0, lblN, OBJPROP_COLOR, fillColor);
ObjectSetInteger(0, lblN, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, lblN, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
}
void DrawFVGPool()
{
if (!InpDebugDraw) return;
for (int i = 0; i < g_FVGCount; i++) DrawOneFVGRecord(i);
}
//----------------------------------------------------------------------
// DrawContextDebug top-left info panel
//----------------------------------------------------------------------
void DrawContextDebug()
{
if (!InpDebugDraw) return;
#define LBL(name,txt,y,clr) \
if(ObjectFind(0,name)<0) ObjectCreate(0,name,OBJ_LABEL,0,0,0); \
ObjectSetInteger(0,name,OBJPROP_CORNER, CORNER_LEFT_UPPER); \
ObjectSetInteger(0,name,OBJPROP_XDISTANCE, 10); \
ObjectSetInteger(0,name,OBJPROP_YDISTANCE, y); \
ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 9); \
ObjectSetInteger(0,name,OBJPROP_COLOR, clr); \
ObjectSetString (0,name,OBJPROP_TEXT, txt);
LBL("DBG_HDR", "── ICT EA v3 ──", 10, clrSilver)
// Bias
color cB = (g_Bias.bias==BIAS_UP)?clrLime:(g_Bias.bias==BIAS_DOWN)?clrTomato:(g_Bias.bias==BIAS_SIDEWAY)?clrOrange:clrGray;
LBL("DBG_BIAS", StringFormat("Bias : %s", EnumToString(g_Bias.bias)), 34, cB)
// H1 trend
color cMT = (g_MiddleTrend.trend==DIR_UP)?clrLime:(g_MiddleTrend.trend==DIR_DOWN)?clrTomato:clrGray;
LBL("DBG_MT", StringFormat("H1 : %s KL=%.5f", EnumToString(g_MiddleTrend.trend), g_MiddleTrend.keyLevel), 58, cMT)
// H1 MSS
if (g_MiddleTrend.lastMssTime > 0)
{
color cMSS = (g_MiddleTrend.lastMssBreak==DIR_UP) ? clrLime : clrTomato;
LBL("DBG_MMSS", StringFormat("H1MSS: %s @ %s", (g_MiddleTrend.lastMssBreak==DIR_UP)?"▲":"▼",
TimeToString(g_MiddleTrend.lastMssTime, TIME_MINUTES)), 82, cMSS)
}
else ObjectDelete(0,"DBG_MMSS");
// M5 trend
color cTT = (g_TriggerTrend.trend==DIR_UP)?clrLime:(g_TriggerTrend.trend==DIR_DOWN)?clrTomato:clrGray;
LBL("DBG_TT", StringFormat("M5 : %s KL=%.5f", EnumToString(g_TriggerTrend.trend), g_TriggerTrend.keyLevel), 106, cTT)
// M5 MSS (v3: đã bao gồm sweep validation)
if (g_TriggerTrend.lastMssTime > 0)
{
color cMSS2 = (g_TriggerTrend.lastMssBreak==DIR_UP) ? clrLime : clrTomato;
string sweepInfo = (g_TriggerTrend.mssSweepTime > 0)
? StringFormat(" sweep@%s", TimeToString(g_TriggerTrend.mssSweepTime, TIME_MINUTES))
: "";
LBL("DBG_TMSS", StringFormat("M5MSS: %s @ %s%s",
(g_TriggerTrend.lastMssBreak==DIR_UP)?"▲":"▼",
TimeToString(g_TriggerTrend.lastMssTime, TIME_MINUTES),
sweepInfo), 130, cMSS2)
}
else ObjectDelete(0,"DBG_TMSS");
// Sweep status (internal, không vẽ riêng nhưng show trong panel)
if (g_LastLiqSweep.valid)
{
color cSw = (g_LastLiqSweep.direction == DIR_UP) ? C'0,210,230' : C'230,80,210';
LBL("DBG_SWEEP", StringFormat("Sweep: %s @%.5f (pending)",
(g_LastLiqSweep.direction==DIR_UP)?"▲":"▼",
g_LastLiqSweep.sweptLevel), 154, cSw)
}
else ObjectDelete(0,"DBG_SWEEP");
// Daily risk
double lostPct = g_DailyRisk.startBalance > 0
? (g_DailyRisk.startBalance - g_DailyRisk.currentBalance) / g_DailyRisk.startBalance * 100.0 : 0.0;
color cR = g_DailyRisk.limitHit?clrRed:(lostPct>InpMaxDailyLossPct*0.7?clrOrange:clrLime);
LBL("DBG_RISK", StringFormat("Risk : %.2f%% / %.2f%%", lostPct, InpMaxDailyLossPct), 178, cR)
LBL("DBG_BAL", StringFormat("Bal : %.2f (start %.2f)", g_DailyRisk.currentBalance, g_DailyRisk.startBalance), 202, clrSilver)
LBL("DBG_LIM", g_DailyRisk.limitHit?"⛔ DAILY LOSS HIT":"✅ Loss OK", 226, g_DailyRisk.limitHit?clrRed:clrLime)
// State
color cS = (g_State==EA_IDLE)?clrSilver:(g_State==EA_WAIT_TOUCH)?clrOrange:(g_State==EA_WAIT_TRIGGER)?clrYellow:clrLime;
LBL("DBG_ST", StringFormat("State: %s", EnumToString(g_State)), 250, cS)
if (g_BlockReason != BLOCK_NONE)
{ LBL("DBG_BLK", StringFormat("Block: %s", EnumToString(g_BlockReason)), 274, clrTomato) }
else ObjectDelete(0,"DBG_BLK");
// FVG pool
int nP=0,nT=0,nU=0;
for(int i=0;i<g_FVGCount;i++)
{
if (g_FVGPool[i].status==FVG_PENDING) nP++;
else if (g_FVGPool[i].status==FVG_TOUCHED) nT++;
else nU++;
}
color cP = (nT>0)?clrDeepSkyBlue:(nP>0)?clrDodgerBlue:clrGray;
LBL("DBG_POOL", StringFormat("Pool : P=%d T=%d U=%d (%d/%d)", nP,nT,nU,g_FVGCount,MAX_FVG_POOL), 298, cP)
// Active FVG
if (g_ActiveFVGIdx >= 0 && g_ActiveFVGIdx < g_FVGCount)
{
int ai = g_ActiveFVGIdx;
color cA = (g_FVGPool[ai].status==FVG_TOUCHED)?clrDeepSkyBlue:clrDodgerBlue;
LBL("DBG_ACT", StringFormat("Act : #%d %s [%.5f%.5f] %s",
g_FVGPool[ai].id, EnumToString(g_FVGPool[ai].direction),
g_FVGPool[ai].low, g_FVGPool[ai].high,
EnumToString(g_FVGPool[ai].status)), 322, cA)
}
else ObjectDelete(0,"DBG_ACT");
// Pending order
if (g_PendingTicket > 0 && g_OrderPlan.valid)
{
string ordDir = (g_OrderPlan.direction > 0) ? "BUY_LIM" : "SELL_LIM";
LBL("DBG_ORD", StringFormat("Order: %s #%llu @ %.5f SL=%.5f TP=%.5f",
ordDir, g_PendingTicket, g_OrderPlan.entry,
g_OrderPlan.stopLoss, g_OrderPlan.takeProfit), 346, clrGold)
}
else ObjectDelete(0,"DBG_ORD");
// TriggerTF FVG
if (g_TrigFVG.valid)
{
string tDir = (g_TrigFVG.direction == DIR_UP) ? "▲" : "▼";
LBL("DBG_TFVG", StringFormat("tFVG : %s [%.5f%.5f] mid=%.5f",
tDir, g_TrigFVG.low, g_TrigFVG.high, g_TrigFVG.mid), 370, C'0,220,150')
}
else ObjectDelete(0,"DBG_TFVG");
#undef LBL
ChartRedraw(0);
}
//----------------------------------------------------------------------
// DrawVisuals master draw function
//----------------------------------------------------------------------
void DrawVisuals()
{
DrawMiddleSwingPoints(); // H1 swing H0/H1/L0/L1
DrawTriggerSwingPoints(); // M5 swing tH0/tH1/tL0/tL1
DrawMSSMarkers(); // MSS + sweep (combined, v3)
DrawFVGPool(); // MiddleTF FVG rectangles
DrawTriggerFVG(); // TriggerTF FVG rectangle
DrawOrderVisualization(); // TradingView-style Entry/SL/TP
DrawContextDebug(); // Info panel
}
//+------------------------------------------------------------------+
//| SECTION 12 EA LIFECYCLE |
//+------------------------------------------------------------------+
int OnInit()
{
ZeroMemory(g_Bias); ZeroMemory(g_MiddleTrend); ZeroMemory(g_TriggerTrend);
ZeroMemory(g_Trigger); ZeroMemory(g_DailyRisk);
ZeroMemory(g_TrigFVG); ZeroMemory(g_OrderPlan);
ZeroMemory(g_LastLiqSweep);
for (int i = 0; i < MAX_FVG_POOL; i++) ZeroMemory(g_FVGPool[i]);
g_FVGCount = 0; g_NextFVGId = 0;
g_ActiveFVGIdx = -1; g_TradeBarIndex = -1;
g_PendingTicket = 0;
g_State = EA_IDLE; g_BlockReason = BLOCK_NONE;
UpdateAllContexts();
ScanAndRegisterFVGs();
DrawVisuals();
PrintFormat("✅ ICT EA v3 init | Bias=%s | H1=%s | M5=%s | FVGPool=%d",
EnumToString(g_Bias.bias), EnumToString(g_MiddleTrend.trend),
EnumToString(g_TriggerTrend.trend), g_FVGCount);
return INIT_SUCCEEDED;
}
void OnTick()
{
UpdateAllContexts();
if (EvaluateGuards())
RunStateMachine();
else if (g_State != EA_IDLE)
ResetToIdle(EnumToString(g_BlockReason));
if (InpDebugDraw) DrawVisuals();
}
void OnDeinit(const int reason)
{
// Giữ lại trên chart: FVGP_, TFVG_, MSS_, ORD_ (post-session review)
ObjectsDeleteAll(0, SW_PREFIX);
ObjectsDeleteAll(0, TS_PREFIX);
ObjectsDeleteAll(0, DBG_PREFIX);
ChartRedraw(0);
PrintFormat("ICT EA v3 deinit | reason=%d | pool had %d FVGs", reason, g_FVGCount);
}