preview
Building a Bar Replay Tool in MQL5

Building a Bar Replay Tool in MQL5

MetaTrader 5Examples |
223 0
Chukwubuikem Okeke
Chukwubuikem Okeke

Introduction

After being taken out of a trade, you begin asking yourself questions. "What did I miss? Did I enter too early? At what point did the market begin to show signs that I ignored?"

You scroll back through the chart, hoping to relive the market candle by candle. But there's a problem—the future is already visible. No matter how disciplined you try to be, your eyes eventually drift to the candles ahead. The outcome is no longer a mystery, and your analysis becomes influenced by hindsight. This is something most traders experience, and I was no different. As I reviewed more trades, I realized I didn't need another static chart. I needed a way to replay the market as it unfolded: pause before the next candle forms, make a decision using only the information available, and then reveal the next candle to validate the analysis.

Although MetaTrader 5 provides a powerful Strategy Tester for back-testing automated trading systems, it doesn't offer an interactive chart replay environment for discretionary traders who want to manually study price action, practice execution, or replay historical markets candle by candle. This creates an exciting opportunity for MQL5 developers.

In this article, we build an interactive Bar Replay Tool that replays historical price action by revealing one candle at a time and providing an intuitive interface. The tool operates through an ON/OFF menu button that activates replay mode, a draggable replay anchor used to select the historical replay starting point, a playback button for playing and pausing the replay, and Buy and Sell buttons that allow users to place a single imaginary (paper) trade during replay. Together, these controls create a simple yet practical environment for studying price action and practicing discretionary trading without exposing future market data. By the end of this article, you will understand how to:
    • Build a custom replay engine using DRAW_COLOR_CANDLES to progressively reveal historical candles.
    • Dynamically manage the chart's appearance by seamlessly switching between native and custom-rendered candlesticks.
    • Implement a draggable replay anchor that defines the replay starting point while providing intuitive play and pause controls.
    • Develop a timer-driven replay engine that simulates a semi-live trading environment by revealing candles one at a time.
    • Implement an imaginary replay paper trading system using interactive Buy and Sell controls.
    • Maintain a consistent replay viewport by automatically keeping the active replay candle in view throughout playback.

    Together, these techniques demonstrate how MQL5's event-driven architecture, custom rendering capabilities, and graphical object framework can be leveraged to build chart replay tools that extend far beyond the native functionality of MetaTrader 5.


    Project Overview

    In this project, we build an interactive bar replay tool that turns historical data into a semi-live simulation on a MetaTrader 5 chart. Rather than displaying the entire price history at once, it progressively reveals candles one at a time, recreating the experience of watching the market unfold in real time. This allows us to replay historical price action, analyze trading decisions, and practice market reading in a controlled environment without exposing future candles. The tool provides an ON/OFF menu button to activate replay mode. In replay mode, a movable vertical anchor sets the starting point and hides candles to the right. A dashboard then provides Play/Pause controls and Buy/Sell buttons for a single paper trade.

    Below is a visual model of the tool described in this project:

    Project Overview

    Fig. 1. Project Overview


    Design Workflow

    Before diving into the implementation, it is important to establish a clear design workflow for the replay system. Breaking the program into well-defined stages simplifies development, makes the implementation easier to follow, and ensures consistent behavior throughout the replay process. From activating replay mode and positioning the replay anchor to progressively revealing historical candles and restoring the chart, each stage is responsible for a specific part of the tool's functionality. Understanding this workflow provides a solid foundation for the implementation that follows.

    Initialization

    The workflow begins during indicator initialization, where the foundation of the replay system is established. First, the program stores the chart's current visual properties—including bullish and bearish candle colors, bar colors, chart line color, and bid/ask colors—so they can be restored once replay mode is disabled. Next, it creates an ON/OFF menu button that serves as the entry point for activating and deactivating the replay tool. Finally, the indicator initializes its candle buffers and starts a two-second timer, which will later drive the replay engine by progressively revealing historical candles during playback.

    Replay Mode Activation

    Replay mode begins when the user clicks the ON/OFF menu button, placing the tool into replay mode. Before any replay data is displayed, the program performs a series of initialization steps to prepare both the chart and the replay environment. First, the program enables the chart object deletion event, allowing it to detect if the replay anchor is accidentally removed. If the anchor is deleted, it is automatically recreated to ensure that replay can continue without interruption.

    Next, the program determines the current chart viewport and positions a vertical replay anchor at its center. This provides a convenient default replay starting point while allowing the user to reposition the anchor to any historical candle before replay begins. Once the replay anchor has been created, the program displays a compact replay dashboard containing a Play/Pause button for controlling playback, together with Buy and Sell buttons that allow the user to place a single imaginary (paper) trade during replay.

    With the replay controls in place, the chart is prepared for custom rendering. The program first hides MetaTrader 5's native candlesticks before drawing its own custom candle representation. Only the candles up to the replay anchor are rendered, while every candle to the right of the anchor remains hidden. This establishes a clean replay starting point, ensuring that no future market information is visible before playback begins.

    Replay Paper Trading

    To simulate a live trading experience during replay, the tool allows users to place a single imaginary (paper) trade by clicking either the Buy or Sell button. When a trade is opened, the program creates three horizontal lines representing the Entry, Stop Loss (SL), and Take Profit (TP) levels. If another Buy or Sell button is clicked while a replay position is already active, the existing position is automatically replaced with the newly created one.

    As the replay progresses, the imaginary position is continuously monitored against each newly revealed candle. Once either the Stop Loss or Take Profit level is reached, the replay position is automatically closed by removing its graphical objects from the chart.

    Event-Driven Replay Control

    The replay tool adopts an event-driven architecture, with all user interactions managed through the OnChartEvent() function. This central event handler responds to menu button clicks, replay dashboard controls, replay anchor selection, drag-and-drop operations, and chart object deletion events. Whenever the replay anchor is repositioned, the replay view is immediately recalculated so that only candles up to the selected point remain visible.

    Timer-Based Candle Replay

    Once replay begins, the timer event becomes the driving force behind the replay engine. Triggered every two seconds, it checks whether replay mode is active and then deselects the replay anchor to indicate playback. The next historical candle is revealed by updating the custom candle buffers, while a horizontal price line is simultaneously repositioned to the candle's closing price, creating the effect of a live market unfolding one candle at a time. This process continues until playback is paused or all replay candles have been displayed.

    Replay Deactivation

    The replay workflow concludes when the user deactivates the system through the ON/OFF menu button. At this point, the program gracefully restores the chart to its original state. It first disables the chart object deletion event, then removes the replay anchor and other replay-related objects. Next, the native MetaTrader 5 candles are restored across the entire chart, while the custom replay candles are cleared, ensuring the chart returns to its normal appearance and behavior.


    MQL5 Implementation

    Having established the design workflow, we can now translate each stage into a practical MQL5 implementation. We progressively implement its core components, including initialization, menu activation, event-driven interaction, timer-based replay, and replay deactivation.

    Compiler Directives

    We begin by defining the compiler directives that configure the indicator and its rendering behavior. The #property directives specify the program metadata and configure the DRAW_COLOR_CANDLES plot used for the replay candles.

    Next, the #define directives declare constant names for the replay objects, ensuring consistency and easier maintenance throughout the program. Finally, the playback controls use Unicode symbols (▶ and ⏸) to provide intuitive Play and Pause button icons.

    //+------------------------------------------------------------------+
    //|                                              Bar Replay Tool.mq5 |
    //|                                             © 2026, ChukwuBuikem |
    //|                             https://www.mql5.com/en/users/bikeen |
    //+------------------------------------------------------------------+
    #property copyright "© 2026, ChukwuBuikem"
    #property link      "https://www.mql5.com/en/users/bikeen"
    #property indicator_chart_window
    #property indicator_plots 1
    #property indicator_buffers 5
    #property indicator_type1 DRAW_COLOR_CANDLES
    #property indicator_color1 clrGreen,clrRed,clrNONE;
    #property indicator_width1 2
    #property indicator_label1 "REPLAY: OPEN;REPLAY: HIGH;REPLAY: LOW;REPLAY: CLOSE"
    
    #define _PROG_NAME "Bar Replay Tool"
    #define _MENU_BUTTON _PROG_NAME + "MENU_BUTTON"
    #define _REPLAY_ANCHORLINE _PROG_NAME + "REPLAY_VLINE"
    #define _REPLAY_PRICE _PROG_NAME + "REPLAY_PRICE"
    #define _REPLAY_PLAY_BUTTON _PROG_NAME + "REPLAY_PLAY_BUTTON"
    #define _REPLAY_DASHBOARD _PROG_NAME + "REPLAY_DASHBOARD"
    //--- PLAY AND PAUSE UNICODE SYMBOLS
    #define _PLAY_SYMBOL ShortToString(0X25B6)
    #define _PAUSE_SYMBOL ShortToString(0X23F8)
    //--- REPLAY TRADE MACROS
    #define _REPLAY_BUY_BUTTON _PROG_NAME + "REPLAY_BUY_BUTTON"
    #define _REPLAY_SELL_BUTTON _PROG_NAME + "REPLAY_SELL_BUTTON"
    #define _REPLAY_POSITION_ENTRY _PROG_NAME + "REPLAY_POSITION_ENTRY"
    #define _REPLAY_POSITION_TP _PROG_NAME + "REPLAY_POSITION_TP"
    #define _REPLAY_POSITION_SL _PROG_NAME + "REPLAY_POSITION_SL"

    Custom Data Structure

    To simplify chart customization, we begin by defining a custom data structure that stores the chart's original color settings. Its constructor automatically retrieves these properties when the structure is initialized, allowing them to be restored later after replay mode is disabled. The member functions responsible for customizing, restoring, and retrieving chart properties are declared here and will be implemented in subsequent sections.
    //--- CUSTOM DATA STRUCTURE
    struct st_ChartInfo
      {
    private:
       color             barUpClr;
       color             barDownClr;
       color             bullClr;
       color             bearClr;
       color             lineClr;
       color             askClr;
       color             bidClr;
    public:
       //---
                         st_ChartInfo()
         {
          barUpClr = (color)ChartGetInteger(0, CHART_COLOR_CHART_UP);
          barDownClr = (color)ChartGetInteger(0, CHART_COLOR_CHART_DOWN);
          bullClr = (color)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL);
          bearClr = (color)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR);
          lineClr = (color)ChartGetInteger(0, CHART_COLOR_CHART_LINE);
          askClr = (color)ChartGetInteger(0, CHART_COLOR_ASK);
          bidClr = (color)ChartGetInteger(0, CHART_COLOR_BID);
         }
       //--- FUNCTIONS
       void              customize();
       void              restore();
       color             getPriceColor();
    
      } chartState;

    Global Variables

    Next, we declare the global variables that maintain the replay tool's state throughout its execution. These include the custom candle buffers, historical price data, replay status flags, replay anchor information, playback progress, and the variables required to manage the imaginary replay trade.
    //--- GLOBAL VARIABLES
    MqlRates myRates[];
    double openBuffer[], highBuffer[];
    double lowBuffer[], closeBuffer[], colorBuffer[];
    bool isPositionOpen = false;
    bool replayMode = false;
    bool isPlay = false;
    datetime vlineTime = 0;
    int stopBar = INT_MIN;
    int bars = INT_MIN;
    int lastCalculated = 0;
    ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY;
    double entryPrice = EMPTY_VALUE, tpPrice = EMPTY_VALUE, slPrice = EMPTY_VALUE;
    double lastPrice = EMPTY_VALUE;

    Helper Functions

    To keep the implementation organized and modular, the replay tool is divided into several helper functions, each responsible for a specific task. These functions handle chart configuration, viewport detection, visualization, replay controls, and custom candle rendering.

    • Chart Configuration
      We begin by implementing the member functions declared earlier in the custom data structure. These functions are responsible for customizing the chart during replay, restoring its original appearance when replay ends, and retrieving the chart's original Ask/Bid price color for rendering the replay price line.
    //+------------------------------------------------------------------+
    //|                     CUSTOMIZE CHART                              |
    //+------------------------------------------------------------------+
    void st_ChartInfo::customize(void)
      {
    //---
       ChartSetInteger(0, CHART_COLOR_CANDLE_BULL, clrNONE);
       ChartSetInteger(0, CHART_COLOR_CANDLE_BEAR, clrNONE);
       ChartSetInteger(0, CHART_COLOR_CHART_UP, clrNONE);
       ChartSetInteger(0, CHART_COLOR_CHART_DOWN, clrNONE);
       ChartSetInteger(0, CHART_COLOR_CHART_LINE, clrNONE);
       ChartSetInteger(0, CHART_COLOR_ASK, clrNONE);
       ChartSetInteger(0, CHART_COLOR_BID, clrNONE);
       ChartRedraw();
      }
    //+------------------------------------------------------------------+
    //|                        RESTORE CHART SETTINGS                    |
    //+------------------------------------------------------------------+
    void st_ChartInfo::restore(void)
      {
    //---
       ChartSetInteger(0, CHART_COLOR_CANDLE_BULL, bullClr);
       ChartSetInteger(0, CHART_COLOR_CANDLE_BEAR, bearClr);
       ChartSetInteger(0, CHART_COLOR_CHART_UP, barUpClr);
       ChartSetInteger(0, CHART_COLOR_CHART_DOWN, barDownClr);
       ChartSetInteger(0, CHART_COLOR_CHART_LINE, lineClr);
       ChartSetInteger(0, CHART_COLOR_ASK, askClr);
       ChartSetInteger(0, CHART_COLOR_BID, bidClr);
       ChartRedraw();
      }
    //+------------------------------------------------------------------+
    //|                      OBTAIN PRICE COLOR                          |
    //+------------------------------------------------------------------+
    color st_ChartInfo::getPriceColor(void)
      {
    //---
       return bidClr;
      }
    • Viewport Detection

      The below function first determines the chart's current viewport. It then calculates the viewport's midpoint and returns the time of the nearest candle, ensuring the result is normalized to a valid candle time that can be used as the default replay starting point.

    //+------------------------------------------------------------------+
    //|                    VIEWPORT DETECTION                            |
    //+------------------------------------------------------------------+
    datetime getViewportMiddleTime(void)
      {
    //---
       datetime start = 0, end = 0;
       int first = (int) ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
       int visibleBars = (int) ChartGetInteger(0, CHART_VISIBLE_BARS);
    
       int last = first - visibleBars + 2;
       if(last < 0)
          last = 0;
    
       start = iTime(_Symbol, PERIOD_CURRENT, last);
       end = iTime(_Symbol, PERIOD_CURRENT, first);
    
       datetime rawMiddleTime = (start + end) / 2;
       int nearestBar = iBarShift(_Symbol, PERIOD_CURRENT, rawMiddleTime, false);
       if(nearestBar < 0)
          return 0;
    
       return iTime(_Symbol, PERIOD_CURRENT, nearestBar);
      }

    • Visualization
      The replay interface is constructed using a collection of visualization utility functions that create the program's graphical objects, including the dashboard, control buttons, the interactive anchor line, and the imaginary trade levels. When a Buy or Sell trade is initiated, the program creates the Entry, Stop Loss, and Take Profit levels, with the TP and SL initialized to ±10,000 points from the entry price, providing default trade management levels that can be adjusted by the user.

    //+------------------------------------------------------------------+
    //|                      BUTTON CREATION                             |
    //+------------------------------------------------------------------+
    void createButton(const string objName, const int xDistance, const int yDistance,
                      const int xSize, const int ySize, const ENUM_BASE_CORNER corner,
                      const color clr, const color bgClr, const color bdClr, const int fontsize,
                      const string tooltip, const string display, const string font = "Arial")
      {
    //---
       if(ObjectFind(0, objName) != -1)
          ObjectDelete(0, objName);
    
       ObjectCreate(0, objName, OBJ_BUTTON, 0, 0, 0);
       ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);
       ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDistance);
       ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDistance);
       ObjectSetInteger(0, objName, OBJPROP_XSIZE, xSize);
       ObjectSetInteger(0, objName, OBJPROP_YSIZE, ySize);
       ChartRedraw();
      }
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    void createDashboard(void)
      {
    //---
       if(ObjectCreate(0, _REPLAY_DASHBOARD, OBJ_RECTANGLE_LABEL, 0, 0, 0))
         {
          ObjectSetInteger(0, _REPLAY_DASHBOARD, OBJPROP_XDISTANCE, 70);
          ObjectSetInteger(0, _REPLAY_DASHBOARD, OBJPROP_YDISTANCE, 50);
          ObjectSetInteger(0, _REPLAY_DASHBOARD, OBJPROP_XSIZE, 110);
          ObjectSetInteger(0, _REPLAY_DASHBOARD, OBJPROP_YSIZE, 40);
          //--- CREATE PLAYBACK, BUY, AND SELL BUTTONS
          createButton(_REPLAY_PLAY_BUTTON, 75, 45, 30, 30, CORNER_LEFT_LOWER,
                       clrWhite, clrDimGray, clrBlack, 20, "Playback Control", _PLAY_SYMBOL, "Segoe UI");
          createButton(_REPLAY_BUY_BUTTON, 110, 45, 30, 30, CORNER_LEFT_LOWER,
                       clrWhite, clrBlue, clrBlack, 8, "BUY", "BUY", "Bold");
          createButton(_REPLAY_SELL_BUTTON, 145, 45, 30, 30, CORNER_LEFT_LOWER,
                       clrWhite, clrRed, clrBlack, 8, "SELL", "SELL", "Bold");
          ChartRedraw();
         }
      }
    //+------------------------------------------------------------------+
    //|                   HORIZONTAL LINE CREATION                       |
    //+------------------------------------------------------------------+
    void createHLine(const string objName, const double price1, const color clr,
                     const string toolTip = "\n", const bool selected = true,
                     const ENUM_LINE_STYLE style = STYLE_SOLID)
      {
    //---
       if(ObjectFind(0, objName) != -1)
          ObjectDelete(0, objName);
    
       if(ObjectCreate(0, objName, OBJ_HLINE, 0, 0, price1))
         {
          ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
          ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
          ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
          ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, true);
          ChartRedraw();
         }
      }
    //+------------------------------------------------------------------+
    //|                      ANCHOR LINE CREATION                        |
    //+------------------------------------------------------------------+
    void drawAnchorLine(const string objName, const datetime vTime,
                        const string tooltip = "\n")
      {
    //---
       if(ObjectFind(0, objName) != -1)
          ObjectDelete(0, objName);
    
       if(ObjectCreate(0, objName, OBJ_VLINE, 0, vTime, 0))
         {
          ObjectSetInteger(0, objName, OBJPROP_COLOR, clrBlue);
          ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
          ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
          ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, true);
          ChartRedraw();
         }
      }
    //+------------------------------------------------------------------+
    //|                  IMAGINARY REPLAY PAPER TRADE                    |
    //+------------------------------------------------------------------+
    void showPosition(const double entry, const bool isBuy)
      {
    //---
       if(isBuy)
         {
          entryPrice = entry;
          slPrice = entry - (10000 * _Point);
          tpPrice = entry + (10000 * _Point);
          createHLine(_REPLAY_POSITION_ENTRY, entryPrice, clrBlue,
                      "Replay Entry Price", false, STYLE_DOT);
          createHLine(_REPLAY_POSITION_SL, slPrice, clrRed,
                      "Replay SL Price", true, STYLE_DASHDOT);
          createHLine(_REPLAY_POSITION_TP, tpPrice, clrLimeGreen,
                      "Replay TP Price", true, STYLE_DASHDOT);
          positionType = POSITION_TYPE_BUY;
         }
       else
         {
          entryPrice = entry;
          slPrice = entry + (10000 * _Point);
          tpPrice = entry - (10000 * _Point);
          createHLine(_REPLAY_POSITION_ENTRY, entryPrice, clrBlue,
                      "Replay Entry Price", false, STYLE_DOT);
          createHLine(_REPLAY_POSITION_SL, slPrice, clrRed,
                      "Replay SL Price", true, STYLE_DASHDOT);
          createHLine(_REPLAY_POSITION_TP, tpPrice, clrLimeGreen,
                      "Replay TP Price", true, STYLE_DASHDOT);
          positionType = POSITION_TYPE_SELL;
         }
       ChartRedraw();
      }

    NOTE: For brevity, the code examples in this section show only the key object properties relevant to the visualization. The remaining properties and complete object configurations are available in the full source code accompanying this article.

    • Button Toggle System
      The replay workflow is coordinated through two toggle functions that manage the tool's operating states. The menu button enables or disables replay mode by configuring the chart, creating or removing the replay interface, and switching between the native and custom chart appearance. The playback button controls replay execution by toggling between Play and Pause states, while also locking or unlocking the replay anchor and starting or stopping the replay engine.
    //+------------------------------------------------------------------+
    //|                     TOGGLE MENU BUTTON                           |
    //+------------------------------------------------------------------+
    void toggleMenuButton(void)
      {
    //---
       static bool isMenuOn = false;
       isMenuOn = !isMenuOn;
       if(!isMenuOn)
         {
          ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_BGCOLOR, clrBlue);
          ObjectSetString(0, _MENU_BUTTON, OBJPROP_TEXT, "ON");
          ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_STATE, false);
          //--- RESTORE CHART SETTINGS
          ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false);
          ChartSetInteger(0, CHART_SHOW_ONE_CLICK, true);
          ChartSetInteger(0, CHART_SHOW_OBJECT_DESCR, false);
          //--- CLEAR AND RESTORE DEFAULT CANDLE SETTINGS
          ObjectsDeleteAll(0, _PROG_NAME + "REPLAY_");
          customCandles(true, iTime(_Symbol, PERIOD_CURRENT, 0));
          chartState.restore();
          isPositionOpen = false;
          replayMode = false;
         }
       else
         {
          ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_BGCOLOR, clrRed);
          ObjectSetString(0, _MENU_BUTTON, OBJPROP_TEXT, "OFF");
          ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_STATE, false);
          //--- CUSTOMIZE CHART SETTINGS
          ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true);
          ChartSetInteger(0, CHART_SHOW_ONE_CLICK, false);
          ChartSetInteger(0, CHART_SHOW_OBJECT_DESCR, true);
          vlineTime = getViewportMiddleTime();
          drawAnchorLine(_REPLAY_ANCHORLINE, vlineTime, "Replay Anchor");
          createDashboard();
          customCandles(true, iTime(_Symbol, PERIOD_CURRENT, 0));
          ChartRedraw();
          //--- CUSTOMIZE CHART
          customCandles(false, vlineTime);
          chartState.customize();
         }
       ChartRedraw();
      }
    //+------------------------------------------------------------------+
    //|                 TOGGLE PLAYBACK BUTTON                           |
    //+------------------------------------------------------------------+
    void togglePlayButton(void)
      {
    //---
       isPlay = !isPlay;
       if(!isPlay)
         {
          ObjectSetString(0, _REPLAY_PLAY_BUTTON, OBJPROP_TEXT, _PLAY_SYMBOL);
          ObjectSetInteger(0, _REPLAY_PLAY_BUTTON, OBJPROP_STATE, false);
          ObjectSetInteger(0, _REPLAY_ANCHORLINE, OBJPROP_SELECTED, true);
          replayMode = false;
         }
       else
         {
          ObjectSetString(0, _REPLAY_PLAY_BUTTON, OBJPROP_TEXT, _PAUSE_SYMBOL);
          ObjectSetInteger(0, _REPLAY_PLAY_BUTTON, OBJPROP_STATE, false);
          ObjectSetInteger(0, _REPLAY_ANCHORLINE, OBJPROP_SELECTED, false);
          if(ObjectFind(0, _REPLAY_POSITION_ENTRY) != -1)
            {
             //--- DESELECT TP AND SL LEVEL
             ObjectSetInteger(0, _REPLAY_POSITION_SL, OBJPROP_SELECTED, false);
             ObjectSetInteger(0, _REPLAY_POSITION_TP, OBJPROP_SELECTED, false);
            }
          replayMode = true;
         }
       ChartRedraw();
      }


    • Custom Replay Candles

      At the core of the replay engine is a custom candle rendering system built with DRAW_COLOR_CANDLES. Instead of relying on MetaTrader 5's native candlesticks, the indicator controls exactly which candles are displayed, allowing historical price action to be progressively revealed while future candles remain hidden.

    //+------------------------------------------------------------------+
    //|                  SHOW OR HIDE CUSTOM REPLAY CANDLES              |
    //+------------------------------------------------------------------+
    void customCandles(const bool isRemove, const datetime anchorTime)
      {
    //---
       static datetime time = 0;
       bars = iBars(_Symbol, PERIOD_CURRENT);
       stopBar = iBarShift(_Symbol, PERIOD_CURRENT, anchorTime);
       if(CopyRates(_Symbol, PERIOD_CURRENT, 0, bars, myRates) > 0)
         {
          for(int w = 0; w < (lastCalculated = ArraySize(myRates) - (stopBar)) && !IsStopped(); w++)
            {
             openBuffer[w] = EMPTY_VALUE;
             highBuffer[w] = EMPTY_VALUE;
             lowBuffer[w] = EMPTY_VALUE;
             closeBuffer[w] = EMPTY_VALUE;
             colorBuffer[w] = 3;
    
             if(!isRemove)
               {
                openBuffer[w] = myRates[w].open;
                highBuffer[w] = myRates[w].high;
                lowBuffer[w] = myRates[w].low;
                closeBuffer[w] = myRates[w].close;
                colorBuffer[w] = (closeBuffer[w] > openBuffer[w]) ? 0 : 1;
                lastPrice = closeBuffer[w];
               }
            }
          if(!isRemove)
            {
             //--- CREATE REPLAY PRICE LINE
             createHLine(_REPLAY_PRICE, 0, chartState.getPriceColor(), "Replay Price", false);
             ChartRedraw();
            }
         }
      }
    //+------------------------------------------------------------------+
    //|              REVEAL ONE CUSTOM REPLAY CANDLE                     |
    //+------------------------------------------------------------------+
    void revealOneCandle(const int prevCalculated)
      {
    //---
       if(CopyRates(_Symbol, PERIOD_CURRENT, 0, bars, myRates) > 0)
         {
          openBuffer[prevCalculated] = myRates[prevCalculated].open;
          highBuffer[prevCalculated] = myRates[prevCalculated].high;
          lowBuffer[prevCalculated] = myRates[prevCalculated].low;
          closeBuffer[prevCalculated] = myRates[prevCalculated].close;
          colorBuffer[prevCalculated] = (closeBuffer[prevCalculated] > openBuffer[prevCalculated]) ? 0 : 1;
          lastPrice = closeBuffer[prevCalculated];
          //--- MONITOR REPLAY TRADE SL AND TP HIT
          if(isPositionOpen)
            {
             switch(positionType)
               {
                case POSITION_TYPE_BUY:
                   if(highBuffer[prevCalculated] >= tpPrice || lowBuffer[prevCalculated] <= slPrice)
                     {
                      ObjectsDeleteAll(0, _PROG_NAME + "REPLAY_POSITION");
                      Print("Replay buy position closed");
                      PlaySound("ok.wav");
                      isPositionOpen = false;
                     }
                   ChartRedraw();
                   break;
                case POSITION_TYPE_SELL:
                   if(highBuffer[prevCalculated] >= slPrice || lowBuffer[prevCalculated] <= tpPrice)
                     {
                      ObjectsDeleteAll(0, _PROG_NAME + "REPLAY_POSITION");
                      PlaySound("ok.wav");
                      Print("Replay sell position closed");
                      isPositionOpen = false;
                     }
                   ChartRedraw();
                   break;
               }
            }
          //--- ENSURE REPLAY BAR IS IN VIEW
          int replayBar = iBarShift(_Symbol, PERIOD_CURRENT, myRates[prevCalculated].time);
          int visible = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
          ChartNavigate(0, CHART_END, -(replayBar - (visible / 4)));
          //--- UPDATE REPLAY PRICE
          ObjectMove(0, _REPLAY_PRICE, 0, 0, closeBuffer[prevCalculated]);
          ChartRedraw();
         }
      }

    Explanation:

    • customCandle()

      This function initializes the replay candles. It copies the chart's historical price data, determines the replay stopping point from the selected anchor, and either displays or hides the custom candles depending on the replay state. Since the indicator buffers are initially filled with EMPTY_VALUE, the function populates them with actual price data up to the anchor replay time when isRemove is set to false, while the buffers beyond the anchor remain EMPTY_VALUE and therefore hidden from view. When replay mode is enabled, it also creates the replay price line used to track the latest replayed closing price.

    • revealOneCandle()

      During playback, this function serves as the core of the replay engine by revealing exactly one new candle at a time. It does this by populating the previously empty indicator-buffer slots with the candle’s price data, making the new candle visible on the chart. In addition to updating the custom candle buffers, it monitors the imaginary replay trade for Take Profit and Stop Loss hits, automatically closes the position when either level is reached, keeps the active replay candle within the visible chart viewport, and updates the replay price line to the latest closing price.

    Initialization Logic (OnInit)

    With the supporting helper functions in place, we can now implement the standard MQL5 event handlers, beginning with OnInit(). During initialization, the indicator creates the ON/OFF menu button, registers the custom candle buffers, initializes them with empty values, configures the indicator precision, and starts a 2-second timer that drives the replay engine.

    //+------------------------------------------------------------------+
    //|                      INITIALIZATION FUNCTION                     |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //---
       createButton(_MENU_BUTTON, 10, 50, 40, 40, CORNER_LEFT_LOWER,
                    clrWhite, clrBlue, clrBlack, 10, "Menu Button", "ON");
    //--- SET INDICATOR BUFFERS
       SetIndexBuffer(0, openBuffer, INDICATOR_DATA);
       SetIndexBuffer(1, highBuffer, INDICATOR_DATA);
       SetIndexBuffer(2, lowBuffer, INDICATOR_DATA);
       SetIndexBuffer(3, closeBuffer, INDICATOR_DATA);
       SetIndexBuffer(4, colorBuffer, INDICATOR_COLOR_INDEX);
    
       ArrayInitialize(openBuffer, EMPTY_VALUE);
       ArrayInitialize(highBuffer, EMPTY_VALUE);
       ArrayInitialize(lowBuffer, EMPTY_VALUE);
       ArrayInitialize(closeBuffer, EMPTY_VALUE);
    
       IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
       EventSetTimer(2);// TWO SECONDS TIMER
       return(INIT_SUCCEEDED);
      }

    Cleanup (OnDeinit)

    When the indicator is removed from the chart, the OnDeinit() handler performs the necessary cleanup. It stops the replay timer, disables object deletion events, removes all replay-related objects, and restores the chart to its original appearance.

    //+------------------------------------------------------------------+
    //|                    DEINITIALIZATION FUNCTION                     |
    //+------------------------------------------------------------------+
    void OnDeinit(const int32_t reason)
      {
    //--- KILL TIMER AND RESTORE CHART SETTINGS
       EventKillTimer();
       ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false);
       ChartSetInteger(0, CHART_SHOW_ONE_CLICK, true);
       ChartSetInteger(0, CHART_SHOW_OBJECT_DESCR, false);
       ObjectsDeleteAll(0, _PROG_NAME);
       chartState.restore();
       ChartRedraw();
      }

    Core Iteration Function

    Since the replay tool is driven entirely by OnChartEvent() and OnTimer(), the OnCalculate() function performs no processing and simply returns the total number of available candles.

    //+------------------------------------------------------------------+
    //|                         ITERATION FUNCTION                       |
    //+------------------------------------------------------------------+
    int OnCalculate(const int32_t rates_total,
                    const int32_t prev_calculated,
                    const datetime &time[],
                    const double &open[],
                    const double &high[],
                    const double &low[],
                    const double &close[],
                    const long &tick_volume[],
                    const long &volume[],
                    const int32_t &spread[])
      {
    //--- NO OPERATION REQUIRED
       return(rates_total);
      }

    Interactive Engine

    The replay tool is built around the OnChartEvent() handler, which serves as the core interaction engine of the application. It processes every user interaction with the replay interface, including button clicks, drag-and-drop operations, and object deletion events, ensuring that the replay environment responds immediately to user actions while maintaining a consistent replay state.
    //+------------------------------------------------------------------+
    //|                   CHART EVENT HANDLER                            |
    //+------------------------------------------------------------------+
    void OnChartEvent(const int32_t id,
                      const long &lparam,
                      const double &dparam,
                      const string &sparam)
      {
    //---
       switch(id)
         {
          //--- OBJECT CLICK (BUTTONS)
          case CHARTEVENT_OBJECT_CLICK:
             //--- TOGGLE MENU BUTTON
             if(sparam == _MENU_BUTTON)
               {
                toggleMenuButton();
                break;
               }
             //--- TOGGLE PLAY BUTTON
             if(sparam == _REPLAY_PLAY_BUTTON)
               {
                togglePlayButton();
                break;
               }
             //--- BUY BUTTON
             if(sparam == _REPLAY_BUY_BUTTON)
               {
                ObjectSetInteger(0, _REPLAY_BUY_BUTTON, OBJPROP_STATE, false);
                //--- OPEN REPLAY BUY POSITION
                showPosition(lastPrice, true);
                PlaySound("ok.wav");
                isPositionOpen = true;
                ChartRedraw();
                break;
               }
             //--- SELL BUTTON
             if(sparam == _REPLAY_SELL_BUTTON)
               {
                ObjectSetInteger(0, _REPLAY_SELL_BUTTON, OBJPROP_STATE, false);
                //--- OPEN REPLAY SELL POSITION
                showPosition(lastPrice, false);
                PlaySound("ok.wav");
                isPositionOpen = true;
                ChartRedraw();
                break;
               }
             //--- DISABLE ANCHOR DESELECTION WHEN IN PLAY MODE
             if(!ObjectGetInteger(0, _REPLAY_ANCHORLINE, OBJPROP_SELECTED))
               {
                if(ObjectGetString(0, _REPLAY_PLAY_BUTTON, OBJPROP_TEXT) == _PLAY_SYMBOL)
                   ObjectSetInteger(0, _REPLAY_ANCHORLINE, OBJPROP_SELECTED, true);
                ChartRedraw();
               }
             break;
          //--- DRAG-AND-DROP OPERATION
          case CHARTEVENT_OBJECT_DRAG:
             if(sparam == _REPLAY_ANCHORLINE)
               {
                //--- CLEAR CHART
                ObjectsDeleteAll(0, _PROG_NAME + "REPLAY_POSITION");
                customCandles(true, iTime(_Symbol, PERIOD_CURRENT, 0));
                vlineTime = (datetime)ObjectGetInteger(0, _REPLAY_ANCHORLINE, OBJPROP_TIME);
                vlineTime = (vlineTime > iTime(_Symbol, PERIOD_CURRENT, 1))
                            ? iTime(_Symbol, PERIOD_CURRENT, 1) : vlineTime;
                ObjectSetInteger(0, _REPLAY_ANCHORLINE, OBJPROP_TIME, vlineTime);
                //--- SHOW CUSTOM REPLAY CANDLES
                customCandles(false, vlineTime);
                isPlay = true;
                togglePlayButton();
                ChartRedraw();
                break;
               }
             //--- ENSURE REPLAY POSITION SL LEVEL IS PLACED CORRECTLY
             if(sparam == _REPLAY_POSITION_SL)
               {
                if(positionType == POSITION_TYPE_BUY)
                  {
                   if(ObjectGetDouble(0, _REPLAY_POSITION_SL, OBJPROP_PRICE) >= entryPrice)
                      ObjectMove(0, _REPLAY_POSITION_SL, 0, 0, entryPrice - (10000 * _Point));
                   ChartRedraw();
                  }
                else
                  {
                   if(ObjectGetDouble(0, _REPLAY_POSITION_SL, OBJPROP_PRICE) <= entryPrice)
                      ObjectMove(0, _REPLAY_POSITION_SL, 0, 0, entryPrice + (10000 * _Point));
                   ChartRedraw();
                  }
                slPrice = ObjectGetDouble(0, _REPLAY_POSITION_SL, OBJPROP_PRICE);
                break;
               }
             //--- ENSURE REPLAY POSITION TP LEVEL IS PLACED CORRECTLY
             if(sparam == _REPLAY_POSITION_TP)
               {
                if(positionType == POSITION_TYPE_BUY)
                  {
                   if(ObjectGetDouble(0, _REPLAY_POSITION_TP, OBJPROP_PRICE) <= entryPrice)
                      ObjectMove(0, _REPLAY_POSITION_TP, 0, 0, entryPrice + (10000 * _Point));
                   ChartRedraw();
                  }
                else
                  {
                   if(ObjectGetDouble(0, _REPLAY_POSITION_TP, OBJPROP_PRICE) >= entryPrice)
                      ObjectMove(0, _REPLAY_POSITION_TP, 0, 0, entryPrice - (10000 * _Point));
                   ChartRedraw();
                  }
                tpPrice = ObjectGetDouble(0, _REPLAY_POSITION_TP, OBJPROP_PRICE);
               }
             break;
          //--- OBJECT DELETION RESTORATION
          case CHARTEVENT_OBJECT_DELETE:
             if(sparam == _REPLAY_ANCHORLINE)
               {
                Print("Replay Anchor line deleted. RESTORED");
                
    drawAnchorLine(_REPLAY_ANCHORLINE, vlineTime, "Replay Anchor");
               }
             ChartRedraw();
             break;
         }
      }

    Explanation:

    • Button Clicks
      The first event handled is CHARTEVENT_OBJECT_CLICK, which processes all button interactions. Clicking the ON/OFF menu button calls toggleMenuButton(), which enables or disables replay mode. When turned ON, it configures the chart for replay, creates the replay anchor line, hides the native MT5 candles, and displays custom candles up to the replay anchor point. When turned OFF, the replay-specific chart configuration is removed and the normal chart view is restored. The Play/Pause button starts or pauses the replay engine. The Buy and Sell buttons create a new imaginary replay trade at the current replay price, automatically replacing any existing replay position before initializing a new one. To preserve the replay workflow, the handler also prevents the replay anchor from being deselected while playback is paused.

    • Drag-And-Drop Operations

      The CHARTEVENT_OBJECT_DRAG event handles every draggable replay object. When the replay anchor is moved, the program clears the current replay state, recalculates the replay starting point, and redraws the custom replay candles so that only historical candles up to the new anchor remain visible. The same event also manages the Stop Loss and Take Profit levels of the imaginary replay trade, ensuring they cannot be positioned on the wrong side of the entry price while updating their corresponding price values.

    • Object Deletion Restoration

      Finally, the CHARTEVENT_OBJECT_DELETE event provides fault tolerance for the replay interface. If the replay anchor is accidentally deleted, the program immediately recreates it at its previous position, ensuring that the replay workflow remains uninterrupted.

    Timer Event

    The final event handler is OnTimer(), which serves as the replay engine’s heartbeat by revealing one candle on each timer tick. It first checks whether replay mode is active and performs a safe boundary check to ensure playback does not exceed the available historical data. If the boundary is reached, playback stops automatically; otherwise, revealOneCandle()

     is called to add the actual price data to the buffer slot corresponding to the lastCalculated variable. Once the candle is revealed, lastCalculated is incremented by one, allowing the next timer tick to reveal the following candle and advance the replay by one bar.

    //+------------------------------------------------------------------+
    //|                         TIMER EVENT                              |
    //+------------------------------------------------------------------+
    void OnTimer(void)
      {
    //---
       if(replayMode)
         {
          bars = iBars(_Symbol, PERIOD_CURRENT);
          //--- SAFETY BOUNDARY
          if(lastCalculated >= bars - 1)
            {
             replayMode = false;
             return;
            }
          revealOneCandle(lastCalculated);
          lastCalculated++;
         }
      }

    NOTE: The lastCalculated index is incremented because the replay data is stored in non-series order, where the oldest bar has index 0 and newer bars have progressively higher indices. Therefore, replay advances from lower to higher indices, making lastCalculated++ the correct direction for revealing the next candle.


    Indicator Testing

    Once the program compiles successfully without errors, it is ready for testing. The GIFs below demonstrate the completed Bar Replay Tool in action, highlighting selected features such as anchor movement, paper trade management, multiple play/pause operations, and chart restoration. Together, these examples demonstrate that the replay workflow and its interactive features function as intended.


    Test for Anchor Movement and Paper Trade Management

    Fig. 2. Test for Anchor Movement and Paper Trade Management


    Test for Multiple Pause and Chart Restoration



    Fig. 3. Test for Multiple Pause/Play and Chart Restoration


    Conclusion

    We implemented a practical Bar Replay Tool in MQL5 that converts historical price data into a controlled, semi-live simulation on the MetaTrader 5 chart. The indicator demonstrates how to replace native candles with custom DRAW_COLOR_CANDLES rendering, tie a draggable vertical anchor to a replay start point, and drive stepwise playback with OnTimer() while handling all user interactions through OnChartEvent(). The finished artifact is a working indicator that:

    • toggles replay mode and reliably restores the chart's original appearance;
    • lets the user pick the start candle by dragging an anchor and hides all future candles to the right;
    • reveals historical data one candle per timer tick while keeping the active bar visible;
    • supports a single paper trade (Buy/Sell) with Entry/SL/TP lines that are monitored and auto‑closed when hit.
            This base architecture meets clear completion criteria—compile, enable replay, select anchor, play/pause, place one paper trade, and restore the chart—and is straightforward to extend (playback speed, closed trade replay, multiple positions, hotkeys) without changing the core event‑driven logic.
            Attached files |
            Bar_Replay_Tool.mq5 (25.59 KB)
            From Basic to Intermediate: Queues, Lists, and Trees (II) From Basic to Intermediate: Queues, Lists, and Trees (II)
            This is an article that you, dear reader, should study carefully. That is due to the nature of the material presented here. Although we have tried to present the material as simply and informatively as possible, the information provided here can certainly seem quite complex to those who are just beginning to learn programming. Nevertheless, this is no reason to lose heart or ignore what is explained here, as this article will establish a link between two completely different, though closely related, topics.
            Price Action Analysis Toolkit Development (Part 81): Adding Persistent Historical Bookmarks to an MQL5 Navigator Price Action Analysis Toolkit Development (Part 81): Adding Persistent Historical Bookmarks to an MQL5 Navigator
            We introduce a persistent bookmark layer for the MetaTrader 5 History Navigator. Bookmarks capture a chart's symbol, timeframe, and historical position with a name and notes, write them to a CSV file, and reload them later without manual date entry. The implementation integrates bookmark management into the current navigation engine, enabling quick creation, selection, navigation, and deletion for efficient historical study.
            Market Simulation: Position View (X) Market Simulation: Position View (X)
            We need a way to handle the graphical objects we create. The approach presented in the previous article works very well for certain scenarios. In this case, we will need something more complex, given the specific nature of the problem at hand. Therefore, we will not attempt to replace the ZOrder management mechanisms already present in MetaTrader 5, nor, of course, will we check which object is in the foreground or covered by another object. We are going to do something completely different. Here, I will show you what changes need to be made to the code in order to use part of what MetaTrader 5 already does for us.
            Market Simulation: Position View (IX) Market Simulation: Position View (IX)
            In this turning-point article, we will begin to explore in greater depth the interaction between the applications we are developing to ensure full support for the replay/simulation system. Here we will analyze a problem that, on the one hand, is quite unpleasant, but on the other hand, is very interesting to explain and solve. The problem is this: how can we restore the take-profit and stop-loss lines after they have been deleted, and do so without using the terminal by performing the operation directly on the chart? At first glance, it seems simple. However, there are several obstacles that must be overcome.