Создание HTML-дашборда для Тестера стратегий и анализа проп-фирменных испытаний в MQL5
Введение
Успешное прохождение Тестера стратегий само по себе не дает ответа на вопрос, сможет ли советник пройти испытание в стиле проп-фирмы. Проп фирменные испытания представляет собой набор правил — целевая прибыль, лимит дневных убытков, общий лимит просадки, минимальное количество торговых дней, а иногда и строгий временной интервал — и единая дата начала тестирования может ввести в заблуждение.
В данной статье представлен практический уровень оценки, пригодный для повторного использования: небольшой модуль на языке MQL5, который отслеживает баланс и капитал во время тестирования, нормализует их до настраиваемого «виртуального» счета для проведения испытаний и имитирует либо одиночную попытку, либо скользящую серию попыток (ежедневные/еженедельные/ежемесячные запуски).
Модуль оценки сообщает, соответствовала ли каждая смоделированная попытка заданным правилам, почему она не удалась или осталась незавершенной, и выдает два мгновенных результата: краткое итоговое резюме и доступный для совместного использования HTML-дашборд. Данный модуль целенаправленно создан для тестирования и не является сертифицированной копией свода юридических правил какой-либо фирмы.
Определение цели анализа
Цель состоит в создании многоразового модуля оценки, который можно будет добавить в советник с минимальными затратами на интеграцию. В ходе тестирования он имитирует одну или несколько попыток проведения испытания и по завершении формирует удобочитаемый отчет. В данной статье под «испытанием» следует понимать набор правил, а под «попыткой» — один смоделированный запуск в соответствии с этими правилами.
Я намеренно использую слово «имитировать». Этот модуль не применяет брокерские правила и не пытается воспроизвести какую-то конкретную компанию. Проп-фирмы могут использовать различные определения для дневных лимитов убытков, общих лимитов просадки, временных ограничений и подтверждения достижения целевой прибыли. Цель состоит в создании практичного каркаса модуля оценки, а не в законодательной или контрактной точной копии какого-либо коммерческого испытания.
Модуль работает с виртуальным балансом для выполнения испытаний. Реальный тестовый счет может начинать работу с 10 000 или 50 000, в то время как испытание оценивается так, как если бы это был счет со 100 000.
Преобразование тестового счета в виртуальный счет для проведения испытаний.
Вот как реальные значения по счетам преобразуются в виртуальные значения для проведения испытаний.
double vBal = m_initialBalance * (rBal / m_challenges[i].startRealBalance); double vEq = m_initialBalance * (rEq / m_challenges[i].startRealBalance);
Для преобразования используется пропорциональная нормализация. В качестве общей базы для конвертации как виртуального баланса, так и виртуального капитала используется реальный баланс на начало проведения испытаний. Если баланс на реальном счете на 2 процента превышает начальный баланс, то виртуальный баланс тоже на 2 процента превышает заданный для испытания баланс. Если открытые позиции приводят к изменению капитала, то и виртуальный капитал перемещается пропорционально.
После определения цели следующим шагом является представление модели испытания посредством настраиваемых входных данных.
Установление правил проведения испытаний
Входные данные группируются по модели счета, лимитам риска, режиму оценки и параметрам вывода. Использование этих параметров в качестве входных данных советника позволяет легко тестировать одну и ту же стратегию при различных условиях испытаний.
input group "═══ Prop Firm Evaluation ═══" input bool InpPropEnable = true; input double InpPropInitialBalance = 100000.0; input double InpPropProfitTargetPct = 8.0; input double InpPropMaxDailyLossPct = 5.0; input double InpPropMaxOverallLossPct = 10.0; input int InpPropDurationDays = 30; input int InpPropMinTradingDays = 5; input ENUM_PROP_DD_MODE InpPropDDMode = PROP_DD_EQUITY; input ENUM_PROP_DAILY_LOSS_MODE InpPropDailyLossMode = DAILY_LOSS_EQUITY; input ENUM_PROP_EVAL_MODE InpPropEvalMode = EVAL_ROLLING; input ENUM_PROP_ROLLING_FREQ InpPropRollingFreq = FREQ_MONTHLY; input bool InpPropWriteHTML = true;
Приведенные выше значения являются лишь примером настройки параметров. Пользователь может изменить начальный баланс для проведения испытания, а также целевой показатель прибыли, дневной лимит убытков, общий лимит просадки и минимальное количество торговых дней. Особенно важны две настройки, отвечающие на вопросы, проверяется ли общий лимит просадки на основе собственного капитала или остатка на счете и основывается ли дневной ориентир убытка на начальном капитале или на балансе на текущий день.
Модуль оценки поддерживает режим однократного испытания или скользящей серии испытаний. В режиме однократного испытания запускается одна попытка, а следующая начинается только после завершения текущей. В режиме скользящей серии испытаний новая попытка запускается каждый день, неделю или месяц, что помогает оценить чувствительность к дате начала испытаний.
Перед подтверждением прохождения проверки проверяются условия, нарушающие правила. Нарушения просадки оцениваются до тех пор, пока не будет достигнута целевая прибыль. С этого момента попытка рассматривается как попытка, при которой целевая прибыль была достигнута, что отражает практическое предположение о завершении задачи после достижения целевого показателя.
Если более строгие правила требуют продолжения проверок просадки до формального подтверждения прохождения проверки, это условие можно соответствующим образом скорректировать.

Рисунок 1. Внешние входные данные модуля PropEvaluator в Тестере стратегий
После получения входных данных для каждой смоделированной попытки необходимо компактное внутреннее состояние.
Подготовка структур данных MQL5
Для каждой попытки прохождения испытания требуется собственное состояние: время начала, время окончания, статус, причина неудачи, количество торговых дней, ежедневный показатель потерь и окончательный результат. Я храню эту информацию в одной структуре и все попытки — в одном и том же массиве.
struct SChallenge { int id; // Challenge attempt identifier datetime startTime; // Start time of the attempt datetime endTime; // End time of the attempt double startRealBalance; // Real balance at challenge start ENUM_CHALLENGE_STATUS status; // Current status of the attempt ENUM_FAILURE_REASON failReason; // Reason for failure if failed ENUM_INCOMPLETE_REASON incompleteReason; // Reason for incomplete if timed out int tradingDays; // Number of trading days counted int lastTradingDay; // Last counted trading day (YYYYMMDD) double dailyStartValue; // Daily starting value for loss calc int lastProcessedDay; // Last processed calendar day (YYYYMMDD) double maxEquity; // Maximum virtual equity reached double minEquity; // Minimum virtual equity reached double finalBalance; // Final virtual balance at end double finalEquity; // Final virtual equity at end bool isTargetReached; // Whether profit target was hit };
Поле lastTradingDay предотвращает двойной учет одного и того же дня. Поле lastProcessedDay обнаруживает новый день и обновляет ежедневный показатель потерь. В dailyStartValue хранится виртуальный баланс или капитал, на основе которого рассчитывается дневной лимит убытков.
Статус, причина неудачи и причина незавершенности представлены в виде перечислений. В модуле используются отдельные перечисления для неудачных и незавершенных результатов: неудачное испытание фиксирует нарушение лимита просадки, а незавершенное испытание — истечение срока испытания или досрочное завершение теста.
enum ENUM_CHALLENGE_STATUS // Challenge attempt status { STATUS_ACTIVE, // Active STATUS_PASSED, // Passed STATUS_FAILED, // Failed STATUS_INCOMPLETE // Incomplete }; enum ENUM_FAILURE_REASON // Reason for challenge failure { REASON_NONE, // No failure REASON_DAILY_DRAWDOWN, // Daily drawdown breach REASON_OVERALL_DRAWDOWN // Overall drawdown breach }; enum ENUM_INCOMPLETE_REASON // Reason for incomplete status { INCOMPLETE_NONE, // No incomplete reason INCOMPLETE_TIMEOUT, // Challenge duration expired INCOMPLETE_END_OF_TEST // Backtest ended before completion };
Структура и перечисления инкапсулированы внутри CPropEvaluator. Советнику нужно лишь инициализировать модуль оценки, вызывать его на каждом тике и завершать его работу в конце теста.
После подготовки структур данных модуль оценки должен смоделировать логику для каждой попытки.
Начало нового испытания
В начале новой попытки прохождения испытания модуль сохраняет реальный баланс на соответствующий момент времени. Это значение становится отправной точкой, используемой для нормализации будущего баланса и капитала на виртуальном счете для проведения испытаний. Вот основная логика инициализации.
//+------------------------------------------------------------------+ //| Starts a new simulated challenge attempt and normalizes balances | //+------------------------------------------------------------------+ void CPropEvaluator::StartNewChallenge(datetime now, double currentBal) { int size = ::ArraySize(m_challenges); ::ArrayResize(m_challenges, size + 1, 100); m_totalSimulated++; MqlDateTime dt; ::TimeToStruct(now, dt); int currentDay = dt.year * 10000 + dt.mon * 100 + dt.day; m_challenges[size].id = m_totalSimulated; m_challenges[size].startTime = now; m_challenges[size].endTime = 0; m_challenges[size].startRealBalance = currentBal > 0 ? currentBal : 1.0; m_challenges[size].status = STATUS_ACTIVE; m_challenges[size].failReason = REASON_NONE; m_challenges[size].incompleteReason = INCOMPLETE_NONE; m_challenges[size].tradingDays = 0; m_challenges[size].lastTradingDay = 0; m_challenges[size].dailyStartValue = m_initialBalance; m_challenges[size].lastProcessedDay = currentDay; m_challenges[size].maxEquity = m_initialBalance; m_challenges[size].minEquity = m_initialBalance; m_challenges[size].finalBalance = m_initialBalance; m_challenges[size].finalEquity = m_initialBalance; m_challenges[size].isTargetReached = false; }
Переход к значению 1.0 — это лишь мера предосторожности против деления на ноль в нештатных ситуациях. В первый день значение dailyStartValue инициализируется из виртуального начального баланса и впоследствии обновляется при обнаружении нового дня.
Режимы однократного испытания и скользящей серии испытаний
В режиме однократных испытаний дается ответ на один прямой вопрос: увенчалась данная конкретная попытка успехом, провалилась или осталась незавершенной? Режим непрерывных испытаний более полезен для исследований, поскольку он создает распределение результатов по различным начальным датам. Ниже показана логика выбора режима из кода обработки события OnTick модуля оценки.
if(m_evalMode == EVAL_SINGLE) { if(!m_singleStarted) { StartNewChallenge(now, rBal); m_singleStarted = true; } } else if(m_evalMode == EVAL_ROLLING) { bool start = false; if(m_rollingFreq == FREQ_DAILY) { if(currentDay != m_lastStartDayKey) { start = true; m_lastStartDayKey = currentDay; } } else if(m_rollingFreq == FREQ_WEEKLY) { int currentWeekKey = (int)((now + 259200) / 604800); if(currentWeekKey != m_lastStartWeekKey) { start = true; m_lastStartWeekKey = currentWeekKey; } } else if(m_rollingFreq == FREQ_MONTHLY) { int currentMonthKey = dt.year * 100 + dt.mon; if(currentMonthKey != m_lastStartMonthKey) { start = true; m_lastStartMonthKey = currentMonthKey; } } if(start) { StartNewChallenge(now, rBal); } }
Ежедневный ключ (daily key) включает в себя год, месяц и день. Ежемесячный ключ (monthly key) включает в себя год и месяц. Еженедельный ключ (weekly key) основан на количестве прошедших недель и не сбрасывается по истечении года. Это позволяет избежать путаницы между новым периодом и предыдущим, имеющим тот же номер дня или месяца.
Например, ежемесячное скользящее тестирование в течение пятнадцати лет может имитировать около 180 попыток. Это не гарантирует дальнейшего поведения, но дает больше информации, чем одна изолированная дата начала.
Определение торговых дней
Для многих испытаний требуется минимальное количество торговых дней. В данной реализации торговым днем считается день, когда открыта хотя бы одна позиция или когда реальный баланс изменился с момента появления предыдущего тика.
bool isTradingToday = (::PositionsTotal() > 0) || (rBal != m_lastRealBalance); m_lastRealBalance = rBal; if(isTradingToday && m_challenges[i].lastTradingDay != currentDay) { m_challenges[i].lastTradingDay = currentDay; m_challenges[i].tradingDays++; }
Это упрощенный индикатор активности на уровне счета, а не формальное определение, соответствующее своду правил, поэтому его следует рассматривать как приблизительное значение. Функция PositionsTotal() отслеживает весь счет, а не конкретный символ, стратегию или «магическое число». Для подсчета сделок с учетом конкретной стратегии эту часть следует заменить детектором на основе истории, который фильтрует сделки по символу и магическому числу.
Приблизительные значения могут существенно отличаться от правил конкретной проп-фирмы. Стратегия, при которой позиции остаются открытыми на ночь, может привести к завышению в подсчете торговых дней, поскольку открытая позиция на счете сохраняется и на начало следующего календарного дня.
Обработка начального значения за день
Дневной лимит убытков устанавливается отдельно от общего лимита просадки. Обычно в начале каждого торгового дня для этого параметра заново задается базовое значение, поэтому модуль оценки сохраняет это значение в переменной dailyStartValue.
if(currentDay != m_challenges[i].lastProcessedDay) { double vBalToday = m_initialBalance * (rBal / m_challenges[i].startRealBalance); double vEqToday = m_initialBalance * (rEq / m_challenges[i].startRealBalance); m_challenges[i].dailyStartValue = (m_dailyLossMode == DAILY_LOSS_EQUITY) ? vEqToday : vBalToday; m_challenges[i].lastProcessedDay = currentDay; }
Параметр m_dailyLossMode определяет, на чем будет основан ежедневный ориентир: на виртуальном капитале или виртуальном балансе.
Расчет целевых значений и пределов просадки
После определения виртуального баланса и виртуального капитала модуль оценки рассчитывает целевой показатель прибыли, общий лимит просадки и лимит дневного убытка.
double targetVal = m_initialBalance * (1.0 + m_profitTargetPct / 100.0); double overallLimit = m_initialBalance * (1.0 - m_maxOverallLossPct / 100.0); double dailyLimit = m_challenges[i].dailyStartValue - m_initialBalance * (m_maxDailyLossPct / 100.0); double currentValForDD = (m_ddMode == PROP_DD_EQUITY) ? vEq : vBal;
if(!m_challenges[i].isTargetReached) { if(currentValForDD < overallLimit) { m_challenges[i].status = STATUS_FAILED; m_challenges[i].failReason = REASON_OVERALL_DRAWDOWN; m_challenges[i].endTime = now; m_challenges[i].finalBalance = vBal; m_challenges[i].finalEquity = vEq; m_failedCount++; m_overallBreachCount++; continue; } if(currentValForDD < dailyLimit) { m_challenges[i].status = STATUS_FAILED; m_challenges[i].failReason = REASON_DAILY_DRAWDOWN; m_challenges[i].endTime = now; m_challenges[i].finalBalance = vBal; m_challenges[i].finalEquity = vEq; m_failedCount++; m_dailyBreachCount++; continue; } }
В сравнении используется символ «<», поэтому попытка не удается, если выбранное значение оказывается ниже заданного предела. Если сбой должен произойти точно в момент достижения предела, сравнение можно изменить на «<=». Если общий лимит просадки следует проверять только при закрытом балансе, параметр m_ddMode можно установить в режим баланса.
Успех, неудача и незавершенное испытание
Попытка пройти испытание может быть успешной, неудачной из-за нарушения порога просадки или оставшейся незавершенной, если необходимые условия не будут выполнены до истечения времени прохождения испытания или до окончания теста. Вот основная логика оценки и незавершения в связи с истечением времени.
if(vEq >= targetVal && !m_challenges[i].isTargetReached) { m_challenges[i].isTargetReached = true; } if(m_challenges[i].isTargetReached && m_challenges[i].tradingDays >= m_minTradingDays) { m_challenges[i].status = STATUS_PASSED; m_challenges[i].endTime = now; m_challenges[i].finalBalance = vBal; m_challenges[i].finalEquity = vEq; m_passedCount++; continue; }
В данной реализации определение целевого значения основано на капитале, поскольку условие использует vEq. В модели, основанной на балансе, вместо этого можно использовать vBal.
Проверка истечения времени выполняется только в том случае, если m_durationDays больше нуля. Если параметр m_durationDays равен нулю, то у испытания нет ограничения по времени.
if(m_durationDays > 0) { if(now - m_challenges[i].startTime >= m_durationDays * 86400) { if(m_challenges[i].isTargetReached && m_challenges[i].tradingDays >= m_minTradingDays) { m_challenges[i].status = STATUS_PASSED; m_challenges[i].endTime = now; m_challenges[i].finalBalance = vBal; m_challenges[i].finalEquity = vEq; m_passedCount++; } else { m_challenges[i].status = STATUS_INCOMPLETE; m_challenges[i].incompleteReason = INCOMPLETE_TIMEOUT; m_challenges[i].endTime = now; m_challenges[i].finalBalance = vBal; m_challenges[i].finalEquity = vEq; m_incompleteCount++; } } }
Если попытка завершается с ошибкой, не выполнив все условия, ее статус устанавливается в STATUS_INCOMPLETE, а значение incompleteReason записывается как INCOMPLETE_TIMEOUT. Это позволяет отделить ошибки, связанные с превышением времени ожидания, от активных сбоев, что делает отчет более информативным.
Завершение активных испытаний
После завершения тестирования на исторических данных некоторые попытки могут оставаться активными. Функция OnDeinit() завершает их работу, используя последнее доступное состояние счета в Тестере стратегий. Попытки, которые уже достигли целевого показателя при минимальном количестве торговых дней, считаются успешными; остальные считаются невыполненными (STATUS_INCOMPLETE) с указанием причины с помощью значения INCOMPLETE_END_OF_TEST.
Следующий фрагмент демонстрирует логику завершения процесса. Полная реализация, представленная в приложенном файле, включает в себя все вызовы функций PrintReport() и WriteHTML().
//+------------------------------------------------------------------+ //| Finalizes incomplete challenges and outputs evaluation reports | //+------------------------------------------------------------------+ void CPropEvaluator::OnDeinit(void) { if(!m_enable) return; datetime now = ::TimeCurrent(); double rBal = ::AccountInfoDouble(ACCOUNT_BALANCE); double rEq = ::AccountInfoDouble(ACCOUNT_EQUITY); //--- Finalize any still active challenges int totalChallenges = ::ArraySize(m_challenges); for(int i = 0; i < totalChallenges; i++) { if(m_challenges[i].status == STATUS_ACTIVE) { double vBal = m_initialBalance * (rBal / m_challenges[i].startRealBalance); double vEq = m_initialBalance * (rEq / m_challenges[i].startRealBalance); //--- Check if it actually met the conditions at the very end if(m_challenges[i].isTargetReached && m_challenges[i].tradingDays >= m_minTradingDays) { m_challenges[i].status = STATUS_PASSED; m_challenges[i].endTime = now; m_challenges[i].finalBalance = vBal; m_challenges[i].finalEquity = vEq; m_passedCount++; } else { m_challenges[i].status = STATUS_INCOMPLETE; m_challenges[i].incompleteReason = INCOMPLETE_END_OF_TEST; m_challenges[i].endTime = now; m_challenges[i].finalBalance = vBal; m_challenges[i].finalEquity = vEq; m_incompleteCount++; } } } //--- Print Report PrintReport(); //--- Write HTML Report if(m_writeHTML) { WriteHTML(); } }
Это предотвращает исчезновение результатов поздних попыток скользящей серии испытаний только потому, что период тестирования закончился до того, как у них появилось достаточно времени для получения окончательного результата.
Включение модуля оценки в состав советника
Модуль написан в виде файла .mqh, поэтому одну и ту же логику можно использовать в разных советниках.
#include "PropEvaluator.mqh"
Я создаю один глобальный экземпляр модуля оценки на уровне советника, поскольку модуль отслеживает во время тестирования весь счет.
CPropEvaluator g_propEvaluator;
Настройка и инициализация модуля оценки происходит в методе OnInit() после стандартной настройки советника. В соответствии с лучшими практиками объектно-ориентированного программирования (ООП), модуль является полностью самодостаточным и не считывает напрямую глобальные входные переменные советника. Вместо этого советник передает входные параметры экземпляру модуля оценки, используя открытые установщики, перед вызовом метода Init(). Приведенные ниже фрагменты кода демонстрируют только интеграционные вызовы; полный код советника будет включать в себя собственную инициализацию, логику стратегии и очистку ресурсов.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- normal EA initialization here g_propEvaluator.SetEnable(InpPropEnable); g_propEvaluator.SetInitialBalance(InpPropInitialBalance); g_propEvaluator.SetProfitTargetPct(InpPropProfitTargetPct); g_propEvaluator.SetMaxDailyLossPct(InpPropMaxDailyLossPct); g_propEvaluator.SetMaxOverallLossPct(InpPropMaxOverallLossPct); g_propEvaluator.SetDurationDays(InpPropDurationDays); g_propEvaluator.SetMinTradingDays(InpPropMinTradingDays); g_propEvaluator.SetDDMode(InpPropDDMode); g_propEvaluator.SetDailyLossMode(InpPropDailyLossMode); g_propEvaluator.SetEvalMode(InpPropEvalMode); g_propEvaluator.SetRollingFreq(InpPropRollingFreq); g_propEvaluator.SetWriteHTML(InpPropWriteHTML); g_propEvaluator.Init(); ::Print("EA initialized successfully."); return INIT_SUCCEEDED; }
В процессе тестирования на исторических данных эту функцию необходимо вызывать на каждом тике. В советнике для управления портфелем я сначала позволяю стратегиям обработать тик, а затем вызываю модуль оценки, чтобы он считывал обновленное состояние счета.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { for(int i = 0; i < 4; i++) { strategies[i].ProcessTick(); } g_propEvaluator.OnTick(); }
В конце теста функция OnDeinit() завершает активные попытки, выводит отчет в терминал и отображает HTML-дашборд, если эта функция включена.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { for(int i = 0; i < 4; i++) strategies[i].Deinit(); ::EventKillTimer(); CleanDashboard(); g_propEvaluator.OnDeinit(); }
Модуль оценки считывает баланс и капитал с помощью функции AccountInfoDouble(), поэтому ему не нужно знать, почему были открыты сделки или как осуществлялось управление выходами.
double rBal = ::AccountInfoDouble(ACCOUNT_BALANCE); double rEq = ::AccountInfoDouble(ACCOUNT_EQUITY);
Точность этой модели по-прежнему зависит от качества тестера, моделирования тиков, комиссий, свопов, спредов и обновлений состояния счета.
В итоге, минимальная интеграция включает пять шагов:
- Включить файл PropEvaluator.mqh в исходный файл советника.
- Создать глобальный экземпляр CPropEvaluator.
- Вызвать метод Init() внутри функции OnInit() советника.
- Вызвать метод OnTick() внутри функции OnTick() советника в соответствии с логикой стратегии.
- Вызвать метод OnDeinit() внутри функции OnDeinit() советника.
Печать отчета терминала
Перед созданием HTML-дашборда модуль оценки выводит краткое текстовое описание в журнал советников (Experts). Это полезно на этапе разработки, поскольку основной результат можно проверить, не открывая отчет в браузере.
Приведённый ниже фрагмент кода содержит основные данные о подсчетах и показателях. Полный модуль, кроме того, выводит диагностическую статистику, например среднее количество торговых дней, время до срабатывания, максимальный и минимальный виртуальный капитал, средний конечный капитал и количество причин сбоя.
SDiagnostics d = ComputeDiagnostics(); ::Print("Total Challenges Simulated: ", ::IntegerToString(m_totalSimulated)); ::Print("Passed: ", ::IntegerToString(m_passedCount)); ::Print("Failed: ", ::IntegerToString(m_failedCount)); ::Print("Incomplete: ", ::IntegerToString(m_incompleteCount)); ::Print("Pass Rate: ", ::DoubleToString(d.passRate, 2), "%"); ::Print("Failure Rate: ", ::DoubleToString(d.failRate, 2), "%"); ::Print("Incomplete Rate: ", ::DoubleToString(d.incRate, 2), "%");
Ниже представлен типичный отчет терминала, который отображается в журнале советников после завершения скользящей серии оценок.

Рисунок 2. Отчет терминала в журнале советников
Генерация HTML-отчетов.
Отчет в терминале полезен для диагностики, но HTML-дашборд удобнее для чтения и обмена информацией. В конце теста модуль открывает локальный файл и записывает полную HTML-страницу с сводными карточками, диагностической статистикой и таблицей истории испытаний.
string filename = "PropFirm_Dashboard_" + ::MQLInfoString(MQL_PROGRAM_NAME) + ".html"; int fileHandle = ::FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_ANSI); if(fileHandle == INVALID_HANDLE) { ::Print("PropFirm: Failed to open file for writing HTML: ", filename); return; }
Функции для MQL5-файлов работают в изолированной среде. Файл создается в каталоге «Файлы» терминала (или в каталоге файлов тестера в Тестере стратегий). Напечатанный путь — это всего лишь вспомогательное сообщение. При таком имени файла каждый новый запуск того же советника перезаписывает предыдущий отчет. Если требуются уникальные отчеты, добавьте к имени файла дату или параметры тестирования.
Страница собирается построчно с помощью функции FileWrite(). Вот как формируются сводные карточки. Для наглядности здесь показан только блок с карточками; полная реализация в приложенном файле содержит полный CSS-код, заголовок, диагностическую сетку, раздел с графиками и таблицу истории проведения испытаний.
::FileWrite(fileHandle, "<div class='grid'>"); ::FileWrite(fileHandle, " <div class='card'><h3>Simulated</h3><div class='value'>" + ::IntegerToString(m_totalSimulated) + "</div><div class='subtext'>Total Challenges</div></div>"); ::FileWrite(fileHandle, " <div class='card'><h3>Passed</h3><div class='value text-green'>" + ::IntegerToString(m_passedCount) + "</div><div class='subtext'>" + ::DoubleToString(d.passRate, 2) + "% Pass Rate</div></div>"); ::FileWrite(fileHandle, " <div class='card'><h3>Failed</h3><div class='value text-red'>" + ::IntegerToString(m_failedCount) + "</div><div class='subtext'>" + ::DoubleToString(d.failRate, 2) + "% Failure Rate</div></div>"); ::FileWrite(fileHandle, " <div class='card'><h3>Incomplete</h3><div class='value text-blue'>" + ::IntegerToString(m_incompleteCount) + "</div><div class='subtext'>" + ::DoubleToString(d.incRate, 2) + "% Incomplete</div></div>"); ::FileWrite(fileHandle, "</div>");
Полная реализация может включать в себя более крупный блок CSS-кода для облегченного стиля дашборда, диагностическую сетку со статистикой средних торговых дней и времени выполнения, а также подробную таблицу истории проведения испытаний, где каждая строка отображает идентификатор попытки, даты, статус, причину неудачи или незавершенности, количество торговых дней и экстремальные значения капитала.
Ниже показан полученный дашборд.

Рисунок 3. HTML-дашборд со сводными карточками и ключевыми статистическими данными
Дополнительные визуальные графики
Кроме того, рабочая версия включает в себя два графика на стороне клиента: один для распределения успешных (Passed), неудачных (Failed) и незавершенных (Incomplete) попыток и один для распределения скорости прохождения испытаний по календарным дням.
Если дашборд загружает Chart.js из сети CDN, HTML-файл не является полностью самодостаточным. Отчет по-прежнему генерируется локально с помощью MQL5, но библиотека графиков является внешней. Для создания отчета полностью в автономном режиме устраните зависимость от сети CDN и используйте только карточки, таблицы или визуализации на основе CSS.
//--- Scripts for Charting : this part is optional. Remove it if the report must remain fully offline. ::FileWrite(fileHandle, "<script src='https://cdn.jsdelivr.net/npm/chart.js'></script>"); ::FileWrite(fileHandle, "<script>"); ::FileWrite(fileHandle, "const ctx1 = document.getElementById('statusChart').getContext('2d');"); ::FileWrite(fileHandle, "new Chart(ctx1, {"); ::FileWrite(fileHandle, " type: 'doughnut',"); ::FileWrite(fileHandle, " data: {"); ::FileWrite(fileHandle, " labels: ['Passed', 'Failed', 'Incomplete'],"); ::FileWrite(fileHandle, " datasets: [{"); ::FileWrite(fileHandle, " data: [" + ::IntegerToString(m_passedCount) + ", " + ::IntegerToString(m_failedCount) + ", " + ::IntegerToString(m_incompleteCount) + "],"); ::FileWrite(fileHandle, " backgroundColor: ['#10b981', '#ef4444', '#3b82f6'],"); ::FileWrite(fileHandle, " borderColor: '#ffffff',"); ::FileWrite(fileHandle, " borderWidth: 2"); ::FileWrite(fileHandle, " }]"); ::FileWrite(fileHandle, " }"); ::FileWrite(fileHandle, "});"); ::FileWrite(fileHandle, "</script>");
Раздел графиков показан на следующем изображении.

Рисунок 4. Дополнительные визуальные графики, созданные с помощью Chart.js.
Интерпретация дашборда
Дашборд должен изменить способ считывания результатов теста. Обычный результат проверки в Тестере стратегий может выглядеть положительным, но модуль оценки может показать, что лишь небольшой процент ежемесячных попыток прошел успешно или что большинство неудач были связаны с превышением лимита дневных потерь, а не с превышением общего лимита просадки.
- Если неудачи в основном вызваны превышением общего лимита просадки, возможно, потребуется нужно доработать управление экспозицией или логику выставления стоп-лоссов.
- Если попытки остаются незавершенными из-за количества торговых дней, проблема может заключаться в частоте совершения сделок или продолжительности испытания.
- Если результаты сильно зависят от начального месяца, скользящая серия испытаний выявляет эту зависимость.
Это превращает смутное впечатление в конкретное диагностическое сообщение.
Простой пример
В одном показательном ежемесячном скользящем тесте на основе длительной исторической выборки стандартный тест на исторических данных завершается прибылью с умеренной просадкой и коэффициентом прибыльности выше 1. Модуль оценки снабжен начальным балансом в размере 100 000 для проведения испытаний, целевая сумма составляет 8%, дневной лимит убытков — 5%, общий лимит просадки — 10%, продолжительность участия не ограничена, минимальное количество торговых дней — 5.
В приведенном примере отчета на дашборде отображается 180 попыток: 149 пройденных, 15 неудачных и 17 незавершенных. Этот результат полезен, но его все же следует рассматривать в совокупности с причинами неудач, средним временем прохождения и минимальным достигнутым капиталом во время попыток.
Стратегия может приносить прибыль в долгосрочной перспективе, но дашборд показывает, совместима ли траектория ее доходности с выбранной моделью испытания. Ниже представлена таблица истории проведения испытаний для данного примера запуска.

Рисунок 5. Таблица истории проведения испытаний, показывающая каждую попытку и ее результат
Ограничения
Ни к одному модулю оценки нельзя относиться как к абсолютной истине. Данная реализация представляет собой универсальный каркас модуля оценки, а не гарантированно соответствующий каким-либо конкретным правилам проп-фирмы модулем. Ее точность зависит от качества тестирования стратегии на исторических данных, условий брокера, тиковых данных, комиссий, свопов, спредов и допущений относительно исполнения сделок.
Еще одним важным ограничением является дневной лимит убытков. Модуль обновляет ежедневное начальное значение при обнаружении новой даты из функции TimeCurrent(), однако в реальном своде правил может потребоваться указание конкретного времени окончания работы сервера, часового пояса или точки отсчета для расчета капитала на конец дня.
Надежность оценки внутридневных колебаний капитала очень зависит от качества моделирования в тестере. Скользящая серия испытаний также остается моделированием на исторических данных: система, успешно прошедшая множество испытаний на истории, все еще может потерпеть неудачу в будущем.
Возможные улучшения
Текущая версия намеренно сделана компактной, но несколько расширений сделали бы ее более гибкой:
- Для фильтрации модуль оценки может поддерживать выбор по «магическому числу», символу или группе стратегий.
- Для экспорта можно использовать файлы выходных данных CSV или JSON, что упростит более глубокий анализ в Python, Excel или другом инструменте.
- Для составления отчетов кривая капитала, реализованная средствами чистого CSS, могла бы поддерживать полностью автономный режим дашборда.
- Для автоматизации модуль может отправлять уведомление об отчете или путь к сохраненному отчету в Telegram либо сохранять копию в общей папке файлов для многотерминальных рабочих процессов.
Заключение
Инструмент PropEvaluator дополняет Тестер стратегий, преобразуя общий тест на исторических данных в основанную на правилах оценку пригодности для проведения испытаний. Благодаря нормализации реальных балансов тестера до размера виртуального испытания, отслеживанию состояния при каждой попытке (начало/конец, количество торговых дней, дневной ориентир, максимальный/минимальный капитал) и поддержке режимов проведения однократного испытания и скользящей серии испытаний, модуль позволяет легко ответить на вопрос: «Соответствовала бы эта стратегия данным ограничениям, а если нет, то почему?»
Интеграция минимальна (необходимо добавить файл .mqh, создать экземпляр и вызвать Init → OnTick → OnDeinit), а выходные данные включают в себя формирование отчетов в терминале, а также HTML-дашборд с историей каждой попытки, причинами сбоев (ежедневная и общая просадка) и базовой диагностикой.
Важные допущения и ограничения четко обозначены: модуль оценки представляет собой практическое средство моделирования (а не юридически точную копию), есть возможность настройки выбора эталонных значений капитала/баланса и поведения по умолчанию, заключающегося в приостановке проверок на просадку после достижения целевого уровня, а точность зависит от качества данных для тестирования на исторических данных и моделирования счетов. При использовании в режиме скользящей серии испытаний модуль оценки выявляет чувствительность к начальной дате и предоставляет более информативный диагностический инструмент, чем единая кривая тестирования на исторических данных.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22969
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 2): Разработка программы с интеграцией ChatGPT и пользовательским интерфейсом
Изучение стандартной библиотеки MQL5 (Часть 11): Как создать матричный индикатор структуры рынка в MQL5
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Банк навыков)
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (D2Skill)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования