MqlCIByLeo/Src/Tester/Base.mqh
Nique_372 af6dc6dd2c
2026-03-11 17:12:08 -05:00

439 Zeilen
16 KiB
MQL5

//+------------------------------------------------------------------+
//| Main.mqh |
//| Copyright 2026, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link "https://www.mql5.com"
#property strict
#ifndef MQLCYBYLEO_SRC_TESTER_BASE_MQH
#define MQLCYBYLEO_SRC_TESTER_BASE_MQH
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
#include "Defines.mqh"
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
class CMqlCIClassCi : public CLoggerBase
{
private:
//--- Varialbes parametro
string m_filename_compiled_log; // Nombre del archivo donde se pondra en caso de fallo de compilacion los logs(RUTA RELATIVA)
// Nombre del archivo donde estara el resultado de la compilacion 0 \ 1.. si es 0 = flase (Se ubicara en mql5\\files\\...)
// El py lo debera de leeer y marcar como "invalido" el action
// La ruta a este archivo no neceista path completo sobr eel nomrbe (Se ubicara en mql5\\files\\...)
string m_file_name_res;
string m_file_name_out_json; // Nombre del archivo json (Se ubicara en mql5\\files\\...)
string m_file_name_read_py; // Archivo de "check para py" este lo leera y sabra uqe ya es hora de emepzar los check
uint m_max_timeout_ms_compile; // Maximo tiempo de espera para compilar
int m_timeout_per_test_sec; // Maximo tiempo a esperar un solo test (segundos)
// Expertos
// Path relativo a Experts
TestExpertCi m_experts_test[];
int m_experts_test_size;
//---
TestingCiLogs m_event_log[]; // Registro de eventos de la ejecucion (tests + eventos del sistema)
long m_last_chart_id; // ID del ultimo chart abierto para el test
int m_real_test_passed_size; // Contador de tests que pasaron exitosamente
int m_event_log_size; // Tamanio actual del array m_event_log
datetime m_ex_start_data; // Fecha/hora de inicio del test actual
datetime m_ex_end_date; // Fecha/hora de fin del test actual
int m_current_idx_expert; // Indice del expert que se esta ejecutando actualmente
//---
bool m_in_test;
//--- Funcoines
void BuildTestJson();
__forceinline void ExtractTextLogFile(const int idx);
bool Compile(); // Compila todos los archivos
void RunTest(); // Corre los test
bool WriteInFileRes(int8_t flags); // Escribe el archivo binario las badenras de exito
void RunPyAndFinish();
public:
CMqlCIClassCi(void): m_in_test(false), m_last_chart_id(-1) {}
~CMqlCIClassCi(void) {}
//---
bool Init(const MqlCiConfigCompiler& config, const TestExpertCi& arr_experts[]);
//---
bool First();
void ChartEvent(const int32_t id, const long& lparam, const double& dparam, const string& sparam);
bool Execute();
void TimerEvent(); // Check
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CMqlCIClassCi::Init(const MqlCiConfigCompiler &config, const TestExpertCi& arr_experts[])
{
//--- ESperamos carga
const long chart_id = ::ChartID();
::Sleep(500); // Primera espera inicial (no tatno por que ya es el primero en cargar)
while(true)
{
// COmprobacion de barras
if(::ChartGetInteger(chart_id, CHART_VISIBLE_BARS) > 0 && ::SeriesInfoInteger(_Symbol, _Period, SERIES_SYNCHRONIZED))
{
break;
}
// Espera para la siguiente iteracion
Sleep(1000);
}
//--- CErramos todos los graficos que pudieran haber menos el actual
CloseAllChartsExceptChart(chart_id);
//---
m_filename_compiled_log = TERMINAL_MT5_ROOT + "Files\\" + config.filename_compiled_log;
m_file_name_res = config.file_name_res;
m_file_name_out_json = config.file_name_out_json;
m_file_name_read_py = config.file_name_read_py;
m_max_timeout_ms_compile = config.max_timeout_ms_compile;
m_timeout_per_test_sec = config.max_timeout_esecution_per_test;
//---
m_experts_test_size = ArrayCopyCts(m_experts_test, arr_experts);
if(m_experts_test_size < 1)
{
//---
::ArrayResize(m_event_log, m_event_log_size + 1);
m_event_log[m_event_log_size].label = "Iniciciion test - Experts size";
m_event_log[m_event_log_size].res = CYBYLEO_CODE_ERROR;
m_event_log[m_event_log_size].log_txt = ::StringFormat("Numero de expertos es invalido=%d", m_experts_test_size);
LogError(m_event_log[m_event_log_size++].log_txt, FUNCION_ACTUAL);
//---
WriteInFileRes(0);
RunPyAndFinish();
return false;
}
//---
m_event_log_size = ::ArrayResize(m_event_log, 0);
m_real_test_passed_size = 0;
m_current_idx_expert = 0;
m_last_chart_id = -1;
m_in_test = false;
//--- Print final}
LogInfo("Iniciado CI - CD", FUNCION_ACTUAL);
//---
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CMqlCIClassCi::Compile(void)
{
for(int i = 0; i < m_experts_test_size; i++)
{
static string log_path;
// EN caso la compilacion falle tenemos que escribir en el archivo compile el log del archivo que fallo y cerrar el terminal y abrir el py
if(!CompileFileWithLogFile(TERMINAL_MT5_ROOT + m_experts_test[i].expert_mq5_path, log_path, m_max_timeout_ms_compile))
{
LogError(::StringFormat("Fallo al compilar:\n%s", m_experts_test[i].expert_mq5_path), FUNCION_ACTUAL);
LogError(::StringFormat("Log path:\n%s", log_path), FUNCION_ACTUAL);
MTTESTER::FileMove(log_path, m_filename_compiled_log, true); // Movemos el archivo con el nombre del path
return false;
}
}
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CMqlCIClassCi::WriteInFileRes(int8_t flags)
{
const int file_handle = ::FileOpen(m_file_name_res, FILE_BIN | FILE_WRITE);
if(file_handle == INVALID_HANDLE)
{
// En caso falle no se creara asi que el py tiene dos cosas... digo 3
// Si [No se puede cargar el archivo por esta corrupta | el archivo tiene valor 0 | el archivo no existe] entonces fallo la compilacion
LogError(::StringFormat("Fallo al crear el archivo = %s", m_file_name_res), FUNCION_ACTUAL);
return false;
}
::FileWriteInteger(file_handle, flags, sizeof(int8_t));
::FileClose(file_handle);
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CMqlCIClassCi::RunTest(void)
{
//---
m_ex_start_data = ::TimeLocal();
//--- chekeamos el tamaño de parametros en caso sea menor a 2 entonces resize
const int size_params = ArraySize(m_experts_test[m_current_idx_expert].params);
if(size_params < 2)
ArrayResize(m_experts_test[m_current_idx_expert].params, 2); // Minimo un tamaño de dos siempre
//--- sobreesciemos los dos primeros parametros
m_experts_test[m_current_idx_expert].params[0].type = TYPE_STRING;
m_experts_test[m_current_idx_expert].params[0].string_value = m_experts_test[m_current_idx_expert].expert_ex5_name;
m_experts_test[m_current_idx_expert].params[1].type = TYPE_LONG;
m_experts_test[m_current_idx_expert].params[1].integer_value = ChartID();
//--- abrimos un nuevo graficos
m_last_chart_id = ::ChartOpen(m_experts_test[m_current_idx_expert].symbol, m_experts_test[m_current_idx_expert].timeframe);
Sleep(750); // Primera espera iniicla
//--- Check sincronizacion del grafico
while(true)
{
// COmprobacion de barras | Verificamos sincronizacion
if(ChartGetInteger(m_last_chart_id, CHART_VISIBLE_BARS) > 0 &&
SeriesInfoInteger(m_experts_test[m_current_idx_expert].symbol, m_experts_test[m_current_idx_expert].timeframe, SERIES_SYNCHRONIZED))
{
break;
}
// Espera para la siguiente iteracion
Sleep(750);
}
// Log informativo
LogInfo(StringFormat("Chart id = %I64d | EA a correr = %s", m_last_chart_id, m_experts_test[m_current_idx_expert].expert_ex5_name), FUNCION_ACTUAL);
//---
if(!EXPERT::Run(m_last_chart_id, m_experts_test[m_current_idx_expert].params))
{
//---
m_ex_end_date = TimeLocal();
//---
::ArrayResize(m_event_log, m_event_log_size + 1);
ExtractTextLogFile(m_event_log_size); // Todos los logs generados antes de correr el ea y ahora los capturamos
m_event_log[m_event_log_size].label = "Ejecucion de EA";
m_event_log[m_event_log_size].res = CYBYLEO_CODE_ERROR;
//---
LogError("Fallo al correr EA", FUNCION_ACTUAL);
WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION); // Solo compilacion
RunPyAndFinish(); // Corremos el py y finalizamos
return;
}
m_in_test = true; // Empezamos
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CMqlCIClassCi::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if(id == CHARTEVENT_CUSTOM + CIBYLEO_ONEVENT_RES) // Ya recibimos
{
//---
m_in_test = false;
m_ex_end_date = ::TimeLocal(); // Fin de la ejeuccion
//--- Añadimos
::ArrayResize(m_event_log, m_event_log_size + 1);
m_event_log[m_event_log_size].label = sparam; // Por convecion nombre del ea
m_event_log[m_event_log_size].res = lparam; // Resultado
// Extraemos el log del journal
ExtractTextLogFile(m_event_log_size); // Extrae de start-end date
m_event_log_size++; // Aumentamos
// Cerramos el chart
::ChartClose(m_last_chart_id);
//---
LogInfo(StringFormat("Recibiendo info de %s, res = %d, puntuacion = %.2f", sparam, lparam, dparam), FUNCION_ACTUAL);
//---
if(lparam == CYBYLEO_CODE_ERROR) // Fallo al ejeuctar el test
{
// Para esto ya tendremos la tarea que fallo en el array asi qeu ya finalizariamos de una vez
WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION); // Solo se jeucto la compilacion bien el test no
RunPyAndFinish();
return;
}
//---
m_real_test_passed_size++; // ya pasamos esta tarea
m_current_idx_expert++; // siguiente test
//---
if(m_current_idx_expert < m_experts_test_size)
{
RunTest(); // Repetimos el proceso
}
else // Fin de las tareas hora de prepara el py
{
WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION | CIBYLEO_FLAG_EXITO_TEST); // Todo se ejeucto bien
RunPyAndFinish();
}
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
__forceinline void CMqlCIClassCi::ExtractTextLogFile(const int idx)
{
ExtractLogLinesAsStr(m_ex_start_data, m_ex_start_data, m_ex_end_date, m_event_log[m_event_log_size].log_txt);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CMqlCIClassCi::RunPyAndFinish(void)
{
//---
::ArrayResize(m_event_log, m_event_log_size + 1);
m_event_log [m_event_log_size].label = "Ejecucion de python";
//---
::ResetLastError();
const int fh = ::FileOpen(m_file_name_read_py, FILE_WRITE | FILE_BIN);
if(fh == INVALID_HANDLE)
{
m_event_log[m_event_log_size].res = CYBYLEO_CODE_ERROR;
m_event_log[m_event_log_size].log_txt = ::StringFormat("Fallo al abrir el archivo = %s, ultimo error = %d", m_file_name_read_py, ::GetLastError());
LogError(m_event_log [m_event_log_size].log_txt, FUNCION_ACTUAL);
}
else
{
FileWriteInteger(fh, 1, sizeof(int8_t));
FileClose(fh);
m_event_log[m_event_log_size].res = CYBYLEO_CODE_SUCCES;
m_event_log[m_event_log_size].log_txt = "";
}
m_event_log_size++; // Aumentamos
//---
BuildTestJson();
//--- Cerramos el terminal
::TerminalClose(CYBYLEO_TERMINAL_RET_CODE_SUCCESS);
Sleep(10000);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CMqlCIClassCi::BuildTestJson(void)
{
//---
/*
{
"summary": {
"tests_passed": 10, // Numero de tareas que se paran (real task passed)
"tests_total": 12, // Numero de tareas totales
},
// Log events..
"log_events": [
{
"label": "Compilacion",
"result": 1,
"log_txt": "..."
}
{
}
]
}
*/
//--- Construccion del json
CJsonBuilder builder;
builder.Obj();
builder.Key("summary").Obj();
builder.Key("tests_passed").Val(m_real_test_passed_size);
builder.Key("tests_total").Val(m_experts_test_size);
builder.EndObj();
builder.Key("log_events").Arr();
for(int i = 0; i < m_event_log_size; i++)
{
builder.Obj();
builder.Key("label").Val(m_event_log[i].label);
builder.Key("result").Val(m_event_log[i].res);
builder.Key("log_txt").Val(m_event_log[i].log_txt);
builder.EndObj();
}
builder.EndArr();
builder.EndObj();
//---
::ResetLastError();
const int fh = ::FileOpen(m_file_name_out_json, FILE_WRITE | FILE_TXT);
if(fh == INVALID_HANDLE)
{
WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION); // Solo compilacion
return;
}
::FileWrite(fh, builder.Build());
::FileClose(fh);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CMqlCIClassCi::First(void)
{
//--- Primero trataremos de compilar todo en caso falle crearemos el archivo res y lo pondremos en 0 y cerramos el terminal
if(!Compile())
{
WriteInFileRes(0); // Escribimos en el archivo result fallo (nada no compilo)
RunPyAndFinish();
return false;
}
//--- Si ya se han compilado todos loa rchivos con exito entonces pasamos al test
RunTest(); // Corremos el primer test (como minimo debera de haber un archivbo esto igualente se compurbea en Init)
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CMqlCIClassCi::TimerEvent(void)
{
if(!m_in_test)
return;
const datetime current_time = TimeLocal();
if(current_time - m_ex_start_data > m_timeout_per_test_sec)
{
LogError(StringFormat("Timeout en test: %s", m_experts_test[m_current_idx_expert].expert_ex5_name), FUNCION_ACTUAL);
::ChartClose(m_last_chart_id);
WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION);
RunPyAndFinish();
}
}
//---
#endif // MQLCYBYLEO_SRC_TESTER_BASE_MQH
//+------------------------------------------------------------------+