//+------------------------------------------------------------------+ //| XRR_Pullback.mq5 | //| Author: Hadi (for XAUUSD,M15) | //+------------------------------------------------------------------+ #property strict #property version "0.1" #property description "M15 pullback EA: Major zone (HH/LL), minor same-direction zone, pullback entry, SL under minor, TP at 2R, 1% risk." //------------------------------- Inputs ----------------------------- input string Inp_Symbol = "XAUUSD"; input ENUM_TIMEFRAMES Inp_WorkTF = PERIOD_M15; // Zone detection params input int Inp_MajorLookbackBars = 400; // bars to scan for HH/LL major zone input int Inp_MinorWindowBars = 80; // window to find first same-direction minor zone post-major input int Inp_ZonePadPoints = 50; // padding around zones (points) // Confirmation / filters input bool Inp_UseHTFConfluence = true; input ENUM_TIMEFRAMES Inp_HTF = PERIOD_H1; // confluence timeframe input int Inp_HTFShiftBars = 3; // HTF confirmation window input bool Inp_UseEngulfingConfirm = true; input bool Inp_UseRSIDivergence = false; input int Inp_RSI_Period = 14; input bool Inp_UseVolumeSpike = false; input double Inp_VolSpikeFactor = 1.5; // vs 20-bar average // Risk management input double Inp_RiskPercent = 1.0; // 1% per trade input bool Inp_UseATRforSL = false; input int Inp_ATR_Period = 14; input double Inp_ATR_Mult = 1.5; input double Inp_MinSLPoints = 200; // floor SL (points) input double Inp_R_MultipleTP = 2.0; // fixed R:R=1:2 // Risk-free (optional) input bool Inp_MoveToBEatTP1 = true; input double Inp_TP1_R = 1.0; // move to BE at 1R input double Inp_BE_OffsetPoints = 10; // lock-in small +offset // Sessions input bool Inp_UseSessionFilter = true; input int Inp_LondonOpenHour = 8; // broker time input int Inp_LondonCloseHour = 17; input int Inp_NYOpenHour = 13; input int Inp_NYCloseHour = 21; input bool Inp_AllowNYOverlapOnly = false; // Entry placement input bool Inp_UseLimitAtZone = true; // limit order at minor zone input int Inp_MaxSlippagePoints = 100; input int Inp_MinRetestWickPoints = 30; // Safety / engine input int Inp_MinBarsLoaded = 600; input int Inp_SignalCoolOffBars = 20; // avoid rapid re-entries input bool Inp_OnePositionOnly = true; //------------------------------- Structures ------------------------- struct Zone { bool isValid; double priceLow; double priceHigh; int barIndex; // starting bar index of zone bool isSupply; // true: resistance (HH), false: support (LL) }; struct MinorZone { bool isValid; double priceLow; double priceHigh; int barIndex; bool isSupply; // same direction as major }; datetime lastSignalTimeBuy = 0; datetime lastSignalTimeSell = 0; // int atrHandler = INVALID_HANDLE; //------------------------------- Helpers ---------------------------- int DigitsAdjust() { return (int)SymbolInfoInteger(Inp_Symbol, SYMBOL_DIGITS); } double PointAdjust() { return SymbolInfoDouble(Inp_Symbol, SYMBOL_POINT); } double TickValue() { double v; SymbolInfoDouble(Inp_Symbol, SYMBOL_TRADE_TICK_VALUE, v); return v; } double TickSize() { double s; SymbolInfoDouble(Inp_Symbol, SYMBOL_TRADE_TICK_SIZE, s); return s; } bool IsSessionAllowed() { if (!Inp_UseSessionFilter) return true; MqlDateTime t; TimeToStruct(TimeCurrent(), t); int hour = t.hour; bool london = (hour >= Inp_LondonOpenHour && hour < Inp_LondonCloseHour); bool ny = (hour >= Inp_NYOpenHour && hour < Inp_NYCloseHour); if (Inp_AllowNYOverlapOnly) return (london && ny); return (london || ny); } bool EngulfingBearish(int shift) { double o0 = iOpen(Inp_Symbol, Inp_WorkTF, shift), c0 = iClose(Inp_Symbol, Inp_WorkTF, shift); double o1 = iOpen(Inp_Symbol, Inp_WorkTF, shift + 1), c1 = iClose(Inp_Symbol, Inp_WorkTF, shift + 1); return (c0 < o0 && c1 > o1 && o0 < c1 && c0 < o1 && (o0 - o1) > 0 && (c1 - c0) > 0 && (o1 < c0)); // rough engulfing } bool EngulfingBullish(int shift) { double o0 = iOpen(Inp_Symbol, Inp_WorkTF, shift), c0 = iClose(Inp_Symbol, Inp_WorkTF, shift); double o1 = iOpen(Inp_Symbol, Inp_WorkTF, shift + 1), c1 = iClose(Inp_Symbol, Inp_WorkTF, shift + 1); return (c0 > o0 && c1 < o1 && o0 > c1 && c0 > o1 && (o0 - o1) > 0 && (c0 - c1) > 0 && (o1 > c0)); // rough engulfing inverse } bool VolumeSpike(int shift) { if (!Inp_UseVolumeSpike) return true; double avg = 0; int n = 20; for (int i = shift + 1; i < shift + 1 + n; i++) avg += (double)iVolume(Inp_Symbol, Inp_WorkTF, i); avg /= n; double v0 = (double)iVolume(Inp_Symbol, Inp_WorkTF, shift); return (v0 >= avg * Inp_VolSpikeFactor); } bool HTFConfluence(bool wantSupply) { if (!Inp_UseHTFConfluence) return true; // Simple HTF direction: last HTF candle close vs open double o = iOpen(Inp_Symbol, Inp_HTF, Inp_HTFShiftBars), c = iClose(Inp_Symbol, Inp_HTF, Inp_HTFShiftBars); bool htfDown = (c < o), htfUp = (c > o); return wantSupply ? htfDown : htfUp; } //------------------------------- Zone detection --------------------- Zone FindMajorZone() { Zone z; z.isValid = false; z.isSupply = false; z.priceLow = 0; z.priceHigh = 0; z.barIndex = -1; int bars = iBars(Inp_Symbol, Inp_WorkTF); if (bars < Inp_MinBarsLoaded) return z; // Highest high / lowest low over lookback double hh = -DBL_MAX, ll = DBL_MAX; int hhBar = -1, llBar = -1; for (int i = 1; i <= Inp_MajorLookbackBars; i++) { double h = iHigh(Inp_Symbol, Inp_WorkTF, i), l = iLow(Inp_Symbol, Inp_WorkTF, i); if (h > hh) { hh = h; hhBar = i; } if (l < ll) { ll = l; llBar = i; } } // Decide which one is closest to current action: use most recent extreme if (hhBar < llBar) { z.isSupply = true; z.isValid = true; z.barIndex = hhBar; z.priceLow = hh - Inp_ZonePadPoints * PointAdjust(); z.priceHigh = hh + Inp_ZonePadPoints * PointAdjust(); } else { z.isSupply = false; z.isValid = true; z.barIndex = llBar; z.priceLow = ll - Inp_ZonePadPoints * PointAdjust(); z.priceHigh = ll + Inp_ZonePadPoints * PointAdjust(); } return z; } MinorZone FindFirstMinorSameDirection(const Zone &major) { MinorZone mz; mz.isValid = false; mz.isSupply = major.isSupply; mz.priceLow = 0; mz.priceHigh = 0; mz.barIndex = -1; if (!major.isValid) return mz; // Scan forward from major's bar toward current for first reaction zone aligned with major int start = major.barIndex - 1; int end = MathMax(1, start - Inp_MinorWindowBars); // Heuristic: supply minor = bearish impulse followed by base; demand minor = bullish impulse followed by base for (int i = start; i >= end; i--) { double o = iOpen(Inp_Symbol, Inp_WorkTF, i), c = iClose(Inp_Symbol, Inp_WorkTF, i); double h = iHigh(Inp_Symbol, Inp_WorkTF, i), l = iLow(Inp_Symbol, Inp_WorkTF, i); bool bearImpulse = (c < o && (o - c) > (h - l) * 0.5); bool bullImpulse = (c > o && (c - o) > (h - l) * 0.5); if (major.isSupply && bearImpulse) { // Base zone from next candle tight range double h1 = iHigh(Inp_Symbol, Inp_WorkTF, i - 1), l1 = iLow(Inp_Symbol, Inp_WorkTF, i - 1); mz.isValid = true; mz.barIndex = i - 1; mz.priceLow = l1 - Inp_ZonePadPoints * PointAdjust(); mz.priceHigh = h1 + Inp_ZonePadPoints * PointAdjust(); break; } if (!major.isSupply && bullImpulse) { double h1 = iHigh(Inp_Symbol, Inp_WorkTF, i - 1), l1 = iLow(Inp_Symbol, Inp_WorkTF, i - 1); mz.isValid = true; mz.barIndex = i - 1; mz.priceLow = l1 - Inp_ZonePadPoints * PointAdjust(); mz.priceHigh = h1 + Inp_ZonePadPoints * PointAdjust(); break; } } return mz; } //------------------------------- Signal logic ----------------------- bool PriceRetestedZone(const MinorZone &mz, bool supply, int shift = 0) { if (!mz.isValid) return false; double high = iHigh(Inp_Symbol, Inp_WorkTF, shift), low = iLow(Inp_Symbol, Inp_WorkTF, shift); // For supply: retest when high touches zone; for demand: retest when low touches zone if (supply) return (high >= mz.priceLow && high <= mz.priceHigh && (high - iClose(Inp_Symbol, Inp_WorkTF, shift)) * 1.0 / PointAdjust() >= Inp_MinRetestWickPoints); else return (low <= mz.priceHigh && low >= mz.priceLow && (iClose(Inp_Symbol, Inp_WorkTF, shift) - low) * 1.0 / PointAdjust() >= Inp_MinRetestWickPoints); } bool ConfirmAtZone(bool supply) { int shift = 0; if (Inp_UseEngulfingConfirm) { if (supply) { if (!EngulfingBearish(shift)) return false; } else { if (!EngulfingBullish(shift)) return false; } } if (!VolumeSpike(shift)) return false; if (!HTFConfluence(supply)) return false; // Optional: RSI divergence could be added here if enabled. return true; } //------------------------------- Risk & orders ----------------------- double CalcPositionSize(double sl_points) { double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (Inp_RiskPercent / 100.0); double tickValue = TickValue(); double tickSize = TickSize(); // Convert SL points to quoted ticks double ticks = sl_points * PointAdjust() / tickSize; if (ticks <= 0.0) return 0.0; double lots = riskMoney / (ticks * tickValue); // Normalize to symbol step double minLot = SymbolInfoDouble(Inp_Symbol, SYMBOL_VOLUME_MIN); double lotStep = SymbolInfoDouble(Inp_Symbol, SYMBOL_VOLUME_STEP); double maxLot = SymbolInfoDouble(Inp_Symbol, SYMBOL_VOLUME_MAX); lots = MathFloor(lots / lotStep) * lotStep; lots = MathMax(minLot, MathMin(lots, maxLot)); return lots; } bool PlaceEntry(const MinorZone &mz, bool supply) { double bid = SymbolInfoDouble(Inp_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(Inp_Symbol, SYMBOL_ASK); double point = PointAdjust(); // SL beneath/above minor zone (or ATR-based) double sl, entry, tp; double sl_points; if (supply) { entry = Inp_UseLimitAtZone ? mz.priceLow : bid; // sell near zone top/low double baseSL = mz.priceHigh + Inp_ZonePadPoints * point; if (Inp_UseATRforSL) { // double atrs[]; CopyBuffer(atrHandler, MAIN_LINE, 0, 1, atrs); double atr = atrs[0]; baseSL = MathMax(baseSL, entry + (atr * Inp_ATR_Mult)); } sl = baseSL; sl_points = (sl - entry) / point; sl_points = MathMax(sl_points, Inp_MinSLPoints); tp = entry - (sl_points * point * Inp_R_MultipleTP); // Position sizing double lots = CalcPositionSize(sl_points); if (lots <= 0) return false; MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_DEAL; req.symbol = Inp_Symbol; req.type = ORDER_TYPE_SELL; req.volume = lots; req.price = Inp_UseLimitAtZone ? entry : bid; req.sl = sl; req.tp = tp; req.deviation = Inp_MaxSlippagePoints; bool ok = OrderSend(req, res); if (ok) { lastSignalTimeSell = TimeCurrent(); } return ok; } else { entry = Inp_UseLimitAtZone ? mz.priceHigh : ask; // buy near zone top/high double baseSL = mz.priceLow - Inp_ZonePadPoints * point; if (Inp_UseATRforSL) { // double atrs[]; CopyBuffer(atrHandler, MAIN_LINE, 0, 1, atrs); double atr = atrs[0]; baseSL = MathMin(baseSL, entry - (atr * Inp_ATR_Mult)); } sl = baseSL; sl_points = (entry - sl) / point; sl_points = MathMax(sl_points, Inp_MinSLPoints); tp = entry + (sl_points * point * Inp_R_MultipleTP); double lots = CalcPositionSize(sl_points); if (lots <= 0) return false; MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_DEAL; req.symbol = Inp_Symbol; req.type = ORDER_TYPE_BUY; req.volume = lots; req.price = Inp_UseLimitAtZone ? entry : ask; req.sl = sl; req.tp = tp; req.deviation = Inp_MaxSlippagePoints; bool ok = OrderSend(req, res); if (ok) { lastSignalTimeBuy = TimeCurrent(); } return ok; } } //------------------------------- Position management ---------------- void ManageOpenPositions() { int total = PositionsTotal(); double point = PointAdjust(); for (int i = 0; i < total; i++) { string sym = PositionGetSymbol(i); if (sym != Inp_Symbol) continue; if (!PositionSelect(sym)) continue; long type = (long)PositionGetInteger(POSITION_TYPE); double price = PositionGetDouble(POSITION_PRICE_OPEN); double sl = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); double current = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(sym, SYMBOL_BID) : SymbolInfoDouble(sym, SYMBOL_ASK); // Risk-free move to BE at TP1 (= 1R) if (Inp_MoveToBEatTP1 && tp > 0 && sl > 0) { double rDist = MathAbs(tp - price) / Inp_R_MultipleTP; bool hitTP1 = (type == POSITION_TYPE_BUY ? (current >= price + rDist) : (current <= price - rDist)); if (hitTP1) { double newSL = (type == POSITION_TYPE_BUY ? price + Inp_BE_OffsetPoints * point : price - Inp_BE_OffsetPoints * point); if ((type == POSITION_TYPE_BUY && newSL > sl) || (type == POSITION_TYPE_SELL && newSL < sl)) { MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_SLTP; req.symbol = sym; req.position = (ulong)PositionGetInteger(POSITION_TICKET); req.sl = newSL; req.tp = tp; OrderSend(req, res); } } } } } //------------------------------- Engine ------------------------------ bool CoolOffOk(bool supply) { datetime last = supply ? lastSignalTimeSell : lastSignalTimeBuy; if (last == 0) return true; // Convert bars to seconds approximate using last M15 duration int seconds = Inp_SignalCoolOffBars * 15 * 60; return (TimeCurrent() - last) >= seconds; } void TrySignal() { if (!IsSessionAllowed()) return; Zone major = FindMajorZone(); if (!major.isValid) return; MinorZone minor = FindFirstMinorSameDirection(major); if (!minor.isValid) return; // Only act on retest + confirmation bool retest = PriceRetestedZone(minor, major.isSupply, 0); if (!retest) return; if (!ConfirmAtZone(major.isSupply)) return; if (Inp_OnePositionOnly && PositionsTotal() > 0) return; if (!CoolOffOk(major.isSupply)) return; PlaceEntry(minor, major.isSupply); } //------------------------------- Standard events -------------------- int OnInit() { // if (Symbol() != Inp_Symbol) { SymbolSelect(Inp_Symbol, true); } // atrHandler = iATR(Inp_Symbol, Inp_WorkTF, Inp_ATR_Period); return (INIT_SUCCEEDED); } void OnDeinit(const int reason) { } void OnTick() { static datetime lastBarTime = 0; MqlRates rates[]; int copied = CopyRates(Inp_Symbol, Inp_WorkTF, 0, Inp_MinBarsLoaded, rates); if (copied < Inp_MinBarsLoaded) return; datetime curBarTime = rates[0].time; if (curBarTime != lastBarTime) { // New bar on M15: evaluate signal TrySignal(); lastBarTime = curBarTime; } // Manage open positions continuously ManageOpenPositions(); }