Tìm FVG hợp lệ

This commit is contained in:
Bell
2026-03-07 23:12:55 +07:00
parent 0ca72536ea
commit fd562bf32e
+407 -151
View File
@@ -8,25 +8,27 @@
// INPUTS
//====================================================
input ENUM_TIMEFRAMES InpBiasTF = PERIOD_D1; // Bias Timeframe
input ENUM_TIMEFRAMES InpMiddleTF = PERIOD_H1; // Middle TF nơi quét FVG
input ENUM_TIMEFRAMES InpTriggerTF = PERIOD_M5; // Trigger TF xác nhận vào lệnh
input ENUM_TIMEFRAMES InpBiasTF = PERIOD_D1; // Bias Timeframe
input ENUM_TIMEFRAMES InpMiddleTF = PERIOD_H1; // Middle TF nơi quét FVG
input ENUM_TIMEFRAMES InpTriggerTF = PERIOD_M5; // Trigger TF xác nhận vào lệnh
input double InpRiskPercent = 1.0;
input double InpRiskReward = 2.0;
input double InpMaxDailyLossPct = 3.0;
input double InpRiskPercent = 1.0;
input double InpRiskReward = 2.0;
input double InpMaxDailyLossPct = 3.0;
input int InpLondonStartHour = 8;
input int InpLondonEndHour = 17;
input int InpNYStartHour = 13;
input int InpNYEndHour = 22;
input int InpSessionAvoidLastMin = 60;
input int InpLondonStartHour = 8;
input int InpLondonEndHour = 17;
input int InpNYStartHour = 13;
input int InpNYEndHour = 22;
input int InpSessionAvoidLastMin = 60;
input int InpFVGMaxAliveMin = 180; // FVG sống tối đa bao nhiêu phút
input int InpTriggerMaxBars = 30; // Số bar TriggerTF chờ sau khi touched
input int InpFVGMaxAliveMin = 4320; // FVG sống tối đa bao nhiêu phút: 3 ngày
input int InpFVGScanBars = 50; // Số bar MiddleTF quét ngược để tìm FVG
input double InpFVGMinBodyPct = 60.0; // Nến giữa phải có body >= X% range
input int InpTriggerMaxBars = 30; // Số bar TriggerTF chờ sau khi touched
input bool InpDebugLog = true;
input bool InpDebugDraw = true;
input bool InpDebugLog = true;
input bool InpDebugDraw = true;
//====================================================
// ENUMS
@@ -34,7 +36,7 @@ input bool InpDebugDraw = true;
enum EAState
{
EA_IDLE, // Chờ điều kiện: bias + structure + FVG candidate
EA_IDLE, // Chờ điều kiện: bias hợp lệ + tìm FVG candidate
EA_WAIT_TOUCH, // Có FVG candidate, chờ price chạm vào vùng FVG
EA_WAIT_TRIGGER, // Price đã chạm FVG, chờ Trigger TF xác nhận entry
EA_IN_TRADE // Đang có lệnh, chờ kết thúc
@@ -68,7 +70,6 @@ enum BlockReason
// STRUCTS
//====================================================
// --- Bias từ Bias TF (D1)
struct BiasContext
{
HTFBias bias;
@@ -77,7 +78,6 @@ struct BiasContext
datetime lastBarTime; // Guard: chỉ update khi bar mới
};
// --- FVG đang theo dõi (quét từ Middle TF)
struct FVGContext
{
bool active; // Đang có FVG candidate
@@ -88,14 +88,13 @@ struct FVGContext
double low; // Cạnh dưới FVG
double mid; // Midpoint dùng làm entry tham chiếu
datetime createdTime; // Thời điểm FVG hình thành
datetime createdTime; // Thời điểm nến phải đóng = FVG confirmed
datetime touchTime; // Thời điểm price chạm lần đầu
int barsAlive; // Số bar TriggerTF đã qua kể từ touched
int touchBarIdx; // Bar index TriggerTF lúc touched
};
// --- Structure của Trigger TF (sau khi FVG touched)
struct TriggerContext
{
bool valid;
@@ -107,7 +106,6 @@ struct TriggerContext
double breakLevel; // Level cần phá để confirm entry
};
// --- Kế hoạch lệnh
struct OrderPlan
{
bool valid;
@@ -118,10 +116,10 @@ struct OrderPlan
double lot;
};
// --- Risk quản lý theo ngày
struct DailyRiskContext
{
double startBalance;
double currentBalance; // Cập nhật mỗi tick dùng lại trong DrawContextDebug
datetime dayStartTime;
bool limitHit;
};
@@ -130,62 +128,44 @@ struct DailyRiskContext
// GLOBAL STATE
//====================================================
EAState g_State = EA_IDLE;
BlockReason g_BlockReason = BLOCK_NONE;
EAState g_State = EA_IDLE;
BlockReason g_BlockReason = BLOCK_NONE;
BiasContext g_Bias;
FVGContext g_FVG;
TriggerContext g_Trigger;
BiasContext g_Bias;
FVGContext g_FVG;
TriggerContext g_Trigger;
DailyRiskContext g_DailyRisk;
int g_TradeBarIndex = -1;
int g_TradeBarIndex = -1;
// Tên objects vẽ FVG trên chart
const string FVG_RECT_NAME = "FVG_RECT";
const string FVG_MID_NAME = "FVG_MID";
const string FVG_LABEL_NAME = "FVG_LABEL";
//====================================================
// CONTEXT UPDATERS
// Mỗi hàm có guard chỉ chạy khi bar mới
//====================================================
HTFBias ResolveBias(
double b1High, double b1Low, double b1Close,
double b2High, double b2Low)
double b1High, double b1Low, double b1Close,
double b2High, double b2Low)
{
// ── Rule 1: Breakout lên trên ──────────────────
if (b1Close > b2High)
return BIAS_UP;
// ── Rule 2: Breakout xuống dưới ───────────────
if (b1Close < b2Low)
return BIAS_DOWN;
// ── Rule 3: Sweep high → reject xuống ─────────
// Wick lên trên bar2 nhưng close quay lại bên trong
if (b1High > b2High && b1Close < b2High)
return BIAS_DOWN;
// ── Rule 4: Sweep low → recover lên ───────────
// Wick xuống dưới bar2 nhưng close quay lại bên trong
if (b1Low < b2Low && b1Close > b2Low)
return BIAS_UP;
// ── Rule 5: Còn lại → sideway ─────────────────
return BIAS_SIDEWAY;
if (b1Close > b2High) return BIAS_UP; // Rule 1: Breakout lên
if (b1Close < b2Low) return BIAS_DOWN; // Rule 2: Breakout xuống
if (b1High > b2High && b1Close < b2High) return BIAS_DOWN; // Rule 3: Sweep high → reject
if (b1Low < b2Low && b1Close > b2Low) return BIAS_UP; // Rule 4: Sweep low → recover
return BIAS_SIDEWAY; // Rule 5: Còn lại
}
void UpdateBiasContext()
{
// Guard: chỉ update khi bar BiasTF mới hình thành
datetime currentBarTime = iTime(_Symbol, InpBiasTF, 0);
if (currentBarTime == g_Bias.lastBarTime)
return;
if (currentBarTime == g_Bias.lastBarTime) return; // Guard: 1 lần/bar
g_Bias.lastBarTime = currentBarTime;
// Đảm bảo đủ data
if (Bars(_Symbol, InpBiasTF) < 4)
{
g_Bias.bias = BIAS_NONE;
return;
}
if (Bars(_Symbol, InpBiasTF) < 4) { g_Bias.bias = BIAS_NONE; return; }
double b1High = iHigh (_Symbol, InpBiasTF, 1);
double b1Low = iLow (_Symbol, InpBiasTF, 1);
@@ -196,85 +176,64 @@ void UpdateBiasContext()
HTFBias prevBias = g_Bias.bias;
g_Bias.bias = ResolveBias(b1High, b1Low, b1Close, b2High, b2Low);
// Reset range (chỉ dùng khi sideway)
g_Bias.rangeHigh = (g_Bias.bias == BIAS_SIDEWAY) ? b2High : 0;
g_Bias.rangeLow = (g_Bias.bias == BIAS_SIDEWAY) ? b2Low : 0;
if (InpDebugLog && g_Bias.bias != prevBias)
PrintFormat("[BIAS] %s → %s | b1[H=%.5f L=%.5f C=%.5f] b2[H=%.5f L=%.5f]",
EnumToString(prevBias),
EnumToString(g_Bias.bias),
b1High, b1Low, b1Close,
b2High, b2Low);
EnumToString(prevBias), EnumToString(g_Bias.bias),
b1High, b1Low, b1Close, b2High, b2Low);
}
void UpdateDailyRiskContext()
{
datetime todayBarTime = iTime(_Symbol, PERIOD_D1, 0); // "ID" của ngày hôm nay thay đổi khi sang ngày mới
datetime todayBarTime = iTime(_Symbol, PERIOD_D1, 0); // "ID" của ngày đổi khi sang ngày mới
if (todayBarTime != g_DailyRisk.dayStartTime) // Bar D1 mới = sang ngày giao dịch mới
if (todayBarTime != g_DailyRisk.dayStartTime)
{
g_DailyRisk.dayStartTime = todayBarTime; // Cập nhật mốc ngày
g_DailyRisk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Chụp balance đầu ngày (dùng BALANCE, không phải EQUITY loại trừ floating P&L)
g_DailyRisk.limitHit = false; // Reset cờ cho phép trade ngày mới
g_DailyRisk.dayStartTime = todayBarTime;
g_DailyRisk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Chụp balance đầu ngày (không tính floating P&L)
g_DailyRisk.limitHit = false; // Reset cho phép trade ngày mới
if (InpDebugLog)
PrintFormat("[DAILY RISK] New day | startBalance=%.2f", g_DailyRisk.startBalance);
}
if (g_DailyRisk.limitHit) // Đã hit rồi → không tính lại, tránh gọi AccountInfoDouble() vô ích mỗi tick
return;
if (g_DailyRisk.limitHit) return; // Đã hit → không tính lại mỗi tick
double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double lostPct = (g_DailyRisk.startBalance - currentBalance)
/ g_DailyRisk.startBalance * 100.0; // VD: start=10000, now=9700 → lostPct=3.0%
g_DailyRisk.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Lưu vào struct, DrawDebug dùng lại
if (lostPct >= InpMaxDailyLossPct) // Vượt ngưỡng → khóa EA cả ngày hôm nay
double lostPct = (g_DailyRisk.startBalance - g_DailyRisk.currentBalance)
/ g_DailyRisk.startBalance * 100.0;
if (lostPct >= InpMaxDailyLossPct)
{
g_DailyRisk.limitHit = true; // IsDailyLossOK() = false → EvaluateGuards() fail → ResetToIdle()
g_DailyRisk.limitHit = true;
PrintFormat("[DAILY RISK] ⛔ Limit hit | lost=%.2f%% | limit=%.2f%% | balance=%.2f",
lostPct,
InpMaxDailyLossPct,
currentBalance);
lostPct, InpMaxDailyLossPct, g_DailyRisk.currentBalance);
}
}
void UpdateAllContexts()
{
UpdateDailyRiskContext();
UpdateBiasContext();
// Không có StructureContext riêng nữa
// Structure check sẽ nằm trong từng state handler khi cần
UpdateDailyRiskContext(); // Mỗi tick (early-exit khi limitHit)
UpdateBiasContext(); // 1 lần/bar (guard lastBarTime)
}
//====================================================
// GUARDS
//====================================================
bool IsSessionAllowed()
{
return true; // TODO
}
bool IsDailyLossOK()
{
return !g_DailyRisk.limitHit;
}
bool IsBiasValid()
{
return g_Bias.bias == BIAS_UP || g_Bias.bias == BIAS_DOWN;
}
bool IsSessionAllowed() { return true; /* TODO */ }
bool IsDailyLossOK() { return !g_DailyRisk.limitHit; }
bool IsBiasValid() { return g_Bias.bias == BIAS_UP || g_Bias.bias == BIAS_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 (!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; }
return true;
}
@@ -285,10 +244,7 @@ bool EvaluateGuards()
void TransitionTo(EAState next)
{
if (InpDebugLog)
PrintFormat("[STATE] %s → %s",
EnumToString(g_State),
EnumToString(next));
PrintFormat("[STATE] %s → %s", EnumToString(g_State), EnumToString(next));
g_State = next;
}
@@ -301,64 +257,196 @@ void ResetToIdle(string reason = "")
ZeroMemory(g_Trigger);
g_TradeBarIndex = -1;
// Xóa FVG objects khi reset
ObjectDelete(0, FVG_RECT_NAME);
ObjectDelete(0, FVG_MID_NAME);
ObjectDelete(0, FVG_LABEL_NAME);
TransitionTo(EA_IDLE);
}
//====================================================
// FVG HELPERS
//====================================================
bool IsCandleStrong(ENUM_TIMEFRAMES tf, int barIndex)
{
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;
double body = MathAbs(close - open);
return (body / range * 100.0) >= InpFVGMinBodyPct;
}
// Fill = bar sau FVG có close đi vào vùng gap
bool IsFVGAlreadyFilled(
ENUM_TIMEFRAMES tf,
int fvgRightIdx, // Index nến phải của FVG pattern
double fvgHigh,
double fvgLow,
MarketDir dir)
{
for (int j = fvgRightIdx - 1; j >= 1; j--)
{
double close = iClose(_Symbol, tf, j);
if (dir == DIR_UP && close <= fvgHigh) return true; // Retraced vào gap bullish
if (dir == DIR_DOWN && close >= fvgLow) return true; // Retraced vào gap bearish
}
return false;
}
bool IsFVGExpired()
{
if (g_FVG.createdTime <= 0) return false;
return (int)(TimeCurrent() - g_FVG.createdTime) > InpFVGMaxAliveMin * 60;
}
// Invalidate = MiddleTF close xuyên hoàn toàn qua FVG ngược chiều
bool IsFVGInvalidated()
{
double lastClose = iClose(_Symbol, InpMiddleTF, 1);
if (g_FVG.direction == DIR_UP && lastClose < g_FVG.low) return true;
if (g_FVG.direction == DIR_DOWN && lastClose > g_FVG.high) return true;
return false;
}
//====================================================
// FVG DETECTION
//
// Pattern 3 nến tại index i (i = nến giữa):
// bar[i+1] = nến trái (older)
// bar[i] = nến giữa (impulse phải mạnh)
// bar[i-1] = nến phải (newer)
//
// Bullish FVG: bar[i+1].high < bar[i-1].low
// gapLow = bar[i+1].high (đỉnh nến trái = cạnh dưới gap)
// gapHigh = bar[i-1].low (đáy nến phải = cạnh trên gap)
//
// Bearish FVG: bar[i+1].low > bar[i-1].high
// gapHigh = bar[i+1].low (đáy nến trái = cạnh trên gap)
// gapLow = bar[i-1].high (đỉnh nến phải = cạnh dưới gap)
//====================================================
bool FindFVGCandidate(FVGContext &fvg)
{
MarketDir targetDir = (g_Bias.bias == BIAS_UP) ? DIR_UP : DIR_DOWN;
int maxBar = MathMin(InpFVGScanBars, Bars(_Symbol, InpMiddleTF) - 2);
for (int i = 2; i <= maxBar; i++)
{
double leftHigh = iHigh (_Symbol, InpMiddleTF, i + 1);
double leftLow = iLow (_Symbol, InpMiddleTF, i + 1);
double rightHigh = iHigh (_Symbol, InpMiddleTF, i - 1);
double rightLow = iLow (_Symbol, InpMiddleTF, i - 1);
double midOpen = iOpen (_Symbol, InpMiddleTF, i);
double midClose = iClose(_Symbol, InpMiddleTF, i);
double gapHigh, gapLow;
if (targetDir == DIR_UP)
{
if (leftHigh >= rightLow) continue; // Không có gap
if (midClose <= midOpen) continue; // Nến giữa phải bullish
if (!IsCandleStrong(InpMiddleTF, i)) continue; // Body >= InpFVGMinBodyPct%
gapLow = leftHigh; // Đỉnh nến trái = cạnh dưới gap
gapHigh = rightLow; // Đáy nến phải = cạnh trên gap
}
else // DIR_DOWN
{
if (leftLow <= rightHigh) continue; // Không có gap
if (midClose >= midOpen) continue; // Nến giữa phải bearish
if (!IsCandleStrong(InpMiddleTF, i)) continue; // Body >= InpFVGMinBodyPct%
gapHigh = leftLow; // Đáy nến trái = cạnh trên gap
gapLow = rightHigh; // Đỉnh nến phải = cạnh dưới gap
}
if (IsFVGAlreadyFilled(InpMiddleTF, i - 1, gapHigh, gapLow, targetDir))
continue;
// ── FVG hợp lệ → điền struct ────────────────────────────────
fvg.active = true;
fvg.touched = false;
fvg.direction = targetDir;
fvg.high = gapHigh;
fvg.low = gapLow;
fvg.mid = (gapHigh + gapLow) / 2.0;
fvg.createdTime = iTime(_Symbol, InpMiddleTF, i - 1); // Nến phải đóng = FVG confirmed
fvg.touchTime = 0;
fvg.barsAlive = 0;
fvg.touchBarIdx = -1;
return true;
}
return false;
}
//====================================================
// STATE HANDLERS
//====================================================
void OnStateIdle()
{
// Điều kiện: bias phải rõ (UP hoặc DOWN)
// TODO: Quét Middle TF tìm FVG candidate khớp với bias
// Nếu tìm được FVG hợp lệ:
// → g_FVG = <FVG vừa tìm>
// → g_FVG.active = true
// → TransitionTo(EA_WAIT_TOUCH)
FVGContext candidate;
ZeroMemory(candidate);
if (!FindFVGCandidate(candidate)) return;
g_FVG = candidate;
if (InpDebugLog)
PrintFormat("[FVG FOUND] dir=%s | high=%.5f | low=%.5f | mid=%.5f | created=%s",
EnumToString(g_FVG.direction), g_FVG.high, g_FVG.low, g_FVG.mid,
TimeToString(g_FVG.createdTime));
TransitionTo(EA_WAIT_TOUCH);
}
void OnStateWaitTouch()
{
// TODO: Kiểm tra FVG còn hợp lệ không
// - Expired (quá InpFVGMaxAliveMin)?
// - Bị invalidate (price close xuyên qua FVG ngược chiều)?
// → ResetToIdle()
if (IsFVGExpired()) { ResetToIdle("FVG expired"); return; }
if (IsFVGInvalidated()) { ResetToIdle("FVG invalidated"); return; }
// TODO: Kiểm tra price có nằm trong vùng FVG không
// Nếu có:
// → g_FVG.touched = true
// → g_FVG.touchTime = now
// → g_FVG.touchBarIdx = <bar hiện tại trên TriggerTF>
// → TransitionTo(EA_WAIT_TRIGGER)
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
bool touched = false;
if (g_FVG.direction == DIR_UP && bid <= g_FVG.high) touched = true; // Retraced xuống chạm cạnh trên gap
if (g_FVG.direction == DIR_DOWN && bid >= g_FVG.low) touched = true; // Retraced lên chạm cạnh dưới gap
if (!touched) return;
g_FVG.touched = true;
g_FVG.touchTime = TimeCurrent();
g_FVG.touchBarIdx = iBarShift(_Symbol, InpTriggerTF, TimeCurrent());
g_FVG.barsAlive = 0;
if (InpDebugLog)
PrintFormat("[FVG TOUCHED] dir=%s | price=%.5f | FVG[%.5f %.5f]",
EnumToString(g_FVG.direction), bid, g_FVG.low, g_FVG.high);
TransitionTo(EA_WAIT_TRIGGER);
}
void OnStateWaitTrigger()
{
// TODO: Kiểm tra FVG bị phá không → ResetToIdle()
// TODO: Đếm barsAlive, nếu > InpTriggerMaxBars → ResetToIdle()
// TODO: Mỗi bar mới của TriggerTF:
// → UpdateTriggerContext()
// TODO: Kiểm tra điều kiện trigger (vd: structure break trên TriggerTF)
// Nếu đủ:
// → plan = BuildOrderPlan()
// → ExecuteOrder(plan)
// → g_TradeBarIndex = Bars(_Symbol, InpTriggerTF)
// → TransitionTo(EA_IN_TRADE)
// TODO: Kiểm tra FVG bị phá → ResetToIdle("FVG broken")
// TODO: barsAlive > InpTriggerMaxBars → ResetToIdle("trigger timeout")
// TODO: Bar TriggerTF mới → UpdateTriggerContext()
// TODO: Điều kiện trigger đủ → BuildOrderPlan → ExecuteOrder → EA_IN_TRADE
}
void OnStateInTrade()
{
// Không xử lý trong cùng bar vừa gửi lệnh
if (Bars(_Symbol, InpTriggerTF) <= g_TradeBarIndex)
return;
if (Bars(_Symbol, InpTriggerTF) <= g_TradeBarIndex) return; // Cùng bar vừa gửi lệnh → bỏ qua
// TODO: Kiểm tra position còn sống không
// (PositionSelect hoặc OrdersTotal)
// Nếu lệnh đã đóng → ResetToIdle()
// TODO: Còn position / pending? Không còn → ResetToIdle("trade closed")
}
//====================================================
@@ -377,10 +465,177 @@ void RunStateMachine()
}
//====================================================
// DRAW (stub)
// FVG DRAWING
//
// Rectangle:
// Cạnh trái = createdTime (nến phải của pattern = thời điểm FVG confirmed)
// Cạnh phải = WAIT_TOUCH : bar MiddleTF hiện tại (live, cập nhật mỗi tick)
// = WAIT_TRIGGER: touchTime + 1 bar MiddleTF (cố định sau khi touched)
// Cạnh trên = FVG.high
// Cạnh dưới = FVG.low
// Đường giữa = FVG.mid (đứt nét)
//====================================================
void DrawVisuals() { /* TODO */ }
void DrawFVGRectangle()
{
if (!InpDebugDraw) return;
if (!g_FVG.active)
{
ObjectDelete(0, FVG_RECT_NAME);
ObjectDelete(0, FVG_MID_NAME);
ObjectDelete(0, FVG_LABEL_NAME);
return;
}
// ── Xác định cạnh phải của rectangle ────────────────────────────────
datetime rectEnd;
if (!g_FVG.touched)
{
rectEnd = iTime(_Symbol, InpMiddleTF, 0); // Live: kéo tới bar hiện tại
}
else
{
int touchShift = iBarShift(_Symbol, InpMiddleTF, g_FVG.touchTime);
int nextShift = MathMax(touchShift - 1, 0); // Bar MiddleTF liền sau touch
rectEnd = iTime(_Symbol, InpMiddleTF, nextShift); // Cố định tại n+1
}
// ── Màu theo state và chiều ──────────────────────────────────────────
color rectColor = !g_FVG.touched
? (g_FVG.direction == DIR_UP ? C'0,80,160' : C'140,40,0') // Đậm khi candidate
: (g_FVG.direction == DIR_UP ? C'0,160,255' : C'255,100,0'); // Sáng khi touched
// ── Rectangle ────────────────────────────────────────────────────────
if (ObjectFind(0, FVG_RECT_NAME) < 0)
ObjectCreate(0, FVG_RECT_NAME, OBJ_RECTANGLE, 0,
g_FVG.createdTime, g_FVG.high,
rectEnd, g_FVG.low);
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_COLOR, rectColor);
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_FILL, true);
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_BACK, true);
ObjectSetInteger(0, FVG_RECT_NAME, OBJPROP_WIDTH, 1);
ObjectMove(0, FVG_RECT_NAME, 0, g_FVG.createdTime, g_FVG.high); // Cạnh trái cố định
ObjectMove(0, FVG_RECT_NAME, 1, rectEnd, g_FVG.low); // Cạnh phải cập nhật
// ── Đường midpoint (đứt nét) ─────────────────────────────────────────
if (ObjectFind(0, FVG_MID_NAME) < 0)
ObjectCreate(0, FVG_MID_NAME, OBJ_TREND, 0,
g_FVG.createdTime, g_FVG.mid,
rectEnd, g_FVG.mid);
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_COLOR, clrSilver);
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, FVG_MID_NAME, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, FVG_MID_NAME, 0, g_FVG.createdTime, g_FVG.mid);
ObjectMove(0, FVG_MID_NAME, 1, rectEnd, g_FVG.mid);
// ── Label ─────────────────────────────────────────────────────────────
string labelText = StringFormat("FVG %s%s",
(g_FVG.direction == DIR_UP ? "▲" : "▼"),
(g_FVG.touched ? " [TOUCHED]" : ""));
if (ObjectFind(0, FVG_LABEL_NAME) < 0)
ObjectCreate(0, FVG_LABEL_NAME, OBJ_TEXT, 0,
g_FVG.createdTime, g_FVG.high);
ObjectMove(0, FVG_LABEL_NAME, 0, g_FVG.createdTime, g_FVG.high);
ObjectSetString (0, FVG_LABEL_NAME, OBJPROP_TEXT, labelText);
ObjectSetInteger(0, FVG_LABEL_NAME, OBJPROP_COLOR, rectColor);
ObjectSetInteger(0, FVG_LABEL_NAME, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, FVG_LABEL_NAME, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
}
//====================================================
// DEBUG DRAW
//====================================================
void DrawContextDebug()
{
if (!InpDebugDraw) return;
#define SET_LABEL(name, text, ypos, clr) \
if (ObjectFind(0, name) < 0) \
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); \
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_UPPER); \
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 10); \
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, ypos); \
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9); \
ObjectSetInteger(0, name, OBJPROP_COLOR, clr); \
ObjectSetString (0, name, OBJPROP_TEXT, text);
SET_LABEL("DBG_HEADER", "── Context Debug ──", 10, clrSilver)
// ── BIAS ──────────────────────────────────────────────────────────────
color biasColor = (g_Bias.bias == BIAS_UP) ? clrLime :
(g_Bias.bias == BIAS_DOWN) ? clrTomato :
(g_Bias.bias == BIAS_SIDEWAY) ? clrOrange : clrGray;
SET_LABEL("DBG_BIAS", StringFormat("Bias : %s", EnumToString(g_Bias.bias)), 30, biasColor)
if (g_Bias.bias == BIAS_SIDEWAY)
{
SET_LABEL("DBG_BIAS_RANGE",
StringFormat(" Range : %.5f %.5f", g_Bias.rangeLow, g_Bias.rangeHigh),
48, clrOrange)
}
else
ObjectDelete(0, "DBG_BIAS_RANGE");
// ── DAILY RISK ────────────────────────────────────────────────────────
double lostPct = g_DailyRisk.startBalance > 0
? (g_DailyRisk.startBalance - g_DailyRisk.currentBalance)
/ g_DailyRisk.startBalance * 100.0
: 0.0;
color riskColor = g_DailyRisk.limitHit ? clrRed :
lostPct > InpMaxDailyLossPct * 0.7 ? clrOrange : clrLime;
SET_LABEL("DBG_RISK", StringFormat("Risk : %.2f%% / %.2f%%", lostPct, InpMaxDailyLossPct), 66, riskColor)
SET_LABEL("DBG_BAL", StringFormat("Bal : %.2f (start %.2f)", g_DailyRisk.currentBalance, g_DailyRisk.startBalance), 84, clrSilver)
SET_LABEL("DBG_LIMIT", g_DailyRisk.limitHit ? "⛔ DAILY LOSS HIT" : "✅ Loss OK", 102, g_DailyRisk.limitHit ? clrRed : clrLime)
// ── EA STATE ──────────────────────────────────────────────────────────
color stateColor = (g_State == EA_IDLE) ? clrSilver :
(g_State == EA_WAIT_TOUCH) ? clrOrange :
(g_State == EA_WAIT_TRIGGER) ? clrYellow :
(g_State == EA_IN_TRADE) ? clrLime : clrGray;
SET_LABEL("DBG_STATE", StringFormat("State : %s", EnumToString(g_State)), 120, stateColor)
// ── FVG INFO ──────────────────────────────────────────────────────────
if (g_FVG.active)
{
color fvgColor = g_FVG.touched ? clrDeepSkyBlue : clrDodgerBlue;
SET_LABEL("DBG_FVG",
StringFormat("FVG : %s [%.5f %.5f]",
EnumToString(g_FVG.direction), g_FVG.low, g_FVG.high),
138, fvgColor)
}
else
ObjectDelete(0, "DBG_FVG");
// ── BLOCK REASON ──────────────────────────────────────────────────────
if (g_BlockReason != BLOCK_NONE)
{
SET_LABEL("DBG_BLOCK",
StringFormat("Block : %s", EnumToString(g_BlockReason)),
156, clrTomato)
}
else
ObjectDelete(0, "DBG_BLOCK");
#undef SET_LABEL
ChartRedraw(0);
}
void DrawVisuals()
{
DrawFVGRectangle();
DrawContextDebug();
}
//====================================================
// EA LIFECYCLE
@@ -397,13 +652,10 @@ int OnInit()
g_BlockReason = BLOCK_NONE;
g_TradeBarIndex = -1;
// Chạy update ngay lần đầu để có data sẵn
UpdateAllContexts();
PrintFormat("✅ EA initialized | BiasTF=%s | MiddleTF=%s | TriggerTF=%s",
EnumToString(InpBiasTF),
EnumToString(InpMiddleTF),
EnumToString(InpTriggerTF));
EnumToString(InpBiasTF), EnumToString(InpMiddleTF), EnumToString(InpTriggerTF));
return INIT_SUCCEEDED;
}
@@ -414,7 +666,7 @@ void OnTick()
if (EvaluateGuards())
RunStateMachine();
else
else if (g_State != EA_IDLE) // Chỉ reset 1 lần khi vừa bị block
ResetToIdle(EnumToString(g_BlockReason));
if (InpDebugDraw)
@@ -423,6 +675,10 @@ void OnTick()
void OnDeinit(const int reason)
{
// TODO: ObjectsDeleteAll hoặc xóa từng object đã vẽ
ObjectDelete(0, FVG_RECT_NAME);
ObjectDelete(0, FVG_MID_NAME);
ObjectDelete(0, FVG_LABEL_NAME);
ObjectsDeleteAll(0, "DBG_");
ChartRedraw(0);
PrintFormat("EA deinitialized | reason=%d", reason);
}