Знакомство с языком MQL5 (Часть 42): Руководство для начинающих по работе с файлами в MQL5 (IV)
Введение
И снова приветствуем вас в Части 42 серии "Знакомство с языком MQL5"! Многие трейдеры хранят историю сделок в CSV-файле со значениями прибыли и убытка по закрытым сделкам, но анализируют ее только как таблицу. Будь то файл Excel или необработанный CSV-файл, данные все равно остаются статичными и разрозненными. Вы видите отдельные результаты, но не можете быстро оценить изменение баланса, динамику просадки или совокупное влияние каждой сделки прямо на графике MetaTrader 5. Информация есть, но она не интегрирована визуально в ту торговую среду, где анализ и должен происходить.
Тестер стратегий в MetaTrader 5 действительно показывает кривые баланса и статистику результатов, но эти данные относятся только к тестируемому советнику. Они не отражают реальные сделки, выполненные на реальном торговом счете, особенно если часть сделок открывалась вручную. В предыдущей статье мы показали, как экспортировать полную торговую историю из MetaTrader 5 в CSV-файл и как читать этот файл в MQL5. Мы извлекли ключевые поля, такие как столбец Profit($) и общее число закрытых сделок, сохранили их в динамических массивах и подготовили данные к структурированной обработке. Опираясь на эту основу, теперь мы можем превратить необработанные исторические данные в практический инструмент анализа прямо в терминале.
К концу статьи у вас будет полностью рабочий индикатор на языке MQL5 для MetaTrader 5, который читает CSV-файл с вашей историей сделок. В качестве примера будет использоваться файл из предыдущей статьи, который также приложен к этому руководству. Вы узнаете, куда поместить CSV-файл, чтобы MetaTrader 5 мог получить к нему доступ. Индикатор извлечет из этого CSV значения Profit($) и рассчитает накопительную кривую баланса. Затем кривая будет построена в отдельном окне индикатора, а вертикальная ось будет автоматически масштабироваться по минимальному и максимальному значениям этой последовательности.
Горизонтальная и вертикальная оси будут отрисованы так, чтобы кривая была выровнена по данным, а входной параметр позволит включать и отключать числовые метки прибыли и убытка для каждой сделки прямо на графике. Используя обработчики событий OnInit и OnTimer, вы настроите индикатор на динамическое обновление и при этом обеспечите, чтобы кривая перерисовывалась только при появлении новых сделок в CSV. В результате получается наглядное и проверяемое визуальное представление результатов по счету, а статичные CSV-данные превращаются в прикладной торговый инструмент.
Знакомство с проектом
Прежде чем переходить к практической реализации, важно точно понимать, что именно мы собираемся создать и с какими данными будем работать. Цель этого проекта – визуализировать кривую баланса на основе торговых данных из CSV-файла. Чтобы реализовать это корректно, сначала нужно понять сам проект и структуру файла (пример файла приложен к этой статье).
Что такое кривая баланса
Прежде чем переходить к работе с индикатором, важно понять, что такое кривая баланса и почему она важна в торговле. Кривая баланса графически показывает, как со временем растет или снижается торговый счет. Она строится путем последовательного отображения накопленного результата по закрытым сделкам. Кривая баланса показывает общий тренд результатов по счету, объединяя результаты отдельных сделок, а не рассматривая их по отдельности. Проще говоря, прибыль или убыток от каждой сделки добавляется к сумме, полученной после предыдущего закрытия. Кривая поднимается, когда сделка приносит прибыль. Кривая опускается, если сделка закрывается с убытком. Последовательно добавляя результат каждой сделки к текущему балансу, мы получаем кривую, которая показывает, как развивался счет.
С помощью кривой баланса трейдер получает ответы на важные вопросы. Насколько стабильно работает этот подход? Есть ли затяжные просадки? Результаты действительно нестабильны или все же устойчивы? Кривая с резкими провалами может указывать на нестабильные результаты или проблемы с управлением рисками, тогда как плавно растущая кривая часто говорит о стабильном прогрессе. Также важно различать баланс и средства счета. Средства счета включают плавающую прибыль и убыток по открытым сделкам, тогда как баланс отражает состояние счета после закрытых сделок. В этом случае кривая баланса будет основана на значениях реализованной прибыли и убытков, поскольку мы извлекаем исторические данные из файла с закрытыми сделками.
Визуализируя кривую баланса по истории прибыли и убытка, сохраненной в файле, мы превращаем необработанные числовые данные в понятную картину результатов. Вместо просмотра строк с цифрами мы сразу видим, растет счет, снижается или стоит на месте. Прежде чем приступать к реализации индикатора, важно понять эту идею: его задача не просто показывать статистику, а осмысленно и аналитически отображать результаты по счету.

В этом проекте вы сможете выбрать, как именно данные будут отображаться на графике. На графике можно сразу показывать и линию кривой баланса, и точные значения прибыли и убытка по каждой сделке. Так проще увидеть, как каждая закрытая сделка влияет на результаты по счету в целом.
Например, чтобы лучше понять, как конкретные сделки повлияли на счет, полезно видеть точную сумму прибыли или убытка в каждой точке кривой. Это особенно полезно при анализе поведения стратегии, а также просадок и фаз восстановления.

Однако можно выбрать и более простой, менее перегруженный вариант отображения. В таком случае можно показывать только линию кривой баланса без отдельных значений прибыли и убытка. На более плавном и менее загроможденном графике без лишних деталей гораздо проще сосредоточиться на общем тренде роста или снижения.

Интерпретация кривой баланса
После построения кривой важно понимать, что именно она показывает в окне индикатора с точки зрения вашего торгового счета. Каждая точка на кривой, показывающей накопленный баланс счета, точно соответствует одной закрытой сделке из CSV-файла. Прибыль или убыток по сделке определяет, будет ли кривая направлена вверх или вниз. Кривая растет после прибыльной сделки и снижается после убыточной. Движение и наклон кривой сразу показывают характер результатов. Просадки на кривой показывают убытки, тогда как устойчивый рост указывает на серию прибыльных сделок и стабильный прогресс. По крутизне этих изменений можно оценить величину прироста и просадок.
Вертикальное расстояние между предыдущим пиком и последующим минимумом служит мерой просадки. Максимальная просадка – это наибольшее падение от пика к минимуму за весь торговый период. Она показывает, какой наибольший убыток переживал счет до восстановления. Это важный показатель риска, поскольку он показывает, насколько сильно может просесть счет в неблагоприятные периоды, и помогает оценить риск торговой стратегии. Поскольку каждая точка привязана к отдельной сделке, можно точно увидеть, какие сделки внесли наибольший вклад в самые глубокие просадки. 
По кривой также можно определить максимальную и минимальную длину серий прибыльных и убыточных сделок. Серия убыточных сделок помогает выявлять периоды, когда стратегия испытывает повышенную нагрузку. Серия прибыльных сделок указывает на периоды высокой результативности и устойчивого движения вверх. Наблюдая за обоими показателями, можно лучше понять, насколько надежен и волатилен ваш торговый подход. Числовые метки прибыли и убытка на кривой делают эту картину еще нагляднее. Показывая фактический P/L по каждой сделке, можно быстро увидеть, какие сделки сильнее всего повлияли на накопленный баланс и почему кривая растет или снижается в конкретных точках.
Эта кривая помогает оценивать общую прибыльность, выявлять рискованные периоды и понимать, насколько хорошо торговая стратегия превращает возможности в рост счета. Максимальная просадка и серии прибыльных или убыточных сделок дают точные ориентиры по риску и стабильности, а сама кривая превращает необработанные CSV-данные в наглядное и полезное представление вашей торговой истории. Числовые метки прибыли и убытка на кривой делают это еще нагляднее. Показывая реальный P/L по каждой сделке, можно легко определить, какие сделки сильнее всего повлияли на накопленный баланс, и понять, почему кривая растет или снижается в конкретных точках. Эта кривая помогает оценивать общую прибыльность, выявлять рискованные периоды и понимать, насколько хорошо торговая стратегия превращает возможности в рост счета. Кривая превращает необработанные CSV-данные в наглядное и прикладное представление результатов торговли.
Структура файла
Первый шаг при работе с внешними данными на языке MQL5 – понять структуру файла. Логика программы полностью зависит от формата CSV-файла, то есть от фиксированного набора столбцов и заголовков.
В примере файла, который используется в этой статье, есть 12 столбцов и 12 соответствующих заголовков. Каждый столбец содержит конкретную информацию о сделке, например время, символ, тип ордера, размер лота, время открытия, прибыль и итоговый результат.

В предыдущей статье мы уже разбирали, как работает индексация при считывании CSV-данных в массивы. Мы используем индекс для доступа к каждому значению, которое хранится в соответствии с его позицией в строке. Поэтому порядок столбцов должен оставаться неизменным.
Для построения кривой баланса нам нужен только столбец Profit($). Все необходимые данные берутся исключительно из значений под этим заголовком. Извлекая значения прибыли по соответствующим индексам и накапливая их сумму, мы можем вычислить кривую баланса. Если порядок столбцов изменится или столбец Profit($) будет смещен, индексы перестанут указывать на правильные данные, и программа будет работать не так, как задумано.
Как получить доступ к файлам из программы на MQL5
В этом разделе показано, как поместить нужный файл в правильную папку и почему это важно. В этой статье мы работаем с CSV-файлом. Для наглядности предположим, что у нас есть CSV-файл (он приложен к этой статье). Чтобы программа на языке MQL5 могла получить доступ к этому файлу, его нужно поместить в один из двух каталогов, у каждого из которых свой путь и свое назначение.
Первый вариант – папка MQL5\\Files, привязанная к конкретному терминалу. Эта папка связана с текущим терминалом MetaTrader 5, и именно в ней стоит хранить данные, относящиеся к конкретной программе. На большинстве систем путь выглядит так:
C:\\Users\\Dell\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075\\MQL5\\Files
Здесь D0E8209F77C8CF37AD8BF550E51FF075 обозначает конкретный экземпляр терминала MetaTrader 5 на моем ПК. Только этот терминал может получить доступ к файлам, хранящимся в этой папке. Это предотвращает конфликты с другими терминалами и гарантирует, что программа будет стабильно находить нужный файл. Если программе нужен регулярный доступ к своим файлам, это самый безопасный вариант.
Второй вариант – общая папка терминала, которой пользуются все терминалы MetaTrader 5, установленные на одном компьютере. Обычно путь к ней выглядит так:
C:\\Users\\Dell\\AppData\\Roaming\\MetaQuotes\\Terminal\\Common\\Files
Файлы, хранящиеся здесь, доступны из любого терминала на этом компьютере. Хотя это удобно для обмена данными между терминалами, проблемы могут возникнуть, если несколько программ одновременно пытаются читать или записывать один и тот же файл. При работе с одной программой обычно рекомендуется использовать папку MQL5\\Files текущего терминала. Если поместить CSV-файл в эту папку, программа сможет стабильно к нему обращаться, а сами файлы файлы будут храниться отдельно от файлов других терминалов.
Для целей этой статьи поместите CSV-файл в папку MQL5\\Files текущего терминала, чтобы программа на MQL5 могла обращаться к нему без дополнительных флагов и настроек. Это упрощает настройку и помогает избежать лишних недоразумений при разборе примеров. Однако в зависимости от того, как вы планируете обращаться к этим файлам и управлять ими во всех своих установках MetaTrader 5, вы сами решаете, хранить ли файлы в общей папке или в папке конкретного терминала.
Построение объектов в окне индикатора
Затем с помощью цикла в файле будет найден заголовок "Profit($)". После определения начальной позиции столбца прибыли можно уверенно извлекать значения прибыли, увеличивая индекс на 12, поскольку структура файла уже проверена и известно, что каждая запись о сделке содержит 12 столбцов. Кроме того, запись "Total Trades:", хранящаяся в файле, будет найдена другим циклом. Это гарантирует, что будут получены только корректные торговые данные, и позволяет вычислить общее количество исполненных сделок. После того как все значения прибыли из столбца "Profit($)" будут собраны, будет создан массив. Затем по этим значениям будет рассчитан накопленный результат, отражающий изменение баланса во времени. Кривая баланса строится на основе этого накопительного расчета.
После вычисления накопленных значений функция определит минимальное и максимальное значения для окна индикатора. Это гарантирует, что кривая баланса будет полностью видна и корректно масштабирована в подокне. При запуске индикатора обработчик событий OnInit() вызовет эту процедуру обработки данных для их загрузки. Кроме того, эта процедура будет вызываться в обработчике событий OnTimer(), что позволит индикатору автоматически обновляться через заданные интервалы.
Наконец, OnCalculate() будет отрисовывать графические элементы, например трендовые линии и текстовые метки. Это необходимо для корректного размещения объектов, так как OnCalculate() предоставляет доступ к данным баров графика. Для повышения производительности будет введено условие: OnCalculate() будет выполнять логику отрисовки только тогда, когда общее количество сделок отличается от ранее определенного значения. Это повышает производительность и исключает лишнюю перерисовку.
Открытие CSV-файла в MQL5
В этом разделе показано, как открыть CSV-файл в MQL5. На этот раз будет использован более системный и эффективный подход, чем в предыдущей статье, где файл открывался прямо внутри обработчиков событий. Открытие файла будет вынесено в отдельную функцию, а не выполняться внутри OnCalculate(). Благодаря такой организации файл не открывается на каждом тике, что снижало бы производительность и расходовало бы больше ресурсов, чем нужно.
Функция будет отвечать за все операции с файлом: открытие CSV-файла, проверку хэндла файла и подготовку данных к обработке. Если вынести эту логику в отдельную функцию, код станет понятнее, модульнее и и его будет проще поддерживать. Чтобы загрузить файл при первой инициализации индикатора, эта функция будет вызвана в обработчике событий OnInit(). Обработчик событий OnTimer() также будет ее вызывать. За счет перезагрузки файла через заданный интервал таймер обеспечивает регулярную обработку изменений, но не на каждом рыночном тике.
При этом обработчик событий OnCalculate() будет отвечать только за отрисовку графических объектов. Для дальнейшего повышения производительности будет добавлено условие: OnCalculate() будет выполнять логику отрисовки только тогда, когда общее количество сделок изменилось по сравнению с ранее вычисленным значением. Это исключает лишние перерасчеты и перерисовку объектов, поскольку индикатор обновляется только при появлении новых торговых данных.
Пример:#property indicator_separate_window //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- create timer EventSetTimer(60); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); } //+------------------------------------------------------------------+ //| Custom indicator 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[]) { //--- return(rates_total); } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { } //+-----------------------------------------------------------------+ //| Balance curve function | //+------------------------------------------------------------------+ void BalanceCurve(string Filename) { int file_handle = FileOpen(Filename, FILE_READ | FILE_CSV | FILE_ANSI, ','); if(file_handle == INVALID_HANDLE) { Print("Failed to open file. Error: ", GetLastError()); } else if(file_handle != INVALID_HANDLE) { FileClose(file_handle); } }
Пояснение:
Indicator_separate_window – это первая важная настройка нашего индикатора. Этот параметр указывает MetaTrader, что индикатор нужно отображать под основным ценовым графиком в отдельном окне. Визуализацию лучше отделить от свечей, поскольку здесь используются данные о результатах счета, а не ценовые данные. В отличие от движения рыночной цены, кривая баланса показывает накопленный результат во времени. Когда она отображается в отдельном окне, график остается чистым, а анализ результатов становится понятнее. Если бы этот режим не использовался, кривая баланса отображалась бы прямо на ценовом графике. Это может затруднить восприятие обоих типов информации, поскольку они смешаются. Отдельное окно делает представление более наглядным и упорядоченным.
Объявляется функция BalanceCurve, которая ничего не возвращает: void BalanceCurve(string Filename). Функция выполняет внутренние действия, но не возвращает результат вызывающему коду, на что и указывает ключевое слово void. Имя CSV-файла, который нужно открыть, задается единственным параметром функции – строкой Filename. Передача имени файла как параметра вместо жесткого задания в коде делает функцию более гибкой и пригодной для повторного использования. Это означает, что одну и ту же функцию можно вызывать для разных CSV-файлов без изменения основной логики.
Внутри функции указанный файл открывается в режиме чтения, с кодировкой ANSI и форматом CSV, а результат сохраняется в переменной file_handle. При успешном открытии файла система MQL5 возвращает целочисленный хэндл файла. Программа может работать с этим конкретным файлом – считывать из него данные и выполнять другие файловые операции – благодаря этому хэндлу, который служит уникальным идентификатором. Если файл по какой-либо причине не удается открыть, функция сигнализирует об ошибке, возвращая недопустимый хэндл.
Первый аргумент Filename указывает программе, какой файл нужно открыть. По умолчанию MQL5 ищет файлы в папке MQL5\\Files текущего терминала. Эта папка относится к той установке терминала MetaTrader 5, которая запущена в данный момент. Любой файл, сохраненный в этом каталоге, можно сразу считывать без дополнительной настройки. При работе с данными конкретного индикатора рекомендуется использовать именно эту папку.
Во втором аргументе задаются флаги доступа к файлу. Этими флагами задается способ открытия и обработки файла. Если установлен флаг FILE_READ, файл будет открыт в режиме только для чтения. Этот параметр необходим, поскольку цель состоит в извлечении данных из CSV-файла, а не в его изменении. Флаг FILE_CSV сообщает MQL5, что файл нужно обрабатывать как CSV-файл. При использовании этого флага система автоматически разделяет данные на поля с учетом заданного разделителя.
В результате читать каждое поле по отдельности становится проще. Кодировка файла задается флагом FILE_ANSI. Флаг FILE_ANSI сообщает MQL5, что файл сохранен в кодировке ANSI. Если файл сохранен в формате Unicode, вместо этого нужно использовать флаг FILE_UNICODE. Чтобы символы считывались корректно, кодировка должна соответствовать кодировке файла.
Последний аргумент – символ запятой – задает разделитель CSV-файла. Поскольку в обычных CSV-файлах значения разделяются запятыми, этот аргумент указывает MQL5, как разбивать каждую строку на отдельные поля. Если в файле используется другой разделитель, например точка с запятой, нужно указать именно его. Если файл находится в общем каталоге терминала, а не в папке MQL5\\Files конкретного терминала, в функцию FileOpen нужно дополнительно добавить флаг FILE_COMMON. Все установленные на компьютере терминалы MetaTrader 5 имеют доступ к папке Terminal\\Common\\Files, и именно там MQL5 ищет файл, когда установлен этот флаг. При совместном использовании файла несколькими терминалами эта возможность особенно полезна.
При попытке открыть файл функция сразу проверяет, равен ли возвращенный хэндл INVALID_HANDLE. Чтобы упростить диагностику проблем, таких как отсутствие файла или неверный каталог, программа в таком случае выводит сообщение об ошибке вместе с точным кодом, полученным из GetLastError(). Если файл открыт успешно, он корректно закрывается с помощью FileClose. Помимо предотвращения возможных конфликтов доступа к файлу, корректное закрытие файла освобождает системные ресурсы. В полной реализации логика чтения файла обычно выполняется до его закрытия.
Хранение элементов файла в динамическом массиве
После открытия файла все его содержимое нужно сохранить в динамическом массиве. Чтобы сделать это безопасно, программа сначала должна определить, сколько элементов содержится в файле. Перед использованием размер динамического массива нужно привести в соответствие с числом сохраняемых элементов. Если массив слишком мал, при попытке обратиться к лишним элементам возникнут ошибки выхода за границы диапазона.
Элементы подсчитываются путем последовательного считывания файла с увеличением счетчика после обработки каждого элемента. Счетчик показывает общее количество записей в файле. Затем позиция в файле сбрасывается в начало, чтобы снова считать его содержимое с самого начала. Как только общее количество элементов становится известно, размер динамического массива изменяется так, чтобы вместить каждый из них. Затем при повторном проходе по файлу каждая запись считывается и добавляется в массив.
Пример://+------------------------------------------------------------------+ //| Balance curve function | //+------------------------------------------------------------------+ void BalanceCurve(string Filename) { int file_handle = FileOpen(Filename, FILE_READ | FILE_CSV | FILE_ANSI, ','); if(file_handle == INVALID_HANDLE) { Print("Failed to open file. Error: ", GetLastError()); } else if(file_handle != INVALID_HANDLE) { //counting total elements int total_elements_count = 0; while(!FileIsEnding(file_handle)) { FileReadString(file_handle); total_elements_count++; } FileSeek(file_handle, 0, SEEK_SET); //storing in array string TotalElements[]; ArrayResize(TotalElements,total_elements_count); for(int i = 0; i < total_elements_count; i++) { TotalElements[i] = FileReadString(file_handle); } FileClose(file_handle); } }
Пояснение:
После подсчета общего количества элементов в файле можно сохранить каждый из них в динамический массив. Сначала объявляется динамический массив строк TotalElements. Динамический массив удобен тем, что его размер можно точно подогнать под количество элементов, которое нужно сохранить. После подсчета общего числа элементов размер массива изменяется в соответствии с этим значением. Это гарантирует, что для каждого элемента файла будет выделено отдельное место в памяти. Кроме того, если размер массива меньше количества сохраняемых элементов, могут возникнуть ошибки выхода за границы диапазона. Этого можно избежать, если заранее корректно увеличить размер массива.
После корректного выделения памяти массив заполняется путем последовательного считывания файла. Для этого используется цикл, который начинается с первого индекса и продолжается до тех пор, пока не будет обработан каждый элемент. На каждом проходе цикла следующий элемент файла считывается и записывается в соответствующий индекс массива. После каждой операции чтения указатель файла автоматически смещается, поэтому данные поступают в упорядоченной и непрерывной последовательности.
К моменту завершения цикла все элементы файла последовательно размещаются в массиве. Благодаря такой структуре к любому нужному элементу позже можно будет легко обратиться по его индексу. Кроме того, такая структура служит основой для более сложной обработки: например, для разбиения данных по столбцам или вычислений вроде накопленной прибыли при построении кривой баланса. Сначала объявляется и инициализируется нулем переменная-счетчик, чтобы определить количество элементов в файле. Поскольку ни один элемент еще не подсчитан, здесь логично начинать с нуля. Затем цикл продолжает выполняться, пока не будет достигнут конец файла. Программа будет продолжать считывание, пока в файле остаются доступные данные.
Программа считывает файл поэлементно внутри цикла. Счетчик увеличивается каждый раз, когда значение успешно считывается, показывая, что был учтен еще один элемент. Далее файл обрабатывается последовательно, поскольку указатель файла автоматически смещается при каждом считывании. К концу цикла счетчик содержит итоговое количество всех элементов в файле. Далее указатель уже находится в конце файла, поскольку весь файл был полностью считан. Если требуется дальнейшая обработка, указатель нужно вернуть в начало файла. Это гарантирует, что последующие операции будут обращаться к данным с начала файла, а не с его конца, где больше нечего считывать.
Поиск столбца Profit($) в CSV-файле
В этой части разберем, как находить индекс нужного фрагмента данных или элемента в файле. В предыдущей статье я уже объяснял, как индексация в CSV-файле позволяет определить позицию каждого элемента. Если посмотреть на пример файла, заголовок Profit($) находится по индексу 22. Однако в этом объяснении я не буду жестко задавать этот индекс. Этот метод позволяет применять тот же подход и к другим файлам: код будет динамически находить нужный столбец, а не зависеть от заранее заданных позиций.

Пример:
#property indicator_separate_window string File_name = "Trading_Journal.csv"; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { BalanceCurve(File_name); //--- create timer EventSetTimer(60); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); } //+------------------------------------------------------------------+ //| Custom indicator 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[]) { //--- return(rates_total); } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { } //+-----------------------------------------------------------------+ //| Balance curve function | //+------------------------------------------------------------------+ void BalanceCurve(string Filename) { int file_handle = FileOpen(Filename, FILE_READ | FILE_CSV | FILE_ANSI, ','); if(file_handle == INVALID_HANDLE) { Print("Failed to open file. Error: ", GetLastError()); } else if(file_handle != INVALID_HANDLE) { //counting total elements int total_elements_count = 0; while(!FileIsEnding(file_handle)) { FileReadString(file_handle); total_elements_count++; } FileSeek(file_handle, 0, SEEK_SET); //storing in array string TotalElements[]; ArrayResize(TotalElements,total_elements_count); for(int i = 0; i < total_elements_count; i++) { TotalElements[i] = FileReadString(file_handle); } //Serching Profit($) header int profit_header_index; for(int i = 0; i < total_elements_count; i++) { if(TotalElements[i] == "Profit($)") { profit_header_index = i; break; } } Comment(profit_header_index); FileClose(file_handle); } }
Пояснение:
Первая часть программы посвящена поиску индекса столбца Profit($) в CSV-файле. Переменная сохраняет этот индекс для дальнейшего использования, поэтому программа знает, с какой позиции начинать считывание значений прибыли. Код последовательно просматривает элементы файла и, как только находит заголовок, сразу завершает поиск и выводит найденную позицию. Для проверки программа кратко выводит найденный индекс, чтобы убедиться, что нужный столбец определен верно. После этого содержимое CSV-файла считывается и раскладывается по массивам, а данные о прибыли подготавливаются для построения кривой баланса.
Поиск общего количества сделок в CSV-файле
Теперь программа может искать в файле строку Total Trades:, так же как она ищет заголовок Profit($). Количество сделок содержится в значении, расположенном справа от найденной метки. Надежнее искать текст, а не число, потому что текст остается неизменным, тогда как число может меняться. После того как программа находит позицию строки "Total Trades:", ей достаточно сместиться на один шаг, чтобы получить фактическое значение общего числа сделок независимо от того, каким оно является в данный момент.

Пример:
//+------------------------------------------------------------------+ //| Balance curve function | //+------------------------------------------------------------------+ void BalanceCurve(string Filename, int &total_deals) { int file_handle = FileOpen(Filename, FILE_READ | FILE_CSV | FILE_ANSI, ','); if(file_handle == INVALID_HANDLE) { Print("Failed to open file. Error: ", GetLastError()); } else if(file_handle != INVALID_HANDLE) { //counting total elements int total_elements_count = 0; while(!FileIsEnding(file_handle)) { FileReadString(file_handle); total_elements_count++; } FileSeek(file_handle, 0, SEEK_SET); //storing in array string TotalElements[]; ArrayResize(TotalElements,total_elements_count); for(int i = 0; i < total_elements_count; i++) { TotalElements[i] = FileReadString(file_handle); } //Serching Profit($) header int profit_header_index; for(int i = 0; i < total_elements_count; i++) { if(TotalElements[i] == "Profit($)") { profit_header_index = i; break; } } //Comment(profit_header_index); //Getting total deals int total_deal_index; for(int i = 0; i < total_elements_count; i++) { if(TotalElements[i] == "Total Trades:") { total_deal_index = i + 1; break; } } total_deals = (int)StringToInteger(TotalElements[total_deal_index]); FileClose(file_handle); } }
string File_name = "Trading_Journal.csv"; int Total_deals; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { BalanceCurve(File_name,Total_deals); Comment("Total Deals: ", Total_deals); //--- create timer EventSetTimer(60); //--- return(INIT_SUCCEEDED); }
Пояснение:
Функция BalanceCurve предназначена для обработки CSV-файла и отслеживания общего количества сделок. Передача по ссылке означает, что функция принимает общее количество сделок через ссылочную переменную. Это фактически позволяет функции "возвращать" значение без обычного оператора return, поскольку любые изменения переменной общего количества сделок внутри функции автоматически обновляют переменную вне ее.
Ссылочная переменная используется для управления тем, когда функция OnCalculate выполняет отрисовку объектов на графике. В частности, элементы графика отрисовываются только тогда, когда изменилось общее количество сделок. Отслеживая текущее количество сделок в ссылочной переменной, программа может определить, была ли с момента предыдущего расчета добавлена новая сделка или удалена какая-то существующая. Это предотвращает лишнюю отрисовку и повышает производительность. Внутри функции программа вычисляет общее количество сделок в CSV-файле. Далее метка "Total Trades:" находится путем поиска в массиве элементов файла. Как только метка найдена, программа определяет элемент, в котором хранится фактическое количество сделок. Как только нужный элемент найден, поиск прекращается, чтобы избежать лишней обработки.
Затем после преобразования из строки в целое число количество сделок сохраняется в ссылочной переменной. Поскольку значение передается по ссылке, оно автоматически становится доступным и вне функции, что позволяет другим компонентам индикатора использовать его по мере необходимости. Наконец, при вызове BalanceCurve с CSV-файлом и переменной общего количества сделок внешняя переменная обновляется текущим числом сделок. Для проверки программа может затем вывести это число на график. Такой подход повышает и эффективность, и точность, потому что индикатор всегда знает текущее количество сделок и может управлять отрисовкой в OnCalculate в зависимости от того, изменилось ли это число.
Расчет динамики баланса
В этом разделе мы пошагово рассчитаем динамику баланса с помощью цикла for. Цель состоит в том, чтобы преобразовать значения прибыли и убытка по каждой сделке в накопительную последовательность, показывающую, как со временем меняется результативность счета. Сначала точно определим, что такое динамика накопленной прибыли. Накопленная сумма прибылей и убытков по мере последовательного добавления сделок называется накоплением прибыли. Вместо того чтобы рассматривать результат каждой сделки по отдельности, мы каждый раз добавляем новую прибыль или убыток к текущему итогу. Когда сделка закрывается с прибылью, накопленное значение растет. Если сделка закрывается с убытком, накопленное значение снижается. Именно на этом накопительном итоге строится кривая баланса.
Затем значения прибыли и убытка из столбца Profit($) собираются в один динамический массив. Это гарантирует, что мы отбираем только те значения, которые нужны для расчета динамики. Каждый элемент этого массива представляет результат одной закрытой сделки.
После того как все значения прибыли и убытка сохранены в одном массиве, накопительная динамика рассчитывается с помощью цикла for. На каждой итерации цикла прибыль или убыток по текущей сделке добавляется к переменной накопительного итога. Полученное накопленное значение одновременно сохраняется в другом динамическом массиве. Во втором массиве хранится вся накопительная последовательность прибыли, где каждый индекс показывает состояние счета после определенной сделки. Именно этот массив используется для построения кривой баланса на графике. Отделяя рассчитанные накопительные значения от исходных значений прибыли, мы сохраняем чистую структуру данных и упрощаем построение графика.
Пример://+------------------------------------------------------------------+ //| Balance curve function | //+------------------------------------------------------------------+ void BalanceCurve(string Filename, int &total_deals, double &cumulative[]) { int file_handle = FileOpen(Filename, FILE_READ | FILE_CSV | FILE_ANSI, ','); if(file_handle == INVALID_HANDLE) { Print("Failed to open file. Error: ", GetLastError()); } else if(file_handle != INVALID_HANDLE) { //counting total elements int total_elements_count = 0; while(!FileIsEnding(file_handle)) { FileReadString(file_handle); total_elements_count++; } FileSeek(file_handle, 0, SEEK_SET); //storing in array string TotalElements[]; ArrayResize(TotalElements,total_elements_count); for(int i = 0; i < total_elements_count; i++) { TotalElements[i] = FileReadString(file_handle); } //Serching Profit($) header int profit_header_index; for(int i = 0; i < total_elements_count; i++) { if(TotalElements[i] == "Profit($)") { profit_header_index = i; break; } } //Comment(profit_header_index); //Getting total deals int total_deal_index; for(int i = 0; i < total_elements_count; i++) { if(TotalElements[i] == "Total Trades:") { total_deal_index = i + 1; break; } } total_deals = (int)StringToInteger(TotalElements[total_deal_index]); // saving all profit/loss in one dynamic array double pro_loss[]; ArrayResize(pro_loss,total_deals); int profit_count = 0; for(int i = profit_header_index + 12; i < total_elements_count; i += 12) { pro_loss[profit_count] = StringToDouble(TotalElements[i]); profit_count++; } Print("Profit/Loss:"); ArrayPrint(pro_loss); //saving all progit progression in one dynamic array ArrayResize(cumulative,total_deals); double running_total = 0.0; for(int i = 0; i < total_deals; i++) { running_total += pro_loss[i]; cumulative[i] = running_total; } FileClose(file_handle); } }
double balance_progression[]; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { BalanceCurve(File_name,Total_deals,balance_progression); Print("\nBalance Progression: "); ArrayPrint(balance_progression); //--- create timer EventSetTimer(60); //--- return(INIT_SUCCEEDED); }
Пояснение:
Поскольку аргумент накопительного массива передается по ссылке, любые изменения внутри функции BalanceCurve сразу обновляют исходный массив, объявленный вне нее, в данном случае balance_progression[]. Передача массива по ссылке принципиально важна, потому что дает другим компонентам индикатора, например модулям проверки данных или построения кривой баланса, доступ к рассчитанной накопительной динамике прибыли. Без передачи по ссылке функция изменяла бы только локальную копию, и результаты были бы недоступны вне функции.
Сначала функция собирает все значения прибыли и убытка в отдельный массив. Каждое значение берется из CSV-файла без учета заголовков, а размер массива подгоняется под общее количество сделок. После преобразования каждого текстового значения в числовой формат, пригодный для вычислений, программа сохраняет порядок элементов. В итоге в массиве последовательно собираются результаты всех сделок, и их можно вывести в лог для отладки или проверки.
После упорядочивания отдельных результатов рассчитывается накопительная динамика баланса. Это накопительный итог, отражающий результативность счета во времени. Затем функция инициализирует накопительный итог и меняет размер накопительного массива в соответствии с количеством сделок. Далее функция, проходя по значениям прибыли и убытка, добавляет результат каждой сделки к накопительному итогу и сохраняет обновленное значение в нужной позиции накопительного массива. Так серия результатов отдельных сделок превращается в накопительную последовательность, где каждый индекс отражает баланс счета после соответствующей сделки.
Динамический массив balance_progression[], объявленный в верхней части индикатора, содержит накопительную динамику баланса. Когда OnInit вызывает BalanceCurve, по ссылке передаются массив, имя CSV-файла и переменная общего количества сделок. Это позволяет функции напрямую записывать накопительные итоги в массив, сразу предоставляя другим компонентам программы доступ к накопленным данным. Чтобы проверить корректность вычислений, после завершения функции накопленную динамику можно вывести в лог. Этот подход связывает вычисления с отображением: исходные и накопленные данные остаются отделенными друг от друга, а структура остается ясной. В итоге накопительный массив служит основой для построения кривой баланса в окне индикатора, что позволяет видеть динамику счета во времени.
Вывод: 
Определение границ окна индикатора
При работе с отдельным окном индикатора в MQL5 необходимо задать границы этого окна. При построении данных, например кривой баланса, индикатор не знает автоматически, какой масштаб значений нужно отображать. Если график не определяет минимальный и максимальный уровни, результаты будет трудно воспринимать визуально, поскольку данные могут оказаться слишком сжатыми или растянутыми. Задав эти границы, можно добиться, чтобы линия аккуратно помещалась в окне индикатора и корректно показывала изменения в данных.
Первый шаг при задании границ – определить диапазон значений, которые могут принимать данные. В данном случае речь идет о массиве накопительной динамики баланса. Максимальное значение этого массива задает верхнюю границу окна индикатора, а минимальное – нижнюю. В результате пики и впадины отображаются точно, а кривая баланса видна целиком. Минимальное и максимальное значения можно напрямую задать в свойствах индикатора. Так отдельному окну задается масштабирование оси Y, чтобы построенная линия эффективно заполняла доступное пространство. Задавая границы окна индикатора, мы получаем читаемый график без искажений, на котором трейдеру легче видеть динамику результатов во времени.
Пример:
//saving all progit progression in one dynamic array ArrayResize(cumulative,total_deals); double running_total = 0.0; for(int i = 0; i < total_deals; i++) { running_total += pro_loss[i]; cumulative[i] = running_total; } //MAX AND MIN Indicator Window double max_cumulative = cumulative[ArrayMaximum(cumulative,0,WHOLE_ARRAY)]; double min_cumulative = cumulative[ArrayMinimum(cumulative,0,WHOLE_ARRAY)]; IndicatorSetDouble(INDICATOR_MAXIMUM, max_cumulative); // maximum value IndicatorSetDouble(INDICATOR_MINIMUM,min_cumulative); // minimum value FileClose(file_handle); } }
Пояснение:
В этой части программа находит максимальное и минимальное значения накопительной динамики баланса, чтобы задать диапазон окна индикатора. Это важно, потому что для корректного отображения кривой баланса отдельное окно индикатора должно знать диапазон данных. Верхняя точка кривой задается максимальным значением накопительного массива, а нижняя – минимальным. Вместе эти два значения задают вертикальный диапазон построения кривой баланса.
Затем вычисленные максимум и минимум выводятся на график функцией Comment, что помогает проверить их перед применением. Функция IndicatorSetDouble используется для задания максимального и минимального значений, то есть фактических границ отображения индикатора. В результате вся кривая баланса аккуратно помещается в окне, поскольку MetaTrader соответствующим образом масштабирует ось Y. Задавая эти границы, индикатор корректно показывает максимумы и минимумы кривой баланса, что облегчает визуализацию трендов, распознавание паттернов и оценку результатов торговли.
Вывод:

Построение горизонтальной и вертикальной осей
Перед построением кривой баланса в индикаторе нужно аккуратно задать оси X и Y. Для этого нужно учитывать и общее количество сделок в CSV-файле, и типичное количество баров на свечном графике. Ось Y показывает уровни прибыли вдоль кривой баланса, а ось X – последовательность сделок во времени.
Сначала нужно определить, насколько далеко назад на графике следует отступить, чтобы правильно сопоставить кривую баланса с историческими данными и отразить первую сделку из файла. Для этого мы отсчитываем назад от текущей свечи, пока не дойдем до бара, соответствующего первой сделке в файле. Этот бар обозначает пересечение осей X и Y, то есть начало координат графика. Начиная с этой точки, ось X проводится горизонтально – от бара первой сделки до текущего бара, а ось Y идет вверх, показывая рост прибыли. Во многих реальных ситуациях интервалы между сделками бывают очень большими, иногда до двух месяцев, поэтому мы используем количество свечных баров на графике, а не время сделок из CSV-файла. Если бы мы опирались только на время сделок, кривая могла бы оказаться чрезмерно сжатой или растянутой.
Окно индикатора всегда синхронизировано с основным окном графика, и это еще одна причина использовать бары графика. Это означает, что, хотя оба окна прокручиваются вместе, окно индикатора не может уйти дальше последнего бара на основном графике. Нужно лишь, чтобы количество баров на графике соответствовало общему количеству сделок в CSV-файле. Использование свечных баров вместо фактического времени сделок также обеспечивает корректную работу индикатора на любых таймфреймах. Если бы ось X строилась по времени сделок, переход на более высокий таймфрейм сжимал бы кривую и затруднял ее чтение и интерпретацию. Если использовать сами бары, кривая баланса сохраняет правильные пропорции и интервалы независимо от таймфрейма графика.
Пример:#property indicator_separate_window string File_name = "Trading_Journal.csv"; int Total_deals; double balance_progression[]; int last_total_deals = 0; string y = "Y Axis"; string x = "X Axis"; double max_cumulative; double min_cumulative; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { BalanceCurve(File_name,Total_deals,balance_progression); // Print("\nBalance Progression: "); // ArrayPrint(balance_progression); //--- create timer EventSetTimer(60); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); } //+------------------------------------------------------------------+ //| Custom indicator 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[]) { //--- if(rates_total >= Total_deals && Total_deals != last_total_deals) { //MAX AND MIN Indicator Window max_cumulative = balance_progression[ArrayMaximum(balance_progression,0,WHOLE_ARRAY)]; min_cumulative = balance_progression[ArrayMinimum(balance_progression,0,WHOLE_ARRAY)]; // Creating y and x axis ObjectCreate(0,y,OBJ_TREND,1,time[rates_total - Total_deals],0,time[rates_total - Total_deals],max_cumulative); ObjectCreate(0,x,OBJ_TREND,1,time[rates_total - Total_deals],0,time[rates_total - 1],0); IndicatorSetDouble(INDICATOR_MAXIMUM, max_cumulative); // maximum value IndicatorSetDouble(INDICATOR_MINIMUM,min_cumulative); // minimum value last_total_deals = Total_deals; } return(rates_total); } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { BalanceCurve(File_name,Total_deals,balance_progression); }
Пояснение:
После вызова функции BalanceCurve с CSV-файлом, переменной общего количества сделок и массивом накопительного баланса программа получает два важных результата. Общее количество сделок хранится в отдельной переменной, а накопительная динамика прибыли рассчитывается и сохраняется в массиве текущих итогов. Этот накопительный массив нужен для построения кривой баланса в окне индикатора, поскольку отражает накопленный результат после каждой сделки.
Поскольку код запускается в обработчике OnInit, индикатор начинает работать сразу после добавления на график. Это гарантирует корректную начальную настройку, включая общее количество сделок и накопительную динамику баланса. Далее с помощью EventSetTimer настраивается вызов OnTimer каждые 60 секунд. В OnTimer функция BalanceCurve обновляет общее количество сделок и накопительную динамику. Если функция вызывается и в OnInit, и в OnTimer, индикатор корректно инициализируется и затем автоматически обновляется через заданные интервалы. EventKillTimer предотвращает лишние обновления и экономит ресурсы системы, останавливая таймер при удалении индикатора или закрытии терминала.
Глобальная переменная хранит последнее известное общее количество сделок. Условие внутри обработчика OnCalculate гарантирует, что код построения графика выполняется только тогда, когда баров достаточно для сопоставления с каждой сделкой и когда общее количество сделок изменилось с момента предыдущего расчета. Чтобы индикатор пересчитывался только при необходимости, после этого обновляется переменная, хранящая последнее известное общее количество сделок. Этот метод устраняет лишние вычисления и повышает эффективность.
Максимальная и минимальная границы окна индикатора задаются внутри OnCalculate, поскольку значения осей нужны для построения графика. Программа находит наибольшее и наименьшее значения в накопительном массиве, чтобы определить верхнюю и нижнюю границы оси Y. Ось X тянется горизонтально от бара первой сделки до последнего бара на графике, а ось Y начинается на баре первой сделки и поднимается до наибольшего накопительного значения. Эти границы гарантируют, что кривая баланса помещается в окне индикатора и правильно выравнивается по осям.
При такой настройке кривая баланса точно вписывается в окно индикатора, выравнивается по свечам и обновляется только при появлении новых сделок. Поскольку построение выполняется по позициям баров, а не по временным меткам, индикатор надежно работает на любых таймфреймах и корректно обрабатывает неравномерные интервалы между сделками. В отличие от подхода, основанного на времени, при котором кривая сжимается или растягивается при смене таймфрейма, привязка к барам гарантирует сохранение правильных пропорций и выравнивания кривой.
Вывод:

Построение кривой баланса на графике индикатора
Заключительный этап подготовки – построение кривой баланса на графике индикатора. К этому моменту мы уже задали оси X и Y, определили максимальное и минимальное значения для окна индикатора, считали из файла данные о прибыли и убытках и рассчитали накопительную динамику баланса. Теперь можно вывести саму кривую баланса, показывающую, как баланс счета меняется во времени на основе прошлых сделок. Чтобы построить кривую баланса, мы последовательно перебираем все значения в массиве накопленной прибыли. Каждое значение сопоставляется с баром на графике и отражает баланс счета после соответствующей сделки. Положение по оси X вычисляется обратным отсчетом от текущей свечи с учетом общего количества сделок, указанных в файле.
Это гарантирует, что самая последняя сделка соответствует текущему бару на графике, а первая сделка из файла – начальной точке кривой. Значением по оси Y служит накопленная прибыль в этой точке, которая задает вертикальное положение точки исходя из баланса счета. Соединяя точки на графике по координатам X и Y, программа формирует непрерывную кривую баланса.
//+------------------------------------------------------------------+ //| Balance curve function | //+------------------------------------------------------------------+ void BalanceCurve(string Filename, int &total_deals, double &cumulative[], double &pro_loss[]) {
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { BalanceCurve(File_name,Total_deals,balance_progression,profit_loss); // Print("\nBalance Progression: "); // ArrayPrint(balance_progression); //--- create timer EventSetTimer(60); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); } //+------------------------------------------------------------------+ //| Custom indicator 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[]) { //--- if(rates_total >= Total_deals && Total_deals != last_total_deals) { IndicatorSetString(INDICATOR_SHORTNAME, "Balance Curve"); int indicator_window = ChartWindowFind(ChartID(),"Balance Curve"); // Comment(indicator_window); ObjectsDeleteAll(ChartID(),indicator_window,OBJ_TREND); //MAX AND MIN Indicator Window max_cumulative = balance_progression[ArrayMaximum(balance_progression,0,WHOLE_ARRAY)]; min_cumulative = balance_progression[ArrayMinimum(balance_progression,0,WHOLE_ARRAY)]; // Creating y and x axis ObjectCreate(0,y,OBJ_TREND,indicator_window,time[rates_total - Total_deals],0,time[rates_total - Total_deals],max_cumulative); ObjectCreate(0,x,OBJ_TREND,indicator_window,time[rates_total - Total_deals],0,time[rates_total - 1],0); IndicatorSetDouble(INDICATOR_MAXIMUM, max_cumulative); // maximum value IndicatorSetDouble(INDICATOR_MINIMUM,min_cumulative); // minimum value int c = 0; for(int i = rates_total - Total_deals; i <= rates_total - 2; i++) { chart_lines = StringFormat("Lines %d", c); if(ObjectFind(0, chart_lines) == -1) { ObjectCreate(0,chart_lines,OBJ_TREND,indicator_window,time[i],balance_progression[c],time[i+1],balance_progression[c+1]); } else { ObjectMove(0, chart_lines, 0, time[i], balance_progression[c]); ObjectMove(0, chart_lines, 1, time[i+1], balance_progression[c+1]); } p_l = StringFormat("Profit-Loss %d", c); if(show_pro == true) { if(ObjectFind(0, p_l) == -1) { ObjectCreate(0,p_l,OBJ_TEXT,1,time[i],balance_progression[c]); } else { ObjectMove(0, p_l, 0, time[i], balance_progression[c]); } } else { ObjectDelete(0,p_l); } if(profit_loss[c] > 0) { d_sign = "$"; } if(profit_loss[c] <= 0) { d_sign = "-$"; } ObjectSetString(0,p_l,OBJPROP_TEXT, d_sign + DoubleToString(MathAbs(profit_loss[c]),2)); c++; } last_total_deals = Total_deals; } return(rates_total); } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { BalanceCurve(File_name,Total_deals,balance_progression,profit_loss); }
Пояснение:
Чтобы улучшить функцию BalanceCurve, в нее добавили дополнительный параметр – массив прибыли и убытка, передаваемый по ссылке. Передача по ссылке позволяет функции работать напрямую с исходным массивом, созданным вне нее, а не создавать локальную копию. Это делает результаты доступными во всей программе и позволяет функции заполнять массив точными значениями прибыли и убытка, извлеченными из столбца Profit($) CSV-файла. Теперь функция также дает доступ к отдельному результату каждой сделки, тогда как раньше она только формировала накопительную динамику баланса. Чтобы это работало, в вызовы BalanceCurve внутри OnInit и OnTimer был добавлен массив прибыли и убытка, объявленный в глобальной области, так что и накопительные итоги, и результаты отдельных сделок доступны во всей программе.
Переменная цикла представляет бары графика, а для отслеживания индекса в массиве накопительного баланса инициализируется счетчик. Хотя эти два индекса движутся синхронно, их функции различны: один отслеживает номер сделки в файле, а другой – положение свечей на графике.
Ранее мы исходили из того, что индикатор всегда подключен ко второму окну графика, и поэтому использовали фиксированный индекс подокна 1 для создания графических объектов, например трендовых линий. Этот метод работает только при таком допущении; он становится ненадежным, если индикатор переместить или добавить другие индикаторы. Кроме того, удаление объектов при использовании этого жестко заданного индекса окна приводило бы к удалению всех трендовых линий из данного подокна в случае изменения количества сделок и могло бы непреднамеренно затронуть другие объекты.
Теперь, чтобы сделать решение динамичным и надежным, мы используем функцию IndicatorSetString() для задания краткого имени индикатора, а функцию ChartWindowFind() – для получения фактического индекса подокна во время выполнения. Сохранив это значение в переменной вроде indicator_window, мы можем последовательно использовать его при добавлении и удалении объектов. Это гарантирует, что управление объектами выполняется в нужном окне независимо от конфигурации графика, и устраняет необходимость считать, что индикатор всегда находится в окне 1.
Чтобы отобразить каждую сделку в пределах графика, цикл построения соединяет каждую пару соседних баров – от бара первой сделки до предпоследнего бара. Индекс сделки используется, чтобы задать каждому участку кривой баланса уникальное имя. С помощью ObjectFind проверяется, существует ли трендовая линия. Если ее нет, ObjectCreate соединяет линией две подряд идущие точки накопительного баланса. Если линия уже существует, ObjectMove перемещает ее, чтобы повысить производительность и обеспечить динамическое обновление кривой баланса при поступлении новых данных или обновлении графика.
Аналогичным образом создается уникальное имя для текстового объекта, который показывает прибыль или убыток по каждой сделке. Отображаются ли эти текстовые метки, зависит от переменной show_pro. Если отображение включено, программа проверяет наличие объекта и либо перемещает существующий объект в обновленную позицию, либо создает новый объект OBJ_TEXT на соответствующем баре и уровне накопительного баланса. Если show_pro равно false, ObjectDelete удаляет текстовые объекты, чтобы сохранить порядок на графике и не затрагивать саму кривую баланса.
С помощью MathAbs программа форматирует каждое значение прибыли или убытка – определяет нужный знак и убирает отрицательный знак из числового значения. Затем с помощью ObjectSetString этот отформатированный текст присваивается объекту, создавая наглядное обозначение вроде "$25.50" для прибыли или "-$12.30" для убытка. В конце счетчик индекса сделок увеличивается, чтобы следующая итерация обработала следующую сделку. Благодаря этой логике индикатор может динамически показывать или скрывать корректно оформленные метки прибыли и убытка, точно привязанные к каждой точке кривой баланса.
Вывод:

Заключение
Из этой статьи вы узнали, как в MQL5 считывать CSV-файл с историей сделок и извлекать из него ключевые данные, такие как значения Profit($) и общее количество сделок. Вы также узнали, как поместить нужный файл в правильную папку. Теперь вы знаете, как рассчитывать накопительную динамику баланса и использовать ее для определения максимального и минимального значений окна индикатора. Вы также освоили построение непрерывной кривой баланса, создание и выравнивание горизонтальной и вертикальной осей, а также при необходимости вывод меток прибыли и убытка для каждой сделки. Кроме того, вы узнали, как сделать так, чтобы кривая перерисовывалась только при появлении новых сделок, и как динамически обновлять индикатор с помощью обработчиков событий OnInit и OnTimer. В целом вы научились использовать MetaTrader 5 для преобразования необработанных CSV-данных по сделкам в наглядный график, который позволяет прямо в программе анализировать результативность счета, просадки и вклад отдельных сделок.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21326
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
TradeMux как Quant Backbone: Подключение институциональных Python-пайплайнов к разным терминалам и брокерам
Разработка инструментария для анализа Price Action (Часть 62): Создание адаптивной системы обнаружения параллельных каналов и пробоев на языке MQL5
Советник для размещения сделок на основе риска с графическим интерфейсом на графике (Часть 1): Проектирование пользовательского интерфейса
Моделирование рынка: Первые шаги на SQL в MQL5 (IV)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Вау, вы великий учитель. Никогда не перестану читать ваши статьи