diff --git a/Experts/SimpleFVG.mq5 b/Experts/SimpleFVG.mq5 index 3e594b6..fb286cf 100644 --- a/Experts/SimpleFVG.mq5 +++ b/Experts/SimpleFVG.mq5 @@ -1,777 +1,67 @@ //+------------------------------------------------------------------+ -//| 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 | +//| SimpleFVG.mq5 – Fair Value Gap Visual Strategy (Test mode) | +//| | +//| Modules: | +//| Config.mqh → Step 0: Inputs, enums, constants | +//| Trend.mqh → Step 1: EMA34/EMA89 trend detection | +//| FVG.mqh → Step 2: FVG scanning & management | +//| Drawing.mqh → Step 4: Chart visualization | //+------------------------------------------------------------------+ #property copyright "SimpleFVG" -#property version "1.00" +#property version "2.00" +#property description "FVG strategy visualizer – EMA trend + FVG zones on chart" -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; +#include "SimpleFVG/Config.mqh" +#include "SimpleFVG/Trend.mqh" +#include "SimpleFVG/FVG.mqh" +#include "SimpleFVG/Drawing.mqh" //+------------------------------------------------------------------+ -//| HELPER FUNCTIONS | +//| OnInit | //+------------------------------------------------------------------+ - -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); + //--- Step 1: Initialize trend (EMA handles + chart indicators) --- + if(!TrendInit()) + return INIT_FAILED; - 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; - } + //--- Step 2: Initialize FVG array --- + FVGInit(); - for (int i = 0; i < MAX_FVG; i++) ZeroMemory(g_Slots[i]); - g_SlotCount = 0; - g_LastBarTime = 0; + //--- Initial scan on existing bars --- + TrendUpdate(); + FVGUpdate(); - ScanFVGs(); - DrawAll(); + //--- Step 4: Draw everything --- + 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; + if(InpDebugLog) + PrintFormat("[INIT] SimpleFVG v2.0 | %s %s | EMA%d/%d | FVG lookback=%d maxAge=%d", + GetTradeSymbol(), EnumToString(InpTimeframe), + InpEMAFastPeriod, InpEMASlowPeriod, + InpFVGLookbackBars, InpFVGMaxAgeBars); + + return INIT_SUCCEEDED; } +//+------------------------------------------------------------------+ +//| OnDeinit | +//+------------------------------------------------------------------+ 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); + TrendDeinit(); + DrawCleanup(); } +//+------------------------------------------------------------------+ +//| OnTick | +//+------------------------------------------------------------------+ void OnTick() { - DailyReset(); - TrailBreakeven(); + if(!IsNewBar()) + return; - if (IsNewBar()) - { - ScanFVGs(); - UpdateSlots(); - } - DrawAll(); + TrendUpdate(); + FVGUpdate(); + DrawAll(); } diff --git a/Experts/SimpleFVG/Config.mqh b/Experts/SimpleFVG/Config.mqh new file mode 100644 index 0000000..dec8630 --- /dev/null +++ b/Experts/SimpleFVG/Config.mqh @@ -0,0 +1,86 @@ +//+------------------------------------------------------------------+ +//| Config.mqh – Step 0: Inputs, Enums, Constants | +//| Cho phép cấu hình symbol, timeframe, EMA, FVG, drawing | +//+------------------------------------------------------------------+ +#ifndef __SIMPLE_FVG_CONFIG_MQH__ +#define __SIMPLE_FVG_CONFIG_MQH__ + +//--- Step 0: Asset & Timeframe --- +input string InpSymbol = ""; // Symbol (empty = current chart) +input ENUM_TIMEFRAMES InpTimeframe = PERIOD_H1; // Analysis Timeframe + +//--- Step 1: Trend (EMA) --- +input int InpEMAFastPeriod = 34; // EMA Fast Period +input int InpEMASlowPeriod = 89; // EMA Slow Period + +//--- Step 2: FVG Detection --- +input int InpFVGLookbackBars = 100; // FVG: Lookback range (bars) +input int InpFVGMaxAgeBars = 50; // FVG: Max age before expiry (bars) +input double InpFVGMinBodyPct = 50.0; // FVG: Min body % of impulse candle +input double InpFVGMinSizePoints = 0; // FVG: Min gap size (points, 0=any) + +//--- Step 4: Drawing --- +input color InpColorBullFVG = C'30,80,140'; // Color: Bullish FVG zone +input color InpColorBearFVG = C'140,30,20'; // Color: Bearish FVG zone +input color InpColorMitigatedFVG = C'60,60,60'; // Color: Mitigated FVG zone +input color InpColorEMAFast = clrDodgerBlue; // Color: EMA Fast line +input color InpColorEMASlow = clrOrangeRed; // Color: EMA Slow line +input bool InpShowPanel = true; // Draw: Show info panel +input bool InpShowFVGLabels = true; // Draw: Show FVG labels + +//--- Debug --- +input bool InpDebugLog = true; // Debug: Enable logging + +//+------------------------------------------------------------------+ +//| Enums | +//+------------------------------------------------------------------+ +enum ENUM_TREND_DIRECTION +{ + TREND_BULLISH = 1, + TREND_BEARISH = -1, + TREND_NEUTRAL = 0 +}; + +enum ENUM_FVG_TYPE +{ + FVG_BULLISH = 1, + FVG_BEARISH = -1 +}; + +//+------------------------------------------------------------------+ +//| Constants | +//+------------------------------------------------------------------+ +const string EA_PREFIX = "SFVG_"; +const int MAX_FVG_SLOTS = 50; + +//+------------------------------------------------------------------+ +//| Resolved symbol (cached) | +//+------------------------------------------------------------------+ +string g_ResolvedSymbol = ""; + +string GetTradeSymbol() +{ + if(g_ResolvedSymbol != "") + return g_ResolvedSymbol; + + g_ResolvedSymbol = (InpSymbol == "" || InpSymbol == "current") + ? _Symbol + : InpSymbol; + return g_ResolvedSymbol; +} + +//+------------------------------------------------------------------+ +//| New-bar detector | +//+------------------------------------------------------------------+ +datetime g_PreviousBarTime = 0; + +bool IsNewBar() +{ + datetime currentBarTime = iTime(GetTradeSymbol(), InpTimeframe, 0); + if(currentBarTime == g_PreviousBarTime) + return false; + g_PreviousBarTime = currentBarTime; + return true; +} + +#endif diff --git a/Experts/SimpleFVG/Drawing.mqh b/Experts/SimpleFVG/Drawing.mqh new file mode 100644 index 0000000..2d42536 --- /dev/null +++ b/Experts/SimpleFVG/Drawing.mqh @@ -0,0 +1,205 @@ +//+------------------------------------------------------------------+ +//| Drawing.mqh – Step 4: Visualize FVG zones, EMA info, trend panel | +//+------------------------------------------------------------------+ +#ifndef __SIMPLE_FVG_DRAWING_MQH__ +#define __SIMPLE_FVG_DRAWING_MQH__ + +#include "Config.mqh" +#include "Trend.mqh" +#include "FVG.mqh" + +//+------------------------------------------------------------------+ +//| Object-creation helpers | +//+------------------------------------------------------------------+ +void DrawRectangle(string name, + datetime timeStart, double priceTop, + datetime timeEnd, double priceBot, + color clr, bool filled = true) +{ + if(ObjectFind(0, name) < 0) + ObjectCreate(0, name, OBJ_RECTANGLE, 0, timeStart, priceTop, timeEnd, priceBot); + + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_FILL, filled); + ObjectSetInteger(0, name, OBJPROP_BACK, true); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); + ObjectMove(0, name, 0, timeStart, priceTop); + ObjectMove(0, name, 1, timeEnd, priceBot); +} + +void DrawTrendLine(string name, + datetime t1, double p1, + datetime t2, double p2, + color clr, int style, int width) +{ + if(ObjectFind(0, name) < 0) + ObjectCreate(0, name, OBJ_TREND, 0, t1, p1, t2, p2); + + 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); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); + ObjectMove(0, name, 0, t1, p1); + ObjectMove(0, name, 1, t2, p2); +} + +void DrawLabel(string name, int xDist, int yDist, + string text, 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, xDist); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, yDist); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetString (0, name, OBJPROP_FONT, "Consolas"); + ObjectSetString (0, name, OBJPROP_TEXT, text); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); +} + +void DrawTextOnChart(string name, datetime t, double price, + string text, color clr, int fontSize, int anchor) +{ + if(ObjectFind(0, name) < 0) + ObjectCreate(0, name, OBJ_TEXT, 0, t, price); + + ObjectMove(0, name, 0, t, price); + ObjectSetString (0, name, OBJPROP_TEXT, text); + ObjectSetString (0, name, OBJPROP_FONT, "Arial"); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); +} + +//+------------------------------------------------------------------+ +//| Draw all active FVG zones as rectangles on the chart | +//+------------------------------------------------------------------+ +void DrawFVGZones() +{ + ObjectsDeleteAll(0, EA_PREFIX + "FVG_"); + + string symbol = GetTradeSymbol(); + datetime currentBar = iTime(symbol, InpTimeframe, 0); + + for(int i = 0; i < g_FVGCount; i++) + { + if(!g_FVGZones[i].isActive) + continue; + + string zoneId = IntegerToString(i); + + //--- Zone color depends on type and mitigation status --- + color zoneColor; + if(g_FVGZones[i].isMitigated) + zoneColor = InpColorMitigatedFVG; + else if(g_FVGZones[i].type == FVG_BULLISH) + zoneColor = InpColorBullFVG; + else + zoneColor = InpColorBearFVG; + + //--- Draw filled rectangle for the FVG zone --- + DrawRectangle( + EA_PREFIX + "FVG_ZONE_" + zoneId, + g_FVGZones[i].createdTime, g_FVGZones[i].upperEdge, + currentBar, g_FVGZones[i].lowerEdge, + zoneColor, true + ); + + //--- Draw mid-line (50% of FVG) --- + double midPrice = (g_FVGZones[i].upperEdge + g_FVGZones[i].lowerEdge) / 2.0; + color midColor = g_FVGZones[i].isMitigated ? clrDimGray : clrGold; + + DrawTrendLine( + EA_PREFIX + "FVG_MID_" + zoneId, + g_FVGZones[i].createdTime, midPrice, + currentBar, midPrice, + midColor, STYLE_DOT, 1 + ); + + //--- Draw text label --- + if(InpShowFVGLabels) + { + string typeStr = (g_FVGZones[i].type == FVG_BULLISH) ? "BULL" : "BEAR"; + string arrow = (g_FVGZones[i].type == FVG_BULLISH) ? "▲" : "▼"; + string mitigStr = g_FVGZones[i].isMitigated ? " [MITIGATED]" : ""; + + color labelColor; + if(g_FVGZones[i].isMitigated) + labelColor = clrDimGray; + else if(g_FVGZones[i].type == FVG_BULLISH) + labelColor = clrDeepSkyBlue; + else + labelColor = clrOrangeRed; + + DrawTextOnChart( + EA_PREFIX + "FVG_LBL_" + zoneId, + g_FVGZones[i].createdTime, + g_FVGZones[i].upperEdge, + StringFormat("%s %s [%.2f–%.2f]%s", + arrow, typeStr, + g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge, + mitigStr), + labelColor, 7, ANCHOR_LEFT_LOWER + ); + } + } +} + +//+------------------------------------------------------------------+ +//| Draw info panel (top-left corner) | +//+------------------------------------------------------------------+ +void DrawInfoPanel() +{ + if(!InpShowPanel) return; + + int bullCount = CountActiveFVGs(FVG_BULLISH); + int bearCount = CountActiveFVGs(FVG_BEARISH); + int mitigCount = CountMitigatedFVGs(); + string trendText = TrendToString(g_CurrentTrend); + color trendColor = TrendToColor(g_CurrentTrend); + + //--- Row 1: EA header --- + DrawLabel(EA_PREFIX + "PNL_HDR", 12, 22, + StringFormat("SimpleFVG | %s %s", + GetTradeSymbol(), EnumToString(InpTimeframe)), + clrSilver, 10); + + //--- Row 2: Trend status --- + DrawLabel(EA_PREFIX + "PNL_TREND", 12, 44, + StringFormat("Trend: %s | EMA%d = %.2f | EMA%d = %.2f", + trendText, + InpEMAFastPeriod, g_EMAFastValue, + InpEMASlowPeriod, g_EMASlowValue), + trendColor, 9); + + //--- Row 3: FVG counts --- + DrawLabel(EA_PREFIX + "PNL_FVG", 12, 66, + StringFormat("FVG Active: %d Bull + %d Bear | Mitigated: %d", + bullCount, bearCount, mitigCount), + clrWheat, 9); +} + +//+------------------------------------------------------------------+ +//| Master draw: call all draw functions | +//+------------------------------------------------------------------+ +void DrawAll() +{ + DrawFVGZones(); + DrawInfoPanel(); + ChartRedraw(0); +} + +//+------------------------------------------------------------------+ +//| Clean up all drawn objects | +//+------------------------------------------------------------------+ +void DrawCleanup() +{ + ObjectsDeleteAll(0, EA_PREFIX); + ChartRedraw(0); +} + +#endif diff --git a/Experts/SimpleFVG/FVG.mqh b/Experts/SimpleFVG/FVG.mqh new file mode 100644 index 0000000..185f14a --- /dev/null +++ b/Experts/SimpleFVG/FVG.mqh @@ -0,0 +1,277 @@ +//+------------------------------------------------------------------+ +//| FVG.mqh – Step 2: Fair Value Gap detection & management | +//| | +//| 3-candle pattern: | +//| CandleA (oldest) ── CandleB (impulse) ── CandleC (newest) | +//| | +//| Bullish FVG: CandleA.High < CandleC.Low → gap UP | +//| zone = [CandleA.High .. CandleC.Low] | +//| CandleB must be bullish with strong body | +//| | +//| Bearish FVG: CandleA.Low > CandleC.High → gap DOWN | +//| zone = [CandleC.High .. CandleA.Low] | +//| CandleB must be bearish with strong body | +//+------------------------------------------------------------------+ +#ifndef __SIMPLE_FVG_FVG_MQH__ +#define __SIMPLE_FVG_FVG_MQH__ + +#include "Config.mqh" + +//+------------------------------------------------------------------+ +//| FVG data structure | +//+------------------------------------------------------------------+ +struct FVGZone +{ + bool isActive; + ENUM_FVG_TYPE type; + double upperEdge; + double lowerEdge; + datetime createdTime; + int ageInBars; + bool isMitigated; +}; + +//+------------------------------------------------------------------+ +//| Module state | +//+------------------------------------------------------------------+ +FVGZone g_FVGZones[]; +int g_FVGCount = 0; + +//+------------------------------------------------------------------+ +//| Init: allocate array | +//+------------------------------------------------------------------+ +void FVGInit() +{ + ArrayResize(g_FVGZones, MAX_FVG_SLOTS); + for(int i = 0; i < MAX_FVG_SLOTS; i++) + ZeroMemory(g_FVGZones[i]); + g_FVGCount = 0; +} + +//+------------------------------------------------------------------+ +//| Check if impulse candle has strong body (body/range >= threshold) | +//+------------------------------------------------------------------+ +bool IsImpulseCandleStrong(string symbol, ENUM_TIMEFRAMES tf, int shift) +{ + double high = iHigh(symbol, tf, shift); + double low = iLow(symbol, tf, shift); + double open = iOpen(symbol, tf, shift); + double close = iClose(symbol, tf, shift); + + double totalRange = high - low; + if(totalRange < _Point) + return false; + + double bodySize = MathAbs(close - open); + double bodyRatio = bodySize / totalRange * 100.0; + + return (bodyRatio >= InpFVGMinBodyPct); +} + +//+------------------------------------------------------------------+ +//| Check if this FVG timestamp already exists in our array | +//+------------------------------------------------------------------+ +bool FVGAlreadyTracked(datetime fvgTime, ENUM_FVG_TYPE fvgType) +{ + for(int i = 0; i < g_FVGCount; i++) + { + if(g_FVGZones[i].isActive + && g_FVGZones[i].createdTime == fvgTime + && g_FVGZones[i].type == fvgType) + return true; + } + return false; +} + +//+------------------------------------------------------------------+ +//| Add a new FVG zone to the array | +//+------------------------------------------------------------------+ +bool AddFVGZone(ENUM_FVG_TYPE type, double upper, double lower, datetime time) +{ + if(g_FVGCount >= MAX_FVG_SLOTS) + return false; + + if(InpFVGMinSizePoints > 0) + { + double gapSizePoints = (upper - lower) / _Point; + if(gapSizePoints < InpFVGMinSizePoints) + return false; + } + + ZeroMemory(g_FVGZones[g_FVGCount]); + g_FVGZones[g_FVGCount].isActive = true; + g_FVGZones[g_FVGCount].type = type; + g_FVGZones[g_FVGCount].upperEdge = upper; + g_FVGZones[g_FVGCount].lowerEdge = lower; + g_FVGZones[g_FVGCount].createdTime = time; + g_FVGZones[g_FVGCount].ageInBars = 0; + g_FVGZones[g_FVGCount].isMitigated = false; + g_FVGCount++; + + if(InpDebugLog) + PrintFormat("[FVG] NEW %s [%.5f – %.5f] at %s", + (type == FVG_BULLISH) ? "BULL" : "BEAR", + lower, upper, TimeToString(time)); + return true; +} + +//+------------------------------------------------------------------+ +//| Scan for new FVG patterns across the lookback range | +//| Finds ALL FVGs regardless of current trend | +//+------------------------------------------------------------------+ +void ScanForNewFVGs() +{ + string symbol = GetTradeSymbol(); + int totalBars = Bars(symbol, InpTimeframe); + int maxShift = MathMin(InpFVGLookbackBars, totalBars - 3); + + for(int shift = 1; shift <= maxShift; shift++) + { + int shiftA = shift + 2; // oldest candle + int shiftB = shift + 1; // impulse (middle) candle + int shiftC = shift; // newest candle + + double candleA_High = iHigh(symbol, InpTimeframe, shiftA); + double candleA_Low = iLow (symbol, InpTimeframe, shiftA); + + double candleB_Open = iOpen (symbol, InpTimeframe, shiftB); + double candleB_Close = iClose(symbol, InpTimeframe, shiftB); + + double candleC_High = iHigh(symbol, InpTimeframe, shiftC); + double candleC_Low = iLow (symbol, InpTimeframe, shiftC); + + datetime fvgTime = iTime(symbol, InpTimeframe, shiftC); + + //--- Bullish FVG: gap between A.High and C.Low --- + if(candleA_High < candleC_Low + && candleB_Close > candleB_Open + && IsImpulseCandleStrong(symbol, InpTimeframe, shiftB)) + { + if(!FVGAlreadyTracked(fvgTime, FVG_BULLISH)) + AddFVGZone(FVG_BULLISH, candleC_Low, candleA_High, fvgTime); + } + + //--- Bearish FVG: gap between C.High and A.Low --- + if(candleA_Low > candleC_High + && candleB_Close < candleB_Open + && IsImpulseCandleStrong(symbol, InpTimeframe, shiftB)) + { + if(!FVGAlreadyTracked(fvgTime, FVG_BEARISH)) + AddFVGZone(FVG_BEARISH, candleA_Low, candleC_High, fvgTime); + } + } +} + +//+------------------------------------------------------------------+ +//| Check if price has mitigated (filled through) any active FVG | +//+------------------------------------------------------------------+ +void CheckMitigationStatus() +{ + string symbol = GetTradeSymbol(); + + for(int i = 0; i < g_FVGCount; i++) + { + if(!g_FVGZones[i].isActive || g_FVGZones[i].isMitigated) + continue; + + double lastLow = iLow (symbol, InpTimeframe, 1); + double lastHigh = iHigh(symbol, InpTimeframe, 1); + + // Bullish FVG mitigated: price drops through lower edge + if(g_FVGZones[i].type == FVG_BULLISH && lastLow <= g_FVGZones[i].lowerEdge) + { + g_FVGZones[i].isMitigated = true; + if(InpDebugLog) + PrintFormat("[FVG] MITIGATED BULL [%.5f – %.5f]", + g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); + } + + // Bearish FVG mitigated: price rises through upper edge + if(g_FVGZones[i].type == FVG_BEARISH && lastHigh >= g_FVGZones[i].upperEdge) + { + g_FVGZones[i].isMitigated = true; + if(InpDebugLog) + PrintFormat("[FVG] MITIGATED BEAR [%.5f – %.5f]", + g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); + } + } +} + +//+------------------------------------------------------------------+ +//| Age out old FVGs beyond max age | +//+------------------------------------------------------------------+ +void ExpireOldFVGs() +{ + for(int i = 0; i < g_FVGCount; i++) + { + if(!g_FVGZones[i].isActive) continue; + + g_FVGZones[i].ageInBars++; + + if(g_FVGZones[i].ageInBars > InpFVGMaxAgeBars) + { + g_FVGZones[i].isActive = false; + if(InpDebugLog) + PrintFormat("[FVG] EXPIRED %s [%.5f – %.5f] age=%d bars", + (g_FVGZones[i].type == FVG_BULLISH) ? "BULL" : "BEAR", + g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge, + g_FVGZones[i].ageInBars); + } + } +} + +//+------------------------------------------------------------------+ +//| Remove inactive FVGs from array to free slots | +//+------------------------------------------------------------------+ +void CompactFVGArray() +{ + int writeIndex = 0; + for(int i = 0; i < g_FVGCount; i++) + { + if(g_FVGZones[i].isActive) + { + if(writeIndex != i) + g_FVGZones[writeIndex] = g_FVGZones[i]; + writeIndex++; + } + } + g_FVGCount = writeIndex; +} + +//+------------------------------------------------------------------+ +//| Main update: mitigation → expiry → scan new → compact | +//+------------------------------------------------------------------+ +void FVGUpdate() +{ + CheckMitigationStatus(); + ExpireOldFVGs(); + ScanForNewFVGs(); + CompactFVGArray(); +} + +//+------------------------------------------------------------------+ +//| Count helpers for panel display | +//+------------------------------------------------------------------+ +int CountActiveFVGs(ENUM_FVG_TYPE type) +{ + int count = 0; + for(int i = 0; i < g_FVGCount; i++) + { + if(g_FVGZones[i].isActive && g_FVGZones[i].type == type) + count++; + } + return count; +} + +int CountMitigatedFVGs() +{ + int count = 0; + for(int i = 0; i < g_FVGCount; i++) + { + if(g_FVGZones[i].isActive && g_FVGZones[i].isMitigated) + count++; + } + return count; +} + +#endif diff --git a/Experts/SimpleFVG/Trend.mqh b/Experts/SimpleFVG/Trend.mqh new file mode 100644 index 0000000..6456eb2 --- /dev/null +++ b/Experts/SimpleFVG/Trend.mqh @@ -0,0 +1,147 @@ +//+------------------------------------------------------------------+ +//| Trend.mqh – Step 1: Trend detection via EMA34 & EMA89 | +//| | +//| Logic: | +//| BULLISH: EMA_fast > EMA_slow AND close > EMA_fast | +//| BEARISH: EMA_fast < EMA_slow AND close < EMA_fast | +//| NEUTRAL: otherwise (EMA crossing or price between EMAs) | +//+------------------------------------------------------------------+ +#ifndef __SIMPLE_FVG_TREND_MQH__ +#define __SIMPLE_FVG_TREND_MQH__ + +#include "Config.mqh" + +//+------------------------------------------------------------------+ +//| Module state | +//+------------------------------------------------------------------+ +int g_HandleEMAFast = INVALID_HANDLE; +int g_HandleEMASlow = INVALID_HANDLE; +string g_NameEMAFast = ""; +string g_NameEMASlow = ""; + +double g_EMAFastValue = 0.0; +double g_EMASlowValue = 0.0; +ENUM_TREND_DIRECTION g_CurrentTrend = TREND_NEUTRAL; + +//+------------------------------------------------------------------+ +//| Init: create EMA indicator handles and add to chart | +//+------------------------------------------------------------------+ +bool TrendInit() +{ + string symbol = GetTradeSymbol(); + + g_HandleEMAFast = iMA(symbol, InpTimeframe, InpEMAFastPeriod, 0, MODE_EMA, PRICE_CLOSE); + if(g_HandleEMAFast == INVALID_HANDLE) + { + PrintFormat("[Trend] FAILED to create EMA%d handle", InpEMAFastPeriod); + return false; + } + + g_HandleEMASlow = iMA(symbol, InpTimeframe, InpEMASlowPeriod, 0, MODE_EMA, PRICE_CLOSE); + if(g_HandleEMASlow == INVALID_HANDLE) + { + PrintFormat("[Trend] FAILED to create EMA%d handle", InpEMASlowPeriod); + return false; + } + + ChartIndicatorAdd(0, 0, g_HandleEMAFast); + int countAfterFast = ChartIndicatorsTotal(0, 0); + if(countAfterFast > 0) + g_NameEMAFast = ChartIndicatorName(0, 0, countAfterFast - 1); + + ChartIndicatorAdd(0, 0, g_HandleEMASlow); + int countAfterSlow = ChartIndicatorsTotal(0, 0); + if(countAfterSlow > 0) + g_NameEMASlow = ChartIndicatorName(0, 0, countAfterSlow - 1); + + if(InpDebugLog) + PrintFormat("[Trend] OK – EMA%d + EMA%d on %s %s", + InpEMAFastPeriod, InpEMASlowPeriod, + symbol, EnumToString(InpTimeframe)); + + return true; +} + +//+------------------------------------------------------------------+ +//| Deinit: release handles and remove from chart | +//+------------------------------------------------------------------+ +void TrendDeinit() +{ + if(g_NameEMAFast != "") + ChartIndicatorDelete(0, 0, g_NameEMAFast); + if(g_NameEMASlow != "") + ChartIndicatorDelete(0, 0, g_NameEMASlow); + + if(g_HandleEMAFast != INVALID_HANDLE) + { + IndicatorRelease(g_HandleEMAFast); + g_HandleEMAFast = INVALID_HANDLE; + } + if(g_HandleEMASlow != INVALID_HANDLE) + { + IndicatorRelease(g_HandleEMASlow); + g_HandleEMASlow = INVALID_HANDLE; + } +} + +//+------------------------------------------------------------------+ +//| Read one EMA value at a given bar shift | +//+------------------------------------------------------------------+ +double ReadEMA(int handle, int shift) +{ + double buffer[1]; + if(handle == INVALID_HANDLE) return 0.0; + if(CopyBuffer(handle, 0, shift, 1, buffer) < 1) return 0.0; + return buffer[0]; +} + +//+------------------------------------------------------------------+ +//| Update trend direction (call once per new bar) | +//+------------------------------------------------------------------+ +ENUM_TREND_DIRECTION TrendUpdate() +{ + g_EMAFastValue = ReadEMA(g_HandleEMAFast, 1); + g_EMASlowValue = ReadEMA(g_HandleEMASlow, 1); + + if(g_EMAFastValue == 0.0 || g_EMASlowValue == 0.0) + { + g_CurrentTrend = TREND_NEUTRAL; + return g_CurrentTrend; + } + + double closePrice = iClose(GetTradeSymbol(), InpTimeframe, 1); + + if(g_EMAFastValue > g_EMASlowValue && closePrice > g_EMAFastValue) + g_CurrentTrend = TREND_BULLISH; + else if(g_EMAFastValue < g_EMASlowValue && closePrice < g_EMAFastValue) + g_CurrentTrend = TREND_BEARISH; + else + g_CurrentTrend = TREND_NEUTRAL; + + return g_CurrentTrend; +} + +//+------------------------------------------------------------------+ +//| Utility: convert trend enum to readable string | +//+------------------------------------------------------------------+ +string TrendToString(ENUM_TREND_DIRECTION trend) +{ + switch(trend) + { + case TREND_BULLISH: return "BULLISH"; + case TREND_BEARISH: return "BEARISH"; + default: return "NEUTRAL"; + } +} + +color TrendToColor(ENUM_TREND_DIRECTION trend) +{ + switch(trend) + { + case TREND_BULLISH: return clrLime; + case TREND_BEARISH: return clrTomato; + default: return clrGray; + } +} + +#endif