Торговые инструменты на MQL5 (Часть 5): Создание бегущей тикерной строки для мониторинга символов в реальном времени
Введение
В своей предыдущей статье (Часть 4) мы улучшили панель сканера по нескольким таймфреймам функциями динамического позиционирования и переключения на MetaQuotes Language 5 (MQL5), реализовав подвижные и сворачиваемые окна для повышения удобства использования. В Части 5 мы создаем бегущую тикерную строку для мониторинга нескольких инструментов в режиме реального времени, в которой отображаются в режиме прокрутки цены Bid, спреды, ежедневные процентные изменения и настраиваемые визуальные эффекты, позволяющие трейдерам моментально быть в курсе событий. В статье рассмотрим следующие темы:
- Понимание архитектуры бегущей тикерной строки
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет универсальный инструмент для работы с тикерами на MQL5, готовый к настройке и интеграции в вашу торговую систему. Давайте начнём!
Понимание архитектуры бегущей тикерной строки
Бегущая тикерная строка, которую мы создаем, - это инструмент для отображения данных в режиме реального времени по нескольким инструментам в формате прокрутки, показывающий цены Bid, спреды и ежедневные процентные изменения, позволяющие оперативно отслеживать изменения. Эта функция важна, поскольку она обеспечивает компактное, динамичное представление о движении рынка, выделяя ценовые тренды и волатильность, не перегружая график, что важно для быстрого принятия решений в условиях быстро меняющегося рынка.
Мы добьемся этого, разделив экран на отдельные линии прокрутки для инструментов, цен, спредов и изменений, используя настраиваемые скорости и цвета для обозначения движения вверх или вниз. Мы планируем использовать массивы для данных об инструментах и таймеры для плавной прокрутки, чтобы тикер адаптировался к предпочтениям пользователя и эффективно работал в фоновом режиме. Давайте продолжим и посмотрим, как мы воплотим это в жизнь! В двух словах, ниже приведена визуализация того, к чему мы стремимся.

Реализация средствами MQL5
Чтобы создать программу на MQL5, нам нужно будет определить метаданные программы, а затем определить некоторые входные параметры, которые позволят легко изменять работу программы, не вмешиваясь непосредственно в код.
//+------------------------------------------------------------------+ //| ROLLING TICKER TIMER EA.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #include <Arrays\ArrayString.mqh> //--- Include ArrayString library for string array operations //--- Input parameters input string Symbols = "EURUSDm,GBPUSDm,USDJPYm,USDCHFm,AUDUSDm,BTCUSDm,TSLAm"; // Symbols to display input int UpdateInterval = 50; // Update interval (milliseconds) input int SymbolFontSize = 10; // Symbol font size (first line) input string SymbolFont = "Arial Bold"; // Symbol font input int AskFontSize = 10; // Ask font size (second line) input string AskFont = "Arial"; // Ask font input int SpreadFontSize = 10; // Spread font size (third line) input string SpreadFont = "Calibri"; // Spread font input int SectionFontSize = 10; // Section currency, bid, and percent change font size input string SectionFont = "Arial"; // Section currency, bid, and percent change font input color FontColor = clrWhite; // Base font color input color UpColor = clrLime; // Color for price increase (Bid text and positive % change) input color DownColor = clrRed; // Color for price decrease (Bid text and negative % change) input color ArrowUpColor = clrBlue; // Color for up arrow input color ArrowDownColor = clrRed; // Color for down arrow input int Y_Position = 30; // Starting Y position (pixels) input int SymbolHorizontalSpacing = 160; // Horizontal spacing for Symbol line (pixels) input int AskHorizontalSpacing = 150; // Horizontal spacing for Ask line (pixels) input int SpreadHorizontalSpacing = 200; // Horizontal spacing for Spread line (pixels) input int SectionHorizontalSpacing = 170; // Horizontal spacing for Section line (pixels) input double SymbolScrollSpeed = 3.0; // Symbol line scroll speed (pixels per update) input double AskScrollSpeed = 1.3; // Ask line scroll speed (pixels per update) input double SpreadScrollSpeed = 10.0; // Spread line scroll speed (pixels per update) input double SectionScrollSpeed = 2.7; // Section scroll speed (pixels per update) input bool ShowSpread = true; // Show spread line input color BackgroundColor = clrBlack; // Background rectangle color input int BackgroundOpacity = 100; // Background opacity (0-255, limited effect)
Здесь мы начинаем реализацию нашей бегущей тикерной строки для мониторинга инструментов в режиме реального времени на MQL5, включая библиотеку "<Arrays\ArrayString.mqh>" и определяя входные параметры для пользовательской настройки. Мы включили "<Arrays\ArrayString.mqh>", чтобы обеспечить эффективные операции с массивами строк, необходимые для обработки и разделения списка отображаемых инструментов. Входной параметр "Symbols" - это строка, установленная на "EURUSDm, GBPUSDm, USDJPYm, USDCHFm, AUDUSDm, BTCUSDm, TSLAm", чтобы указать инструменты для мониторинга, что позволяет настроить, какие активы отображаются в тикере. Мы установили параметр "UpdateInterval" равным 50 миллисекундам для частоты обновления, стремясь к балансу между быстродействием и производительностью.
Для визуальной настройки определяем "SymbolFontSize" как 10, "SymbolFont" как "Arial Bold" для строки инструментов, "AskFontSize" как 10, "AskFont" как "Arial" для строки цен ask, "SpreadFontSize" как 10, "SpreadFont" как "Calibri" для строки спреда, "SectionFontSize" - как 10 и "SectionFont" как "Arial" для раздела с валютой, предложением цены и процентным изменением.
Используем "FontColor" как clrWhite для основного текста, "UpColor" как "clrLime" и "DownColor" как "clrRed" для изменения цены, "ArrowUpColor" как "clrBlue" и "ArrowDownColor" как "clrRed" для стрелок направления. Входные параметры для позиционирования и интервалов включают "Y_Position" (30 пикселей) для начального вертикального размещения, "SymbolHorizontalSpacing" (160 пикселей), "AskHorizontalSpacing" (150 пикселей), "SpreadHorizontalSpacing" (200 пикселей) и "SectionHorizontalSpacing" (170 пикселей) для управления раскладкой.
Скорость прокрутки задается следующим образом: "SymbolScrollSpeed" - 3,0 пикселя за обновление, "AskScrollSpeed" - 1,3, "SpreadScrollSpeed" - 10,0 и "SectionScrollSpeed" - 2,7 для независимого перемещения строк. Устанавливаем для параметра "ShowSpread" значение true, чтобы включить строку спреда, для параметра "BackgroundColor" значение "clrBlack", а для параметра "BackgroundOpacity" значение 100 для фонового прямоугольника. Эти параметры позволяют настроить внешний вид и поведение тикера для оптимального мониторинга в режиме реального времени. После компиляции получаем следующие наборы входных параметров.

Определив входные параметры, мы можем продолжить определение некоторых глобальных переменных и структур, которые мы будем использовать во всей программе и использовать для хранения повторяющейся информации для тикерной строки, соответственно.
//--- Global variables string symbolArray[]; //--- Array to store symbol names int totalSymbols; //--- Total number of symbols struct SymbolData //--- Structure to hold symbol price data { double bid; //--- Current bid price double ask; //--- Current ask price double spread; //--- Current spread double prev_bid; //--- Previous bid price double daily_open; //--- Daily opening price color bid_color; //--- Color for bid price display double percent_change; //--- Daily percentage change color percent_color; //--- Color for percentage change string arrow_char; //--- Arrow character for price direction color arrow_color; //--- Color for arrow }; SymbolData prices[]; //--- Array of symbol data structures string dashboardName = "TickerDashboard"; //--- Name for dashboard objects string backgroundName = "TickerBackground"; //--- Name for background object CArrayString objManager; //--- Object manager for text and image objects datetime lastDay = 0; //--- Track last day for daily open update
Здесь мы определяем глобальные переменные и структуру для управления данными по инструментам и элементами панели. Объявляем "symbolArray" как массив строк для хранения имен инструментов из входного параметра "Symbols". Целое число "totalSymbols" будет отслеживать количество инструментов после разделения входной строки. Определяем структуру "SymbolData" для хранения информации о ценах по каждому инструменту, включая "bid" для текущей цены bid, "ask" для текущей цены ask, "spread" для рассчитанного спреда, "prev_bid" для предыдущей цены bid для обнаружения изменений, "daily_open" для дневной цены открытия, "bid_color" - для цвета отображения цены bid, "percent_change" - для дневного процентного сдвига, "percent_color" - для окраски изменения, "arrow_char" - для стрелок направления и "arrow_color" - для цвета стрелок.
Создаем "prices" в виде массива структур "SymbolData" для хранения данных по всем инструментам. Для строки "dashboardName" задано значение "TickerDashboard" для именования объектов информационной панели, а для "backgroundName" - значение "TickerBackground" для фонового прямоугольника. Для упрощения очистки используем "CArrayString objManager" для управления именами всех текстовых и графических объектов. Наконец, параметр "lastDay" в формате datetime будет отслеживать последний день для обновления данных о ежедневных ценах открытия. Эти глобальные переменные организуют данные инструментов и управление объектами, обеспечивая эффективное обновление в реальном времени и прокрутку. Далее определим несколько глобальных служебных функций для создания основной панели тикеров следующим образом.
//+------------------------------------------------------------------+ //| Utility Functions | //+------------------------------------------------------------------+ void LogError(string message) // Log error messages { Print(message); //--- Output message to log } //+------------------------------------------------------------------+ //| Create Text Label Function | //+------------------------------------------------------------------+ bool createText(string objName, string text, int x, int y, color clrTxt, int fontsize, string font) { ResetLastError(); //--- Clear last error code if(ObjectFind(0, objName) < 0) //--- Check if object does not exist { if(!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) //--- Create text label object { LogError(__FUNCTION__ + ": Failed to create label: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log creation failure return false; //--- Return failure } objManager.Add(objName); //--- Add object name to manager } ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x); //--- Set x-coordinate ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y); //--- Set y-coordinate ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner alignment ObjectSetString(0, objName, OBJPROP_TEXT, text); //--- Set text content ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set text color ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontsize); //--- Set font size ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font type ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Disable background ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selection ObjectSetInteger(0, objName, OBJPROP_ZORDER, 0); //--- Set z-order return true; //--- Return success } //+------------------------------------------------------------------+ //| Create Panel Function | //+------------------------------------------------------------------+ bool createPanel(string objName, int y, int width, int height, color clr) { ResetLastError(); //--- Clear last error code if(ObjectFind(0, objName) < 0) //--- Check if panel does not exist { if(!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) //--- Create rectangle panel { LogError(__FUNCTION__ + ": Failed to create panel: " + objName + ", Error: " + IntegerToString(GetLastError())); //--- Log creation failure return false; //--- Return failure } objManager.Add(objName); //--- Add panel to object manager } ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, 0); //--- Set x-coordinate to 0 ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y); //--- Set y-coordinate ObjectSetInteger(0, objName, OBJPROP_XSIZE, width); //--- Set panel width ObjectSetInteger(0, objName, OBJPROP_YSIZE, height); //--- Set panel height ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clr); //--- Set background color ObjectSetInteger(0, objName, OBJPROP_FILL, true); //--- Enable fill ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); //--- Set border color ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID); //--- Set border style ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); //--- Set border width ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Enable background drawing ObjectSetInteger(0, objName, OBJPROP_ZORDER, -1); //--- Set z-order behind other objects return true; //--- Return success }
Реализуем служебные функции для обработки регистрации ошибок, создания текстовых меток и настройки панели, обеспечивая надежную работу элементов пользовательского интерфейса (UI) и отладку. Начнём с функции "LogError", которая принимает строку "message" и выводит её в лог с помощью функции Print. Далее создаём функцию "createText" для генерации текстовых меток для отображения бегущей строки. В качестве параметров она принимает "objName", "text", "x", "y", "clrTxt", "fontsize" и "font".
Сбрасываем последнюю ошибку с помощью функции ResetLastError и проверяем, существует ли объект, используя функцию ObjectFind. В противном случае создаём метку с помощью функции ObjectCreate под именем OBJ_LABEL, выводя ошибки и возвращаем значение false. Для управления добавляем "objName" в "objManager", затем устанавливаем свойства с помощью ObjectSetInteger для OBJPROP_XDISTANCE и всех остальных целочисленных свойств, а также с помощью "ObjectSetString" для "OBJPROP_TEXT" и "OBJPROP_FONT". Эта функция обеспечит единообразное отображение текста для инструментов, цен и изменений.
Затем определяем функцию "createPanel" для создания фоновой панели. Она принимает "objName", "y", "width", "height", и "clr" в качестве параметров и использует ту же структуру, что и функция "createText", которая предоставляет настраиваемый фон для бегущей строки, поддерживая эффекты, подобные непрозрачности, с помощью выбора цвета. Теперь можем перейти к созданию панели тикера, но сначала давайте упорядочим необходимые данные, что включает в себя разделение строки инструментов на отдельные независимые инструменты, которые мы можем использовать, и инициализацию данных о ценах и цветах. Мы сделаем это в обработчике OnInit.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Split symbols string into array totalSymbols = StringSplit(Symbols, ',', symbolArray); //--- Split input symbols into array ArrayResize(prices, totalSymbols); //--- Resize prices array to match symbol count //--- Verify symbols exist and initialize data for(int i = 0; i < totalSymbols; i++) //--- Iterate through all symbols { if(!SymbolSelect(symbolArray[i], true)) //--- Select symbol for market watch { LogError("OnInit: Symbol " + symbolArray[i] + " not found"); //--- Log symbol not found return(INIT_FAILED); //--- Return initialization failure } prices[i].bid = 0; //--- Initialize bid price prices[i].ask = 0; //--- Initialize ask price prices[i].spread = 0; //--- Initialize spread prices[i].prev_bid = 0; //--- Initialize previous bid prices[i].daily_open = iOpen(symbolArray[i], PERIOD_D1, 0); //--- Set daily opening price prices[i].bid_color = FontColor; //--- Set initial bid color prices[i].percent_change = 0; //--- Initialize percentage change prices[i].percent_color = FontColor; //--- Set initial percent color prices[i].arrow_char = CharToString(236); //--- Set default up arrow prices[i].arrow_color = FontColor; //--- Set initial arrow color } ArrayPrint(symbolArray); ArrayPrint(prices); }
В обработчике OnInit инициализируем нашу программу, устанавливая инструменты и структуры данных. Начнем с разделения входной строки "Symbols" на "symbolArray" с помощью StringSplit с разделителем-запятой, сохраняя количество инструментов в "totalSymbols". Если вы указали какой-либо другой разделитель, просто используйте его здесь. Затем изменяем размер массива "prices" на "totalSymbols" с помощью функции ArrayResize, чтобы он соответствовал количеству инструментов. Затем перебираем каждый инструмент в массиве "symbolArray", выбирая его для отслеживания рынка с помощью функции SymbolSelect и выводя ошибку с помощью функции "LogError", если это не удается, возвращая "INIT_FAILED".
Для каждого инструмента инициализируем "prices[i].bid", "prices[i].ask", "prices[i].spread" и "prices[i].prev_bid" значением 0, устанавливаем "prices[i].daily_open" равным цене открытия дня с помощью iOpen на "PERIOD_D1" и присваиваем начальные цвета и значения для "prices[i].bid_color", "prices[i].percent_change", "prices[i].percent_color", "prices[i].arrow_char" (используя CharToString для стрелки вверх) и "prices[i].arrow_color". Для отладки выводим значения "symbolArray" и "prices" с помощью функции ArrayPrint. Это гарантирует корректность всех инструментов и подготовку данных для обновления в режиме реального времени. После компиляции получаем следующий результат.

На изображении видно, что мы успешно инициализировали все инструменты и держатели данных, что означает, что теперь все готово. Теперь мы можем создать фон панели.
//+------------------------------------------------------------------+ //| Create background function | //+------------------------------------------------------------------+ void CreateBackground() { int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width int height = (ShowSpread ? 4 : 3) * (MathMax(MathMax(MathMax(SymbolFontSize, AskFontSize), SpreadFontSize), SectionFontSize) + 2) + 40; //--- Calculate panel height createPanel(backgroundName, Y_Position - 5, width, height, BackgroundColor); //--- Create background panel }
Здесь мы реализуем функцию "CreateBackground" и настраиваем фоновую панель для отображения бегущей строки. Начинаем с получения ширины графика с помощью функции ChartGetInteger, используя CHART_WIDTH_IN_PIXELS, и преобразуем ее в целое число в переменной «width». Вычисляем высоту панели в переменной "height", используя тернарный оператор на "ShowSpread", чтобы определить, имеется 4 строки или 3, умножая на максимальный размер шрифта из переменных "SymbolFontSize", "AskFontSize", "SpreadFontSize" и "SectionFontSize" (плюс 2 на заполнение) и добавляя 40 на дополнительное пространство. Наконец, вызываем функцию "createPanel" с параметрами "backgroundName", "Y_Position - 5" для вертикального выравнивания, "width", "height" и "BackgroundColor" для отрисовки прямоугольника фона, обеспечивая единообразную основу для прокручивания текстовых элементов. При вызове функции после инициализации получаем следующий результат.

После создания фона мы можем продолжить создание остальных элементов панели. Мы создаём функцию для размещения всего, как показано ниже.
//+------------------------------------------------------------------+ //| Create dashboard function | //+------------------------------------------------------------------+ void CreateDashboard() { //--- Create text and image objects for each symbol for(int i = 0; i < totalSymbols; i++) //--- Iterate through all symbols { // Determine image based on symbol string imageFile; //--- Variable for image file path if(symbolArray[i] == "EURUSDm") //--- Check for EURUSDm imageFile = "\\Images\\euro.bmp"; //--- Set EURUSD image else if(symbolArray[i] == "GBPUSDm") //--- Check for GBPUSDm imageFile = "\\Images\\gbpusd.bmp"; //--- Set GBPUSD image else if(symbolArray[i] == "USDJPYm") //--- Check for USDJPYm imageFile = "\\Images\\usdjpy.bmp"; //--- Set USDJPY image else if(symbolArray[i] == "USDCHFm") //--- Check for USDCHFm imageFile = "\\Images\\usdchf.bmp"; //--- Set USDCHF image else if(symbolArray[i] == "AUDUSDm") //--- Check for AUDUSDm imageFile = "\\Images\\audusd.bmp"; //--- Set AUDUSD image else if(symbolArray[i] == "BTCUSDm") //--- Check for BTCUSDm imageFile = "\\Images\\btcusd.bmp"; //--- Set BTCUSD image else if(symbolArray[i] == "TSLAm") //--- Check for TSLAm imageFile = "\\Images\\tesla.bmp"; //--- Set Tesla image else imageFile = "\\Images\\euro.bmp"; //--- Set default image // Symbol line (first line) createText(dashboardName + "_Symbol_" + IntegerToString(i), "", (i * SymbolHorizontalSpacing), Y_Position, FontColor, SymbolFontSize, SymbolFont); //--- Create symbol text label // Ask line (second line) createText(dashboardName + "_Ask_" + IntegerToString(i), "", (i * AskHorizontalSpacing), Y_Position + SymbolFontSize + 2, FontColor, AskFontSize, AskFont); //--- Create ask price text label // Spread line (third line, if enabled) if(ShowSpread) //--- Check if spread display is enabled { createText(dashboardName + "_Spread_" + IntegerToString(i), "", (i * SpreadHorizontalSpacing), Y_Position + SymbolFontSize + 2 + AskFontSize + 2, FontColor, SpreadFontSize, SpreadFont); //--- Create spread text label } // Section: Image (left) string imageName = dashboardName + "_Image_" + IntegerToString(i); //--- Define image object name if(ObjectFind(0, imageName) < 0) //--- Check if image object does not exist { if(!ObjectCreate(0, imageName, OBJ_BITMAP_LABEL, 0, 0, 0)) //--- Create image object { LogError("CreateDashboard: Failed to create image: " + imageName + ", Error: " + IntegerToString(GetLastError())); //--- Log image creation failure return; //--- Exit function } objManager.Add(imageName); //--- Add image to object manager } ObjectSetInteger(0, imageName, OBJPROP_XDISTANCE, (i * SectionHorizontalSpacing)); //--- Set image x-coordinate ObjectSetInteger(0, imageName, OBJPROP_YDISTANCE, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14)); //--- Set image y-coordinate ObjectSetString(0, imageName, OBJPROP_BMPFILE, imageFile); //--- Set image file ObjectSetInteger(0, imageName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set image corner alignment // Section: Currency (top, right of image) string currencyName = dashboardName + "_Currency_" + IntegerToString(i); //--- Define currency text object name createText(currencyName, StringFormat("%-10s", symbolArray[i]), (i * SectionHorizontalSpacing) + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), FontColor, SectionFontSize, SectionFont); //--- Create currency text label // Section: Percent Change (next to currency, horizontal) string percentChangeName = dashboardName + "_PercentChange_" + IntegerToString(i); //--- Define percent change object name string percentText = prices[i].percent_change >= 0 ? StringFormat("+%.2f%%", prices[i].percent_change) : StringFormat("%.2f%%", prices[i].percent_change); //--- Format percent change text createText(percentChangeName, percentText, (i * SectionHorizontalSpacing) + 105, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), prices[i].percent_color, SectionFontSize, SectionFont); //--- Create percent change text label // Section: Arrow (below currency, right of image, Wingdings) string arrowName = dashboardName + "_Arrow_" + IntegerToString(i); //--- Define arrow object name createText(arrowName, prices[i].arrow_char, (i * SectionHorizontalSpacing) + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].arrow_color, SectionFontSize, "Wingdings"); //--- Create arrow text label // Section: Bid Price (next to arrow, horizontal) string bidName = dashboardName + "_Bid_" + IntegerToString(i); //--- Define bid price object name createText(bidName, StringFormat("%.5f", prices[i].bid), (i * SectionHorizontalSpacing) + 50, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].bid_color, SectionFontSize, SectionFont); //--- Create bid price text label } }
Здесь мы реализуем функцию "CreateDashboard" для настройки визуальных элементов отображения бегущей строки, включая текстовые метки и изображения для каждого инструмента. Начинаем с перебора "totalSymbols" и определения "imageFile" на основе "symbolArray[i]" с условиями if-else, назначая конкретные файлы Bitmap (BMP) для таких инструментов, как "EURUSDm", или по умолчанию для других. Создаем текст строки инструментов с помощью "createText" для "dashboardName + "Symbol" + IntegerToString(i)", расположенного в "(i * SymbolHorizontalSpacing)" и "Y_Position".
Для линии ask создаем другую текстовую метку с помощью "createText" для "dashboardName + 'Ask' + IntegerToString(i)", расположенную в "(i * AskHorizontalSpacing)" и "Y_Position + SymbolFontSize + 2". Если "ShowSpread" равно true, добавляем текст линии спреда с помощью функции "createText" для "dashboardName + "Spread" + IntegerToString(i)", расположенный соответствующим образом.
Для этого раздела создаем объект изображения с помощью ObjectCreate как OBJ_BITMAP_LABEL, если он не существует, добавляем его в "objManager", задаем его положение с помощью "ObjectSetInteger" и присваиваем "imageFile" с помощью "ObjectSetString". Обратите внимание, что вам нужны файлы изображений в формате BMP. Мы использовали каталог по умолчанию следующим образом, но вы можете использовать собственный каталог.

Затем создаем текст валюты с помощью "createText" для "dashboardName + "Currency" + IntegerToString(i)", отформатированный с помощью StringFormat. Для изменения процента форматируем "percentText" на основе "prices[i].percent_change" и создаем текст с помощью "createText". Добавляем метку со стрелкой с надписью "createText", используя "prices[i].arrow_char" и шрифт "Wingdings". Наконец, создаем текст цены bid с помощью "createText", используя "StringFormat" для "prices[i].bid". Эта функция позволит создать многострочный макет бегущей строки с изображениями и динамическим текстом для прокрутки данных в режиме реального времени. Теперь просто вызовем функцию при инициализации, и вот какой результат мы получим.

В итоге получаем статическую панель. Теперь нам нужно обновить панель. Для обновлений в реальном времени нам не надо полагаться на обновления, основанные на тиках, поскольку это будет полностью зависеть от частоты тиков инструмента, к которому привязана программа. Нам следует использовать обновления по таймеру, чтобы они выполнялись часто. Сначала определим функции для обновления информационной панели и фона, когда это необходимо.
//+------------------------------------------------------------------+ //| Update background function | //+------------------------------------------------------------------+ void UpdateBackground() { int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get current chart width int height = (ShowSpread ? 4 : 3) * (MathMax(MathMax(MathMax(SymbolFontSize, AskFontSize), SpreadFontSize), SectionFontSize) + 2) + 40; //--- Recalculate panel height ObjectSetInteger(0, backgroundName, OBJPROP_XSIZE, width); //--- Update panel width ObjectSetInteger(0, backgroundName, OBJPROP_YSIZE, height); //--- Update panel height } //+------------------------------------------------------------------+ //| Update dashboard function | //+------------------------------------------------------------------+ void UpdateDashboard() { static double symbolOffset = 0; //--- Track symbol line offset static double askOffset = 0; //--- Track ask line offset static double spreadOffset = 0; //--- Track spread line offset static double sectionOffset = 0; //--- Track section offset int totalWidthSymbol = totalSymbols * SymbolHorizontalSpacing; //--- Calculate total symbol line width int totalWidthAsk = totalSymbols * AskHorizontalSpacing; //--- Calculate total ask line width int totalWidthSpread = totalSymbols * SpreadHorizontalSpacing; //--- Calculate total spread line width int totalWidthSection = totalSymbols * SectionHorizontalSpacing; //--- Calculate total section width int rightEdge = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart right boundary //--- Update text and image objects for(int i = 0; i < totalSymbols; i++) //--- Iterate through all symbols { // Symbol line (first line) string symbolName = dashboardName + "_Symbol_" + IntegerToString(i); //--- Define symbol object name double symbolXPos = (i * SymbolHorizontalSpacing) - symbolOffset; //--- Calculate symbol x-position if(symbolXPos < -SymbolHorizontalSpacing) symbolXPos += totalWidthSymbol; //--- Wrap around if off-screen createText(symbolName, StringFormat("%-10s", symbolArray[i]), (int)symbolXPos, Y_Position, FontColor, SymbolFontSize, SymbolFont); //--- Update symbol text ObjectSetInteger(0, symbolName, OBJPROP_HIDDEN, symbolXPos > rightEdge || symbolXPos < 0); //--- Hide if off-screen // Ask line (second line) string askName = dashboardName + "_Ask_" + IntegerToString(i); //--- Define ask object name double askXPos = (i * AskHorizontalSpacing) - askOffset; //--- Calculate ask x-position if(askXPos < -AskHorizontalSpacing) askXPos += totalWidthAsk; //--- Wrap around if off-screen createText(askName, StringFormat("%.5f", prices[i].ask), (int)askXPos, Y_Position + SymbolFontSize + 2, clrMagenta, AskFontSize, AskFont); //--- Update ask text ObjectSetInteger(0, askName, OBJPROP_HIDDEN, askXPos > rightEdge || askXPos < 0); //--- Hide if off-screen // Spread line (third line) if(ShowSpread) //--- Check if spread display is enabled { string spreadName = dashboardName + "_Spread_" + IntegerToString(i); //--- Define spread object name double spreadXPos = (i * SpreadHorizontalSpacing) - spreadOffset; //--- Calculate spread x-position if(spreadXPos < -SpreadHorizontalSpacing) spreadXPos += totalWidthSpread; //--- Wrap around if off-screen createText(spreadName, StringFormat("%.1f", prices[i].spread), (int)spreadXPos, Y_Position + SymbolFontSize + 2 + AskFontSize + 2, clrAqua, SpreadFontSize, SpreadFont); //--- Update spread text ObjectSetInteger(0, spreadName, OBJPROP_HIDDEN, spreadXPos > rightEdge || spreadXPos < 0); //--- Hide if off-screen } // Section (Image, Currency, Percent Change, Arrow, Bid Price) double sectionXPos = (i * SectionHorizontalSpacing) - sectionOffset; //--- Calculate section x-position if(sectionXPos < -SectionHorizontalSpacing) sectionXPos += totalWidthSection; //--- Wrap around if off-screen // Image (left) string imageName = dashboardName + "_Image_" + IntegerToString(i); //--- Define image object name ObjectSetInteger(0, imageName, OBJPROP_XDISTANCE, (int)sectionXPos); //--- Update image x-coordinate ObjectSetInteger(0, imageName, OBJPROP_YDISTANCE, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14)); //--- Update image y-coordinate ObjectSetInteger(0, imageName, OBJPROP_HIDDEN, sectionXPos > rightEdge || sectionXPos < 0); //--- Hide if off-screen // Currency (top, right of image) string currencyName = dashboardName + "_Currency_" + IntegerToString(i); //--- Define currency object name createText(currencyName, StringFormat("%-10s", symbolArray[i]), (int)sectionXPos + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), FontColor, SectionFontSize, "Arial Bold"); //--- Update currency text // Percent Change (next to currency, horizontal) string percentChangeName = dashboardName + "_PercentChange_" + IntegerToString(i); //--- Define percent change object name string percentText = prices[i].percent_change >= 0 ? StringFormat("+%.2f%%", prices[i].percent_change) : StringFormat("%.2f%%", prices[i].percent_change); //--- Format percent change createText(percentChangeName, percentText, (int)sectionXPos + 105, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14), prices[i].percent_color, SectionFontSize, SectionFont); //--- Update percent change text // Arrow (below currency, right of image, Wingdings) string arrowName = dashboardName + "_Arrow_" + IntegerToString(i); //--- Define arrow object name createText(arrowName, prices[i].arrow_char, (int)sectionXPos + 35, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].arrow_color, SectionFontSize, "Wingdings"); //--- Update arrow text // Bid Price (next to arrow, horizontal) string bidName = dashboardName + "_Bid_" + IntegerToString(i); //--- Define bid object name createText(bidName, StringFormat("%.5f", prices[i].bid), (int)sectionXPos + 50, Y_Position + (ShowSpread ? SymbolFontSize + 2 + AskFontSize + 2 + SpreadFontSize + 14 : SymbolFontSize + 2 + AskFontSize + 14) + SectionFontSize + 2, prices[i].bid_color, SectionFontSize, SectionFont); //--- Update bid price text } //--- Increment offsets for scrolling effect symbolOffset = fmod(symbolOffset + SymbolScrollSpeed, totalWidthSymbol); //--- Update symbol line offset askOffset = fmod(askOffset + AskScrollSpeed, totalWidthAsk); //--- Update ask line offset spreadOffset = fmod(spreadOffset + SpreadScrollSpeed, totalWidthSpread); //--- Update spread line offset sectionOffset = fmod(sectionOffset + SectionScrollSpeed, totalWidthSection); //--- Update section offset //--- Redraw chart ChartRedraw(); //--- Refresh chart display }
Здесь реализуем функцию "UpdateBackground" для настройки фоновой панели при изменении размера графика. Получаем текущую ширину графика с помощью ChartGetInteger, используя CHART_WIDTH_IN_PIXELS, и преобразуем ее в целое число в переменной «width». Пересчитываем высоту панели в "height", используя троичный оператор в "ShowSpread", чтобы определить, имеется ли 4 или 3 строки, умножая на максимальный размер шрифта из "SymbolFontSize", "AskFontSize", "SpreadFontSize" и "SectionFontSize" (плюс 2 для заполнения) и добавляя 40 для дополнительного пространства. Наконец, обновляем размеры панели с помощью ObjectSetInteger для "OBJPROP_XSIZE" и OBJPROP_YSIZE для "backgroundName".
Далее реализуем функцию "UpdateDashboard" для обработки прокрутки и обновлений текстовых и графических объектов. Для отслеживания позиций строк определяем статические смещения "symbolOffset", "askOffset", "spreadOffset" и "sectionOffset". Вычисляем общую ширину "totalWidthSymbol", "totalWidthAsk", "totalWidthSpread" и "totalWidthSection", умножая "totalSymbols" на соответствующие значения интервалов. Правый край графика получаем с помощью "ChartGetInteger", используя значение "CHART_WIDTH_IN_PIXELS". Перебираем "totalSymbols", обновляя положение каждого инструмента с помощью "symbolXPos", скорректированного с помощью "symbolOffset", перенося по модулю, если он находится за пределами экрана, и вызываем "createText" для обновления текста, скрывая его с помощью "ObjectSetInteger" для "OBJPROP_HIDDEN", если он находится за пределами "rightEdge".
Мы делаем аналогичные обновления для ask, spread (если "ShowSpread") и элементов раздела, включая изображения с "ObjectSetInteger" для OBJPROP_XDISTANCE и "OBJPROP_YDISTANCE", валюты, процентного изменения (в формате StringFormat), стрелки (с использованием "prices[i].arrow_char") и текста bid. Увеличиваем смещения с помощью "fmod", используя скорость прокрутки, и вызываем ChartRedraw для обновления отображения. Эти функции гарантируют, что тикер адаптируется к изменениям и плавно прокручивается для мониторинга в режиме реального времени. Затем можем вызвать обработчик OnTimer, но сначала нужно будет установить интервал таймера. Это необходимо.
//--- Set timer EventSetMillisecondTimer(UpdateInterval); //--- Set timer for updates //--- Initialize last day lastDay = TimeCurrent() / 86400; //--- Set current day for daily open tracking
Здесь просто устанавливаем интервал таймера, вызвав EventSetMillisecondTimer и передавая определенный интервал обновления, и, наконец, инициализируем переменную last day для отслеживания нового дня. Теперь можем определить логику таймера.
//+------------------------------------------------------------------+ //| Expert timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- Check for new day to update daily open datetime currentDay = TimeCurrent() / 86400; //--- Calculate current day if(currentDay > lastDay) //--- Check if new day { for(int i = 0; i < totalSymbols; i++) //--- Iterate through symbols { prices[i].daily_open = iOpen(symbolArray[i], PERIOD_D1, 0); //--- Update daily open price } lastDay = currentDay; //--- Update last day } //--- Update background size in case chart is resized UpdateBackground(); //--- Update background dimensions //--- Update dashboard display UpdateDashboard(); //--- Update dashboard visuals }
Здесь мы реализуем обработчик OnTimer для управления периодическими обновлениями в нашей бегущей тикерной строке для мониторинга инструментов в режиме реального времени, запускаемыми с интервалом, заданным "UpdateInterval". Начинаем с вычисления "currentDay" как TimeCurrent, деленного на 86400, чтобы получить день в секундах, который равен 1 дню * 24 часам * 60 минутам * 60 секундам. Если значение "currentDay" больше, чем "lastDay", перебираем "totalSymbols" и обновляем "prices[i].daily_open" для каждого инструмента, используя iOpen на "PERIOD_D1" со сдвигом 0, затем устанавливаем для "lastDay" значение "currentDay", чтобы отслеживать новый день. Это гарантирует корректный сброс ежедневных процентных изменений в полночь.
Затем вызываем "UpdateBackground", чтобы настроить фоновую панель при изменении размера графика. Наконец, вызываем "UpdateDashboard", чтобы обновить все текстовые и графические объекты с текущими данными и позициями прокрутки, сохраняя бегущую строку динамичной и реагирующей на изменения во времени. Получаем следующий результат.

На изображении видно, что у нас есть эффективная прокручивающаяся лента. Теперь нам нужно обновить цены, и на этом все. Давайте также включим эту логику обновления в функцию.
//+------------------------------------------------------------------+ //| Update prices function | //+------------------------------------------------------------------+ void UpdatePrices() { for(int i = 0; i < totalSymbols; i++) //--- Iterate through all symbols { double bid = SymbolInfoDouble(symbolArray[i], SYMBOL_BID); //--- Retrieve current bid price double ask = SymbolInfoDouble(symbolArray[i], SYMBOL_ASK); //--- Retrieve current ask price //--- Validate prices if(bid == 0 || ask == 0) //--- Check for invalid prices { LogError("UpdatePrices: Failed to retrieve prices for " + symbolArray[i]); //--- Log price retrieval failure continue; //--- Skip to next symbol } //--- Update color and arrow based on price change (tick-to-tick for bid and arrow) if(bid > prices[i].prev_bid && prices[i].prev_bid != 0) //--- Check if bid increased { prices[i].bid_color = UpColor; //--- Set bid color to up color prices[i].arrow_char = CharToString(236); //--- Set up arrow character prices[i].arrow_color = ArrowUpColor; //--- Set arrow to up color } else if(bid < prices[i].prev_bid && prices[i].prev_bid != 0) //--- Check if bid decreased { prices[i].bid_color = DownColor; //--- Set bid color to down color prices[i].arrow_char = CharToString(238); //--- Set down arrow character prices[i].arrow_color = ArrowDownColor; //--- Set arrow to down color } else //--- Handle no change or first tick { prices[i].bid_color = FontColor; //--- Set bid color to default prices[i].arrow_char = CharToString(236); //--- Set default up arrow prices[i].arrow_color = FontColor; //--- Set arrow to default color } //--- Calculate daily percentage change prices[i].percent_change = prices[i].daily_open != 0 ? ((bid - prices[i].daily_open) / prices[i].daily_open) * 100 : 0; //--- Compute percentage change prices[i].percent_color = prices[i].percent_change >= 0 ? UpColor : DownColor; //--- Set percent color based on change //--- Update data prices[i].bid = bid; //--- Store current bid prices[i].ask = ask; //--- Store current ask prices[i].spread = (ask - bid) * MathPow(10, SymbolInfoInteger(symbolArray[i], SYMBOL_DIGITS)); //--- Calculate spread prices[i].prev_bid = bid; //--- Update previous bid } }
Реализуем функцию "UpdatePrices" для обновления данных об инструментах. Перебираем "totalSymbols" и извлекаем "bid" и "ask" для каждого "symbolArray[i]", используя функцию "SymbolInfoDouble" с SYMBOL_BID и "SYMBOL_ASK". Если "bid" или "ask" равно 0, выводим ошибку с помощью "LogError" и переходим к следующему инструменту. Обновляем "bid_color", "arrow_char" (используя функцию CharToString для стрелок вверх или вниз) и "arrow_color" в зависимости от того, является ли "bid" больше, меньше или равно "prev_bid" (игнорируя начальный 0). Стрелки находятся в структуре Wingdings MQL5 по умолчанию, которая выглядит следующим образом.

Однако вы можете использовать код со стрелкой, который вам больше нравится. Затем вычисляем "percent_change", используя "daily_open", и устанавливаем "percent_color" с помощью троичного оператора для движения вверх или вниз. Наконец, обновляем "prices[i].bid", "prices[i].ask", "spread" (рассчитывается с помощью MathPow и SymbolInfoInteger для "SYMBOL_DIGITS") и "prev_bid", обеспечивая текущие данные для отображения и изменений, поддерживая цены и индикаторы бегущей строки в актуальном состоянии на каждом тике. Теперь вызываем эту функцию на каждом тике, чтобы обрабатывать изменения цены, или также можно вызвать ее в функции on-timer. Опять же, выбор за вами.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Update prices on every tick for live changes UpdatePrices(); //--- Update symbol prices } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Clean up objects for(int i = objManager.Total() - 1; i >= 0; i--) //--- Iterate through all managed objects { string name = objManager.At(i); //--- Get object name if(ObjectFind(0, name) >= 0) //--- Check if object exists { if(!ObjectDelete(0, name)) //--- Delete object LogError("OnDeinit: Failed to delete object: " + name + ", Error: " + IntegerToString(GetLastError())); //--- Log deletion failure } objManager.Delete(i); //--- Remove object from manager } EventKillTimer(); //--- Stop timer }
В обработчике OnTick вызываем "UpdatePrices", чтобы обновить данные о Bid, ask, спредах и изменениях для всех инструментов, гарантируя, что бегущая строка будет оперативно отражать текущие рыночные движения. Далее реализуем функцию OnDeinit для очистки при удалении программы. Выполняем обратный цикл через "objManager", используя "Total", получая "name" каждого объекта с помощью "At". Если объект существует посредством ObjectFind, удаляем его с помощью ObjectDelete, регистрируя сбои с помощью "LogError" в случае неудачи. Удаляем имя из "objManager" с помощью оператора Delete. Наконец, останавливаем таймер с помощью EventKillTimer, чтобы завершить периодические обновления. Это необходимо. Это гарантирует, что все объекты будут правильно очищены, предотвращая появление оставшихся элементов на графике. При запуске программы получаем следующий результат.

На изображении видно, что все работает так, как ожидалось, а значит наши цели достигнуты. Теперь осталось протестировать работоспособность проекта, и это рассматривается в предыдущем разделе.
Тестирование на истории
Мы провели тестирование, а ниже представлена скомпилированная визуализация в едином формате растрового изображения Graphics Interchange Format (GIF).

Заключение
В заключение, мы разработали бегущую тикерную строку на MQL5 для мониторинга инструментов в реальном времени, отображающую цены Bid, спреды и ежедневные изменения с настраиваемыми шрифтами, цветами и скоростью, что позволяет эффективно выделять движения рынка. Мы продемонстрировали архитектуру и реализацию, начиная от структур данных, таких как "SymbolData", и заканчивая функциями, такими как "UpdateDashboard" и "UpdatePrices", обеспечивая плавную прокрутку и точные обновления для эффективного анализа торговых данных. Вы можете настроить эту бегущую строку в соответствии со своими потребностями, что значительно расширит ваши возможности по отслеживанию нескольких инструментов и реагированию на ценовые тенденции в режиме реального времени.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18844
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Трейдинг с экономическим календарем MQL5 (Часть 9): Расширение интерактивности с новостями через динамический скроллбар и улучшенное отображение
Нейросети в трейдинге: Адаптивная факторная токенизация (Основные компоненты)
Пользовательские инструменты отладки и профилирования для разработки на MQL5 (Часть I): Расширенное логирование
Возможности Мастера MQL5, которые вам нужно знать (Часть 66): Использование паттернов FrAMA и индекса силы с ядром скалярного произведения
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Не работает
Вы вообще читали статью?