forked from antekov/Adwizard
		
	
		
			
				
	
	
		
			341 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			MQL5
		
	
	
	
	
	
			
		
		
	
	
			341 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			MQL5
		
	
	
	
	
	
//+------------------------------------------------------------------+
 | 
						|
//|                                                TesterHandler.mqh |
 | 
						|
//|                                 Copyright 2024-2025, Yuriy Bykov |
 | 
						|
//|                            https://www.mql5.com/ru/users/antekov |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
#property copyright "Copyright 2024-2025, Yuriy Bykov"
 | 
						|
#property link      "https://www.mql5.com/ru/users/antekov"
 | 
						|
#property version   "1.07"
 | 
						|
 | 
						|
#include "../Database/Database.mqh"
 | 
						|
#include "VirtualStrategy.mqh"
 | 
						|
//#include "VirtualFactory.mqh"
 | 
						|
#include "../Optimization/OptimizerTask.mqh"
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Класс для обработки событий оптимизации                          |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
class CTesterHandler {
 | 
						|
   static string     s_fileName;                   // Имя базы данных оптимизации
 | 
						|
   static string     s_frameFileName;              // Имя файла для записи данных фрейма
 | 
						|
   static void       ProcessFrame(string values);  // Обработка данных одиночного прохода
 | 
						|
   static void       ProcessFrames();              // Обработка пришедших фреймов
 | 
						|
   static string     GetFrameInputs(ulong pass);   // Получение input-параметров прохода
 | 
						|
 | 
						|
   // Формирование SQL-запроса на вставку результатов прохода
 | 
						|
   static string     GetInsertQuery(string values, string inputs, ulong pass = 0);
 | 
						|
public:
 | 
						|
   static int        TesterInit(ulong p_idTask = 0, string p_fileName = NULL);   // Обработка начала оптимизации в главном терминале
 | 
						|
   static void       TesterDeinit();   // Обработка завершения оптимизации в главном терминале
 | 
						|
   static void       TesterPass();     // Обработка завершения прохода на агенте в главном терминале
 | 
						|
 | 
						|
   static void       Tester(const double OnTesterValue,
 | 
						|
                            const string params);  // Обработка завершения прохода тестера для агента
 | 
						|
 | 
						|
   // Экспорт массива стратегий в заданную базу данных эксперта как новой группы стратегий
 | 
						|
   static void       Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName);
 | 
						|
 | 
						|
   static ulong      s_idTask;   // Идентификатор задачи оптимизации
 | 
						|
   static ulong      s_idPass;   // Идентификатор текущего прохода оптимизации
 | 
						|
};
 | 
						|
 | 
						|
string CTesterHandler::s_fileName = "";   // Имя базы данных оптимизации
 | 
						|
string CTesterHandler::s_frameFileName = "data.bin";    // Имя файла для записи данных фрейма
 | 
						|
ulong CTesterHandler::s_idTask = 0;
 | 
						|
ulong CTesterHandler::s_idPass = 0;
 | 
						|
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Обработка начала оптимизации в главном терминале                 |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
int CTesterHandler::TesterInit(ulong p_idTask, string p_fileName) {
 | 
						|
// Устанавливаем идентификатор задачи
 | 
						|
   s_idTask = p_idTask;
 | 
						|
 | 
						|
   s_fileName = p_fileName;
 | 
						|
 | 
						|
// Открываем существующую базу данных
 | 
						|
   DB::Connect(s_fileName);
 | 
						|
 | 
						|
// Если открыть не удалось, то не запускаем оптимизацию
 | 
						|
   if(!DB::IsOpen()) {
 | 
						|
      return INIT_FAILED;
 | 
						|
   }
 | 
						|
 | 
						|
// Закрываем успешно открытую базу данных
 | 
						|
   DB::Close();
 | 
						|
 | 
						|
   return INIT_SUCCEEDED;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Обработка завершения оптимизации в главном терминале             |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CTesterHandler::TesterDeinit(void) {
 | 
						|
// Обрабатываем последние пришедшие от агентов фреймы данных
 | 
						|
   ProcessFrames();
 | 
						|
 | 
						|
// Закрываем график с советником, запущенным в режиме сбора фреймов
 | 
						|
   ChartClose();
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Обработка завершения прохода на агенте в главном терминале       |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CTesterHandler::TesterPass(void) {
 | 
						|
// Обрабатываем поступившие от агента фреймы данных
 | 
						|
   ProcessFrames();
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Обработка завершения прохода тестера для агента                  |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CTesterHandler::Tester(double custom,   // Пользовательский критерий
 | 
						|
                            string params    // Описание параметров советника в текущем проходе
 | 
						|
                           ) {
 | 
						|
// Массив имён сохраняемых статистических характеристик прохода
 | 
						|
   ENUM_STATISTICS statNames[] = {
 | 
						|
      STAT_INITIAL_DEPOSIT,
 | 
						|
      STAT_WITHDRAWAL,
 | 
						|
      STAT_PROFIT,
 | 
						|
      STAT_GROSS_PROFIT,
 | 
						|
      STAT_GROSS_LOSS,
 | 
						|
      STAT_MAX_PROFITTRADE,
 | 
						|
      STAT_MAX_LOSSTRADE,
 | 
						|
      STAT_CONPROFITMAX,
 | 
						|
      STAT_CONPROFITMAX_TRADES,
 | 
						|
      STAT_MAX_CONWINS,
 | 
						|
      STAT_MAX_CONPROFIT_TRADES,
 | 
						|
      STAT_CONLOSSMAX,
 | 
						|
      STAT_CONLOSSMAX_TRADES,
 | 
						|
      STAT_MAX_CONLOSSES,
 | 
						|
      STAT_MAX_CONLOSS_TRADES,
 | 
						|
      STAT_BALANCEMIN,
 | 
						|
      STAT_BALANCE_DD,
 | 
						|
      STAT_BALANCEDD_PERCENT,
 | 
						|
      STAT_BALANCE_DDREL_PERCENT,
 | 
						|
      STAT_BALANCE_DD_RELATIVE,
 | 
						|
      STAT_EQUITYMIN,
 | 
						|
      STAT_EQUITY_DD,
 | 
						|
      STAT_EQUITYDD_PERCENT,
 | 
						|
      STAT_EQUITY_DDREL_PERCENT,
 | 
						|
      STAT_EQUITY_DD_RELATIVE,
 | 
						|
      STAT_EXPECTED_PAYOFF,
 | 
						|
      STAT_PROFIT_FACTOR,
 | 
						|
      STAT_RECOVERY_FACTOR,
 | 
						|
      STAT_SHARPE_RATIO,
 | 
						|
      STAT_MIN_MARGINLEVEL,
 | 
						|
      STAT_DEALS,
 | 
						|
      STAT_TRADES,
 | 
						|
      STAT_PROFIT_TRADES,
 | 
						|
      STAT_LOSS_TRADES,
 | 
						|
      STAT_SHORT_TRADES,
 | 
						|
      STAT_LONG_TRADES,
 | 
						|
      STAT_PROFIT_SHORTTRADES,
 | 
						|
      STAT_PROFIT_LONGTRADES,
 | 
						|
      STAT_PROFITTRADES_AVGCON,
 | 
						|
      STAT_LOSSTRADES_AVGCON,
 | 
						|
      STAT_COMPLEX_CRITERION
 | 
						|
   };
 | 
						|
 | 
						|
// Массив для значений статистических характеристик прохода в виде строк
 | 
						|
   string stats[];
 | 
						|
   ArrayResize(stats, ArraySize(statNames));
 | 
						|
 | 
						|
// Заполняем массив значений статистических характеристик прохода
 | 
						|
   FOREACH(statNames) stats[i] = DoubleToString(TesterStatistics(statNames[i]), 2);
 | 
						|
 | 
						|
// Добавляем в него значение пользовательского критерия
 | 
						|
   APPEND(stats, DoubleToString(custom, 2));
 | 
						|
 | 
						|
// Объединяем статистические характеристики в строку
 | 
						|
   string data = "";
 | 
						|
   JOIN(stats, data, ",");
 | 
						|
 | 
						|
// В описании параметров экранируем кавычки (на всякий случай на будущее)
 | 
						|
   StringReplace(params, "'", "\\'");
 | 
						|
 | 
						|
// Формируем строку с данными о проходе
 | 
						|
   data = StringFormat("%d, %d, %s,'%s'",
 | 
						|
                       MQLInfoInteger(MQL_OPTIMIZATION),
 | 
						|
                       MQLInfoInteger(MQL_FORWARD),
 | 
						|
                       data, params);
 | 
						|
 | 
						|
// Если это проход в рамках процесса оптимизации, то
 | 
						|
   if(MQLInfoInteger(MQL_OPTIMIZATION)) {
 | 
						|
      // Открываем файл для записи данных для фрейма
 | 
						|
      int f = FileOpen(s_frameFileName, FILE_WRITE | FILE_TXT | FILE_ANSI);
 | 
						|
 | 
						|
      // Записываем описание параметров советника
 | 
						|
      FileWriteString(f, data);
 | 
						|
 | 
						|
      // Закрываем файл
 | 
						|
      FileClose(f);
 | 
						|
 | 
						|
      // Создаём фрейм с данными из записанного файла и отправляем его в главный терминал
 | 
						|
      if(!FrameAdd("", 0, 0, s_frameFileName)) {
 | 
						|
         PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
 | 
						|
      }
 | 
						|
   } else {
 | 
						|
      // Иначе это одиночный проход, вызываем метод добавления его результатов
 | 
						|
      // в базу данных оптимизации (если она была задана)
 | 
						|
      if (s_fileName != "") {
 | 
						|
         CTesterHandler::ProcessFrame(data);
 | 
						|
      }
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Экспорт массива стратегий в заданную базу данных эксперта        |
 | 
						|
//| как новой группы стратегий                                       |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) {
 | 
						|
// Создаём объект задачи оптимизации
 | 
						|
   COptimizerTask task(s_fileName);
 | 
						|
// Загружаем в него данные текущей задачи оптимизации
 | 
						|
   task.Load(CTesterHandler::s_idTask);
 | 
						|
 | 
						|
// Подключаемся к нужной базе данных эксперта
 | 
						|
   if(DB::Connect(p_advFileName, DB_TYPE_ADV)) {
 | 
						|
      string fromDate = task.m_params.from_date; // Дата начала интервала оптимизации
 | 
						|
      string toDate = task.m_params.to_date;     // Дата конца  интервала оптимизации
 | 
						|
 | 
						|
      // Создаём запись для новой группы стратегий
 | 
						|
      string query = StringFormat("INSERT INTO strategy_groups VALUES(NULL, '%s', '%s', '%s', NULL)"
 | 
						|
                                  " RETURNING rowid;",
 | 
						|
                                  p_groupName, fromDate, toDate);
 | 
						|
      ulong groupId = DB::Insert(query);
 | 
						|
 | 
						|
      PrintFormat(__FUNCTION__" | Export %d strategies into new group [%s] with ID=%I64u",
 | 
						|
                  ArraySize(p_strategies), p_groupName, groupId);
 | 
						|
 | 
						|
      // Для каждой стратегии
 | 
						|
      FOREACH(p_strategies) {
 | 
						|
         CVirtualStrategy *strategy = p_strategies[i];
 | 
						|
         // Формируем строку инициализации в виде группы из одной стратегии с нормирующим множителем
 | 
						|
         string params = StringFormat("class CVirtualStrategyGroup([%s],%0.5f)",
 | 
						|
                                      ~strategy,
 | 
						|
                                      strategy.Scale());
 | 
						|
 | 
						|
         // Сохраняем её в базе данных эксперта с указанием нового идентификатора группы
 | 
						|
         string query = StringFormat("INSERT INTO strategies "
 | 
						|
                                     "VALUES (NULL, %I64u, '%s', '%s')",
 | 
						|
                                     groupId, strategy.Hash(~strategy), params);
 | 
						|
         DB::Execute(query);
 | 
						|
      }
 | 
						|
 | 
						|
      // Закрываем базу данных
 | 
						|
      DB::Close();
 | 
						|
   }
 | 
						|
   
 | 
						|
   // TODO: Добавить сохранение группы в базу данных оптимизации
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Формирование SQL-запроса на вставку результатов прохода          |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
 | 
						|
   return StringFormat("INSERT INTO passes "
 | 
						|
                       "VALUES (NULL, %d, %I64u, %s,\n'%s',\nNULL) RETURNING rowid;",
 | 
						|
                       s_idTask, pass, values, inputs);
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Обработка данных одиночного прохода                              |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CTesterHandler::ProcessFrame(string values) {
 | 
						|
// Открываем базу данных
 | 
						|
   DB::Connect(s_fileName);
 | 
						|
 | 
						|
// Формируем SQL-запрос из полученных данных
 | 
						|
   string query = GetInsertQuery(values, "", 0);
 | 
						|
 | 
						|
// Выполняем запрос
 | 
						|
   s_idPass = DB::Insert(query);
 | 
						|
 | 
						|
// Закрываем базу данных
 | 
						|
   DB::Close();
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Обработка пришедших фреймов                                      |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
void CTesterHandler::ProcessFrames(void) {
 | 
						|
// Открываем базу данных
 | 
						|
   DB::Connect(s_fileName);
 | 
						|
 | 
						|
// Переменные для чтения данных из фреймов
 | 
						|
   string   name;      // Название фрейма (не используется)
 | 
						|
   ulong    pass;      // Индекс прохода фрейма
 | 
						|
   long     id;        // Идентификатор типа фрейма (не используется)
 | 
						|
   double   value;     // Одиночное значение фрейма (не используется)
 | 
						|
   uchar    data[];    // Массив данных фрейма в виде массива символа
 | 
						|
 | 
						|
   string   values;    // Данные фрейма в виде строки
 | 
						|
   string   inputs;    // Строка с именами и значениями параметров прохода
 | 
						|
   string   query;     // Строка одного SQL-запроса
 | 
						|
   string   queries[]; // SQL-запросы на добавление записей в БД
 | 
						|
 | 
						|
 | 
						|
// Проходим по фреймам и читаем данные из них
 | 
						|
   while(FrameNext(pass, name, id, value, data)) {
 | 
						|
      // Переводим в строку массив символов, прочитанный из фрейма
 | 
						|
      values = CharArrayToString(data);
 | 
						|
 | 
						|
      // Формируем строку с именами и значениями параметров прохода
 | 
						|
      inputs = GetFrameInputs(pass);
 | 
						|
 | 
						|
      // Формируем SQL-запрос из полученных данных
 | 
						|
      query = GetInsertQuery(values, inputs, pass);
 | 
						|
 | 
						|
      // Добавляем его в массив SQL-запросов
 | 
						|
      APPEND(queries, query);
 | 
						|
   }
 | 
						|
 | 
						|
// Выполняем все запросы
 | 
						|
   DB::ExecuteTransaction(queries);
 | 
						|
 | 
						|
// Закрываем базу данных
 | 
						|
   DB::Close();
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
//| Формирует строку с именами и значениями input-переменных прохода |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
string CTesterHandler::GetFrameInputs(ulong pass) {
 | 
						|
   string  params[];    // Массив описаний input-переменных
 | 
						|
   uint    count;       // Количество input-переменных
 | 
						|
   string  inputs = ""; // Строка для результата
 | 
						|
 | 
						|
   if(FrameInputs(pass, params, count)) {
 | 
						|
      // собираем оптимизируемые параметры и их значения
 | 
						|
      for(uint i = 0; i < count; i++) {
 | 
						|
         string name2value[];
 | 
						|
         string delimeter = (i == count - 1 ? "" : ",");
 | 
						|
         // Делим описание очередной input-переменной по символу '='
 | 
						|
         int n = StringSplit(params[i], '=', name2value);
 | 
						|
         if(n == 2) {
 | 
						|
            // Получаем значение по имени в pvalue
 | 
						|
            double pvalue, pstart, pstep, pstop;
 | 
						|
            bool enabled = false;
 | 
						|
            if(ParameterGetRange(name2value[0],
 | 
						|
                                 enabled, pvalue, pstart, pstep, pstop)) {
 | 
						|
               // Добавляем в выходную строку имя и значение input-переменной
 | 
						|
               if(MathAbs(pvalue - (long) pvalue) < 1e-6) {
 | 
						|
                  // как целое число
 | 
						|
                  inputs += StringFormat("%s=%d%s", name2value[0], (long) pvalue, delimeter);
 | 
						|
               } else {
 | 
						|
                  // как вещественное число
 | 
						|
                  inputs += StringFormat("%s=%.2f%s", name2value[0], pvalue, delimeter);
 | 
						|
               }
 | 
						|
            }
 | 
						|
         }
 | 
						|
      }
 | 
						|
   }
 | 
						|
//PrintFormat(__FUNCTION__" | pass %d: %s", pass, inputs);
 | 
						|
 | 
						|
   return inputs;
 | 
						|
}
 | 
						|
 | 
						|
//+------------------------------------------------------------------+
 |