//+------------------------------------------------------------------+ //| 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(); }