Adwizard/Virtual/TesterHandler.mqh
2025-04-11 13:28:40 +03:00

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;
}
//+------------------------------------------------------------------+