Files
Bell-PriceActionWithEma-EA/Experts/SimpleFVG.mq5
T
2026-03-11 01:01:25 +07:00

778 lines
25 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.
//+------------------------------------------------------------------+
//| SimpleFVG EA Pure Fair Value Gap Strategy |
//| |
//| Logic: |
//| 1. Scan 3-candle pattern: gap between High[i+2] and Low[i] |
//| 2. Bullish FVG: High(candle1) < Low(candle3) + strong candle2 |
//| 3. Place Buy Limit at FVG upper edge (Low of candle3) |
//| 4. SL = Low(candle1), TP = entry + 2R |
//| 5. Invalidation: close below FVG or timeout |
//| |
//| Bearish FVG: mirror logic |
//+------------------------------------------------------------------+
#property copyright "SimpleFVG"
#property version "1.00"
input ENUM_TIMEFRAMES InpTimeframe = PERIOD_H1;
input double InpRiskPercent = 1.0;
input double InpRiskReward = 2.0;
input double InpFVGMinBodyPct = 50.0; // body% tối thiểu của nến giữa
input int InpFVGMaxAgeBars = 20; // FVG chưa fill sau N bars → hủy
input int InpSLBufferPts = 20; // buffer thêm cho SL (points)
input double InpEntryDepthPct = 50.0; // entry tại X% chiều sâu FVG (0=edge, 50=mid, 100=đáy)
input int InpEMAPeriod = 50; // EMA trend filter trên TF chính
input ENUM_TIMEFRAMES InpHTF = PERIOD_D1; // Higher TF confirmation
input int InpHTFEMAPeriod = 50; // EMA period trên HTF
input bool InpTrailBE = true; // dời SL về entry khi đạt 1R profit
input long InpMagicNumber = 20260311;
input int InpSlippage = 5;
input bool InpDebugLog = true;
//--- Constants ---
const string PFX = "SFVG_";
const string PFX_ORDER = "SFVG_ORD_";
//--- EMA handles ---
int g_EMAHandle = INVALID_HANDLE;
int g_HTFEMAHandle = INVALID_HANDLE;
string g_EMAShortName = "";
string g_HTFEMAShortName = "";
//--- FVG record ---
struct FVGSlot
{
bool active;
int direction; // 1=bullish, -1=bearish
double high; // cạnh trên FVG
double low; // cạnh dưới FVG
double slPrice; // SL level (Low candle1 cho bull, High candle1 cho bear)
double entryPrice; // giá entry thực tế (midpoint hoặc edge tùy InpEntryDepthPct)
datetime createdTime;
int ageBars;
ulong ticket; // ticket lệnh limit đã đặt (0 = chưa đặt)
};
#define MAX_FVG 10
FVGSlot g_Slots[MAX_FVG];
int g_SlotCount = 0;
datetime g_LastBarTime = 0;
int g_LastDay = -1;
//+------------------------------------------------------------------+
//| HELPER FUNCTIONS |
//+------------------------------------------------------------------+
bool IsNewBar()
{
datetime t = iTime(_Symbol, InpTimeframe, 0);
if (t == g_LastBarTime) return false;
g_LastBarTime = t;
return true;
}
double GetEMA(int shift)
{
double buf[1];
if (g_EMAHandle == INVALID_HANDLE) return 0;
if (CopyBuffer(g_EMAHandle, 0, shift, 1, buf) < 1) return 0;
return buf[0];
}
double GetHTFEMA(int shift)
{
double buf[1];
if (g_HTFEMAHandle == INVALID_HANDLE) return 0;
if (CopyBuffer(g_HTFEMAHandle, 0, shift, 1, buf) < 1) return 0;
return buf[0];
}
/** Kiểm tra cả 2 EMA (TF chính + HTF) đều đồng thuận hướng dir. */
int GetTrendDirection()
{
double ema = GetEMA(1);
double htf = GetHTFEMA(1);
double cls = iClose(_Symbol, InpTimeframe, 1);
double hCls = iClose(_Symbol, InpHTF, 1);
if (ema == 0 || htf == 0) return 0;
bool tfBull = cls > ema;
bool htfBull = hCls > htf;
bool tfBear = cls < ema;
bool htfBear = hCls < htf;
if (tfBull && htfBull) return 1;
if (tfBear && htfBear) return -1;
return 0;
}
bool IsCandleStrong(int shift)
{
double h = iHigh (_Symbol, InpTimeframe, shift);
double l = iLow (_Symbol, InpTimeframe, shift);
double o = iOpen (_Symbol, InpTimeframe, shift);
double c = iClose(_Symbol, InpTimeframe, shift);
double range = h - l;
if (range < _Point) return false;
return (MathAbs(c - o) / range * 100.0) >= InpFVGMinBodyPct;
}
double CalcLot(double entry, double sl)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskMoney = balance * InpRiskPercent / 100.0;
double slDist = MathAbs(entry - sl) / _Point;
if (slDist < 1) return 0;
double tickVal = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if (tickVal <= 0 || tickSize <= 0) return 0;
double pipVal = tickVal * (_Point / tickSize);
double lot = riskMoney / (slDist * pipVal);
double minL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if (step <= 0) step = 0.01;
lot = MathFloor(lot / step) * step;
lot = MathMax(minL, MathMin(maxL, lot));
return NormalizeDouble(lot, 2);
}
bool HasPendingOrPosition()
{
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if (ticket > 0
&& OrderGetInteger(ORDER_MAGIC) == InpMagicNumber
&& OrderGetString(ORDER_SYMBOL) == _Symbol)
return true;
}
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if (ticket > 0
&& PositionGetInteger(POSITION_MAGIC) == InpMagicNumber
&& PositionGetString(POSITION_SYMBOL) == _Symbol)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| FVG SCANNING |
//+------------------------------------------------------------------+
void ScanFVGs()
{
int trendDir = GetTrendDirection();
int maxBar = MathMin(InpFVGMaxAgeBars + 5, Bars(_Symbol, InpTimeframe) - 3);
for (int i = 1; i <= maxBar; i++)
{
double c1High = iHigh (_Symbol, InpTimeframe, i + 2);
double c1Low = iLow (_Symbol, InpTimeframe, i + 2);
double c2Open = iOpen (_Symbol, InpTimeframe, i + 1);
double c2Close= iClose(_Symbol, InpTimeframe, i + 1);
double c3High = iHigh (_Symbol, InpTimeframe, i);
double c3Low = iLow (_Symbol, InpTimeframe, i);
datetime fvgTime = iTime(_Symbol, InpTimeframe, i);
bool alreadyExists = false;
for (int j = 0; j < g_SlotCount; j++)
if (g_Slots[j].active && g_Slots[j].createdTime == fvgTime)
{ alreadyExists = true; break; }
if (alreadyExists) continue;
// --- Bullish FVG: chỉ khi trend = +1 ---
if (trendDir >= 1 && c1High < c3Low
&& c2Close > c2Open && IsCandleStrong(i + 1))
{
if (g_SlotCount >= MAX_FVG) continue;
double fvgH = c3Low;
double fvgL = c1High;
double depth = (fvgH - fvgL) * InpEntryDepthPct / 100.0;
double entry = NormalizeDouble(fvgH - depth, _Digits);
ZeroMemory(g_Slots[g_SlotCount]);
g_Slots[g_SlotCount].active = true;
g_Slots[g_SlotCount].direction = 1;
g_Slots[g_SlotCount].high = fvgH;
g_Slots[g_SlotCount].low = fvgL;
g_Slots[g_SlotCount].slPrice = c1Low;
g_Slots[g_SlotCount].entryPrice = entry;
g_Slots[g_SlotCount].createdTime = fvgTime;
g_Slots[g_SlotCount].ageBars = 0;
g_Slots[g_SlotCount].ticket = 0;
g_SlotCount++;
if (InpDebugLog)
PrintFormat("[FVG+] BULL [%.5f%.5f] entry=%.5f SL=%.5f trend=%d | %s",
fvgL, fvgH, entry, c1Low, trendDir, TimeToString(fvgTime));
}
// --- Bearish FVG: chỉ khi trend = -1 ---
if (trendDir <= -1 && c1Low > c3High
&& c2Close < c2Open && IsCandleStrong(i + 1))
{
if (g_SlotCount >= MAX_FVG) continue;
double fvgH = c1Low;
double fvgL = c3High;
double depth = (fvgH - fvgL) * InpEntryDepthPct / 100.0;
double entry = NormalizeDouble(fvgL + depth, _Digits);
ZeroMemory(g_Slots[g_SlotCount]);
g_Slots[g_SlotCount].active = true;
g_Slots[g_SlotCount].direction = -1;
g_Slots[g_SlotCount].high = fvgH;
g_Slots[g_SlotCount].low = fvgL;
g_Slots[g_SlotCount].slPrice = c1High;
g_Slots[g_SlotCount].entryPrice = entry;
g_Slots[g_SlotCount].createdTime = fvgTime;
g_Slots[g_SlotCount].ageBars = 0;
g_Slots[g_SlotCount].ticket = 0;
g_SlotCount++;
if (InpDebugLog)
PrintFormat("[FVG+] BEAR [%.5f%.5f] entry=%.5f SL=%.5f trend=%d | %s",
fvgL, fvgH, entry, c1High, trendDir, TimeToString(fvgTime));
}
}
}
//+------------------------------------------------------------------+
//| ORDER MANAGEMENT |
//+------------------------------------------------------------------+
void PlaceLimitForSlot(int idx)
{
if (g_Slots[idx].ticket != 0) return;
if (HasPendingOrPosition()) return;
double entry, sl, tp;
ENUM_ORDER_TYPE orderType;
if (g_Slots[idx].direction > 0)
{
entry = NormalizeDouble(g_Slots[idx].entryPrice, _Digits);
sl = NormalizeDouble(g_Slots[idx].slPrice - InpSLBufferPts * _Point, _Digits);
double rD = entry - sl;
if (rD <= 0) return;
tp = NormalizeDouble(entry + InpRiskReward * rD, _Digits);
orderType = ORDER_TYPE_BUY_LIMIT;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if (entry >= ask) return;
}
else
{
entry = NormalizeDouble(g_Slots[idx].entryPrice, _Digits);
sl = NormalizeDouble(g_Slots[idx].slPrice + InpSLBufferPts * _Point, _Digits);
double rD = sl - entry;
if (rD <= 0) return;
tp = NormalizeDouble(entry - InpRiskReward * rD, _Digits);
orderType = ORDER_TYPE_SELL_LIMIT;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if (entry <= bid) return;
}
double lot = CalcLot(entry, sl);
if (lot <= 0) return;
MqlTradeRequest req;
MqlTradeResult res;
ZeroMemory(req);
ZeroMemory(res);
req.action = TRADE_ACTION_PENDING;
req.symbol = _Symbol;
req.volume = lot;
req.type = orderType;
req.price = entry;
req.sl = sl;
req.tp = tp;
req.deviation = (ulong)InpSlippage;
req.magic = InpMagicNumber;
req.comment = StringFormat("FVG_%s", TimeToString(g_Slots[idx].createdTime, TIME_DATE));
req.type_filling = ORDER_FILLING_RETURN;
req.type_time = ORDER_TIME_GTC;
if (!OrderSend(req, res))
{
PrintFormat("[ORDER] ❌ retcode=%u", res.retcode);
return;
}
if (res.retcode != TRADE_RETCODE_DONE && res.retcode != TRADE_RETCODE_PLACED)
{
PrintFormat("[ORDER] ❌ rejected %u: %s", res.retcode, res.comment);
return;
}
g_Slots[idx].ticket = res.order;
PrintFormat("[ORDER] ✅ %s #%llu | %.2f @ %.5f SL=%.5f TP=%.5f",
(g_Slots[idx].direction > 0) ? "BUY_LIM" : "SELL_LIM",
res.order, lot, entry, sl, tp);
}
bool IsTicketStillPending(ulong ticket)
{
for (int i = OrdersTotal() - 1; i >= 0; i--)
if (OrderGetTicket(i) == ticket) return true;
return false;
}
void CancelOrder(ulong ticket)
{
MqlTradeRequest req;
MqlTradeResult res;
ZeroMemory(req);
ZeroMemory(res);
req.action = TRADE_ACTION_REMOVE;
req.order = ticket;
if (OrderSend(req, res))
{
if (InpDebugLog)
PrintFormat("[CANCEL] #%llu removed", ticket);
}
}
//+------------------------------------------------------------------+
//| TRAILING SL (BREAKEVEN) |
//+------------------------------------------------------------------+
/** Dời SL về entry khi profit đạt 1R. Gọi mỗi tick. */
void TrailBreakeven()
{
if (!InpTrailBE) return;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if (ticket == 0) continue;
if (PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double curSL = PositionGetDouble(POSITION_SL);
double curTP = PositionGetDouble(POSITION_TP);
bool isBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
double slDist = MathAbs(openPrice - curSL);
if (slDist < _Point) continue;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
bool alreadyBE = isBuy ? (curSL >= openPrice) : (curSL <= openPrice);
if (alreadyBE) continue;
double profit1R = isBuy ? (openPrice + slDist) : (openPrice - slDist);
bool reached = isBuy ? (bid >= profit1R) : (ask <= profit1R);
if (!reached) continue;
double newSL = NormalizeDouble(openPrice + (isBuy ? 1 : -1) * _Point, _Digits);
MqlTradeRequest req;
MqlTradeResult res;
ZeroMemory(req);
ZeroMemory(res);
req.action = TRADE_ACTION_SLTP;
req.position = ticket;
req.symbol = _Symbol;
req.sl = newSL;
req.tp = curTP;
if (OrderSend(req, res))
{
if (InpDebugLog)
PrintFormat("[TRAIL] #%llu SL moved to breakeven %.5f", ticket, newSL);
}
}
}
//+------------------------------------------------------------------+
//| DAILY RESET |
//+------------------------------------------------------------------+
/** Hủy tất cả pending limits + slots khi sang ngày mới. */
void DailyReset()
{
MqlDateTime now;
TimeCurrent(now);
if (now.day == g_LastDay) return;
g_LastDay = now.day;
int cancelled = 0;
for (int i = g_SlotCount - 1; i >= 0; i--)
{
if (!g_Slots[i].active) continue;
if (g_Slots[i].ticket != 0 && IsTicketStillPending(g_Slots[i].ticket))
{
CancelOrder(g_Slots[i].ticket);
cancelled++;
}
g_Slots[i].active = false;
}
int writeIdx = 0;
for (int i = 0; i < g_SlotCount; i++)
{
if (g_Slots[i].active)
{
if (writeIdx != i) g_Slots[writeIdx] = g_Slots[i];
writeIdx++;
}
}
g_SlotCount = writeIdx;
if (InpDebugLog && cancelled > 0)
PrintFormat("[DAILY] New day %04d.%02d.%02d → cancelled %d pending orders, cleared slots",
now.year, now.mon, now.day, cancelled);
}
//+------------------------------------------------------------------+
//| SLOT UPDATE & INVALIDATION |
//+------------------------------------------------------------------+
void UpdateSlots()
{
double lastClose = iClose(_Symbol, InpTimeframe, 1);
for (int i = g_SlotCount - 1; i >= 0; i--)
{
if (!g_Slots[i].active) continue;
g_Slots[i].ageBars++;
// Invalidation 1: close hoàn toàn bên ngoài FVG → hủy
bool invalidated = false;
if (g_Slots[i].direction > 0 && lastClose < g_Slots[i].low)
invalidated = true;
if (g_Slots[i].direction < 0 && lastClose > g_Slots[i].high)
invalidated = true;
// Invalidation 2: timeout
if (g_Slots[i].ageBars > InpFVGMaxAgeBars)
invalidated = true;
// Invalidation 3: limit đã fill → position mở → slot hoàn thành
if (g_Slots[i].ticket != 0 && !IsTicketStillPending(g_Slots[i].ticket))
{
g_Slots[i].active = false;
if (InpDebugLog)
PrintFormat("[SLOT] #%llu filled or gone → deactivate", g_Slots[i].ticket);
continue;
}
if (invalidated)
{
if (g_Slots[i].ticket != 0 && IsTicketStillPending(g_Slots[i].ticket))
CancelOrder(g_Slots[i].ticket);
g_Slots[i].active = false;
if (InpDebugLog)
PrintFormat("[SLOT] %s FVG [%.5f%.5f] invalidated | age=%d",
(g_Slots[i].direction > 0) ? "BULL" : "BEAR",
g_Slots[i].low, g_Slots[i].high, g_Slots[i].ageBars);
continue;
}
// Chưa đặt lệnh → đặt limit
if (g_Slots[i].ticket == 0)
PlaceLimitForSlot(i);
}
// Dọn slots không active
int writeIdx = 0;
for (int i = 0; i < g_SlotCount; i++)
{
if (g_Slots[i].active)
{
if (writeIdx != i) g_Slots[writeIdx] = g_Slots[i];
writeIdx++;
}
}
g_SlotCount = writeIdx;
}
//+------------------------------------------------------------------+
//| DRAWING |
//+------------------------------------------------------------------+
void SetRect(string name, datetime t1, double p1, datetime t2, double p2, color clr)
{
if (ObjectFind(0, name) < 0)
ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, p1, t2, p2);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_FILL, true);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectMove(0, name, 0, t1, p1);
ObjectMove(0, name, 1, t2, p2);
}
void SetHLine(string name, datetime t1, double p, datetime t2, color clr, int style, int width)
{
if (ObjectFind(0, name) < 0)
ObjectCreate(0, name, OBJ_TREND, 0, t1, p, t2, p);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_STYLE, style);
ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
ObjectMove(0, name, 0, t1, p);
ObjectMove(0, name, 1, t2, p);
}
void SetLabel(string name, int x, int y, string txt, color clr, int fontSize)
{
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, x);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetString (0, name, OBJPROP_TEXT, txt);
}
void SetText(string name, datetime t, double p, string txt, color clr, int fontSize, int anchor)
{
if (ObjectFind(0, name) < 0)
ObjectCreate(0, name, OBJ_TEXT, 0, t, p);
ObjectMove(0, name, 0, t, p);
ObjectSetString (0, name, OBJPROP_TEXT, txt);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
}
void DrawFVGZones()
{
ObjectsDeleteAll(0, PFX + "FVG_");
datetime now = iTime(_Symbol, InpTimeframe, 0);
for (int i = 0; i < g_SlotCount; i++)
{
if (!g_Slots[i].active) continue;
string sid = IntegerToString(i);
bool isBull = (g_Slots[i].direction > 0);
color zClr = isBull ? C'0,60,120' : C'100,25,0';
color bClr = isBull ? C'0,120,200' : C'200,60,0';
SetRect(PFX + "FVG_Z_" + sid,
g_Slots[i].createdTime, g_Slots[i].high,
now, g_Slots[i].low, zClr);
SetHLine(PFX + "FVG_E_" + sid,
g_Slots[i].createdTime, g_Slots[i].entryPrice, now,
C'200,200,0', STYLE_DOT, 1);
SetText(PFX + "FVG_L_" + sid,
g_Slots[i].createdTime, g_Slots[i].high,
StringFormat("FVG %s [%.3f%.3f] ent=%.3f",
isBull ? "▲" : "▼", g_Slots[i].low, g_Slots[i].high, g_Slots[i].entryPrice),
bClr, 7, ANCHOR_LEFT_LOWER);
}
}
void DrawOrderVisual()
{
string entN = PFX_ORDER + "ENT";
string slN = PFX_ORDER + "SLL";
string tpN = PFX_ORDER + "TPL";
string tpZ = PFX_ORDER + "TPZ";
string slZ = PFX_ORDER + "SLZ";
string eL = PFX_ORDER + "ELBL";
string sL = PFX_ORDER + "SLBL";
string tL = PFX_ORDER + "TPLBL";
// Tìm slot có lệnh pending
int activeIdx = -1;
for (int i = 0; i < g_SlotCount; i++)
{
if (g_Slots[i].active && g_Slots[i].ticket != 0)
{ activeIdx = i; break; }
}
if (activeIdx < 0)
{
ObjectDelete(0, entN); ObjectDelete(0, slN); ObjectDelete(0, tpN);
ObjectDelete(0, tpZ); ObjectDelete(0, slZ);
ObjectDelete(0, eL); ObjectDelete(0, sL); ObjectDelete(0, tL);
return;
}
bool isBull = (g_Slots[activeIdx].direction > 0);
double entry, sl, tp;
if (isBull)
{
entry = g_Slots[activeIdx].entryPrice;
sl = NormalizeDouble(g_Slots[activeIdx].slPrice - InpSLBufferPts * _Point, _Digits);
double rD = entry - sl;
if (rD <= 0) return;
tp = NormalizeDouble(entry + InpRiskReward * rD, _Digits);
}
else
{
entry = g_Slots[activeIdx].entryPrice;
sl = NormalizeDouble(g_Slots[activeIdx].slPrice + InpSLBufferPts * _Point, _Digits);
double rD = sl - entry;
if (rD <= 0) return;
tp = NormalizeDouble(entry - InpRiskReward * rD, _Digits);
}
datetime tStart = g_Slots[activeIdx].createdTime;
datetime tEnd = iTime(_Symbol, InpTimeframe, 0) + PeriodSeconds(InpTimeframe) * 8;
color entClr = isBull ? C'33,150,243' : C'255,152,0';
color tpClr = C'38,166,91';
color slClr = C'229,57,53';
double tpTop = isBull ? tp : entry;
double tpBot = isBull ? entry : tp;
SetRect(tpZ, tStart, tpTop, tEnd, tpBot, C'15,60,30');
double slTop = isBull ? entry : sl;
double slBot = isBull ? sl : entry;
SetRect(slZ, tStart, slTop, tEnd, slBot, C'75,12,12');
SetHLine(entN, tStart, entry, tEnd, entClr, STYLE_SOLID, 2);
SetHLine(tpN, tStart, tp, tEnd, tpClr, STYLE_DASH, 1);
SetHLine(slN, tStart, sl, tEnd, slClr, STYLE_DASH, 1);
datetime lblT = tEnd + PeriodSeconds(InpTimeframe);
double slPips = MathAbs(entry - sl) / _Point;
double tpPips = MathAbs(tp - entry) / _Point;
double rr = (slPips > 0) ? tpPips / slPips : 0;
double lot = CalcLot(entry, sl);
SetText(eL, lblT, entry,
StringFormat("%s %.3f | %.2f lot", isBull ? "BUY LIM" : "SELL LIM", entry, lot),
entClr, 8, ANCHOR_LEFT);
SetText(tL, lblT, tp,
StringFormat("TP %.3f | +%.0fp (%.1fR)", tp, tpPips, rr),
tpClr, 7, isBull ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
SetText(sL, lblT, sl,
StringFormat("SL %.3f | -%.0fp", sl, slPips),
slClr, 7, isBull ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER);
}
void DrawDebugPanel()
{
int pending = 0, total = 0;
for (int i = 0; i < g_SlotCount; i++)
if (g_Slots[i].active) { total++; if (g_Slots[i].ticket != 0) pending++; }
bool inPos = false;
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong t = PositionGetTicket(i);
if (t > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber
&& PositionGetString(POSITION_SYMBOL) == _Symbol)
{ inPos = true; break; }
}
int trendDir = GetTrendDirection();
string trendTxt = (trendDir > 0) ? "BULL" : (trendDir < 0) ? "BEAR" : "NEUTRAL";
color trendClr = (trendDir > 0) ? clrLime : (trendDir < 0) ? clrTomato : clrGray;
double ema = GetEMA(1);
double htfEma = GetHTFEMA(1);
SetLabel(PFX + "HDR", 10, 20,
StringFormat("SimpleFVG v2 | %s | Slots=%d Pend=%d | Trail=%s",
EnumToString(InpTimeframe), total, pending, InpTrailBE ? "ON" : "OFF"),
clrSilver, 9);
SetLabel(PFX + "TRD", 10, 44,
StringFormat("Trend: %s | EMA%.0f=%.3f | HTF EMA%.0f=%.3f",
trendTxt, (double)InpEMAPeriod, ema, (double)InpHTFEMAPeriod, htfEma),
trendClr, 9);
color sClr = inPos ? clrLime : (pending > 0) ? clrOrange : clrGray;
string sTxt = inPos ? "IN TRADE" : (pending > 0) ? "LIMIT PLACED" : "SCANNING";
SetLabel(PFX + "ST", 10, 68, StringFormat("State: %s", sTxt), sClr, 9);
}
void DrawAll()
{
DrawFVGZones();
DrawOrderVisual();
DrawDebugPanel();
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| EVENT HANDLERS |
//+------------------------------------------------------------------+
int OnInit()
{
g_EMAHandle = iMA(_Symbol, InpTimeframe, InpEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
if (g_EMAHandle == INVALID_HANDLE)
{
Print("[INIT] Failed to create EMA handle");
return INIT_FAILED;
}
ChartIndicatorAdd(0, 0, g_EMAHandle);
int n1 = ChartIndicatorsTotal(0, 0);
if (n1 > 0) g_EMAShortName = ChartIndicatorName(0, 0, n1 - 1);
g_HTFEMAHandle = iMA(_Symbol, InpHTF, InpHTFEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
if (g_HTFEMAHandle == INVALID_HANDLE)
{
Print("[INIT] Failed to create HTF EMA handle");
return INIT_FAILED;
}
for (int i = 0; i < MAX_FVG; i++) ZeroMemory(g_Slots[i]);
g_SlotCount = 0;
g_LastBarTime = 0;
ScanFVGs();
DrawAll();
if (InpDebugLog)
PrintFormat("[INIT] SimpleFVG v2 | TF=%s HTF=%s | EMA=%d/%d | R=%.1f%% RR=%.1f | DepthPct=%.0f%% Trail=%s",
EnumToString(InpTimeframe), EnumToString(InpHTF),
InpEMAPeriod, InpHTFEMAPeriod,
InpRiskPercent, InpRiskReward,
InpEntryDepthPct, InpTrailBE ? "ON" : "OFF");
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if (g_EMAShortName != "")
ChartIndicatorDelete(0, 0, g_EMAShortName);
if (g_EMAHandle != INVALID_HANDLE)
IndicatorRelease(g_EMAHandle);
if (g_HTFEMAHandle != INVALID_HANDLE)
IndicatorRelease(g_HTFEMAHandle);
ObjectsDeleteAll(0, PFX);
ObjectsDeleteAll(0, PFX_ORDER);
ChartRedraw(0);
}
void OnTick()
{
DailyReset();
TrailBreakeven();
if (IsNewBar())
{
ScanFVGs();
UpdateSlots();
}
DrawAll();
}