Advertisement IC Markets

Moving Average Bounce Scalping

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
PTScalper
Site Admin
Posts: 3352
Joined: Mon Jul 20, 2026 1:28 pm

Re: Moving Average Bounce Scalping

Post by PTScalper »

1. MetaTrader 4 (MQL4) Expert Advisor

In MQL4, signal detection is evaluated on candle close (shift = 1) to avoid intra-bar false signals, while trailing stops are updated tick-by-tick. Save this code in the MetaEditor as an Expert Advisor (.mq4).

Code: Select all

//+------------------------------------------------------------------+
//|                                     EMA_Pullback_Continuation.mq4|
//|                                  Copyright 2026, Scalping Trader |
//+------------------------------------------------------------------+
#property strict

// --- Input Parameters ---
input int      InpFastEMAPeriod   = 20;     // Fast EMA Period
input int      InpSlowEMAPeriod   = 50;     // Slow EMA Period
input int      InpMacroEMAPeriod  = 200;    // Macro 200 EMA Period
input int      InpSwingLookback   = 5;      // Swing Lookback Bars for Initial SL
input int      InpATRPeriod       = 14;     // ATR Period for Trailing Stop
input double   InpATRMultiplier   = 2.5;    // ATR Trailing Multiplier
input double   InpLotSize         = 0.10;   // Fixed Lot Size
input int      InpSlippage        = 3;      // Max Slippage (Points)
input int      InpMagicNumber     = 102030; // Unique EA Identifier

// Global variable for new-bar detection
datetime glLastBarTime = 0;

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 1. Manage Active Positions (Trailing Stop on Every Tick)
   ManageTrailingStop();

   // 2. Check for New Bar Close (Signals evaluate on bar 1 completion)
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   if(currentBarTime == glLastBarTime)
      return; // Wait for current bar to close
   
   glLastBarTime = currentBarTime;

   // 3. Prevent Multiple Concurrent Positions
   if(CountOpenOrders() > 0)
      return;

   // --- Indicator Values (Evaluated on Bar 1) ---
   double fastEMA  = iMA(_Symbol, _Period, InpFastEMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);
   double slowEMA  = iMA(_Symbol, _Period, InpSlowEMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);
   double macroEMA = iMA(_Symbol, _Period, InpMacroEMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);

   double barOpen  = iOpen(_Symbol, _Period, 1);
   double barClose = iClose(_Symbol, _Period, 1);
   double barHigh  = iHigh(_Symbol, _Period, 1);
   double barLow   = iLow(_Symbol, _Period, 1);
   double barRange = barHigh - barLow;

   if(barRange <= 0) return; // Prevent division by zero

   // --- Trend Filters ---
   bool bullishTrend = (barClose > macroEMA) && (fastEMA > slowEMA);
   bool bearishTrend = (barClose < macroEMA) && (fastEMA < slowEMA);

   // --- Conviction & Pullback Rules ---
   // Long Setup
   bool pulledBackLong = (barLow <= fastEMA) && (barClose > fastEMA);
   bool bullishConviction = (barClose > barOpen) && ((barClose - barOpen) / barRange > 0.5);

   // Short Setup
   bool pulledBackShort = (barHigh >= fastEMA) && (barClose < fastEMA);
   bool bearishConviction = (barOpen > barClose) && ((barOpen - barClose) / barRange > 0.5);

   // --- Execution ---
   if(bullishTrend && pulledBackLong && bullishConviction)
   {
      int lowestIndex = iLowest(_Symbol, _Period, MODE_LOW, InpSwingLookback, 1);
      double stopLoss = NormalizeDouble(iLow(_Symbol, _Period, lowestIndex), _Digits);
      
      double ask = MarketInfo(_Symbol, MODE_ASK);
      if(stopLoss < ask)
      {
         int ticket = OrderSend(_Symbol, OP_BUY, InpLotSize, ask, InpSlippage, stopLoss, 0, "EMA Pullback Long", InpMagicNumber, 0, clrBlue);
         if(ticket < 0)
            Print("Error opening Buy order: ", GetLastError());
      }
   }
   else if(bearishTrend && pulledBackShort && bearishConviction)
   {
      int highestIndex = iHighest(_Symbol, _Period, MODE_HIGH, InpSwingLookback, 1);
      double stopLoss = NormalizeDouble(iHigh(_Symbol, _Period, highestIndex), _Digits);
      
      double bid = MarketInfo(_Symbol, MODE_BID);
      if(stopLoss > bid)
      {
         int ticket = OrderSend(_Symbol, OP_SELL, InpLotSize, bid, InpSlippage, stopLoss, 0, "EMA Pullback Short", InpMagicNumber, 0, clrRed);
         if(ticket < 0)
            Print("Error opening Sell order: ", GetLastError());
      }
   }
}

//+------------------------------------------------------------------+
//| Dynamic ATR Ratchet Trailing Stop                                |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
   double atr = iATR(_Symbol, _Period, InpATRPeriod, 0);
   double stopLevel = MarketInfo(_Symbol, MODE_STOPLEVEL) * _Point;

   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;

      if(OrderSymbol() == _Symbol && OrderMagicNumber() == InpMagicNumber)
      {
         if(OrderType() == OP_BUY)
         {
            double highPrice = iHigh(_Symbol, _Period, 0);
            double calculatedTrail = NormalizeDouble(highPrice - (atr * InpATRMultiplier), _Digits);
            double bid = MarketInfo(_Symbol, MODE_BID);

            // Ratchet check: Only move stop up, respect broker minimum stop level
            if(calculatedTrail > OrderStopLoss() && (bid - calculatedTrail) > stopLevel)
            {
               bool res = OrderModify(OrderTicket(), OrderOpenPrice(), calculatedTrail, OrderTakeProfit(), 0, clrBlue);
               if(!res) Print("Error modifying Buy SL: ", GetLastError());
            }
         }
         else if(OrderType() == OP_SELL)
         {
            double lowPrice = iLow(_Symbol, _Period, 0);
            double calculatedTrail = NormalizeDouble(lowPrice + (atr * InpATRMultiplier), _Digits);
            double ask = MarketInfo(_Symbol, MODE_ASK);

            // Ratchet check: Only move stop down, respect broker minimum stop level
            if((OrderStopLoss() == 0.0 || calculatedTrail < OrderStopLoss()) && (calculatedTrail - ask) > stopLevel)
            {
               bool res = OrderModify(OrderTicket(), OrderOpenPrice(), calculatedTrail, OrderTakeProfit(), 0, clrRed);
               if(!res) Print("Error modifying Sell SL: ", GetLastError());
            }
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Count active positions for this EA                               |
//+------------------------------------------------------------------+
int CountOpenOrders()
{
   int count = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == _Symbol && OrderMagicNumber() == InpMagicNumber)
            count++;
      }
   }
   return count;
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 3352
Joined: Mon Jul 20, 2026 1:28 pm

Re: Moving Average Bounce Scalping

Post by PTScalper »

2. MetaTrader 5 (MQL5) Expert Advisor

MQL5 uses explicit indicator handles, the native object-oriented CTrade class, and strict position inspection. Save this file as an Expert Advisor (.mq5).

Code: Select all

//+------------------------------------------------------------------+
//|                                     EMA_Pullback_Continuation.mq5|
//|                                  Copyright 2026, Scalping Trader |
//+------------------------------------------------------------------+
#property copyright "2026"
#property link      ""
#property version   "1.00"

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>

// --- Inputs ---
input group "=== Indicator Settings ==="
input int      InpFastEMAPeriod   = 20;     // Fast EMA (20)
input int      InpSlowEMAPeriod   = 50;     // Slow EMA (50)
input int      InpMacroEMAPeriod  = 200;    // Macro Trend EMA (200)
input int      InpSwingLookback   = 5;      // Swing Lookback Bars for Initial SL
input int      InpATRPeriod       = 14;     // ATR Period
input double   InpATRMultiplier   = 2.5;    // ATR Trailing Multiplier

input group "=== Trade Settings ==="
input double   InpLotSize         = 0.10;   // Lot Size
input ulong    InpMagicNumber     = 102031; // Magic Number
input ulong    InpDeviation       = 10;     // Execution Deviation (Points)

// --- Handles & Helpers ---
int             glFastEMAHandle;
int             glSlowEMAHandle;
int             glMacroEMAHandle;
int             glATRHandle;
datetime        glLastBarTime;

CTrade          glTrade;
CPositionInfo   glPosition;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   glTrade.SetExpertMagicNumber(InpMagicNumber);
   glTrade.SetDeviationInPoints(InpDeviation);

   // Initialize Indicator Handles
   glFastEMAHandle  = iMA(_Symbol, _Period, InpFastEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   glSlowEMAHandle  = iMA(_Symbol, _Period, InpSlowEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   glMacroEMAHandle = iMA(_Symbol, _Period, InpMacroEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   glATRHandle      = iATR(_Symbol, _Period, InpATRPeriod);

   if(glFastEMAHandle == INVALID_HANDLE || glSlowEMAHandle == INVALID_HANDLE || 
      glMacroEMAHandle == INVALID_HANDLE || glATRHandle == INVALID_HANDLE)
   {
      Print("Failed to initialize indicator handles.");
      return INIT_FAILED;
   }

   glLastBarTime = 0;
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   IndicatorRelease(glFastEMAHandle);
   IndicatorRelease(glSlowEMAHandle);
   IndicatorRelease(glMacroEMAHandle);
   IndicatorRelease(glATRHandle);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 1. Dynamic ATR Trailing Stop (Ticks)
   ManageTrailingStop();

   // 2. Bar Close Trigger: Signals only run on the completion of bar 1
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   if(currentBarTime == glLastBarTime)
      return;

   glLastBarTime = currentBarTime;

   // 3. Do not open new trades if an active position already exists
   if(HasOpenPosition())
      return;

   // --- Fetch Buffer Data ---
   double fastEMA[], slowEMA[], macroEMA[];
   ArraySetAsSeries(fastEMA, true);
   ArraySetAsSeries(slowEMA, true);
   ArraySetAsSeries(macroEMA, true);

   if(CopyBuffer(glFastEMAHandle, 0, 1, 1, fastEMA) <= 0 ||
      CopyBuffer(glSlowEMAHandle, 0, 1, 1, slowEMA) <= 0 ||
      CopyBuffer(glMacroEMAHandle, 0, 1, 1, macroEMA) <= 0)
      return;

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   if(CopyRates(_Symbol, _Period, 1, InpSwingLookback + 1, rates) < InpSwingLookback + 1)
      return;

   // Bar 1 Properties
   double barOpen  = rates[0].open;
   double barClose = rates[0].close;
   double barHigh  = rates[0].high;
   double barLow   = rates[0].low;
   double barRange = barHigh - barLow;

   if(barRange <= 0.0) return;

   // --- Trend Filters ---
   bool bullishTrend = (barClose > macroEMA[0]) && (fastEMA[0] > slowEMA[0]);
   bool bearishTrend = (barClose < macroEMA[0]) && (fastEMA[0] < slowEMA[0]);

   // --- Pullback & Conviction Conditions ---
   bool pulledBackLong     = (barLow <= fastEMA[0]) && (barClose > fastEMA[0]);
   bool bullishConviction  = (barClose > barOpen) && ((barClose - barOpen) / barRange > 0.5);

   bool pulledBackShort    = (barHigh >= fastEMA[0]) && (barClose < fastEMA[0]);
   bool bearishConviction  = (barOpen > barClose) && ((barOpen - barClose) / barRange > 0.5);

   // --- Execution ---
   if(bullishTrend && pulledBackLong && bullishConviction)
   {
      double lowestLow = rates[0].low;
      for(int i = 1; i < InpSwingLookback; i++)
      {
         if(rates[i].low < lowestLow)
            lowestLow = rates[i].low;
      }
      
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double sl  = NormalizeDouble(lowestLow, _Digits);

      if(sl < ask)
         glTrade.Buy(InpLotSize, _Symbol, ask, sl, 0.0, "EMA Pullback Long");
   }
   else if(bearishTrend && pulledBackShort && bearishConviction)
   {
      double highestHigh = rates[0].high;
      for(int i = 1; i < InpSwingLookback; i++)
      {
         if(rates[i].high > highestHigh)
            highestHigh = rates[i].high;
      }
      
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double sl  = NormalizeDouble(highestHigh, _Digits);

      if(sl > bid)
         glTrade.Sell(InpLotSize, _Symbol, bid, sl, 0.0, "EMA Pullback Short");
   }
}

//+------------------------------------------------------------------+
//| Dynamic Ratchet ATR Trailing Stop                                |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
   double atr[];
   ArraySetAsSeries(atr, true);
   if(CopyBuffer(glATRHandle, 0, 0, 1, atr) <= 0)
      return;

   double stopLevelPoints = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double stopLevel = stopLevelPoints * _Point;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      if(glPosition.SelectByIndex(i))
      {
         if(glPosition.Symbol() == _Symbol && glPosition.Magic() == InpMagicNumber)
         {
            ulong  ticket = glPosition.Ticket();
            double currentSL = glPosition.StopLoss();

            if(glPosition.PositionType() == POSITION_TYPE_BUY)
            {
               double highPrice = iHigh(_Symbol, _Period, 0);
               double trailSL = NormalizeDouble(highPrice - (atr[0] * InpATRMultiplier), _Digits);
               double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

               if(trailSL > currentSL && (bid - trailSL) > stopLevel)
                  glTrade.PositionModify(ticket, trailSL, glPosition.TakeProfit());
            }
            else if(glPosition.PositionType() == POSITION_TYPE_SELL)
            {
               double lowPrice = iLow(_Symbol, _Period, 0);
               double trailSL = NormalizeDouble(lowPrice + (atr[0] * InpATRMultiplier), _Digits);
               double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

               if((currentSL == 0.0 || trailSL < currentSL) && (trailSL - ask) > stopLevel)
                  glTrade.PositionModify(ticket, trailSL, glPosition.TakeProfit());
            }
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Check if an open position already exists                         |
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      if(glPosition.SelectByIndex(i))
      {
         if(glPosition.Symbol() == _Symbol && glPosition.Magic() == InpMagicNumber)
            return true;
      }
   }
   return false;
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3352
Joined: Mon Jul 20, 2026 1:28 pm

Re: Moving Average Bounce Scalping

Post by PTScalper »

Key Architectural Differences from Pine Script

Execution Model (New Bar vs. Tick): Pine Script's default backtesting engine evaluates variables on candle close. In MT4/MT5, running code directly on OnTick() without gating would evaluate patterns on live unclosed wicks. Both EAs use a glLastBarTime check to ensure the pattern is confirmed strictly on closed candles (shift = 1), while the trailing stop executes on every incoming price tick.

Broker Stop Levels: Both EAs query MODE_STOPLEVEL (MT4) or SYMBOL_TRADE_STOPS_LEVEL (MT5) before modifying orders. If market price approaches the trailing stop too closely, the broker would otherwise reject the modification order.

Lookback Arrays: While Pine Script uses historical indexing syntax like low[1] and ta.lowest(), MT4 uses iLowest()/iHighest() and MT5 uses CopyRates() populated into an inverted (ArraySetAsSeries) structure.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3352
Joined: Mon Jul 20, 2026 1:28 pm

Re: Moving Average Bounce Scalping

Post by PTScalper »

Here is the strategy translated into a cBot (C#) for the cTrader platform.

In cTrader's API, it is best practice to handle signal generation strictly inside the OnBar() method (which natively triggers only when a candle closes), and handle the dynamic ATR trailing stop inside the OnTick() method (which updates on every live price change).

cTrader cBot (C#)

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class EMAPullbackContinuation : Robot
    {
        // --- Input Parameters ---
        [Parameter("Fast EMA (Pullback Target)", Group = "Indicators", DefaultValue = 20)]
        public int FastEmaPeriod { get; set; }

        [Parameter("Slow EMA (Trend Direction)", Group = "Indicators", DefaultValue = 50)]
        public int SlowEmaPeriod { get; set; }

        [Parameter("Macro 200 EMA", Group = "Indicators", DefaultValue = 200)]
        public int MacroEmaPeriod { get; set; }

        [Parameter("Swing Lookback (Initial SL)", Group = "Risk", DefaultValue = 5)]
        public int SwingLookback { get; set; }

        [Parameter("ATR Period", Group = "Trailing Stop", DefaultValue = 14)]
        public int AtrPeriod { get; set; }

        [Parameter("ATR Multiplier", Group = "Trailing Stop", DefaultValue = 2.5)]
        public double AtrMultiplier { get; set; }

        [Parameter("Trade Volume (Units)", Group = "Risk", DefaultValue = 1000)]
        public double TradeVolume { get; set; }

        // --- Indicator Variables ---
        private ExponentialMovingAverage _fastEma;
        private ExponentialMovingAverage _slowEma;
        private ExponentialMovingAverage _macroEma;
        private AverageTrueRange _atr;

        private const string BotLabel = "EMAPullback_Bot";

        protected override void OnStart()
        {
            // Initialize indicators
            _fastEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, FastEmaPeriod);
            _slowEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, SlowEmaPeriod);
            _macroEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, MacroEmaPeriod);
            _atr = Indicators.AverageTrueRange(AtrPeriod, MovingAverageType.Simple);
        }

        protected override void OnBar()
        {
            // Prevent multiple concurrent positions for this symbol
            if (Positions.Count(p => p.SymbolName == SymbolName && p.Label == BotLabel) > 0)
                return;

            // Use the last closed bar (index: Count - 2) for signal evaluation
            int index = Bars.Count - 2;
            
            double close = Bars.ClosePrices[index];
            double open = Bars.OpenPrices[index];
            double high = Bars.HighPrices[index];
            double low = Bars.LowPrices[index];
            double barRange = high - low;

            if (barRange <= 0) return; // Prevent division by zero

            double fast = _fastEma.Result[index];
            double slow = _slowEma.Result[index];
            double macro = _macroEma.Result[index];

            // --- Trend Filters ---
            bool bullishTrend = (close > macro) && (fast > slow);
            bool bearishTrend = (close < macro) && (fast < slow);

            // --- Pullback & Conviction Logic ---
            bool pulledBackLong = (low <= fast) && (close > fast);
            bool bullishConviction = (close > open) && ((close - open) / barRange > 0.5);

            bool pulledBackShort = (high >= fast) && (close < fast);
            bool bearishConviction = (open > close) && ((open - close) / barRange > 0.5);

            // --- Execution ---
            if (bullishTrend && pulledBackLong && bullishConviction)
            {
                double lowestLow = GetLowestLow(index, SwingLookback);
                
                // cTrader ExecuteMarketOrder requires stop loss in Pips
                double slPips = Math.Round((Symbol.Ask - lowestLow) / Symbol.PipSize, 1);
                
                if (slPips > 0)
                {
                    ExecuteMarketOrder(TradeType.Buy, SymbolName, TradeVolume, BotLabel, slPips, null);
                }
            }
            else if (bearishTrend && pulledBackShort && bearishConviction)
            {
                double highestHigh = GetHighestHigh(index, SwingLookback);
                
                // cTrader ExecuteMarketOrder requires stop loss in Pips
                double slPips = Math.Round((highestHigh - Symbol.Bid) / Symbol.PipSize, 1);
                
                if (slPips > 0)
                {
                    ExecuteMarketOrder(TradeType.Sell, SymbolName, TradeVolume, BotLabel, slPips, null);
                }
            }
        }

        protected override void OnTick()
        {
            // Dynamic ATR Trailing Stop (Updates on every incoming tick)
            var activePositions = Positions.FindAll(BotLabel, SymbolName);

            foreach (var position in activePositions)
            {
                double currentAtr = _atr.Result.LastValue;

                if (position.TradeType == TradeType.Buy)
                {
                    // Calculate trailing level from the current forming High
                    double currentHigh = Bars.HighPrices.LastValue;
                    double trailLevel = Math.Round(currentHigh - (currentAtr * AtrMultiplier), Symbol.Digits);

                    // Ratchet Mechanism: Only move SL up
                    if (position.StopLoss == null || trailLevel > position.StopLoss)
                    {
                        ModifyPosition(position, trailLevel, position.TakeProfit);
                    }
                }
                else if (position.TradeType == TradeType.Sell)
                {
                    // Calculate trailing level from the current forming Low
                    double currentLow = Bars.LowPrices.LastValue;
                    double trailLevel = Math.Round(currentLow + (currentAtr * AtrMultiplier), Symbol.Digits);

                    // Ratchet Mechanism: Only move SL down
                    if (position.StopLoss == null || trailLevel < position.StopLoss)
                    {
                        ModifyPosition(position, trailLevel, position.TakeProfit);
                    }
                }
            }
        }

        // --- Helper Methods to Find Swing Highs/Lows ---
        private double GetLowestLow(int startIndex, int periods)
        {
            double min = double.MaxValue;
            for (int i = 0; i < periods; i++)
            {
                if (Bars.LowPrices[startIndex - i] < min)
                    min = Bars.LowPrices[startIndex - i];
            }
            return min;
        }

        private double GetHighestHigh(int startIndex, int periods)
        {
            double max = double.MinValue;
            for (int i = 0; i < periods; i++)
            {
                if (Bars.HighPrices[startIndex - i] > max)
                    max = Bars.HighPrices[startIndex - i];
            }
            return max;
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply