Создание инструмента воспроизведения баров на MQL5
Введение
После вынужденного выхода из сделки вы начинаете задавать себе вопросы. «Что я упустил? Может, я вошёл слишком рано? В какой момент рынок начал подавать сигналы, которые я проигнорировал?»
Вы прокручиваете график назад, надеясь заново проследить динамику рынка свеча за свечой. Но есть одна проблема — будущее уже просматривается. Как бы вы ни старались соблюдать дисциплину, взгляд в конце концов всё равно смещается к последующим свечам. Исход уже известен, и это знание начинает влиять на ваш анализ. С этим сталкивается большинство трейдеров, и я не был исключением. По мере того как я анализировал всё больше сделок, я понял, что мне не нужен ещё один статический график. Мне нужен был способ воспроизводить движение рынка так, как оно развивалось: делать паузу перед формированием следующей свечи, принимать решение, опираясь только на доступную информацию, а затем показывать следующую свечу, чтобы проверить правильность анализа.
Хотя MetaTrader 5 предоставляет мощный тестер стратегий для тестирования автоматизированных торговых систем на исторических данных, в нём нет интерактивной среды воспроизведения графика для дискреционных трейдеров, которые хотят вручную изучать движение цены, отрабатывать исполнение сделок или воспроизводить исторические рыночные данные свеча за свечой. Это открывает интересные возможности для разработчиков MQL5.
В этой статье мы создадим интерактивный инструмент воспроизведения баров с интуитивно понятным интерфейсом, который воспроизводит историческое движение цены, показывая свечи по одной. Для управления инструментом предусмотрены кнопка меню «ON/OFF», активирующая режим воспроизведения, перетаскиваемый якорь воспроизведения для выбора начальной точки на истории, кнопка запуска и приостановки воспроизведения, а также кнопки «Buy» и «Sell», позволяющие пользователю совершить одну виртуальную сделку во время воспроизведения. В совокупности эти элементы управления создают простую, но практичную среду для изучения движения цены и отработки навыков дискреционной торговли без раскрытия будущих рыночных данных. К концу этой статьи вы узнаете, как:- Создавать собственный механизм воспроизведения, используя DRAW_COLOR_CANDLES для постепенного отображения исторических свечей.
- Динамически управлять внешним видом графика, плавно переключаясь между стандартными свечами и свечами с пользовательской отрисовкой.
- Реализовать перетаскиваемый якорь воспроизведения, задающий начальную точку воспроизведения, и интуитивно понятные элементы управления запуском и паузой.
- Разработать механизм воспроизведения, работающий по таймеру и имитирующий торговлю в условиях, приближенных к реальному времени, за счет последовательного отображения свечей по одной.
- Реализовать систему имитационной торговли в режиме воспроизведения с интерактивными кнопками «Buy» и «Sell».
- Обеспечить стабильность видимой области графика, автоматически удерживая активную свечу в поле зрения на протяжении всего воспроизведения.
В совокупности эти методы демонстрируют, как событийно-ориентированная архитектура MQL5, возможности пользовательской отрисовки и средства работы с графическими объектами позволяют создавать инструменты воспроизведения графиков, значительно расширяющие стандартные возможности MetaTrader 5.
Обзор проекта
В рамках этого проекта мы создаем интерактивный инструмент воспроизведения баров, который на основе исторических данных имитирует работу рынка в условиях, приближенных к реальному времени, на графике MetaTrader 5. Вместо того чтобы отображать всю историю цен сразу, инструмент постепенно показывает свечи по одной, воссоздавая ощущение наблюдения за развитием рынка в режиме реального времени. Это позволяет нам воспроизводить историческое движение цены, анализировать торговые решения и отрабатывать навыки чтения рынка в контролируемой среде, не показывая будущие свечи. В этом инструменте предусмотрена кнопка «ON/OFF» в меню для включения режима воспроизведения. В режиме воспроизведения перемещаемый вертикальный якорь задает начальную точку, а свечи справа от него скрываются. Затем на панели управления появляются элементы управления запуском и паузой, а также кнопки «Buy» и «Sell» для одной виртуальной сделки.Ниже представлена визуальная модель инструмента, описанного в данном проекте:

Рис. 1. Обзор проекта
Рабочий процесс проектирования
Прежде чем приступить к реализации, важно четко определить последовательность работы системы воспроизведения. Разделение программы на четко определенные этапы упрощает разработку, делает реализацию более понятной и обеспечивает согласованную работу на протяжении всего процесса воспроизведения. От включения режима воспроизведения и размещения якоря воспроизведения до постепенного отображения исторических свечей и восстановления графика — каждый этап отвечает за определенную часть функциональности инструмента. Понимание этого рабочего процесса создает прочную основу для последующей реализации.
Инициализация
Рабочий процесс начинается на этапе инициализации индикатора, когда закладывается основа системы воспроизведения. Сначала программа сохраняет текущие параметры оформления графика, включая цвета бычьих и медвежьих свечей, цвета баров, цвет линии графика и цвета линий Bid и Ask, чтобы восстановить их после отключения режима воспроизведения. Затем программа создает кнопку меню «ON/OFF», которая служит для включения и выключения инструмента воспроизведения баров. Наконец, индикатор инициализирует свои индикаторные буферы для свечей и запускает таймер с интервалом в две секунды, который впоследствии будет управлять механизмом воспроизведения, обеспечивая постепенное отображение исторических свечей.
Включение режима воспроизведения
Режим воспроизведения включается при нажатии пользователем кнопки меню «ON/OFF». Прежде чем отобразить какие-либо данные воспроизведения, программа выполняет ряд шагов инициализации для подготовки как графика, так и среды воспроизведения. Сначала программа включает обработку событий удаления объектов графика, что позволяет обнаружить случайное удаление якоря воспроизведения. Если якорь удалён, он автоматически создаётся заново, чтобы воспроизведение могло продолжаться без перерыва.
Далее программа определяет текущую видимую область графика и размещает в её центре вертикальный якорь воспроизведения. Это обеспечивает удобную отправную точку для воспроизведения по умолчанию, при этом пользователь может переместить якорь на любую историческую свечу до начала воспроизведения. После создания якоря воспроизведения программа отображает компактную панель управления с кнопкой «Воспроизведение/Пауза» для управления воспроизведением, а также кнопками «Buy» и «Sell», позволяющими пользователю открыть одну виртуальную сделку во время воспроизведения.
После размещения элементов управления воспроизведением график подготавливается к пользовательской отрисовке свечей. Сначала программа скрывает стандартные свечи MetaTrader 5, а затем выполняет пользовательскую отрисовку свечей. Отображаются только свечи до якоря воспроизведения, а все свечи справа от него остаются скрытыми. Это задаёт исходное состояние воспроизведения, в котором до его запуска не видна никакая информация о будущем движении рынка.
Имитационная торговля при воспроизведении
Чтобы имитировать реальную торговлю во время воспроизведения, инструмент позволяет пользователю открыть одну виртуальную сделку, нажав кнопку «Buy» или «Sell». При открытии сделки программа прочерчивает три горизонтальные линии, обозначающие уровни входа, стоп-лосса (SL) и тейк-профита (TP). Если при уже открытой позиции в режиме воспроизведения снова нажать кнопку «Buy» или «Sell», существующая позиция автоматически заменяется новой.
По мере воспроизведения виртуальная позиция проверяется на каждой новой отображаемой свече. При достижении уровня стоп-лосса или тейк-профита виртуальная позиция автоматически закрывается путём удаления её графических объектов с графика.
Управление воспроизведением на основе событий
В инструменте воспроизведения используется событийно-ориентированная архитектура: все действия пользователя обрабатываются функцией OnChartEvent(). Этот центральный обработчик событий реагирует на нажатия кнопок меню, действия с элементами панели управления воспроизведением, выбор якоря воспроизведения, операции перетаскивания и события удаления графических объектов. При каждом перемещении якоря воспроизведения отображение немедленно пересчитывается, так что видимыми остаются только свечи до выбранной точки.
Воспроизведение свечей по таймеру
Как только начинается воспроизведение, событие таймера становится движущей силой механизма воспроизведения. При каждом срабатывании таймера, то есть каждые две секунды, обработчик проверяет, активен ли режим воспроизведения, а затем снимает выделение с якоря воспроизведения, обозначая тем самым, что воспроизведение идёт. Следующая историческая свеча отображается за счёт обновления индикаторных буферов пользовательских свечей, а горизонтальная ценовая линия одновременно перемещается на уровень цены закрытия этой свечи, создавая эффект работы рынка в реальном времени с появлением свечей одна за другой. Этот процесс продолжается до тех пор, пока воспроизведение не будет приостановлено или не будут показаны все свечи воспроизведения.
Отключение воспроизведения
Процесс воспроизведения завершается, когда пользователь отключает систему с помощью кнопки ON/OFF в меню. На этом этапе программа корректно возвращает график в исходное состояние. Сначала программа отключает уведомления об удалении графических объектов, затем удаляет якорь воспроизведения и другие объекты, связанные с воспроизведением. Затем на всем графике восстанавливаются стандартные свечи MetaTrader 5, а пользовательские свечи воспроизведения удаляются, благодаря чему график возвращается к своему обычному виду и поведению.
Реализация на MQL5
Определив порядок работы инструмента, мы теперь можем перейти к практической реализации каждого этапа на MQL5. Мы последовательно реализуем основные компоненты инструмента: инициализацию, активацию меню, взаимодействие на основе событий, воспроизведение по таймеру и отключение воспроизведения.
Директивы компилятора
Для начала определим директивы компилятора, которые настраивают индикатор и его поведение при отображении. Директивы #property задают метаданные программы и настраивают графическое построение типа DRAW_COLOR_CANDLES, используемое для отображения свечей воспроизведения.
Далее директивы #define задают постоянные имена объектов воспроизведения, что обеспечивает единообразие во всей программе и упрощает сопровождение кода. Наконец, в элементах управления воспроизведением используются символы Unicode (▶ и ⏸), которые служат интуитивно понятными значками кнопок «Воспроизведение» и «Пауза».
//+------------------------------------------------------------------+ //| 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 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 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;
Вспомогательные функции
Чтобы обеспечить упорядоченность и модульность реализации, инструмент воспроизведения разбит на несколько вспомогательных функций, каждая из которых отвечает за выполнение конкретной задачи. Эти функции отвечают за настройку графика, определение его видимой области, визуализацию, управление воспроизведением и пользовательскую отрисовку свечей.- Настройка графика
Начнём с реализации функций-членов, объявленных ранее в пользовательской структуре данных. Эти функции отвечают за настройку графика во время воспроизведения, восстановление его исходного вида по окончании воспроизведения и получение исходного цвета линий цен Ask/Bid на графике для отрисовки линии цены воспроизведения.
//+------------------------------------------------------------------+ //| 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 | //+------------------------------------------------------------------+ 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); }
- Визуализация
Интерфейс воспроизведения создаётся с помощью набора вспомогательных функций визуализации, которые создают графические объекты программы, включая панель управления, кнопки управления, интерактивную линию якоря воспроизведения и уровни виртуальной сделки. При открытии сделки на покупку или продажу программа создаёт уровни входа, стоп-лосса (SL) и тейк-профита (TP). Начальные уровни TP и SL задаются со смещением ±10 000 пунктов от цены входа; пользователь может изменять эти уровни управления сделкой.
//+------------------------------------------------------------------+ //| 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(); }
ПРИМЕЧАНИЕ: В целях краткости в примерах кода, приведённых в этом разделе, показаны только ключевые свойства объектов, имеющие отношение к визуализации. Остальные свойства и полные конфигурации объектов приведены в полном исходном коде, прилагаемом к данной статье.
- Система переключения с помощью кнопок
Рабочий процесс воспроизведения координируется с помощью двух функций переключения, которые управляют рабочими состояниями инструмента. Кнопка меню включает или отключает режим воспроизведения: при этом настраивается график, создаётся или удаляется интерфейс воспроизведения и выполняется переключение между стандартным и пользовательским отображением графика. Кнопка воспроизведения управляет процессом воспроизведения, переключая его между состояниями «Воспроизведение» и «Пауза», а также блокирует или разблокирует якорь воспроизведения и запускает или останавливает механизм воспроизведения.
//+------------------------------------------------------------------+ //| 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(); }
- Пользовательские свечи воспроизведения
В основе механизма воспроизведения лежит система пользовательской отрисовки свечей, реализованная с помощью DRAW_COLOR_CANDLES. Вместо того чтобы полагаться на стандартные свечи MetaTrader 5, индикатор сам точно определяет, какие свечи отображать, благодаря чему историческое движение цены раскрывается постепенно, а будущие свечи остаются скрытыми.
//+------------------------------------------------------------------+ //| 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(); } }
Пояснение
- customCandle()
Эта функция инициализирует свечи воспроизведения. Она копирует исторические ценовые данные графика, определяет точку остановки воспроизведения по выбранному якорю воспроизведения и отображает или скрывает пользовательские свечи в зависимости от состояния воспроизведения. Поскольку индикаторные буферы изначально заполнены значением EMPTY_VALUE, при isRemove, равном false, функция заполняет их фактическими ценовыми данными до времени, заданного якорем воспроизведения; элементы буферов после этого времени сохраняют значение EMPTY_VALUE, поэтому соответствующие свечи не отображаются. Когда режим воспроизведения включен, функция также создает линию цены воспроизведения для отслеживания цены закрытия последней воспроизведенной свечи. - revealOneCandle()
Во время воспроизведения эта функция выступает в качестве ядра механизма воспроизведения, отображая ровно одну новую свечу за раз. Для этого функция заполняет ранее пустые элементы индикаторных буферов ценовыми данными свечи, благодаря чему новая свеча становится видимой на графике. Помимо обновления буферов пользовательских свечей, функция отслеживает достижение уровней тейк-профита (TP) и стоп-лосса (SL) по виртуальной сделке в режиме воспроизведения, автоматически закрывает позицию при достижении любого из этих уровней, удерживает активную свечу воспроизведения в видимой области графика и перемещает линию цены воспроизведения на уровень последней цены закрытия.
Логика инициализации (OnInit)
Теперь, когда вспомогательные функции готовы, мы можем приступить к реализации стандартных обработчиков событий MQL5, начиная с OnInit(). Во время инициализации индикатор создает кнопку меню ON/OFF, регистрирует буферы пользовательских свечей, инициализирует их пустыми значениями, настраивает точность индикатора и запускает таймер с интервалом 2 секунды, управляющий механизмом воспроизведения.
//+------------------------------------------------------------------+ //| 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); }
Очистка (OnDeinit)
Когда индикатор удаляется с графика, обработчик OnDeinit() выполняет необходимые действия по очистке. Этот обработчик останавливает таймер воспроизведения, отключает события удаления объектов, удаляет все объекты, связанные с воспроизведением, и восстанавливает исходный вид графика.
//+------------------------------------------------------------------+ //| 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(); }
Основная функция итерации
Поскольку работа инструмента воспроизведения полностью управляется функциями OnChartEvent() и OnTimer(), функция OnCalculate() не выполняет никакой обработки, а просто возвращает общее количество доступных свечей.
//+------------------------------------------------------------------+ //| 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); }
Механизм взаимодействия
Инструмент воспроизведения построен на основе обработчика OnChartEvent(), который выступает в качестве основного механизма взаимодействия приложения. Он обрабатывает каждое взаимодействие пользователя с интерфейсом воспроизведения, включая нажатия кнопок, операции перетаскивания и события удаления объектов, обеспечивая мгновенную реакцию среды воспроизведения на действия пользователя при сохранении согласованного состояния воспроизведения.//+------------------------------------------------------------------+ //| 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; } }
Пояснение
- Нажатия кнопок
Первым обрабатывается событие CHARTEVENT_OBJECT_CLICK, при обработке которого выполняются все действия, связанные с кнопками. При нажатии кнопки меню «ON/OFF» вызывается toggleMenuButton(), которая включает или выключает режим воспроизведения. При включении режима эта функция настраивает график для воспроизведения, создаёт линию якоря воспроизведения, скрывает стандартные свечи MT5 и отображает свечи с пользовательской отрисовкой до точки якоря воспроизведения. При выключении режима настройки графика, используемые для воспроизведения, сбрасываются, и восстанавливается обычный вид графика. Кнопка «Воспроизведение/Пауза» запускает или приостанавливает работу механизма воспроизведения. Кнопки «Buy» и «Sell» создают новую виртуальную сделку по текущей цене воспроизведения, автоматически заменяя существующую позицию в режиме воспроизведения перед инициализацией новой. Чтобы сохранить порядок работы в режиме воспроизведения, обработчик также предотвращает снятие выделения с якоря воспроизведения, пока воспроизведение приостановлено.
- Операции с перетаскиванием
Обработчик события CHARTEVENT_OBJECT_DRAG обрабатывает перетаскивание всех перемещаемых объектов воспроизведения. При перемещении якоря воспроизведения программа сбрасывает текущее состояние воспроизведения, пересчитывает его начальную точку и перерисовывает свечи с пользовательской отрисовкой так, чтобы видимыми оставались только исторические свечи до нового положения якоря. Обработчик этого же события также управляет уровнями стоп-лосса и тейк-профита виртуальной сделки в режиме воспроизведения, не позволяя размещать их с неправильной стороны от цены входа и обновляя соответствующие значения цен.
-
Восстановление удаленных объектов
Наконец, событие CHARTEVENT_OBJECT_DELETE обеспечивает отказоустойчивость интерфейса воспроизведения. Если якорь воспроизведения случайно удалён, программа немедленно воссоздаёт его в прежнем положении, обеспечивая непрерывность работы в режиме воспроизведения.
вызывается для добавления фактических ценовых данных в ячейку буфера, соответствующую значению переменной lastCalculated. После отображения свечи значение lastCalculated увеличивается на единицу, что позволяет при следующем срабатывании таймера отобразить следующую свечу и продвинуть воспроизведение на один бар.
//+------------------------------------------------------------------+ //| TIMER EVENT | //+------------------------------------------------------------------+ void OnTimer(void) { //--- if(replayMode) { bars = iBars(_Symbol, PERIOD_CURRENT); //--- SAFETY BOUNDARY if(lastCalculated >= bars - 1) { replayMode = false; return; } revealOneCandle(lastCalculated); lastCalculated++; } }
ПРИМЕЧАНИЕ: Индекс lastCalculated увеличивается, поскольку данные воспроизведения хранятся с прямым порядком индексации: самый старый бар имеет индекс 0, а более новые бары — всё более высокие индексы. Следовательно, воспроизведение идет от меньших индексов к большим, поэтому операция lastCalculated++ обеспечивает переход в правильном направлении для отображения следующей свечи.
Тестирование индикатора
Как только программа успешно скомпилируется без ошибок, она готова к тестированию. Приведенные ниже GIF-анимации демонстрируют готовый инструмент воспроизведения баров в действии, показывая отдельные функции: перемещение якоря воспроизведения, управление виртуальной сделкой, многократный запуск и приостановку воспроизведения, а также восстановление графика. В совокупности эти примеры показывают, что рабочий процесс воспроизведения и его интерактивные функции работают так, как и предполагалось. 
Рис. 2. Проверка перемещения якоря воспроизведения и управления виртуальной сделкой

Рис. 3. Проверка многократной приостановки и запуска воспроизведения, а также восстановления графика
Заключение
Мы реализовали на MQL5 практичный инструмент воспроизведения баров, который преобразует исторические ценовые данные в управляемую имитацию работы рынка в режиме, приближенном к реальному времени, на графике MetaTrader 5. Индикатор демонстрирует, как заменить стандартные свечи пользовательской отрисовкой свечей с помощью DRAW_COLOR_CANDLES, привязать перетаскиваемый вертикальный якорь воспроизведения к начальной точке воспроизведения и управлять пошаговым воспроизведением через OnTimer(), обрабатывая все действия пользователя через OnChartEvent(). В результате получен работающий индикатор, который:
- включает/выключает режим воспроизведения и надежно восстанавливает исходный вид графика;
- позволяет пользователю выбрать начальную свечу, перетаскивая якорь воспроизведения, и скрывает все последующие свечи справа;
- отображает исторические данные по одной свече за каждое срабатывание таймера, сохраняя активный бар видимым;
- поддерживает одну виртуальную сделку (покупка/продажа) с линиями уровня входа, стоп-лосса (SL) и тейк-профита (TP), отслеживая их и автоматически закрывая сделку при достижении уровня SL или TP.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23823
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Здравствуйте, Чуквубуикем;
Сразу хочу поблагодарить вас за уникальную идею для этой статьи. Она очень хорошая, но я пока прочитал только введение, а не всю статью до конца. Сначала я решил проверить её на практике. Я перетащил индикатор на график, и вот в чём его недостаток:
очевидно, в каком направлении будет двигаться цена. Пришлю скриншот:
Очевидно, что цена будет двигаться вверх. Вам следует отключить автоматическое масштабирование графика в вашем индикаторе, либо пользователь должен сделать это вручную.
Здравствуйте, Чуквубуикем;
Очевидно, что цена будет расти. Вам следует отключить автоматическое масштабирование графика в индикаторе, либо пользователь должен сделать это вручную.
Спасибо, что поделились своими наблюдениями и предложением. Те, кто также хочет скрыть шкалу цен на графике, могут легко реализовать это, добавив следующие строки кода в функцию toggleMenuButton(), представленную в статье:
Кроме того, добавьте следующую строку в функцию деинициализации, чтобы восстановить шкалу цены при удалении индикатора: