Пользовательские инструменты отладки и профилирования для разработки на MQL5 (Часть III): Регрессионный контроль производительности и торговых правил
Ниже приведено содержание.
- Введение
- Что сохраняется из Части II
- Организация кодовой базы и процесса формирования отчетов
- Создание барьера регрессионного контроля
- Добавление проверок утверждений с учетом специфики MQL5 без переписывания TestLite
- Скрипт модуля запуска регрессионной проверки
- Запуск рабочего процесса и интерпретация результатов
- Заключение
Введение
В Части II были получены два полезных результата: CSV-файл профилировщика и детерминированный отчет о модульных тестах. Это уже дает проекту больше, чем простое отсутствие сообщений на вкладке "Эксперты", но отдельный отчет по-прежнему описывает лишь один прогон. Часть III добавляет недостающий уровень принятия решений, сохраняя при этом контракты из Части II: профилировщик записывает CSV-файл в прежнем формате, средство запуска модульных тестов формирует отчет TestLite в прежнем формате, а чистые функции по-прежнему находятся во вспомогательном модуле торговых расчетов. Барьер регрессионного контроля сравнивает текущие диагностические данные с принятым эталоном и на основе этого сравнения принимает решение.
Практическая проблема хорошо известна: советник может скомпилироваться, а тестер стратегий — завершить работу, в то время как регрессия остается скрытой за внешне безобидным журналом. Ниже приведены типичные примеры.
- После изменения индикатора участок кода, выполняющий копирование данных из буфера, становится более ресурсоемким.
- Участок кода, который раньше выполнялся при появлении новых баров, начинает выполняться на каждом тике.
- Правило определения размера лота проходит проверку на EURUSD, но не проходит ее на символе с другим шагом объема.
- После рефакторинга сигнал по ошибке считывает данные текущего формирующегося бара.
Барьер отвечает на три практических вопроса.
- Достаточно ли изменились показатели выбранных профилируемых участков, чтобы присвоить статус PASS, WARN, SKIP или FAIL?
- По-прежнему ли успешно проходят тесты чистых функций торговых расчетов из Части II?
- Остаются ли верными допущения о свойствах конкретного символа, используемые в примере советника?
Это не заменяет профилирование в MetaEditor. В MetaEditor уже есть встроенный профилировщик, который может собирать статистику выполнения функций и строк кода, а также работать на графике или с историческими данными в тестере стратегий. Этот инструмент по-прежнему остается оптимальным выбором для общего анализа производительности. Встроенный барьер решает следующую задачу сопровождения: после того как нужные участки определены, необходимо выяснить, возникла ли на них существенная регрессия по сравнению с проверенным эталоном. Выходные данные намеренно просты: файлы статусов, изменений и ошибок, а также отчеты о проверках торговых утверждений и допущениях о свойствах символа. Рабочий процесс остается простым: запустить модульные тесты, запустить профилируемый советник, назначить профиль без ошибок эталоном и сравнить с ним текущий запуск, прежде чем этот запуск станет новой точкой отсчета.
Что сохраняется из Части II
В Части III существующие файлы рассматриваются как контракты, а не как черновики, подлежащие переработке. В Части II уже представлены профилировщик, вспомогательный инструмент для тестирования, модуль чистых функций торговых расчетов, детерминированный тестовый скрипт и советник, который не торгует, а записывает диагностические данные профилировщика. Файлы RegressionGate.mqh и TradeAssertions.mqh построены с учетом этих ролей.
Ниже перечислены контракты, которые сохраняются из предыдущей части.
- CSV-отчет профилировщика: CPerfMeter::WriteCsvReport() записывает фиксированный набор полей: section, calls, total_us, min_us, max_us, avg_us, slow_calls, threshold_us и status. Барьер регрессионного контроля считывает данные в этом формате, не требуя открывать доступ к закрытым массивам профилировщика или добавлять в него новые методы.
- Отчет о тестировании: CTestLite::WriteReport() записывает файл TradeMathCore_TestReport.txt со строкой статуса в формате "ключ — значение". Барьер регрессионного контроля считывает статус верхнего уровня, вместо того чтобы заново разбирать каждую строку с ранее зафиксированной ошибкой.
- Торговые расчеты: CTradeMathCore по-прежнему отвечает за пересчет пипсов и пунктов, нормализацию объема, проверку расстояния до стопов, классификацию пересечений скользящих средних и преобразование сигналов в строки.
- Профилируемый советник: ProfilerExampleEA.mq5 создает дескрипторы скользящих средних, копирует данные двух закрытых баров, классифицирует пересечение с помощью CTradeMathCore и записывает отчет ProfilerExampleEA_Profile.csv.
Следующий фрагмент кода фиксирует неизменные элементы советника: директивы подключения, экземпляр CPerfMeter, перечисление сигналов CTradeMathCore и исходное имя файла отчета. Барьер использует эти имена как часть контракта.
#property strict #include <Debugging_Profiling_Part3/PerfMeter.mqh> #include <Debugging_Profiling_Part3/TradeMathCore.mqh> input bool EnableProfiling = true; input int ProfileEveryNTicks = 10; input int SlowCallThresholdMicroseconds = 250; input int FastMAPeriod = 10; input int SlowMAPeriod = 30; CPerfMeter g_profiler; int g_fast_ma_handle = INVALID_HANDLE; int g_slow_ma_handle = INVALID_HANDLE; ulong g_ticks_seen = 0; TradeMathSignal g_last_signal = TM_SIGNAL_NONE; string g_report_name = "ProfilerExampleEA_Profile.csv";
В блоке инициализации создание индикаторов по-прежнему выделено в именованный участок профилировщика. Благодаря этому затраты на запуск входят в тот же набор диагностических данных, что и измерения последующих участков обработки тиков.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(EnableProfiling) { //--- Measure initialization as two nested sections: total and handle creation. g_profiler.Reset(); g_profiler.SetSlowCallThreshold(SlowCallThresholdMicroseconds); g_profiler.Start("OnInit.total"); g_profiler.Start("OnInit.create_indicators"); } g_fast_ma_handle = iMA(_Symbol, _Period, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE); g_slow_ma_handle = iMA(_Symbol, _Period, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE); if(EnableProfiling) { g_profiler.Stop("OnInit.create_indicators"); g_profiler.Stop("OnInit.total"); } if(g_fast_ma_handle == INVALID_HANDLE || g_slow_ma_handle == INVALID_HANDLE) { PrintFormat("ProfilerExampleEA: failed to create MA handles, error=%d", GetLastError()); return INIT_FAILED; } PrintFormat("ProfilerExampleEA initialized on %s %s, fast=%d, slow=%d, profiling=%s", _Symbol, EnumToString(_Period), FastMAPeriod, SlowMAPeriod, (EnableProfiling ? "true" : "false")); return INIT_SUCCEEDED; }
Механизм записи отчетов не изменился. ProfilerExampleEA записывает данные через экземпляр CPerfMeter из Части II и сохраняет CSV-файл в FILE_COMMON, тогда как UnitTestRunner.mq5 записывает TradeMathCore_TestReport.txt в стандартную папку MQL5\Files. Поэтому модуль запуска имеет отдельные входные параметры для расположения отчета профиля, отчета о модульных тестах и выходных данных барьера регрессионного контроля.
//+------------------------------------------------------------------+ //| Write profiler CSV report | //+------------------------------------------------------------------+ void WriteProfilerReport(const string origin) { if(!EnableProfiling) return; bool report_written = g_profiler.WriteCsvReport(g_report_name, true); if(report_written) PrintFormat("ProfilerExampleEA: %s profile report saved: %s in FILE_COMMON. %s", origin, g_report_name, g_profiler.Summary()); else PrintFormat("ProfilerExampleEA: %s profile report could not be written: %s. %s", origin, g_report_name, g_profiler.Summary()); }
Файлы PerfMeter.mqh и TestLite.mqh сохраняют свои первоначальные функции. В приложенный пакет включены вспомогательные файлы Части II, поскольку в рабочем процессе Части III они непосредственно запускаются, подключаются или используются создаваемые ими отчеты. Теперь эти файлы размещены в соответствующих подпапках проекта, чтобы пакет имел упорядоченную структуру. Регистратор из Части I включается в пакет только при наличии его файлов в целевом проекте. В новом модуле запуска сохранена небольшая функция LogDiagnosticGateResult(), которая в приложенном пакете использует PrintFormat(); в реальном проекте ее можно заменить вызовом регистратора из Части I.
Организация кодовой базы и процесса формирования отчетов
Приложенный пакет повторяет структуру папки данных MetaTrader 5. После распаковки в папку данных терминала все исходные файлы размещаются в подпапках проекта Debugging_Profiling_Part3 внутри каталогов MQL5\Include, MQL5\Scripts и MQL5\Experts. Такая структура позволяет избежать путаницы с путями подключения и загромождения стандартных папок, а также сохраняет запускаемые файлы там, где их ожидают найти MetaEditor и "Навигатор".
В пакете есть две категории файлов.
- Исходные файлы: к ним относятся общие подключаемые файлы, скрипты и советник, необходимые для компиляции и выполнения.
- Сформированные диагностические данные: это отчеты, создаваемые локально после запуска скрипта или советника. Эти отчеты описаны в материалах пакета, но устаревшие результаты не включены в него в качестве исходных материалов.
Раздельное хранение исходных файлов и сгенерированных отчетов позволяет не включать во вложение устаревшие примеры результатов и упрощает проверку содержимого архива.
После распаковки содержимое основных папок легко проверить.
- В папке MQL5\Include\Debugging_Profiling_Part3 содержатся общие вспомогательные функции и подключаемые файлы.
- В папке MQL5\Experts\Debugging_Profiling_Part3 находится профилируемый советник, используемый в тестере стратегий.
- В папке MQL5\Scripts\Debugging_Profiling_Part3 находятся модуль запуска модульных тестов, скрипт назначения профиля эталоном и модуль запуска диагностического барьера регрессионного контроля.
Отчеты намеренно формируются в строгой последовательности.
- Запустите файл UnitTestRunner.mq5, чтобы создать файл TradeMathCore_TestReport.txt.
- Запустите файл ProfilerExampleEA.mq5 в тестере стратегий, чтобы сохранить файл ProfilerExampleEA_Profile.csv в общем каталоге файлов с использованием флага FILE_COMMON.
- Если профиль относится к утвержденной версии, запустите скрипт PromoteProfilerBaseline.mq5, чтобы создать файл ProfilerExampleEA_Baseline.csv.
- После внесения изменений в код снова запустите профилируемый советник, а затем — DiagnosticGateRunner.mq5.
Порядок имеет значение. Эталон — это отчет, полученный при запуске версии, которую разработчик готов считать приемлемой. Если он получен при некорректном выполнении программы, барьер регрессионного контроля лишь закрепляет это некорректное поведение. Скрипт назначения профиля эталоном остается небольшим, поскольку его задача проста: скопировать текущий отчет профилирования в файл эталона с помощью файловых функций платформы. Имя отчета по умолчанию совпадает с именем существующего отчета советника из Части II.
#property strict #property script_show_inputs input bool UseCommonProfileFiles = true; input string CurrentProfileFile = "ProfilerExampleEA_Profile.csv"; input string BaselineProfileFile = "ProfilerExampleEA_Baseline.csv"; //+------------------------------------------------------------------+ //| Copy the latest profiler report to the accepted baseline file | //+------------------------------------------------------------------+ bool PromoteProfilerBaseline(const string current_file, const string baseline_file, const bool common_file) { int source_location = (common_file ? FILE_COMMON : 0); int target_mode = FILE_REWRITE; if(common_file) target_mode |= FILE_COMMON; //--- FileCopy keeps promotion explicit: current profile becomes accepted baseline. ResetLastError(); bool copied = FileCopy(current_file, source_location, baseline_file, target_mode); if(!copied) { PrintFormat("PromoteProfilerBaseline: cannot copy '%s' to '%s', error=%d", current_file, baseline_file, GetLastError()); return false; } PrintFormat("PromoteProfilerBaseline: baseline updated from '%s' to '%s', common=%s", current_file, baseline_file, (common_file ? "true" : "false")); return true; }
Правила выбора папки для выходных файлов заданы явно.
- По умолчанию отчеты о профилях сохраняются в каталоге файлов с флагом FILE_COMMON, поскольку профилируемый советник уже записывает данные туда, а результаты работы тестера стратегий проще получить из общей папки.
- Отчет модульных тестов из Части II по умолчанию сохраняется в стандартной папке файлов, поскольку UnitTestRunner.mq5 записывает его именно туда.
- По умолчанию отчеты барьера регрессионного контроля сохраняются в общем каталоге файлов с флагом FILE_COMMON, чтобы после выполнения модуля запуска было легко найти итоговый статус, изменения и сведения об ошибках.
Сгенерированные отчеты:
| Отчет | Создается с помощью | Назначение |
|---|---|---|
| TradeMathCore_TestReport.txt | UnitTestRunner.mq5 | Статус детерминированных тестов из части II и строки с ошибками. |
| ProfilerExampleEA_Profile.csv | ProfilerExampleEA.mq5 | Текущие диагностические данные профилировщика, полученные при работе существующего профилируемого советника. |
| ProfilerExampleEA_Baseline.csv | PromoteProfilerBaseline.mq5 | Принятые диагностические данные профилировщика, используемые в качестве эталона для сравнения. |
| DiagnosticGate_Deltas.csv | DiagnosticGateRunner.mq5 | Все строки профилировщика: сопоставленные, новые, отсутствующие, пропущенные, с предупреждениями и с ошибками. |
| DiagnosticGate_Failures.csv | DiagnosticGateRunner.mq5 | Только те строки, которые требуют внимания: WARN, FAIL или SKIP. |
| TradeAssertions_Report.txt | DiagnosticGateRunner.mq5 | Отчет о проверках утверждений с учетом свойств символа в формате TestLite из Части II. |
| SymbolAssumptions.txt | DiagnosticGateRunner.mq5 | Свойства символа, используемые модулем запуска проверок утверждений. |
| DiagnosticGate_Status.txt | DiagnosticGateRunner.mq5 | Окончательное решение в машиночитаемом формате: "PASS", "WARN" или "FAIL". |
При таком разделении у каждого компонента одна задача: файлы Части II формируют диагностические данные, а файлы Части III определяют, остаются ли текущие диагностические данные приемлемыми.
Создание барьера регрессионного контроля
RegressionGate.mqh — основной новый подключаемый файл. Он не измеряет время выполнения кода — это по-прежнему задача профилировщика. Он загружает два отчета профилировщика, сопоставляет строки по именам профилируемых участков, вычисляет разницу, классифицирует изменение и записывает статус, который может прочитать человек или обработать скрипт.
В основе устройства барьера регрессионного контроля лежат три практических правила.
- Имена профилируемых участков служат ключами. Такие имена, как OnTick.copy_buffers и OnTick.signal_calculation, полезны тем, что позволяют определить, какая именно работа измеряется. Такое название, как "Work", слишком расплывчато, а название, которое меняется с каждой версией, нарушает историю сравнений.
- Структуры остаются простыми. SProfilerCsvRow воспроизводит структуру CSV-файла из Части II, SProfileDeltaRow хранит результаты сравнения, а SRegressionGateConfig — политику пороговых значений.
- Поля с результатами измерения времени не скрываются. Поля avg_us, max_us, baseline_calls и current_calls сохраняются в отчете об изменениях, чтобы изменение планирования вызовов не приняли за замедление функции.
Отчет об изменениях намеренно содержит больше подробностей, чем однословный результат. Разработчик, анализирующий строку с отметкой "WARN" или "FAIL", должен рассматривать столбцы в совокупности.
- Количество вызовов: поля baseline_calls и current_calls показывают, выполнялся ли данный путь кода сопоставимое число раз.
- Среднее время: поля baseline_avg_us и current_avg_us показывают, стало ли выполнение обычной работы занимать больше времени.
- Максимальное время: поля baseline_max_us и current_max_us показывают, изменилась ли длительность самого медленного из наблюдавшихся вызовов.
- Относительное изменение: показатели avg_delta_pct и max_delta_pct отражают изменение относительно принятого эталона.
- Причина: в столбце reason объясняется, почему строке присвоен статус WARN, FAIL или SKIP.
#property strict //+------------------------------------------------------------------+ //| RegressionGate.mqh | //| Compares Part II profiler reports and writes gate outputs. | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Structure: SProfilerCsvRow | //| One row loaded from the Part II CPerfMeter CSV report. | //+------------------------------------------------------------------+ struct SProfilerCsvRow { string section; long calls; double total_us; double min_us; double max_us; double avg_us; long slow_calls; long threshold_us; string status; bool matched; }; //+------------------------------------------------------------------+ //| Structure: SProfileDeltaRow | //| One comparison row written by the regression gate. | //+------------------------------------------------------------------+ struct SProfileDeltaRow { string section; long baseline_calls; long current_calls; double baseline_avg_us; double current_avg_us; double avg_delta_us; double avg_delta_pct; double baseline_max_us; double current_max_us; double max_delta_us; double max_delta_pct; string status; string reason; }; //+------------------------------------------------------------------+ //| Structure: SRegressionGateConfig | //| Thresholds used while comparing profiler reports. | //+------------------------------------------------------------------+ struct SRegressionGateConfig { double warn_avg_increase_pct; double fail_avg_increase_pct; double warn_max_increase_pct; double fail_max_increase_pct; double warn_abs_avg_us; double fail_abs_avg_us; double warn_abs_max_us; double fail_abs_max_us; double min_comparable_avg_us; long min_calls; bool warn_on_new_sections; bool warn_on_missing_sections; };
Загрузчик считывает CSV строго в том формате, который использует CPerfMeter при записи. Он пропускает заголовок, а затем считывает по девять полей в каждой строке. Барьер регрессионного контроля связан с контрактом отчета, а не с внутренним устройством профилировщика, поэтому профилировщик может менять внутренний способ хранения данных, пока контракт CSV остается неизменным.
//--- Store the row exactly in the shape used by the delta builder. int size = ArraySize(rows); ArrayResize(rows, size + 1); rows[size].section = section; rows[size].calls = (long)StringToInteger(calls_text); rows[size].total_us = StringToDouble(total_text); rows[size].min_us = StringToDouble(min_text); rows[size].max_us = StringToDouble(max_text); rows[size].avg_us = StringToDouble(avg_text); rows[size].slow_calls = (long)StringToInteger(slow_calls_text); rows[size].threshold_us = (long)StringToInteger(threshold_text); rows[size].status = status_text; rows[size].matched = false;
В правиле классификации используются как относительные, так и абсолютные пороговые значения, поскольку при измерениях в микросекундах вблизи крайне малых значений наблюдается значительный уровень шума. Изменение с 0,02 микросекунды до 0,04 микросекунды означает увеличение на 100 процентов, но это не является веским основанием для отклонения сборки. Если значение эталона в CSV равно нулю или округлено до нуля, барьер рассматривает положительное текущее значение как большое процентное изменение, но значимость этого изменения по-прежнему определяется абсолютным порогом.
В политике по умолчанию используются следующие пороговые значения.
- присваивать статус WARN, когда среднее время увеличивается как минимум на 20 процентов и как минимум на 5 микросекунд;
- присваивать статус FAIL, когда среднее время увеличивается как минимум на 35 процентов и как минимум на 10 микросекунд;
- использовать отдельные пороговые значения для максимального времени, поскольку отдельные скачки времени выполнения не следует оценивать так же, как обычную работу;
- помечать строки как SKIP, если в них слишком мало вызовов или среднее время выполнения слишком мало для объективного сравнения.
Абсолютные пороговые значения не менее важны, чем процентные показатели. Без них при очень малых значениях могут получаться огромные процентные изменения, не имеющие практического значения. Без учета процентных изменений значительное абсолютное изменение на и без того медленном участке может показаться менее серьезным, чем оно есть на самом деле. Сочетание обоих условий делает регрессионный контроль строже там, где это важно, и сокращает число срабатываний там, где измеренные значения ниже уровня шумов.
if(row.avg_delta_pct >= m_config.fail_avg_increase_pct && row.avg_delta_us >= m_config.fail_abs_avg_us) { //--- Average time is the main signal for routine section cost. fail = true; AddReason(reason, "avg_us_failed"); } else if(row.avg_delta_pct >= m_config.warn_avg_increase_pct && row.avg_delta_us >= m_config.warn_abs_avg_us) { warn = true; AddReason(reason, "avg_us_warned"); } if(row.max_delta_pct >= m_config.fail_max_increase_pct && row.max_delta_us >= m_config.fail_abs_max_us) { //--- Maximum time catches spikes that average time can hide. fail = true; AddReason(reason, "max_us_failed"); } else if(row.max_delta_pct >= m_config.warn_max_increase_pct && row.max_delta_us >= m_config.warn_abs_max_us) { warn = true; AddReason(reason, "max_us_warned"); } if(fail) row.status = "FAIL"; else if(warn) row.status = "WARN"; else row.status = "PASS"; if(reason != "") row.reason = reason;
Новые и отсутствующие профилируемые участки по умолчанию получают статус WARN, поскольку меняют состав измеряемых участков. Новый участок может реализовывать намеренно добавленный фильтр рисков, а может скрывать работу, которая раньше выполнялась внутри измеряемого блока. Отсутствие участка может быть результатом очистки кода, а может означать, что имя участка изменилось и возможность сравнения с предыдущими результатами утрачена. Политика предупреждений позволяет сохранять эти изменения видимыми, не блокируя по умолчанию каждый рефакторинг.
Итоговый статус определяется по наивысшему уровню серьезности среди результатов сравнения производительности, статуса модульных тестов из Части II и статуса проверок торговых утверждений из Части III. Неудачный результат модульного теста означает непрохождение барьера регрессионного контроля, даже если сравнение результатов профилировщика не выявило проблем, поскольку оптимизация некорректной логики — пустая трата усилий. Барьер регрессионного контроля по-прежнему работает на основе файлов и остается в рамках стандартной среды MetaTrader. Для его работы не требуются ни DLL, ни база данных, ни отдельный настольный модуль запуска.
Добавление проверок утверждений с учетом специфики MQL5 без переписывания TestLite
Универсальные проверки утверждений полезны, но торговому проекту также нужны проверки с учетом свойств символа. Файл TradeAssertions.mqh подключает TestLite.mqh и TradeMathCore.mqh и предоставляет статические вспомогательные методы, принимающие ссылку на объект CTestLite. Повторно используются те же формат отчета, строка статуса и счетчики проверок. Новый подключаемый файл лишь добавляет проверки с учетом специфики MQL5.
Группы проверок утверждений охватывают следующие аспекты:
- Параметры объема символа: SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX и SYMBOL_VOLUME_STEP должны допускать запрошенный в примере объем.
- Общая функция нормализации: результат CTradeMathCore::NormalizeVolume() должен соответствовать текущим ограничениям символа.
- Расстояние до стопов: значения SYMBOL_POINT и SYMBOL_TRADE_STOPS_LEVEL передаются в CTradeMathCore::IsStopDistanceValid().
- Доступ к закрытым барам: код формирования сигнала должен копировать данные из закрытых баров, а не из текущего формирующегося бара.
Эти проверки ориентированы на реальные риски в примере советника. Они не пытаются моделировать каждое правило брокера, каждый тип ордера или каждое условие исполнения. Вместо этого они проверяют допущения, которые легко нарушить при разработке и нарушение которых дорого выявлять на поздних этапах: соответствие объема допустимому шагу, расстояние до стопов и различие между закрытым и формирующимся баром.
//+------------------------------------------------------------------+ //| Assert that a requested volume is aligned with symbol settings | //+------------------------------------------------------------------+ static bool AssertValidVolumeStep(CTestLite &test, const string symbol, const double requested_volume, const string message = "requested volume must align with broker limits") { double min_volume; double max_volume; double volume_step; string details; if(!LoadVolumeSettings(symbol, min_volume, max_volume, volume_step, details)) return test.AssertTrue(false, message + ": invalid symbol volume settings: " + details); //--- Reuse the production normalization helper instead of duplicating volume logic. double normalized = CTradeMathCore::NormalizeVolume(requested_volume, min_volume, max_volume, volume_step); bool in_range = (requested_volume + 0.0000001 >= min_volume && requested_volume <= max_volume + 0.0000001); bool aligned = (MathAbs(normalized - requested_volume) <= MathMax(volume_step * 0.0001, 0.00000001)); string actual = StringFormat("requested=%.8f normalized=%.8f %s", requested_volume, normalized, details); return test.AssertTrue(in_range && aligned, message + ": " + actual); }
Функция AssertValidVolumeStep() не реализует второй алгоритм нормализации. Она вызывает CTradeMathCore::NormalizeVolume(), а затем проверяет, равно ли нормализованное значение запрошенному с учетом небольшого допуска. Если будущее изменение нарушит работу существующей функции нормализации, эта проверка утверждения сможет выявить проблему с использованием той же системы отчетов из Части II. При проверке расстояния до стопов соблюдается то же разделение обязанностей: модуль запуска считывает SYMBOL_POINT и SYMBOL_TRADE_STOPS_LEVEL, а затем передает размер пункта и уровень стопов в CTradeMathCore::IsStopDistanceValid().
Проверки использования закрытых баров защищают от ошибок другого рода. Советник из Части II копирует данные из буферов индикаторов для двух баров, начиная со сдвига 1. Согласно контракту функции CopyBuffer в MQL5, начальная позиция 0 означает текущий бар. Поэтому стратегия, для которой заявлена работа на закрытых барах, не должна использовать сдвиг 0 при принятии торговых решений по сигналам. Проверка утверждения не может автоматически контролировать каждый будущий вызов CopyBuffer; она фиксирует предусмотренный контракт в том месте, где ее вызывает модуль запуска. Польза этой проверки заключается в том, что она явно фиксирует допущение.
//+------------------------------------------------------------------+ //| Assert that CopyBuffer is not reading the forming bar | //+------------------------------------------------------------------+ static bool AssertNoCurrentBarLookahead(CTestLite &test, const int copy_start_pos, const string message = "closed-bar logic must not copy from bar zero") { //--- CopyBuffer start position 0 is the forming bar; closed-bar logic starts at 1. bool ok = (copy_start_pos >= 1); string details = StringFormat("copy_start_pos=%d", copy_start_pos); return test.AssertTrue(ok, message + ": " + details); } //+------------------------------------------------------------------+ //| Assert that enough closed bars are copied for a crossover rule | //+------------------------------------------------------------------+ static bool AssertClosedBarWindow(CTestLite &test, const int copy_start_pos, const int copy_count, const string message = "closed-bar crossover logic needs two closed bars") { bool ok = (copy_start_pos >= 1 && copy_count >= 2); string details = StringFormat("copy_start_pos=%d copy_count=%d", copy_start_pos, copy_count); return test.AssertTrue(ok, message + ": " + details); }
Не перегружайте эти проверки. Они проверяют допущения, на которых основаны работа советника из примера и процесс диагностики: пригодность настроек объема символа, допустимость объемов из примера, соответствие расстояния до стопов ограничениям символа и доступ к данным закрытых баров в буферах. В реальный советник впоследствии можно добавить вспомогательные функции для фильтров спреда, уровней заморозки, режима расчета маржи, правил торговых сессий и разрешенных типов ордеров.
Формируемые диагностические данные проверок утверждений представлены на двух уровнях.
- В файле TradeAssertions_Report.txt используется строка статуса TestLite в формате "ключ — значение", что дает барьеру регрессионного контроля единый способ анализа отчетов детерминированных проверок и проверок с учетом свойств символа.
- В файле SymbolAssumptions.txt записываются полученные модулем запуска сведения: символ, размер пункта, число знаков после запятой, ограничения объема, значение SYMBOL_TRADE_STOPS_LEVEL и уровень заморозки.
Этот второй файл важен, поскольку неуспешная проверка не всегда указывает на дефект кода. Она может показать, что скрипт был запущен на символе с параметрами контракта, отличными от ожидаемых. Отчет о допущениях содержит данные, которые следует проверить в первую очередь, прежде чем менять вспомогательную функцию или ослаблять требования теста.
Скрипт модуля запуска регрессионной проверки
Программа DiagnosticGateRunner.mq5 объединяет все эти элементы воедино. Скрипт не торгует, не выполняет оптимизацию и не запускает тестер стратегий; он читает файлы, созданные на предыдущих этапах, и записывает итоговое диагностическое решение. Входные параметры позволяют задавать расположение файлов и пороговые значения; при этом сохранены имена файлов из Части II: ProfilerExampleEA_Profile.csv, ProfilerExampleEA_Baseline.csv и TradeMathCore_TestReport.txt.
#property strict #property script_show_inputs #include <Debugging_Profiling_Part3/RegressionGate.mqh> #include <Debugging_Profiling_Part3/TradeAssertions.mqh> input bool UseCommonProfileFiles = true; input bool UseCommonUnitTestReport = false; input bool UseCommonGateReports = true; input string BaselineProfileFile = "ProfilerExampleEA_Baseline.csv"; input string CurrentProfileFile = "ProfilerExampleEA_Profile.csv"; input string UnitTestReportFile = "TradeMathCore_TestReport.txt"; input bool RequireUnitTestReport = true; input string DeltaReportFile = "DiagnosticGate_Deltas.csv"; input string FailureReportFile = "DiagnosticGate_Failures.csv"; input string StatusReportFile = "DiagnosticGate_Status.txt"; input string TradeAssertionReportFile = "TradeAssertions_Report.txt"; input string SymbolAssumptionReportFile = "SymbolAssumptions.txt"; input double WarnAvgIncreasePct = 20.0; input double FailAvgIncreasePct = 35.0; input double WarnMaxIncreasePct = 25.0; input double FailMaxIncreasePct = 50.0; input double WarnAbsAvgMicroseconds = 5.0; input double FailAbsAvgMicroseconds = 10.0; input double WarnAbsMaxMicroseconds = 25.0; input double FailAbsMaxMicroseconds = 50.0; input double MinComparableAvgMicroseconds = 2.0; input int MinComparableCalls = 20; input bool WarnOnNewProfilerSections = true; input bool WarnOnMissingProfilerSections = true;
Модуль запуска использует три переключателя для выбора папок.
- По умолчанию для параметра UseCommonProfileFiles установлено значение true, поскольку профилируемый советник записывает свой отчет через FILE_COMMON.
- По умолчанию для параметра UseCommonUnitTestReport установлено значение false, поскольку модуль UnitTestRunner.mq5 записывает файл TradeMathCore_TestReport.txt в стандартную папку с файлами.
- По умолчанию для параметра UseCommonGateReports установлено значение true, поскольку файлы итогового статуса и изменений проще найти, если они хранятся в общей папке.
Порядок выполнения скрипта прост.
- Настройте пороговые значения барьера регрессионного контроля.
- Сравните отчеты об эталонном и текущем профилях.
- Запишите CSV-файлы с изменениями и ошибками.
- Прочитайте статус модульных тестов из Части II.
- Выполните проверки утверждений о торговых правилах и запишите отчеты по ним.
- Запишите файл итогового статуса и выведите одну сводную строку в неизменном формате.
Такой порядок полезен при возникновении сбоев, поскольку каждый этап оставляет диагностические данные. Если эталон профилировщика отсутствует, в файле статуса прямо сообщается об этом. Отсутствие отчета о модульных тестах также не остается незамеченным. При неуспешных проверках утверждений о торговых правилах формируются отчет по этим проверкам и файл с допущениями о свойствах символа. В результате за один запуск формируется отчет о диагностическом состоянии без остановки на первом отсутствующем файле.
CRegressionGate gate; gate.Configure(WarnAvgIncreasePct, FailAvgIncreasePct, WarnMaxIncreasePct, FailMaxIncreasePct, WarnAbsAvgMicroseconds, FailAbsAvgMicroseconds, WarnAbsMaxMicroseconds, FailAbsMaxMicroseconds, MinComparableAvgMicroseconds, MinComparableCalls); gate.SetNewSectionPolicy(WarnOnNewProfilerSections, WarnOnMissingProfilerSections); bool compared = gate.CompareProfilerReports(BaselineProfileFile, CurrentProfileFile, UseCommonProfileFiles); if(!compared) PrintFormat("DiagnosticGateRunner: profiler comparison problem: %s", gate.LastError()); //--- Write comparison files even when the comparison status is not clean. gate.WriteDeltaCsv(DeltaReportFile, UseCommonGateReports); gate.WriteFailuresCsv(FailureReportFile, UseCommonGateReports); string unit_test_status = ReadUnitTestStatus(UnitTestReportFile, UseCommonUnitTestReport, RequireUnitTestReport); gate.SetUnitTestStatus(unit_test_status); CTestLite trade_assertions("MQL5 symbol and closed-bar assertions"); RunTradeAssertions(trade_assertions, _Symbol); trade_assertions.WriteReport(TradeAssertionReportFile, UseCommonGateReports); //--- Keep symbol properties beside the assertion report for faster triage. CTradeAssertions::WriteSymbolAssumptionReport(_Symbol, SymbolAssumptionReportFile, UseCommonGateReports); gate.SetTradeAssertionStatus(trade_assertions.Status()); gate.WriteStatusFile(StatusReportFile, UseCommonGateReports, BaselineProfileFile, CurrentProfileFile, DeltaReportFile, FailureReportFile, TradeAssertionReportFile, UnitTestReportFile); LogDiagnosticGateResult(gate.OverallStatus(), gate.EffectiveFailureCount(), gate.EffectiveWarningCount());
Интеграция с регистратором намеренно сведена к минимуму. Если в проекте есть регистратор из Части I, замените вызов PrintFormat() внутри LogDiagnosticGateResult() на вызов Info() этого регистратора; в противном случае скрипт остается автономным. Модуль запуска также не завершает работу досрочно при неудачном сравнении результатов профилировщика. Он записывает доступные отчеты, выполняет проверки утверждений о торговых правилах и фиксирует полное диагностическое состояние, а не только первую обнаруженную проблему.
Файл итогового статуса намеренно сделан текстовым, в формате "ключ — значение". Скрипт может прочитать значение overall_status=FAIL, не разбирая CSV-файл с изменениями, а разработчик — определить, связана ли проблема с производительностью, модульными тестами или проверками с учетом свойств символа.
Для ручной проверки откройте файлы в указанном порядке.
- DiagnosticGate_Status.txt — для просмотра окончательного решения: PASS, WARN или FAIL.
- DiagnosticGate_Failures.csv, если значение performance_status равно WARN или FAIL.
- TradeMathCore_TestReport.txt, если значение unit_test_status не равно PASS.
- Файлы TradeAssertions_Report.txt и SymbolAssumptions.txt, если значение trade_assert_status не равно PASS.
- DiagnosticGate_Deltas.csv, если краткого отчета об ошибках недостаточно для объяснения изменения показателей.
Поля статуса:
| Поле | Значение |
|---|---|
| overall_status | Окончательное решение "PASS", "WARN" или "FAIL", сформированное на основе данных всех диагностических источников. |
| performance_status | Результат сравнения текущих строк отчета профилировщика с принятым эталоном. |
| unit_test_status | Статус, считанный из файла TradeMathCore_TestReport.txt из Части II. |
| trade_assert_status | Статус из отчета TestLite о проверках утверждений с учетом свойств символа из Части III. |
| profile_failures | Количество строк отчета профилировщика, классифицированных как FAIL. |
| profile_warnings | Количество строк профилировщика, отнесенных к категории WARN. |
| profile_skipped | Строки, пропущенные из-за невозможности сравнения. |
| delta_rows | Общее количество строк с результатами сравнения результатов профилировщика, записанных в файл DiagnosticGate_Deltas.csv. |
Используйте файл статуса как ориентир для первичной диагностики.
- overall_status=FAIL: не продолжайте работу по повышению производительности, пока не выясните причину сбоя.
- unit_test_status=FAIL: сначала откройте файл TradeMathCore_TestReport.txt.
- trade_assert_status=FAIL: откройте файлы TradeAssertions_Report.txt и SymbolAssumptions.txt.
- performance_status=FAIL: откройте файл DiagnosticGate_Failures.csv перед просмотром полного отчета об изменениях.
Запуск рабочего процесса и интерпретация результатов
Последовательность запуска короткая, и для наилучших результатов ее следует каждый раз строго соблюдать. Сначала скомпилируйте запускаемые файлы: UnitTestRunner.mq5, ProfilerExampleEA.mq5, PromoteProfilerBaseline.mq5 и DiagnosticGateRunner.mq5. Подключаемые файлы не запускаются самостоятельно, но должны компилироваться при подключении к скриптам и советнику.
Затем запустите рабочий процесс в указанном порядке.
- Запустите файл UnitTestRunner.mq5 из папки "Scripts". Он проверяет правила, реализованные чистыми функциями в TradeMathCore.mqh, и записывает результаты в файл TradeMathCore_TestReport.txt.
- Если отчет о модульных тестах указывает на ошибки, остановитесь на этом этапе и сначала исправьте торговые расчеты. Сравнение производительности некорректной логики дает лишь шум.
- Запустите файл ProfilerExampleEA.mq5 в тестере стратегий с теми же символом, таймфреймом, диапазоном дат, моделью и входными параметрами, которые использовались для принятого эталона.
- Если профиль относится к версии, которую вы принимаете, запустите скрипт PromoteProfilerBaseline.mq5, чтобы создать файл ProfilerExampleEA_Baseline.csv на основе файла ProfilerExampleEA_Profile.csv.
- После внесения изменений в код запустите профилируемый советник ещё раз, чтобы файл ProfilerExampleEA_Profile.csv отражал текущую версию.
- Запустите DiagnosticGateRunner.mq5 и в первую очередь откройте файл DiagnosticGate_Status.txt.
Штатный профилировщик по-прежнему полезен для выявления неизвестных узких мест, а профилировщик, встроенный в код, измеряет время выполнения именованных участков, выбранных для проверок на регрессии. Сравнение имеет смысл только в контролируемой тестовой среде, а назначать профиль эталоном следует лишь тогда, когда версия действительно пригодна в качестве эталона.
Приведенные ниже диагностические данные получены в контролируемых условиях. Сохранение этих параметров неизменными обеспечивает сопоставимость эталона и текущего профиля.
- Символ и период: EURUSD, M15.
- Диапазон дат в тестере стратегий: с 20 мая 2026 г. по 22 мая 2026 г.
- Модель: OHLC на M1.
- Параметры профилировщика: ProfileEveryNTicks=10 и SlowCallThresholdMicroseconds=250.
Примеры результатов диагностики:
| Проверка | Результат | Интерпретация |
|---|---|---|
| TradeMathCore_TestReport.txt | Успешных проверок: 21, неуспешных: 0, status=PASS. | Проверки чистых функций торговых расчетов прошли успешно до принятия результатов сравнения профилей. |
| Назначение профиля профилировщика эталоном | Файл ProfilerExampleEA_Baseline.csv создан на основе принятого профиля. | Первый прогон в тестере стратегий в контролируемых условиях стал эталоном для следующего сравнения. |
| TradeAssertions_Report.txt | Успешно пройдено: 5, не пройдено: 0, status=PASS. | Для выбранного символа допущения об объеме, расстоянии до стопов и закрытых барах оказались применимыми. |
| DiagnosticGate_Status.txt | overall_status=PASS. | На основании диагностических данных о производительности, модульных тестах и проверках утверждений о торговых правилах регрессионный контроль был успешно пройден. |
Файл статуса должен выглядеть как контрольный список, а не как дамп журнала. В примере запуска важные поля сгруппированы по назначению.
- Окончательное решение: overall_status=PASS.
- Решения по отдельным источникам данных: performance_status=PASS; unit_test_status=PASS; trade_assert_status=PASS.
- Счетчики профилировщика: profile_failures=0; profile_warnings=0; profile_skipped=8; delta_rows=8.
- Итоговые счетчики: effective_failures=0; effective_warnings=0.
Отчет об изменениях также содержит пропущенные строки. В них показаны профилируемые участки, которые присутствовали в профиле, но не подлежали сравнению по заданным правилам учета числа вызовов и уровня шумов.
Примеры строк отчета об изменениях профилировщика:
| Профилируемый участок | Среднее значение эталона | Текущее среднее значение | Максимальное значение эталона | Текущее максимальное значение | Статус барьера регрессионного контроля |
|---|---|---|---|---|---|
| OnTick.total_sampled | 2,78 микросекунды | 0,74 микросекунды | 2217 микросекунд | 24 микросекунды | PASS |
| OnTick.copy_buffers | 0,26 микросекунды | 0,26 микросекунды | 4 микросекунды | 5 микросекунд | SKIP |
| OnTick.signal_calculation | 1,98 микросекунды | 0,05 микросекунды | 2215 микросекунд | 1 микросекунда | SKIP |
| OnTick.decision_block | 0,05 микросекунды | 0,08 микросекунды | 1 микросекунда | 23 микросекунды | SKIP |
В этом примере показаны два ожидаемых результата: основной участок обработки тиков, измеряемый выборочно, поддается сравнению и проходит проверку, тогда как менее длительные участки пропускаются, поскольку их среднее время выполнения ниже заданного уровня шумов. Барьер регрессионного контроля должен отклонять результаты при существенных отклонениях производительности, но не должен считать доли микросекунды надежным показателем ее изменения.
Не трактуйте итоговый статус расширительно.
- Статус PASS означает, что выбранные диагностические данные не нарушили выбранные пороговые ограничения.
- Статус WARN означает, что диагностические данные изменились достаточно, чтобы потребовать проверки, но недостаточно для автоматического отклонения.
- Статус FAIL означает, что имеется хотя бы одно блокирующее условие: строка профилировщика не прошла проверку порогового значения, модульные тесты из Части II завершились с ошибкой или отсутствовали, хотя были обязательны, либо проверки утверждений о торговых правилах из Части III завершились с ошибкой.
Результат "PASS" не означает, что стратегия является прибыльной. Это означает лишь то, что данный барьер регрессионного контроля не отклонил проверенные им диагностические данные о реализации. Статус SKIP также требует осторожной интерпретации: сам по себе он не является ни хорошим, ни плохим, а лишь указывает, что для объективного сравнения в строке было слишком мало вызовов или слишком малое среднее время выполнения.
Если строка отчета профилировщика не прошла проверку, прежде чем менять код, прочитайте поле reason: каждая причина указывает на свой способ исправления.
- Нарушение порога среднего времени: avg_us_failed означает, что выполнение типичной работы стало занимать больше времени.
- Нарушение порога максимального времени: max_us_failed означает, что время самого длительного из наблюдавшихся вызовов увеличилось.
- Новый участок: new_section_no_baseline означает, что появился новый профилируемый участок, для которого еще нет принятого эталона.
- Отсутствующий участок: baseline_section_missing_in_current означает, что профилируемый участок исчез.
Основной источник ложной уверенности — изменение условий, а не код сравнения. Результат PASS без замечаний полезен лишь тогда, когда профиль получен при рыночных условиях, условиях тестирования и входных параметрах того же типа, что и эталон. Прежде чем менять код или назначать новый профиль эталоном, проверьте следующие моменты.
- Условия: Прежде чем считать результат значимым, проверьте символ, период, диапазон дат, модель, входные параметры и нагрузку на терминал.
- Корректность: Прежде чем исследовать изменения показателей профилировщика, устраните ошибки, выявленные модульными тестами или проверками утверждений о торговых правилах; быстрая, но некорректная логика остается некорректной.
- Поле reason: прежде чем выбирать способ исправления, проверьте указанную причину: avg_us_failed, max_us_failed, new_section_no_baseline или baseline_section_missing_in_current.
- Охват профилирования: профилируйте достаточное количество именованных участков. Если единственный профилируемый участок — OnTick.total_sampled, барьер может показать, что обработка тика замедлилась, но не объяснить причину.
- Допущения о свойствах символа: перед ослаблением правила ознакомьтесь с файлом SymbolAssumptions.txt. Неуспешная проверка утверждения может выявить символ, поддержка которого изначально не была предусмотрена стратегией.
- Храните примечания к эталону отдельно от CSV-файла. Указывайте символ, таймфрейм, режим моделирования, диапазон дат, значимые входные параметры, интервал выборки, номер сборки терминала, если он имеет значение, и порог длительности медленного вызова.
- Изменяйте пороговые значения постепенно. Повышайте лимит только тогда, когда команда понимает, почему прежнее значение больше не подходит. Используйте правила для отдельных профилируемых участков вместо того, чтобы ослаблять весь барьер.
- Осознанно назначайте профили эталонами. Профиль, назначенный эталоном после тестирования рефакторинга логики сигналов, не должен незаметно становиться эталоном для последующего многосимвольного сканера, если это не было запланировано.
Не перегружайте барьер регрессионного контроля вопросами прибыльности. Он контролирует соблюдение допущений, заложенных в реализацию, и отдельных характеристик производительности; прибыльность, устойчивость, чувствительность к параметрам и качество исполнения требуют отдельных этапов тестирования. Наиболее эффективен простой и последовательный подход: храните один принятый эталон для каждого сценария, сохраняйте имена профилируемых участков неизменными, выполняйте одни и те же проверки после существенных изменений и объясняйте причины появления строк со статусом WARN или FAIL, прежде чем назначить новый профиль эталоном.
Итоговый обзор файлов
В приложении содержатся только файлы, используемые в рабочем процессе Части III. Файлы, впервые представленные в Части II, сохранены в пакете только в тех случаях, когда в рабочем процессе Части III они непосредственно запускаются, подключаются или используются созданные ими отчеты.
| Файл | Краткое назначение в Части III |
|---|---|
| MQL5\Include\Debugging_Profiling_Part3\PerfMeter.mqh | вспомогательный модуль профилировщика, используемый советником ProfilerExampleEA.mq5 для создания CSV-файла, данные которого сравнивает барьер регрессионного контроля. |
| MQL5\Include\Debugging_Profiling_Part3\TestLite.mqh | вспомогательный модуль для проверок и формирования отчетов, используемый в UnitTestRunner.mq5 и TradeAssertions.mqh. |
| MQL5\Include\Debugging_Profiling_Part3\TradeMathCore.mqh | общие торговые расчеты, используемые в модульных тестах, советнике и проверках с учетом свойств символа. |
| MQL5\Include\Debugging_Profiling_Part3\RegressionGate.mqh | сравнивает эталонный и текущий CSV-файлы профилировщика и записывает данные о статусе барьера регрессионного контроля. |
| MQL5\Include\Debugging_Profiling_Part3\TradeAssertions.mqh | добавляет проверки об объеме символа, расстоянии до стопов и закрытом баре для модуля запуска барьера регрессионного контроля. |
| MQL5\Experts\Debugging_Profiling_Part3\ProfilerExampleEA.mq5 | советник для тестера стратегий, который создает файл ProfilerExampleEA_Profile.csv для сравнения текущего профиля с эталоном. |
| MQL5\Scripts\Debugging_Profiling_Part3\UnitTestRunner.mq5 | создает файл TradeMathCore_TestReport.txt, который барьер регрессионного контроля считывает перед принятием результатов прогона. |
| MQL5\Scripts\Debugging_Profiling_Part3\PromoteProfilerBaseline.mq5 | копирует принятый CSV-файл профилировщика в файл ProfilerExampleEA_Baseline.csv. |
| MQL5\Scripts\Debugging_Profiling_Part3\DiagnosticGateRunner.mq5 | запускает итоговую проверку барьера регрессионного контроля, объединяя изменения показателей профилировщика, статус модульных тестов и проверки утверждений о торговых правилах. |
Заключение
В Части III на основе отчетов, представленных в Части II, принимается решение. PerfMeter.mqh измеряет время выполнения именованных участков кода, TestLite проверяет торговые расчеты, реализованные чистыми функциями, а TradeMathCore содержит расчеты для многократного использования. Новый барьер регрессионного контроля считывает эти результаты, добавляет проверки с учетом свойств символа и формирует итоговый статус, который можно оценить без догадок. Пакет добавляет уровень принятия решений без создания отдельных ветвей разработки прежнего профилировщика или вспомогательного инструмента тестирования.
Практическая ценность заключается в барьере регрессионного контроля на уровне проекта: профиль-эталон, текущий профиль, статус модульных тестов, проверки утверждений о свойствах символа и итоговое машиночитаемое решение. Используйте этот барьер дисциплинированно: не назначайте профили эталонами без должного обоснования, не считайте сравнение результатов из разных тестовых сред точным и не допускайте, чтобы строки со статусом WARN стали привычным фоновым шумом. Большинство корректных диагностических прогонов должны проходить без неожиданностей; их ценность в том, что на их фоне прогон с неожиданными результатами труднее проигнорировать.
Следующим логичным шагом является автоматизация. Скрипт может считывать файл DiagnosticGate_Status.txt и блокировать подготовку пакета или публикацию при overall_status=FAIL, не требуя изменений в профилировщике или вспомогательном инструменте тестирования. Такова последовательность действий в этой серии: наблюдать за событиями, измерять затраты, проверять правила и отклонять недопустимые изменения до того, как они приведут к значительным затратам. Та же схема применима и к более крупным проектам советников, особенно когда несколько разработчиков используют общие диагностические данные эталона при последовательном выпуске новых версий.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17956
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.