English Русский 中文 Deutsch 日本語 Português
preview
Desarrollamos un asesor experto multidivisa (Parte 9): Recopilamos los resultados de optimización de las instancias individuales de una estrategia comercial

Desarrollamos un asesor experto multidivisa (Parte 9): Recopilamos los resultados de optimización de las instancias individuales de una estrategia comercial

MetaTrader 5Probador | 29 octubre 2024, 16:44
285 0
Yuriy Bykov
Yuriy Bykov

Introducción

En los artículos anteriores, ya hemos hecho bastantes cosas que son valiosas por sí mismas. Tenemos una estrategia comercial o varias estrategias comerciales que podemos implementar en el EA. Asimismo, hemos desarrollado un esquema para combinar muchas instancias de estrategias comerciales en un asesor experto. También hemos añadido herramientas para gestionar la reducción máxima permitida, y hemos estudiado posibles formas de automatizar la selección de conjuntos de parámetros de estrategias para que funcionen mejor en un grupo. Además, hemos montado un EA a partir de grupos de instancias de estrategias e incluso a partir de grupos de diferentes grupos de instancias de estrategias. Pero el valor de los resultados ya obtenidos aumentará enormemente si logramos combinarlos.

En el marco de este artículo, intentaremos esbozar un esquema general del proceso: las estrategias comerciales individuales en la entrada, así como la obtención en la salida de un asesor experto listo para usar que utiliza las instancias seleccionadas y agrupadas de las estrategias comerciales originales que ofrecen los mejores resultados comerciales.

Después de trazar un mapa aproximado, podremos ver más de cerca cualquier sección del mapa, analizar qué necesitamos para poner en marcha la etapa elegida y pasar a la ejecución.


Etapas principales

Vamos a enumerar las principales etapas por las que tenemos que pasar en el proceso de desarrollo de un asesor comercial:

  1. Aplicación de la estrategia comercial. Desarrollamos una clase heredera de CVirtualStrategy que implementa la lógica comercial de apertura, el mantenimiento y el cierre de posiciones y órdenes virtuales. De esto nos hemos ocupado en las cuatro primeras partes de la serie.

  2. Optimización de la estrategia comercial. Seleccionamos buenos conjuntos de parámetros de entrada de estrategias comerciales que muestran resultados dignos de mención. Si no se detecta ninguno, regresamos al paso 1.
    Por lo general, nos sentimos más cómodos realizando la optimización en un único símbolo y marco temporal. Para la optimización genética, probablemente tendremos que ejecutarla varias veces con distintos criterios de optimización, incluidos algunos propios. La optimización por fuerza bruta total solo puede utilizarse en estrategias con un número muy reducido de parámetros. Incluso en nuestra estrategia de modelado, la iteración completa resulta demasiado costosa. Así que a continuación, cuando hablemos de optimización,
    nos referiremos precisamente a la optimización genética en el simulador de estrategias de MetaTrader5. No hemos descrito el proceso de optimización detalladamente en los artículos, ya que es estándar.

  3. Realización de la clusterización de conjuntos. Este paso es opcional, pero nos ahorrará tiempo para el siguiente paso. Aquí reducimos significativamente el número de conjuntos de parámetros de instancias de estrategias comerciales entre los que seleccionaremos buenos grupos. Se menciona en la sexta parte.

  4. Selección de grupos de conjuntos de parámetros. Basándonos en los resultados del paso anterior, realizamos una optimización que selecciona
    los conjuntos de parámetros más compatibles de las instancias de estrategias comerciales que ofrecen los mejores resultados. También se describe en su mayor parte en la sexta parte y se desarrolla en la séptima parte.

  5. Selección de grupos a partir de grupos de conjuntos de parámetros. Ahora combinamos los resultados del paso anterior en grupos según el mismo principio con el que combinamos los conjuntos de parámetros de instancias individuales del modo comercial de la recopilación de frames de la estrategia.

  6. Iteración de símbolos y marcos temporales. Luego repetiremos los pasos 2 - 5 para todos los símbolos y marcos temporales deseados. Tal vez, además del símbolo y el marco temporal, para algunas estrategias comerciales sea posible realizar una optimización por separado de determinadas clases de otros parámetros de entrada. 

  7. Otras estrategias. Si tiene en mente otras estrategias comerciales, repita los pasos 1 a 6 para cada una de ellas.

  8. Montaje del asesor experto. Todos los mejores grupos de grupos encontrados para diferentes estrategias comerciales, símbolos, plazos y otros parámetros se recogen en un asesor experto final.

Cada etapa, una vez completada, genera unos datos que deben almacenarse y usarse en las etapas siguientes. Hasta ahora nos hemos conformado con herramientas temporales, lo bastante prácticas para utilizarse una o dos veces, pero no especialmente prácticas para un uso repetido.

Por ejemplo, los resultados de la optimización tras la segunda fase los guardábamos en un archivo Excel, le añadíamos manualmente las columnas que faltaban y lo utilizábamos en la tercera fase guardándolo como archivo CSV.

Los resultados de la tercera fase los utilizábamos o bien directamente desde la interfaz del simulador de estrategias o los guardábamos en archivos Excel; ahí realizábamos algunos procesamientos y volvíamos a utilizar los resultados obtenidos desde la interfaz del simulador.

En realidad, no realizábamos la quinta fase, sino que nos limitábamos a señalar la posibilidad de llevarla a cabo. Por lo tanto, no llegamos a los resultados.

Para todos estos datos resultantes nos gustaría realizar un esquema unificado de almacenamiento y uso. 


Opciones de implementación

De hecho, el principal tipo de datos que necesitamos almacenar y usar son los resultados de la optimización de múltiples EAs. Como usted sabrá, el simulador de estrategias registra todos los resultados de la optimización en un archivo de caché separado con la extensión *.opt, que puede ser nuevamente abierto en el simulador o incluso abierto en el simulador de otro terminal MetaTrader5. El nombre del archivo se determina a partir de un hash calculado según el nombre del asesor experto que se está optimizando y de los parámetros de optimización. Esto nos permite no perder información sobre las pasadas ya realizadas al continuar la optimización tras su interrupción anticipada o tras cambiar el criterio de optimización.

Por lo tanto, una de las opciones a considerar sería utilizar archivos de caché de optimización para almacenar los resultados intermedios. Existe una buena biblioteca de fxsaber para trabajar con ellos, gracias a la cual podemos acceder a toda la información guardada de los programas MQL5.

Sin embargo, a medida que crezca el número de optimizaciones realizadas, también lo hará el número de archivos con sus resultados. Para no confundirnos en ellos, tendremos que idear algún esquema adicional para organizar el almacenamiento y trabajar con estos archivos de caché. Si la optimización se va a realizar en más de un servidor, tendremos que implementar la sincronización o el apilamiento de todos los archivos de caché en un solo lugar. Además, para la siguiente etapa todavía necesitaremos algún procesamiento para exportar los resultados de la optimización al EA en la siguiente etapa.

A continuación, organizaremos el almacenamiento de todos los resultados en una base de datos. A primera vista, la aplicación de esta medida requeriría mucho tiempo, pero este trabajo puede dividirse en etapas más pequeñas y sus resultados pueden utilizarse inmediatamente, sin esperar a la aplicación completa. Además, este enfoque tiene más libertad a la hora de elegir los medios más cómodos para el procesamiento intermedio de los resultados almacenados. Por ejemplo, podemos asignar parte del procesamiento a consultas SQL simples, otra parte se calculará en MQL5 y otra parte se calculará en programas escritos en Python o R, por ejemplo. Podemos probar distintas opciones de procesamiento y elegir la más adecuada.

MQL5 ofrece funciones incorporadas para trabajar con la base de datos SQLite. También hemos encontrado implementaciones de bibliotecas de terceros que permiten trabajar con MySQL, por ejemplo. Aún no está claro si SQLite nos bastará, pero lo más probable es que esta base de datos sea suficiente para nuestras necesidades. Si no es suficiente, entonces pensaremos en migrar a otro SGBD.


Empezamos a diseñar la base de datos

En primer lugar, tendremos que identificar las entidades cuya información queremos almacenar. Este es sin duda el caso de una sola pasada del simulador. Los campos de esta entidad incluirán campos de datos de entrada de prueba y campos de resultados de prueba. En principio, resulta posible separarlas en entidades distintas. La entidad de datos de entrada puede desglosarse en entidades más pequeñas: el asesor experto, los ajustes de optimización y los parámetros de una sola pasada del asesor experto. Sin embargo, le propongo seguir guiándonos por el principio de la menor acción. Para empezar, bastará con una tabla que contenga campos para los resultados de las pasadas que utilizamos en artículos anteriores y uno o dos campos de texto para colocar la información necesaria sobre los parámetros de entrada de la pasada.

Una tabla de este tipo puede crearse usando una consulta SQL:

CREATE TABLE passes (
    id                    INTEGER  PRIMARY KEY AUTOINCREMENT,
    pass                  INT,	-- pass index

    inputs                TEXT, -- pass input values
    params                TEXT, -- additional pass data

    initial_deposit       REAL, -- pass results...
    withdrawal            REAL,
    profit                REAL,
    gross_profit          REAL,
    gross_loss            REAL,
    max_profittrade       REAL,
    max_losstrade         REAL,
    conprofitmax          REAL,
    conprofitmax_trades   REAL,
    max_conwins           REAL,
    max_conprofit_trades  REAL,
    conlossmax            REAL,
    conlossmax_trades     REAL,
    max_conlosses         REAL,
    max_conloss_trades    REAL,
    balancemin            REAL,
    balance_dd            REAL,
    balancedd_percent     REAL,
    balance_ddrel_percent REAL,
    balance_dd_relative   REAL,
    equitymin             REAL,
    equity_dd             REAL,
    equitydd_percent      REAL,
    equity_ddrel_percent  REAL,
    equity_dd_relative    REAL,
    expected_payoff       REAL,
    profit_factor         REAL,
    recovery_factor       REAL,
    sharpe_ratio          REAL,
    min_marginlevel       REAL,
    deals                 REAL,
    trades                REAL,
    profit_trades         REAL,
    loss_trades           REAL,
    short_trades          REAL,
    long_trades           REAL,
    profit_shorttrades    REAL,
    profit_longtrades     REAL,
    profittrades_avgcon   REAL,
    losstrades_avgcon     REAL,
    complex_criterion     REAL,
    custom_ontester       REAL,
    pass_date             DATETIME DEFAULT (datetime('now') ) 
                                   NOT NULL
);

Vamos a crear la clase auxiliar CDatabase, que contendrá los métodos de trabajo con la base de datos. Podemos hacerla estática, ya que no necesitaremos muchas instancias en un programa, con una será suficiente. Como por ahora pensamos acumular toda la información en una sola base de datos, podemos establecer rígidamente el nombre del archivo de la base de datos en el código fuente. 

Como parte de esta clase, tendremos el campo s_db para almacenar el handle de la base de datos abierta. El método Open(), encargado de abrir la base de datos, establecerá su valor. Si la base de datos aún no se ha creado en el momento de la apertura, se creará llamando al método Create(). Una vez abierto, podremos ejecutar consultas SQL individuales a la base de datos utilizando el método Execute() o consultas SQL masivas en una única transacción utilizando el método ExecuteTransaction(). Al final, cerraremos la base de datos usando el método Close().

También podemos declarar una macro corta que nos permita utilizar el nombre DB, más breve, en lugar del nombre de clase CDatabase, más largo.

#define DB CDatabase

//+------------------------------------------------------------------+
//| Class for handling the database                                  |
//+------------------------------------------------------------------+
class CDatabase {
   static int        s_db;          // DB connection handle
   static string     s_fileName;    // DB file name
public:
   static bool       IsOpen();      // Is the DB open?

   static void       Create();      // Create an empty DB
   static void       Open();        // Opening DB
   static void       Close();       // Closing DB

   // Execute one query to the DB
   static bool       Execute(string &query);

   // Execute multiple DB queries in one transaction
   static bool       ExecuteTransaction(string &queries[]);
};

int    CDatabase::s_db       =  INVALID_HANDLE;
string CDatabase::s_fileName = "database.sqlite";


En el método de creación de la base de datos, por ahora nos limitaremos a crear un array con consultas SQL para crear tablas y ejecutarlas en una única transacción:

//+------------------------------------------------------------------+
//| Create an empty DB                                               |
//+------------------------------------------------------------------+
void CDatabase::Create() {
   // Array of DB creation requests 
   string queries[] = {
      "DROP TABLE IF EXISTS passes;",

      "CREATE TABLE passes ("
      "id                    INTEGER  PRIMARY KEY AUTOINCREMENT,"
      "pass                  INT,"
      "inputs                TEXT,"
      "params                TEXT,"
      "initial_deposit       REAL,"
      "withdrawal            REAL,"
      "profit                REAL,"
      "gross_profit          REAL,"
      "gross_loss            REAL,"
      ...
      "pass_date             DATETIME DEFAULT (datetime('now') ) NOT NULL"
      ");"
      ,
   };

   // Execute all requests
   ExecuteTransaction(queries);
}


En el método de apertura de base de datos, primero intentaremos abrir un archivo de base de datos existente. Si no existe, la crearemos y la abriremos; luego crearemos la estructura de la base de datos llamando al método Create()

//+------------------------------------------------------------------+
//| Is the DB open?                                                  |
//+------------------------------------------------------------------+
bool CDatabase::IsOpen() {
   return (s_db != INVALID_HANDLE);
}
...

//+------------------------------------------------------------------+
//| Open DB                                                          |
//+------------------------------------------------------------------+
void CDatabase::Open() {
// Try to open an existing DB file
   s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_COMMON);

// If the DB file is not found, try to create it when opening
   if(!IsOpen()) {
      s_db = DatabaseOpen(s_fileName,
                          DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |
                          DATABASE_OPEN_COMMON);

      // Report an error in case of failure
      if(!IsOpen()) {
         PrintFormat(__FUNCTION__" | ERROR: %s open failed with code %d",
                     s_fileName, GetLastError());
         return;
      }

      // Create the database structure
      Create();
   }
   PrintFormat(__FUNCTION__" | Database %s opened successfully", s_fileName);
}

En el método ExecuteTransaction() de ejecución de consultas múltiples, crearemos una transacción y comenzaremos a ejecutar todas las consultas SQL una a una en un ciclo. Si se produce un error durante la ejecución de la siguiente solicitud, interrumpiremos el ciclo, informaremos sobre el error y cancelaremos todas las solicitudes anteriores dentro de esta transacción. Si no hay errores, confirmaremos la transacción:

//+------------------------------------------------------------------+
//| Execute multiple DB queries in one transaction                   |
//+------------------------------------------------------------------+
bool CDatabase::ExecuteTransaction(string &queries[]) {
// Open a transaction
   DatabaseTransactionBegin(s_db);

   bool res = true;
// Send all execution requests
   FOREACH(queries, {
      res &= Execute(queries[i]);
      if(!res) break;
   });

// If an error occurred in any request, then
   if(!res) {
      // Report it
      PrintFormat(__FUNCTION__" | ERROR: Transaction failed, error code=%d", GetLastError());
      // Cancel transaction
      DatabaseTransactionRollback(s_db);
   } else {
      // Otherwise, confirm transaction
      DatabaseTransactionCommit(s_db);
      PrintFormat(__FUNCTION__" | Transaction done successfully");
   }
   return res;
}

Luego guardaremos los cambios realizados en el archivo Database.mqh en la carpeta actual.


Modificación del asesor experto para recoger los datos de optimización

Cuando en el proceso de optimización solo se usan agentes en la computadora local, podemos hacer que los resultados de la pasada se guarden en la base de datos o bien en el manejador de eventos OnTester() o bien en el manejador de eventos OnDeinit(). Al usar agentes en la red local o agentes en la MQL5 Cloud Network, será muy difícil, si no imposible, guardar los resultados. Por fortuna, MQL5 ofrece un fabuloso método estándar para obtener cualquier información de los agentes de prueba, estén donde estén, a través de la creación, el envío y la recepción de marcos de datos.

Este mecanismo se describe con suficiente detalle en la guía y en el manual sobre trading algorítmico. Para poder utilizarlo, tendremos que añadir tres manejadores de eventos adicionales al EA optimizado: OnTesterInit(), OnTesterPass() y OnTesterDeinit()

La optimización se iniciará siempre desde cualquier terminal MetaTrader 5; en lo sucesivo, nos referiremos a este como terminal principal. Al iniciar un EA con dichos manejadores desde el terminal principal, antes de distribuir las instancias del EA a los agentes de prueba para que realicen pasadas de optimización normales con diferentes conjuntos de parámetros, se abrirá un nuevo gráfico en el terminal principal y se iniciará otra instancia del EA en este gráfico.

Esta instancia se ejecutará en un modo especial: no ejecutará los manejadores estándar OnInit(), OnTick() y OnDeinit(), sino solo estos tres nuevos manejadores. Este modo tiene incluso su propio nombre: el modo de recogida de frames de los resultados de la optimización. Si es necesario, en las funciones del asesor experto, podemos comprobar que el EA se esté ejecutando en este modo utilizando esta llamada a la función MQLInfoInteger():

// Check if the EA is running in data frame collection mode 
bool isFrameMode = MQLInfoInteger(MQL_FRAME_MODE); 

Como sugieren sus nombres, en el modo de recogida de frames, el manejador OnTesterInit() se ejecuta una vez antes de que comience el proceso de optimización, OnTesterPass() se ejecuta cada vez que uno de los agentes de prueba completa una pasada, y OnTesterDeinit() se ejecuta una vez después de que todas las pasadas de optimización programadas se hayan completado o interrumpido.

Es precisamente la instancia del EA ejecutada en el gráfico del terminal principal en el modo de recogida de frames la que se encargará de recoger los frames de datos de todos los agentes de prueba. El "frame de datos" es solo un nombre cómodo de usar al describir los procesos de intercambio de información entre los agentes de prueba y el EA en el terminal principal. Denota un conjunto de datos con un nombre y un identificador numérico que el agente de pruebas ha creado y enviado al terminal principal tras completar la siguiente pasada de optimización. 

Cabe señalar que tiene sentido crear frames de datos solo en las instancias del EA que se ejecutan en el modo normal en los agentes de pruebas, y recopilar y procesar frames de datos solo en la instancia del EA en el terminal principal que se ejecuta en el modo de recopilación de frames. Empezaremos con el proceso de creación de frames.

Podemos ubicar la creación de frames en el asesor experto en el manejador OnTester() o en cualquier función o método llamado desde OnTester(). Este manejador se iniciará justo después de finalizar la pasada, y podremos obtener en él los valores de todas las características estadísticas de la pasada realizada y, si es necesario, calcular el valor del criterio de usuario para evaluar los resultados de la pasada.

En este momento tenemos un código que calcula un criterio personalizado que muestra el beneficio proyectado que podría obtenerse suponiendo una reducción máxima alcanzable del 10%:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = fixedBalance_ * 0.1 / balanceDrawdown;

// Recalculate the profit
   double fittedProfit = profit * coeff;

   return fittedProfit;
}

Vamos a trasladar este código del archivo del EA SimpleVolumesExpertSingle.mq5 a un nuevo método de la clase CVirtualAdvisor, y a dejar solo el retorno del resultado de la llamada a este método en el EA:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   return expert.Tester();
}

Al realizar al traslado, deberemos tener en cuenta que ya no podemos utilizar la variable fixedBalance_ dentro del método, ya que puede no existir en otro EA. Pero su valor puede obtenerse de la clase estática CMoney llamando al método CMoney::FixedBalance(). Mientras tanto, introduciremos un cambio adicional en el cálculo de nuestro criterio personalizado. Una vez determinado el beneficio previsto, lo convertiremos en una unidad de tiempo, como el beneficio por año. Esto nos permitirá comparar a grandes rasgos los resultados de las pasadas a lo largo de distintos periodos de tiempo entre sí.

Para ello, necesitaremos recordar la fecha inicial de la prueba en el asesor experto. Vamos a añadir la nueva propiedad m_fromDate en la que escribiremos la hora actual en el constructor del objeto de asesor experto.

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

   datetime          m_fromDate;

public:
   ...
   virtual double    Tester() override;         // OnTester event handler
   ...
};


//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown;

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ;

// Perform data frame generation on the test agent
   CTesterHandler::Tester(fittedProfit,
                          ~((CVirtualStrategy *) m_strategies[0]));

   return fittedProfit;
}

Puede que en el futuro realicemos algunos criterios de optimización personalizados y entonces este código se traslade otra vez a una nueva ubicación. No obstante, por ahora nos mantendremos alejados del extenso tema de la investigación de varias funciones de aptitud para optimizar EAs y dejaremos este código como está.

Ahora vamos a añadir al archivo del asesor experto SimpleVolumesExpertSingle.mq5 los nuevos manejadores OnTesterInit(), OnTesterPass() y OnTesterDeinit(). Como según nuestra idea la lógica de estas funciones debe ser la misma para todos los asesores expertos, primero bajaremos su implementación al nivel del asesor experto (un objeto de la clase CVirtualAdvisor).

Hay que tener en cuenta que cuando el asesor experto se inicie en el terminal principal en el modo de recogida de frames, la función OnInit(), en la que se crea la instancia del EA, no se ejecutará. Por lo tanto, para no añadir también la creación/eliminación de la instancia del EA a los nuevos manejadores, haremos que los métodos para procesar estos eventos sean estáticos en la clase CVirtualAdvisor. A continuación, tendremos que añadir el siguiente código al EA:  

//+------------------------------------------------------------------+
//| Initialization before starting optimization                      |
//+------------------------------------------------------------------+
int OnTesterInit(void) {
   return CVirtualAdvisor::TesterInit();
}

//+------------------------------------------------------------------+
//| Actions after completing the next optimization pass              |
//+------------------------------------------------------------------+
void OnTesterPass() {
   CVirtualAdvisor::TesterPass();
}

//+------------------------------------------------------------------+
//| Actions after optimization is complete                           |
//+------------------------------------------------------------------+
void OnTesterDeinit(void) {
   CVirtualAdvisor::TesterDeinit();
}

Un cambio más que podemos introducir en el futuro es deshacernos de la llamada aparte para el método de adición de estrategias comerciales al asesor experto CVirtualAdvisor::Add() después de crear el asesor experto. En su lugar, transmitiremos la información sobre las estrategias al constructor del EA, y este llamará al método Add() por sí mismo. Entonces este método podrá eliminarse de la parte pública.

Con este enfoque, la función de inicialización OnInit() del EA tendrá este aspecto:

int OnInit() {
   CMoney::FixedBalance(fixedBalance_);

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(
      new CSimpleVolumesStrategy(
         symbol_, timeframe_,
         signalPeriod_, signalDeviation_, signaAddlDeviation_,
         openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
         maxCountOfOrders_, 0), // One strategy instance
      magic_, "SimpleVolumesSingle", true);

   return(INIT_SUCCEEDED);
}

Luego guardaremos los cambios realizados en el archivo SimpleVolumesExpertSingle.mq5 en la carpeta actual.

Modificación de la clase de experto

Para no sobrecargar de código la clase de experto CVirtualAdvisor, sacaremos el código de los manejadores de eventos TesterInit, TesterPass y OnTesterDeinit a la clase aparte CTesterHandler, en la que crearemos los métodos estadísticos para procesar cada uno de estos eventos. Entonces en la clase CVirtualAdvisor solo necesitaremos añadir aproximadamente el mismo código que en el archivo de EA principal:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
   ...

public:
   ...
   static int        TesterInit();     // OnTesterInit event handler
   static void       TesterPass();     // OnTesterDeinit event handler
   static void       TesterDeinit();   // OnTesterDeinit event handler
};


//+------------------------------------------------------------------+
//| Initialization before starting optimization                      |
//+------------------------------------------------------------------+
int CVirtualAdvisor::TesterInit() {
   return CTesterHandler::TesterInit();
}

//+------------------------------------------------------------------+
//| Actions after completing the next optimization pass              |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterPass() {
   CTesterHandler::TesterPass();
}

//+------------------------------------------------------------------+
//| Actions after optimization is complete                           |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterDeinit() {
   CTesterHandler::TesterDeinit();
}

Asimismo, realizaremos algunas adiciones al código del constructor del objeto de asesor experto. De cara al futuro, trasladaremos todas las acciones del constructor al nuevo método de inicialización Init(). Esto nos permitirá añadir múltiples constructores con distintos conjuntos de parámetros que utilizarán el mismo método de inicialización después de un pequeño preprocesamiento de los parámetros.

Luego añadiremos constructores cuyo primer argumento será un objeto de estrategia o un objeto de grupo de estrategias. Entonces podremos añadir estrategias al asesor experto directamente en el constructor. En este caso, ya no necesitaremos llamar al método Add() en la función OnInit() del asesor experto.

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

   datetime          m_fromDate;

public:
                     CVirtualAdvisor(CVirtualStrategy *p_strategy, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor
                     CVirtualAdvisor(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor
   void              CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group,
                                           ulong p_magic = 1,
                                           string p_name = "",
                                           bool p_useOnlyNewBar = false
                                          );
   ...
};

...

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategy *p_strategy,
                                 ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false
                                ) {
   CVirtualStrategy *strategies[] = {p_strategy};
   Init(new CVirtualStrategyGroup(strategies), p_magic, p_name, p_useOnlyNewBar);
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategyGroup *p_group,
                                 ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false
                                ) {
   Init(p_group, p_magic, p_name, p_useOnlyNewBar);
};

//+------------------------------------------------------------------+
//| EA initialization method                                         |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group,
                           ulong p_magic = 1,
                           string p_name = "",
                           bool p_useOnlyNewBar = false
                          ) {
// Initialize the receiver with a static receiver
   m_receiver = CVirtualReceiver::Instance(p_magic);
// Initialize the interface with the static interface
   m_interface = CVirtualInterface::Instance(p_magic);
   m_lastSaveTime = 0;
   m_useOnlyNewBar = p_useOnlyNewBar;
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );

   m_fromDate = TimeCurrent();

   Add(p_group);
   delete p_group;
};

Guardaremos los cambios realizados en el archivo VirtualExpert.mqh en la carpeta actual.


Clase de procesamiento de eventos de optimización

Ahora nos centraremos directamente en la aplicación de las acciones anteriores y posteriores a la pasada, así como de las acciones tras la finalización de la optimización. Crearemos la clase CTesterHandler y le añadiremos los métodos de procesamiento de los eventos necesarios, además de un par de métodos auxiliares situados en la parte privada de la clase: 

//+------------------------------------------------------------------+
//| Optimization event handling class                                |
//+------------------------------------------------------------------+
class CTesterHandler {
   static string     s_fileName;                   // File name for writing frame data
   static void       ProcessFrames();              // Handle incoming frames
   static string     GetFrameInputs(ulong pass);   // Get pass inputs
public:
   static int        TesterInit();     // Handle the optimization start in the main terminal
   static void       TesterDeinit();   // Handle the optimization completion in the main terminal
   static void       TesterPass();     // Handle the completion of a pass on an agent in the main terminal

   static void       Tester(const double OnTesterValue,
                            const string params);  // Handle completion of tester pass for agent
};

string CTesterHandler::s_fileName = "data.bin";    // File name for writing frame data

Los manejadores de eventos para el terminal principal parecen muy simples, ya que pondremos el código principal en funciones auxiliares:

//+------------------------------------------------------------------+
//| Handling the optimization start in the main terminal             |
//+------------------------------------------------------------------+
int CTesterHandler::TesterInit(void) {
// Open / create a database
   DB::Open();

// If failed to open it, we do not start optimization
   if(!DB::IsOpen()) {
      return INIT_FAILED;
   }

// Close a successfully opened database
   DB::Close();

   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Handling the optimization completion in the main terminal        |
//+------------------------------------------------------------------+
void CTesterHandler::TesterDeinit(void) {
// Handle the latest data frames received from agents
   ProcessFrames();

// Close the chart with the EA running in frame collection mode
   ChartClose();
}

//+--------------------------------------------------------------------+
//| Handling the completion of a pass on an agent in the main terminal |
//+--------------------------------------------------------------------+
void CTesterHandler::TesterPass(void) {
// Handle data frames received from the agent
   ProcessFrames();
}

Las acciones realizadas tras completar una pasada existirán en dos variantes:

  • Para el agente de pruebas. Allí se recogerá la información necesaria tras la pasada y se creará un frame de datos que se enviará al terminal principal. Estas acciones se recogerán en el manejador de eventos Tester().

  • Para el terminal principal. Aquí podremos realizar acciones sobre la obtención de los frames de datos de los agentes de pruebas, el análisis sintáctico de la información obtenida en el frame y su introducción en la base de datos. Estas acciones se recogerán en el manejador TesterPass().

Las acciones para generar un frame de datos para el agente de pruebas deberán realizarse en el EA dentro del manejador OnTester. Como hemos trasladado su código al nivel del objeto de asesor experto (a la clase CVirtualAsesor), es ahí donde deberemos añadir la llamada al método CTesterHandler::Tester(). Como parámetros de este método, transmitiremos el valor recién calculado del criterio de optimización personalizado y una cadena que describa los parámetros de la estrategia utilizada en el asesor experto que estamos optimizando. Para formar dicha cadena, utilizaremos el operador ~ (tilde o virgulilla) para los objetos de la clase CVirtualStrategy.

//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown;

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ;

// Perform data frame generation on the test agent
   CTesterHandler::Tester(fittedProfit,
                          ~((CVirtualStrategy *) m_strategies[0]));

   return fittedProfit;
}

En el propio método CTesterHandler::Tester(), iteraremos todos los nombres posibles de las características estadísticas disponibles, obtendremos sus valores, los convertiremos en cadenas y añadiremos estas cadenas al array stats. ¿Por qué debemos convertir las características numéricas reales en cadenas? Solo para que podamos transmitirlas en el mismo frame con una descripción de cadena de los parámetros de la estrategia. En un frame, podremos transmitir un array de valores de uno de los tipos simples (a los que no pertenecen las cadenas), o un archivo creado previamente con cualquier dato. Así que, para evitar el lío de enviar dos frames diferentes (uno con números y otro con cadenas de un archivo), convertiremos todos los datos en cadenas, los escribiremos en un archivo y enviaremos su contenido en un único frame: 

//+------------------------------------------------------------------+
//| Handling completion of tester pass for agent                     |
//+------------------------------------------------------------------+
void CTesterHandler::Tester(double custom,   // Custom criteria
                            string params    // Description of EA parameters in the current pass
                           ) {
// Array of names of saved statistical characteristics of the pass
   ENUM_STATISTICS statNames[] = {
      STAT_INITIAL_DEPOSIT,
      STAT_WITHDRAWAL,
      STAT_PROFIT,
      ... 
   };

// Array for values of statistical characteristics of the pass as strings
   string stats[];
   ArrayResize(stats, ArraySize(statNames));

// Fill the array of values of statistical characteristics of the pass
   FOREACH(statNames, stats[i] = DoubleToString(TesterStatistics(statNames[i]), 2));

// Add the custom criterion value to it
   APPEND(stats, DoubleToString(custom, 2));

// Screen the quotes in the description of parameters just in case
   StringReplace(params, "'", "\\'");

// Open the file to write data for the frame
   int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI);

// Write statistical characteristics
   FOREACH(stats, FileWriteString(f, stats[i] + ","));

// Write a description of the EA parameters
   FileWriteString(f, StringFormat("'%s'", params));

// Close the file
   FileClose(f);

// Create a frame with data from the recorded file and send it to the main terminal
   if(!FrameAdd("", 0, 0, s_fileName)) {
      PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
   }
}

Por último, un método auxiliar que aceptará frames de datos y guardará la información de los mismos en la base de datos. En él, iteraremos en un ciclo todos los frames entrantes que aún no hayan sido procesados en el momento actual. Luego obtendremos los datos de cada frame como un array de símbolos y los convertiremos en una cadena. A continuación formaremos una cadena con los nombres y valores de los parámetros de la pasada con el índice dado. Después utilizaremos los valores obtenidos para formar una consulta SQL que inserte una nueva fila en la tabla passes de nuestra base de datos. Acto seguido, añadiremos la consulta SQL creada al array de consultas SQL.

Tras procesar todos los frames de datos obtenidos hasta el momento, ejecutaremos todas las consultas SQL del array dentro de una única transacción.

//+------------------------------------------------------------------+
//| Handling incoming frames                                         |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
// Open the database
   DB::Open();

// Variables for reading data from frames
   string   name;      // Frame name (not used)
   ulong    pass;      // Frame pass index
   long     id;        // Frame type ID (not used) 
   double   value;     // Single frame value (not used)
   uchar    data[];    //  Frame data array as a character array

   string   values;    // Frame data as a string
   string   inputs;    // String with names and values of pass parameters
   string   query;     // A single SQL query string
   string   queries[]; // SQL queries for adding records to the database


// Go through frames and read data from them
   while(FrameNext(pass, name, id, value, data)) {
      // Convert the array of characters read from the frame into a string
      values = CharArrayToString(data);

      // Form a string with names and values of the pass parameters
      inputs = GetFrameInputs(pass);

      // Form an SQL query from the received data
      query = StringFormat("INSERT INTO passes "
                           "VALUES (NULL, %d, %s,\n'%s',\n'%s');",
                           pass, values, inputs,
                           TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));

      // Add it to the SQL query array
      APPEND(queries, query);
   }

// Execute all requests
   DB::ExecuteTransaction(queries);

// Close the database
   DB::Close();
}

El método auxiliar encargado de formar una cadena con los nombres y valores de las variables de entrada de la pasada GetFrameInputs() está tomado del manual sobre trading algorítmico y ligeramente complementado para nuestras necesidades.

Luego guardaremos el código obtenido en el archivo TesterHandler.mqh en la carpeta actual.


Comprobación del funcionamiento

Para comprobar el rendimiento, ejecutaremos la optimización con un número reducido de parámetros a buscar durante un periodo de tiempo no muy amplio. Una vez finalizado el proceso de optimización, podremos consultar los resultados en el simulador de estrategias y en la base de datos creada.

Figura 1. Resultados de la optimización en el simulador de estrategias

Figura 2. Resultados de la optimización en la base de datos 

Como podemos observar, los resultados en la base de datos coinciden con los resultados en el simulador: con la misma clasificación según el criterio de usuario, observamos la misma secuencia de valores de beneficio en ambos lugares. La mejor pasada nos informa de que en un año el beneficio esperado puede ser superior a 5 000$ con un depósito inicial de 10 000$ y una reducción de fondos máxima alcanzable del 10% del depósito inicial (1 000$). Pero ahora no nos preocupan tanto las características cuantitativas de los resultados de la optimización como el hecho de que estos resultados puedan almacenarse en una base de datos.


Conclusión

Así pues, nos encontramos un paso más cerca de nuestro objetivo. Hemos logrado guardar en nuestra base de datos los resultados de las optimizaciones realizadas en los parámetros del EA. De este modo, hemos sentado las bases para la posterior aplicación automatizada de la segunda fase del desarrollo del asesor comercial.

Hasta ahora quedan bastantes preguntas entre bastidores. Durante el desarrollo de este artículo, hemos tenido que posponer muchas cosas para el futuro porque su aplicación habría sido costosa. Pero una vez obtenidos los resultados actuales, podemos formular con mayor claridad la dirección del futuro desarrollo del proyecto.

El almacenamiento implementado funciona hasta ahora solo para el proceso de optimización en el sentido de que guardamos la información sobre las pasadas, pero sigue resultando difícil identificar grupos de cadenas pertenecientes a un proceso de optimización. Para ello, deberemos introducir cambios en la estructura de la base de datos, lo cual ahora resulta extremadamente sencillo. En el futuro, intentaremos automatizar la ejecución de varios procesos de optimización consecutivos con la especificación previa de distintas variantes de los parámetros a optimizar.

¡Gracias por su atención y hasta la próxima!

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/14680

Archivos adjuntos |
Advisor.mqh (4.4 KB)
Database.mqh (13.37 KB)
Interface.mqh (3.21 KB)
Macros.mqh (2.28 KB)
Money.mqh (4.61 KB)
NewBarEvent.mqh (11.52 KB)
Receiver.mqh (1.79 KB)
Strategy.mqh (1.73 KB)
TesterHandler.mqh (17.5 KB)
VirtualAdvisor.mqh (22.65 KB)
VirtualOrder.mqh (39.52 KB)
VirtualReceiver.mqh (17.43 KB)
Añadimos un LLM personalizado a un robot comercial (Parte 4): Entrena tu propio LLM con GPU Añadimos un LLM personalizado a un robot comercial (Parte 4): Entrena tu propio LLM con GPU
Con el rápido desarrollo de la inteligencia artificial en la actualidad, los modelos lingüísticos (LLM) son una parte importante de la inteligencia artificial, por lo que deberíamos pensar en cómo integrar potentes LLM en nuestras operaciones algorítmicas. Para la mayoría de la gente, es difícil ajustar estos potentes modelos a sus necesidades, desplegarlos localmente y luego aplicarlos a la negociación algorítmica. Esta serie de artículos abordará paso a paso la consecución de este objetivo.
Creación de un modelo de restricción de tendencia de velas (Parte 5): Sistema de notificaciones (Parte II) Creación de un modelo de restricción de tendencia de velas (Parte 5): Sistema de notificaciones (Parte II)
Hoy discutiremos sobre la integración funcional de Telegram para las notificaciones de indicadores de MetaTrader 5 utilizando el poder de MQL5, en asociación con Python y la API Telegram Bot. Lo explicaremos todo con detalle para que nadie se pierda ningún punto. Al finalizar este proyecto, habrá adquirido conocimientos valiosos para aplicar en sus proyectos.
Desarrollo de Sistemas Avanzados de Trading ICT: Implementación de Order Blocks en un Indicador Desarrollo de Sistemas Avanzados de Trading ICT: Implementación de Order Blocks en un Indicador
En este artículo, aprenderemos cómo crear un indicador que detecte, dibuje y emita alertas sobre la mitigación de Order Blocks. Exploraremos en detalle cómo identificar estos bloques en el gráfico, establecer alertas precisas y visualizar su posición con rectángulos para tener una mejor comprensión del comportamiento del precio. Este indicador será una herramienta clave para quienes siguen la metodología Smart Money Concepts e Inner Circle Trader.
Dominar la dinámica del mercado: Crear un asesor experto (EA) de soportes y resistencias Dominar la dinámica del mercado: Crear un asesor experto (EA) de soportes y resistencias
Una guía completa para desarrollar un algoritmo de trading automatizado basado en la estrategia de soportes y resistencias. Información detallada sobre todos los aspectos de la creación de un asesor experto en MQL5 y su prueba en MetaTrader 5, desde el análisis del comportamiento del rango de precios hasta la gestión de riesgos.