English Русский 中文 Deutsch 日本語 Português
preview
Evaluación visual y ajuste comercial en MetaTrader 5

Evaluación visual y ajuste comercial en MetaTrader 5

MetaTrader 5Ejemplos |
369 16
Artyom Trishkin
Artyom Trishkin

Contenido



Introducción

Imaginemos una situación: en cualquier cuenta durante bastante tiempo se da un comercio más o menos activo en diferentes instrumentos por parte de diferentes asesores expertos e incluso manualmente. Y ahora, pasado un tiempo, queremos ver los resultados del trabajo. Naturalmente, podemos consultar los informes estándar sobre transacciones en el terminal pulsando la combinación de teclas Alt+E. O podemos cargar los iconos de transacciones en el gráfico y ver las entradas y salidas de las posiciones. Pero, ¿y si queremos ver en dinámica cómo se ha negociado, dónde y cómo se han abierto y cerrado las posiciones? O por ejemplo, visualizar por separado para cada símbolo, o para todos a la vez, la apertura y el cierre de posiciones, a qué niveles se han colocado las órdenes stop y si su tamaño está justificado. ¿Y si nos preguntamos "qué pasaría si..." (y aquí hay muchas opciones: otros stops, utilizar diferentes algoritmos y criterios, utilizar posiciones finales, o mover los stops hasta el punto de equilibrio, etc.); y luego probamos todos nuestros "si" con un resultado claramente visible? ¿Cómo habría cambiado el comercio si?...

Pues resulta que ya está todo preparado para abordar estas cuestiones. Todo lo que deberemos hacer es cargar la historia de la cuenta en un archivo, es decir, todas las transacciones ejecutadas, y luego ejecutar el asesor experto en el simulador de estrategias, que leerá las transacciones del archivo y abrirá/cerrará posiciones en el simulador de estrategias del terminal cliente. Con un asesor experto de este tipo, podemos añadirle código para cambiar las condiciones de salida de las posiciones y comparar cómo ha cambiado el comercio con ello, y qué habría pasado si...

¿Qué ganamos nosotros? Otra herramienta para encontrar los mejores resultados y hacer ajustes en las transacciones ya realizadas en la cuenta durante algún tiempo. Las pruebas visuales nos permitirán ver en dinámica si las posiciones se han abierto correctamente en tal o cual instrumento, y si se han cerrado en el momento adecuado, etc. Y lo principal es que podremos simplemente añadir un nuevo algoritmo al código del asesor experto, probarlo, obtener resultados y hacer ajustes a los asesor expertos que trabajan en esta cuenta.

Implementaremos la siguiente lógica de comportamiento del asesor experto:

  • si el asesor experto se inicia en el gráfico de cualquier instrumento, recopilará la historia comercial completa en la cuenta actual, guardará todas las transacciones en un archivo y no hará nada más;
  • si el asesor experto se ejecuta en el simulador, leerá la historia de transacciones registradas en el archivo y, mientras pasa la prueba, repetirá todas las transacciones del archivo, abriendo y cerrando posiciones.

Por lo tanto, el asesor experto primero preparará un archivo con la historia de transacciones (cuando se ejecuta en un gráfico), y luego ejecutará las transacciones desde el archivo, repitiendo completamente la negociación en la cuenta (al ejecutarse en el simulador de estrategias).

A continuación, realizaremos mejoras en el asesor experto para poder establecer diferentes tamaños de StopLoss y TakeProfit para las posiciones abiertas en el simulador.



Guardamos la historia comercial en un archivo

En el directorio del terminal \MQL5\Experts\ crearemos la nueva carpeta TradingByHistoryDeals, y en ella, un nuevo archivo del asesor con el nombre TradingByHistoryDeals.mq5.

El asesor experto deberá ser capaz de elegir el símbolo y el número mágico utilizados para realizar las pruebas. Si varios asesores expertos operan en la cuenta para varios símbolos o números mágicos, podremos elegir en los ajustes qué símbolo o número mágico nos interesa, o todos a la vez.

//+------------------------------------------------------------------+
//|                                        TradingByHistoryDeals.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

Los valores por defecto para el símbolo y número mágico serán la línea vacía y -1. Con estos valores, el asesor experto no filtrará la historia de transacciones ni por el símbolo ni por el número mágico: se comprobará la historia comercial completa. La tercera línea indicará al asesor experto que muestre (o no) en el diario de registro las descripciones de todas las transacciones guardadas en el archivo, para que podamos comprobar visualmente la corrección de los datos guardados.

Cada transacción supone un conjunto de diferentes parámetros descritos por diferentes propiedades de transacción. Así que lo más sencillo será escribir todas las propiedades de la transacción en una estructura. Para escribir un gran número de transacciones en un archivo, deberemos utilizar un array de estructuras. Y luego guardar este array en un archivo. MQL5 lo tiene todo para ello. La lógica para guardar la historia de transacciones en un archivo será la siguiente:

  • iteramos las transacciones históricas;
  • obtenemos la siguiente transacción y escribimos sus datos en la estructura;
  • la estructura de transacciones creada se guardará en el array de transacciones;
  • al final del ciclo, guardamos el array de estructuras preparado en un archivo.

Escribiremos todos los códigos adicionales (estructuras, clases, enumeraciones) en un archivo separado. Lo llamaremos según el nombre de la futura clase de objeto comercial del símbolo.

En la misma carpeta crearemos un nuevo archivo de inclusión llamado SymbolTrade.mqh.

Luego escribiremos directamente una macrosustitución para el nombre del directorio en el que se escribirá el archivo de la historia, el nombre del archivo y la ruta al mismo, y conectaremos al archivo creado todos los archivos necesarios para el desarrollo de la Biblioteca Estándar:

//+------------------------------------------------------------------+
//|                                                  SymbolTrade.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define   DIRECTORY  "TradingByHistoryDeals"
#define   FILE_NAME  "HistoryDealsData.bin"
#define   PATH       DIRECTORY+"\\"+FILE_NAME

#include <Arrays\ArrayObj.mqh>
#include <Trade\Trade.mqh>

A continuación, escribiremos la estructura de la transacción:

//+------------------------------------------------------------------+
//|  Deal structure. Used to create a deal history file              |
//+------------------------------------------------------------------+
struct SDeal
  {
   ulong             ticket;                 // Deal ticket
   long              order;                  // Order the deal is based on
   long              pos_id;                 // Position ID
   long              time_msc;               // Time in milliseconds
   datetime          time;                   // Time
   double            volume;                 // Volume
   double            price;                  // Price
   double            profit;                 // Profit
   double            commission;             // Deal commission
   double            swap;                   // Accumulated swap at closing
   double            fee;                    // Payment for the deal is accrued immediately after the deal is completed
   double            sl;                     // Stop Loss level
   double            tp;                     // Take Profit level
   ENUM_DEAL_TYPE    type;                   // Type
   ENUM_DEAL_ENTRY   entry;                  // Position change method
   ENUM_DEAL_REASON  reason;                 // Deal reason or source
   long              magic;                  // EA ID
   int               digits;                 // Symbol digits
   ushort            symbol[16];             // Symbol
   ushort            comment[64];            // Deal comment
   ushort            external_id[256];       // Deal ID in an external trading system (on the exchange)
   
//--- Set string properties
   bool              SetSymbol(const string deal_symbol)          { return(::StringToShortArray(deal_symbol, symbol)==deal_symbol.Length());                }
   bool              SetComment(const string deal_comment)        { return(::StringToShortArray(deal_comment, comment)==deal_comment.Length());             }
   bool              SetExternalID(const string deal_external_id) { return(::StringToShortArray(deal_external_id, external_id)==deal_external_id.Length()); }
                       
//--- Return string properties
   string            Symbol(void)                                 { return(::ShortArrayToString(symbol));                                                   }
   string            Comment(void)                                { return(::ShortArrayToString(comment));                                                  }
   string            ExternalID(void)                             { return(::ShortArrayToString(external_id));                                              }
  };

Como vamos a guardar estructuras de las transacciones en un archivo, y solo las estructuras de tipos simples pueden escribirse en un archivo (véase FileWriteArray()), entonces deberemos sustituir todas las variables string por arrays ushort y crear métodos para escribir y retornar las propiedades string de la estructura.

La estructura creada solo será necesaria para guardar la historia de transacciones en un archivo y leer la historia registrada desde el mismo. En el propio asesor experto se creará una lista de objetos en la que se almacenarán los objetos de la clase transacción. Para buscar la transacción requerida en la lista y clasificar el array, será necesario especificar la propiedad de la transacción según la cual se buscará en la lista. Para buscar una transacción, deberemos clasificar la lista de objetos según la propiedad buscada.

Vamos a escribir ahora una enumeración de todas las propiedades del objeto de transacción para realizar búsquedas:

//--- Deal sorting types
enum ENUM_DEAL_SORT_MODE
  {
   SORT_MODE_DEAL_TICKET = 0,          // Mode of comparing/sorting by a deal ticket
   SORT_MODE_DEAL_ORDER,               // Mode of comparing/sorting by the order a deal is based on
   SORT_MODE_DEAL_TIME,                // Mode of comparing/sorting by a deal time
   SORT_MODE_DEAL_TIME_MSC,            // Mode of comparing/sorting by a deal time in milliseconds
   SORT_MODE_DEAL_TYPE,                // Mode of comparing/sorting by a deal type
   SORT_MODE_DEAL_ENTRY,               // Mode of comparing/sorting by a deal direction
   SORT_MODE_DEAL_MAGIC,               // Mode of comparing/sorting by a deal magic number
   SORT_MODE_DEAL_REASON,              // Mode of comparing/sorting by a deal reason or source
   SORT_MODE_DEAL_POSITION_ID,         // Mode of comparing/sorting by a position ID
   SORT_MODE_DEAL_VOLUME,              // Mode of comparing/sorting by a deal volume
   SORT_MODE_DEAL_PRICE,               // Mode of comparing/sorting by a deal price
   SORT_MODE_DEAL_COMMISSION,          // Mode of comparing/sorting by commission
   SORT_MODE_DEAL_SWAP,                // Mode of comparing/sorting by accumulated swap on close
   SORT_MODE_DEAL_PROFIT,              // Mode of comparing/sorting by a deal financial result
   SORT_MODE_DEAL_FEE,                 // Mode of comparing/sorting by a deal fee
   SORT_MODE_DEAL_SL,                  // Mode of comparing/sorting by Stop Loss level
   SORT_MODE_DEAL_TP,                  // Mode of comparing/sorting by Take Profit level
   SORT_MODE_DEAL_SYMBOL,              // Mode of comparing/sorting by a name of a traded symbol
   SORT_MODE_DEAL_COMMENT,             // Mode of comparing/sorting by a deal comment
   SORT_MODE_DEAL_EXTERNAL_ID,         // Mode of comparing/sorting by a deal ID in an external trading system
   SORT_MODE_DEAL_TICKET_TESTER,       // Mode of comparing/sorting by a deal ticket in the tester
   SORT_MODE_DEAL_POS_ID_TESTER,       // Mode of comparing/sorting by a position ID in the tester
  };

Aquí, además de las propiedades de transacción estándar, habrá dos más: el ticket de transacción en el simulador y el ID de la posición en el simulador. La cuestión es que vamos a operar en el simulador sobre la base de transacciones reales, y las posiciones abiertas en el simulador y, en consecuencia, las transacciones sobre la base de las cuales se han abierto las posiciones, tienen un ticket y un ID completamente diferentes en el simulador. Para que podamos comparar una transacción real con una transacción en el simulador (así como el ID), tendremos que guardar el ticket y el ID de la posición en el simulador en las propiedades del objeto de transacción, y utilizar estos datos guardados para comparar una transacción en el simulador con una transacción real en la historia.

Vamos a dejar este archivo por ahora y pasar al archivo de asesor experto creado un poco antes. Ahora vamos a añadir un array de estructuras, donde añadiremos las estructuras de todas las transacciones de la historia:

//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

//--- global variables
SDeal          ExtArrayDeals[]={};

Y luego escribiremos las funciones para trabajar con transacciones históricas.

Función que guarda la historia de transacciones en un array:

//+------------------------------------------------------------------+
//| Save deals from history into the array                           |
//+------------------------------------------------------------------+
int SaveDealsToArray(SDeal &array[], bool logs=false)
  {
//--- deal structure
   SDeal deal={};
   
//--- request the deal history in the interval from the very beginning to the current moment 
   if(!HistorySelect(0, TimeCurrent()))
     {
      Print("HistorySelect() failed. Error ", GetLastError());
      return 0;
     }
   
//--- total number of deals in the list 
   int total=HistoryDealsTotal(); 

//--- handle each deal 
   for(int i=0; i<total; i++) 
     { 
      //--- get the ticket of the next deal (the deal is automatically selected to get its properties)
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket==0)
         continue;
      
      //--- save only balance and trading deals
      ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket, DEAL_TYPE);
      if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL && deal_type!=DEAL_TYPE_BALANCE)
         continue;
      
      //--- save the deal properties in the structure
      deal.ticket=ticket;
      deal.type=deal_type;
      deal.order=HistoryDealGetInteger(ticket, DEAL_ORDER);
      deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket, DEAL_ENTRY);
      deal.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON);
      deal.time=(datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
      deal.time_msc=HistoryDealGetInteger(ticket, DEAL_TIME_MSC);
      deal.pos_id=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
      deal.volume=HistoryDealGetDouble(ticket, DEAL_VOLUME);
      deal.price=HistoryDealGetDouble(ticket, DEAL_PRICE);
      deal.profit=HistoryDealGetDouble(ticket, DEAL_PROFIT);
      deal.commission=HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      deal.swap=HistoryDealGetDouble(ticket, DEAL_SWAP);
      deal.fee=HistoryDealGetDouble(ticket, DEAL_FEE);
      deal.sl=HistoryDealGetDouble(ticket, DEAL_SL);
      deal.tp=HistoryDealGetDouble(ticket, DEAL_TP);
      deal.magic=HistoryDealGetInteger(ticket, DEAL_MAGIC);
      deal.SetSymbol(HistoryDealGetString(ticket, DEAL_SYMBOL));
      deal.SetComment(HistoryDealGetString(ticket, DEAL_COMMENT));
      deal.SetExternalID(HistoryDealGetString(ticket, DEAL_EXTERNAL_ID));
      deal.digits=(int)SymbolInfoInteger(deal.Symbol(), SYMBOL_DIGITS);
      
      //--- increase the array and
      int size=(int)array.Size();
      ResetLastError();
      if(ArrayResize(array, size+1, total)!=size+1)
        {
         Print("ArrayResize() failed. Error ", GetLastError());
         continue;
        }
      //--- save the deal in the array
      array[size]=deal;
      //--- if allowed, display the description of the saved deal to the journal
      if(logs)
         DealPrint(deal, i);
     }
//--- return the number of deals stored in the array
   return (int)array.Size();
  }

El código de la función se comenta con todo detalle. Después seleccionaremos la historia completa de transacciones desde el principio hasta el momento actual, obtendremos cada transacción histórica subsiguiente, guardaremos sus propiedades en los campos de la estructura y guardaremos la variable de la estructura en el array. Al final del ciclo a través de la historia de tratos, retornaremos el tamaño del array de transacciones obtenido. Para supervisar el progreso del registro de transacciones en el array, podemos imprimir cada transacción procesada en el diario de registro. Para ello, al llamar a la función, deberemos indicar la bandera logs en los parámetros formales de la función igual a true.

Función que imprime en el diario de registro todas las transacciones del array de transacciones:

//+------------------------------------------------------------------+
//| Display deals from the array to the journal                      |
//+------------------------------------------------------------------+
void DealsArrayPrint(SDeal &array[])
  {
   int total=(int)array.Size();
//--- if an empty array is passed, report this and return 'false'
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return;
     }
//--- In a loop through the deal array, print out a description of each deal
   for(int i=0; i<total; i++)
     {
      DealPrint(array[i], i);
     }
  }

Para mostrar la descripción de una transacción en el registro, crearemos algunas funciones.

Función que retorna la descripción del tipo de transacción:

//+------------------------------------------------------------------+
//| Return the deal type description                                 |
//+------------------------------------------------------------------+
string DealTypeDescription(const ENUM_DEAL_TYPE type)
  {
   switch(type)
     {
      case DEAL_TYPE_BUY                     :  return "Buy";
      case DEAL_TYPE_SELL                    :  return "Sell";
      case DEAL_TYPE_BALANCE                 :  return "Balance";
      case DEAL_TYPE_CREDIT                  :  return "Credit";
      case DEAL_TYPE_CHARGE                  :  return "Additional charge";
      case DEAL_TYPE_CORRECTION              :  return "Correction";
      case DEAL_TYPE_BONUS                   :  return "Bonus";
      case DEAL_TYPE_COMMISSION              :  return "Additional commission";
      case DEAL_TYPE_COMMISSION_DAILY        :  return "Daily commission";
      case DEAL_TYPE_COMMISSION_MONTHLY      :  return "Monthly commission";
      case DEAL_TYPE_COMMISSION_AGENT_DAILY  :  return "Daily agent commission";
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  return "Monthly agent commission";
      case DEAL_TYPE_INTEREST                :  return "Interest rate";
      case DEAL_TYPE_BUY_CANCELED            :  return "Canceled buy deal";
      case DEAL_TYPE_SELL_CANCELED           :  return "Canceled sell deal";
      case DEAL_DIVIDEND                     :  return "Dividend operations";
      case DEAL_DIVIDEND_FRANKED             :  return "Franked (non-taxable) dividend operations";
      case DEAL_TAX                          :  return "Tax charges";
      default                                :  return "Unknown deal type: "+(string)type;
     }
  }

Dependiendo del tipo de transacción transmitido a la función, se mostrará la línea correspondiente.

Función que retorna una descripción de cómo se ha cambiado la posición:

//+------------------------------------------------------------------+
//| Return position change method                                    |
//+------------------------------------------------------------------+
string DealEntryDescription(const ENUM_DEAL_ENTRY entry)
  {
   switch(entry)
     {
      case DEAL_ENTRY_IN      :  return "Entry In";
      case DEAL_ENTRY_OUT     :  return "Entry Out";
      case DEAL_ENTRY_INOUT   :  return "Entry InOut";
      case DEAL_ENTRY_OUT_BY  :  return "Entry OutBy";
      default                 :  return "Unknown entry: "+(string)entry;
     }
  }

Dependiendo del método de cambio de posición transmitido a la función, se mostrará la línea correspondiente.

Función que devuelve una descripción de la transacción:

//+------------------------------------------------------------------+
//| Return deal description                                          |
//+------------------------------------------------------------------+
string DealDescription(SDeal &deal, const int index)
  {
   string indexs=StringFormat("% 5d", index);
   if(deal.type!=DEAL_TYPE_BALANCE)
      return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                          indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                          deal.pos_id, deal.Symbol(), deal.magic, deal.digits, deal.price,
                          TimeToString(deal.time, TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.digits, deal.sl, deal.digits, deal.tp));
   else
      return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                          indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                          deal.profit, AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.time)));
  }

Si se trata de una transacción de balance, la descripción aparecerá como sigue

    0: deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48

De lo contrario, la descripción de la transacción se mostrará en un formato diferente:

    1: deal #190724678 Entry In, type Buy, Position #225824633 USDCHF (magic 600), Price 0.84940 at 2024.09.13 23:49:03, sl 0.84811, tp 0.84983

Función que imprime la descripción de una transacción en el diario:

//+------------------------------------------------------------------+
//| Print deal data in the journal                                   |
//+------------------------------------------------------------------+
void DealPrint(SDeal &deal, const int index)
  {
   Print(DealDescription(deal, index));
  }

Aquí todo está claro: simplemente imprimiremos la línea obtenida de la función DealDescription().

Luego escribiremos las funciones para escribir y leer un array de transacciones en un archivo/desde un archivo.

Función que abre un archivo para la escritura:

//+------------------------------------------------------------------+
//| Open a file for writing, return a handle                         |
//+------------------------------------------------------------------+
bool FileOpenToWrite(int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
      return false;
     }
//--- successful
   return true;
  }

Función que abre un archivo para su lectura:

//+------------------------------------------------------------------+
//| Open a file for reading, return a handle                         |
//+------------------------------------------------------------------+
bool FileOpenToRead(int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_READ|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
      return false;
     }
//--- successful
   return true;
  }

Las funciones abren un archivo para lectura/escritura. En los parámetros formales, transmitiremos por referencia una variable en la que se escribirá el manejador del archivo. Estos retornarán true en caso de apertura exitosa del archivo y false en caso de error.

Función que guarda en un archivo los datos de una transacción de un array:

//+------------------------------------------------------------------+
//| Save deal data from the array to the file                        |
//+------------------------------------------------------------------+
bool FileWriteDealsFromArray(SDeal &array[], ulong &file_size)
  {
//--- if an empty array is passed, report this and return 'false'
   if(array.Size()==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
     
//--- open the file for writing, get its handle
   int handle=INVALID_HANDLE;
   if(!FileOpenToWrite(handle))
      return false;
   
//--- move the file pointer to the end of the file
   bool res=true;
   ResetLastError();
   res&=FileSeek(handle, 0, SEEK_END);
   if(!res)
      PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError());
   
//--- write the array data to the end of the file 
   file_size=0;
   res&=(FileWriteArray(handle, array)==array.Size());
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- close the file 
   FileClose(handle);
   return res;
  }

A la función se le transmitirá un array de estructuras que deberán guardarse en un archivo. La variable para obtener el tamaño del archivo creado se transmitirá por referencia en los parámetros formales de la función. A continuación abriremos el archivo, desplazaremos el puntero del archivo al final del mismo y escribiremos los datos del array de estructuras en el archivo a partir del puntero. Una vez finalizada la escritura, el archivo se cerrará.

Tras guardar el array de estructuras de transacciones en un archivo, podremos volver a leer todas las transacciones en el array desde este archivo y utilizarlo para crear listas de transacciones y trabajar con ellas en el simulador.

Función que carga a un array los datos de una transacción desde un archivo:

//+------------------------------------------------------------------+
//| Load the deal data from the file into the array                  |
//+------------------------------------------------------------------+
bool FileReadDealsToArray(SDeal &array[], ulong &file_size)
  {
//--- open the file for reading, get its handle
   int handle=INVALID_HANDLE;
   if(!FileOpenToRead(handle))
      return false;
   
//--- move the file pointer to the end of the file 
   bool res=true;
   ResetLastError();
   
//--- read data from the file into the array
   file_size=0;
   res=(FileReadArray(handle, array)>0);
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- close the file 
   FileClose(handle);
   return res;
  }

Basándonos en las funciones creadas anteriormente, escribiremos una función para leer la historia de transacciones y escribirlas en un archivo.

Función que prepara un archivo con las transacciones de la historia:

//+------------------------------------------------------------------+
//| Prepare a file with history deals                                |
//+------------------------------------------------------------------+
bool PreparesDealsHistoryFile(SDeal &deals_array[])
  {
//--- save all the account deals in the deal array
   int total=SaveDealsToArray(deals_array);
   if(total==0)
      return false;
      
//--- write the deal array data to the file
   ulong file_size=0;
   if(!FileWriteDealsFromArray(deals_array, file_size))
      return false;
      
//--- print in the journal how many deals were read and saved to the file, the path to the file and its size
   PrintFormat("%u deals were saved in an array and written to a \"%s\" file of %I64u bytes in size",
               deals_array.Size(), "TERMINAL_COMMONDATA_PATH\\Files\\"+ PATH, file_size);
   
//--- now, to perform a check, we will read the data from the file into the array
   ArrayResize(deals_array, 0, total);
   if(!FileReadDealsToArray(deals_array, file_size))
      return false;
      
//--- print in the journal how many bytes were read from the file and the number of deals received in the array
   PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, deals_array.Size());
   return true;
  }

Partiendo de los comentarios en el código, toda la lógica aquí está clara. La función se iniciará en el manejador OnInit() y preparará un archivo con transacciones para seguir trabajando con él:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- If the EA is not running in the tester
   if(!MQLInfoInteger(MQL_TESTER))
     {
      //--- prepare a file with all historical deals
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- print all deals in the journal after loading them from the file 
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- get the first balance deal, create the message text and display it using Alert
      SDeal    deal=ExtArrayDeals[0];
      long     leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
      double   start_money=deal.profit;
      datetime first_time=deal.time;
      string   start_time=TimeToString(deal.time, TIME_DATE);
      string   message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage);
      
      //--- notify via alert of the recommended parameters of the strategy tester for starting the test
      Alert(message);
     }
     
//--- All is successful
   return(INIT_SUCCEEDED);
  }

Además de guardar todas las transacciones históricas en un archivo, aquí se mostrará una alerta con un mensaje sobre los ajustes recomendados del simulador: el balance inicial, el apalancamiento y la hora de inicio de la prueba correspondiente a la fecha de la primera transacción de balance. Por ejemplo, así:

Alert: Now you can run testing
Interval: 2024.09.13 - current date
Initial deposit: 3000.00, leverage 1:500

Estos ajustes en el simulador ofrecerán unos resultados finales lo más próximos posibles a los obtenidos en la realidad.

La estructura de la transacción escrita en el archivo \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh ha sido pensada solo para guardar la historia de transacciones en un archivo y para leer la historia guardada desde el archivo. Para seguir trabajando, necesitaremos crear una clase de transacción cuyos objetos se almacenarán en listas, mientras que las propias listas se encontrarán en los objetos de la clase comercial para el simulador. A su vez, los objetos comerciales también serán objetos de una clase que se almacenará en su propia lista. Cada objeto comercial se definirá por su pertenencia a un determinado símbolo: cuantos más símbolos intervengan en una transacción, más objetos comerciales habrá. Los propios objetos de transacción solo contendrán una lista de transacciones para su símbolo y sus objetos de la clase CTrade de la biblioteca estándar. Esto permitirá ajustar cada objeto comercial de la clase CTrade según las condiciones del símbolo negociado.

Escribiremos la clase de transacción en el archivo \MQL5\Expertos\TradingByHistoryDeals\SymbolTrade.mqh.

//+------------------------------------------------------------------+
//| Deal class. Used for trading in the strategy tester              |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
protected:
//--- Integer properties
   ulong             m_ticket;            // Deal ticket. Unique number assigned to each deal
   long              m_order;             // Deal order number
   datetime          m_time;              // Deal execution time
   long              m_time_msc;          // Deal execution time in milliseconds since 01.01.1970
   ENUM_DEAL_TYPE    m_type;              // Deal type
   ENUM_DEAL_ENTRY   m_entry;             // Deal entry - entry in, entry out, reverse
   long              m_magic;             // Magic number for a deal (see ORDER_MAGIC)
   ENUM_DEAL_REASON  m_reason;            // Deal execution reason or source
   long              m_pos_id;            // The ID of the position opened, modified or closed by the deal
   
//--- Real properties
   double            m_volume;            // Deal volume
   double            m_price;             // Deal price
   double            m_commission;        // Deal commission
   double            m_swap;              // Accumulated swap when closing
   double            m_profit;            // Deal financial result
   double            m_fee;               // Fee for making a deal charged immediately after performing a deal
   double            m_sl;                // Stop Loss level
   double            m_tp;                // Take Profit level

//--- String properties
   string            m_symbol;            // Name of the symbol for which the deal is executed
   string            m_comment;           // Deal comment
   string            m_external_id;       // Deal ID in an external trading system (on the exchange)
   
//--- Additional properties
   int               m_digits;            // Symbol digits
   double            m_point;             // Symbol point
   ulong             m_ticket_tester;     // Position ticket in the tester
   long              m_pos_id_tester;     // Position ID in the tester
   
public:
//--- Set deal propertie
   void              SetTicket(const ulong ticket)             { this.m_ticket=ticket;          }
   void              SetOrder(const long order)                { this.m_order=order;            }
   void              SetTime(const datetime time)              { this.m_time=time;              }
   void              SetTimeMsc(const long value)              { this.m_time_msc=value;         }
   void              SetType(const ENUM_DEAL_TYPE type)        { this.m_type=type;              }
   void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.m_entry=entry;            }
   void              SetMagic(const long magic)                { this.m_magic=magic;            }
   void              SetReason(const ENUM_DEAL_REASON reason)  { this.m_reason=reason;          }
   void              SetPositionID(const long id)              { this.m_pos_id=id;              }
   void              SetVolume(const double volume)            { this.m_volume=volume;          }
   void              SetPrice(const double price)              { this.m_price=price;            }
   void              SetCommission(const double commission)    { this.m_commission=commission;  }
   void              SetSwap(const double swap)                { this.m_swap=swap;              }
   void              SetProfit(const double profit)            { this.m_profit=profit;          }
   void              SetFee(const double fee)                  { this.m_fee=fee;                }
   void              SetSL(const double sl)                    { this.m_sl=sl;                  }
   void              SetTP(const double tp)                    { this.m_tp=tp;                  }
   void              SetSymbol(const string symbol)            { this.m_symbol=symbol;          }
   void              SetComment(const string comment)          { this.m_comment=comment;        }
   void              SetExternalID(const string ext_id)        { this.m_external_id=ext_id;     }
   void              SetTicketTester(const ulong ticket)       { this.m_ticket_tester=ticket;   }
   void              SetPosIDTester(const long pos_id)         { this.m_pos_id_tester=pos_id;   }
   
//--- Return deal properties
   ulong             Ticket(void)                        const { return this.m_ticket;          }
   long              Order(void)                         const { return this.m_order;           }
   datetime          Time(void)                          const { return this.m_time;            }
   long              TimeMsc(void)                       const { return this.m_time_msc;        }
   ENUM_DEAL_TYPE    TypeDeal(void)                      const { return this.m_type;            }
   ENUM_DEAL_ENTRY   Entry(void)                         const { return this.m_entry;           }
   long              Magic(void)                         const { return this.m_magic;           }
   ENUM_DEAL_REASON  Reason(void)                        const { return this.m_reason;          }
   long              PositionID(void)                    const { return this.m_pos_id;          }
   double            Volume(void)                        const { return this.m_volume;          }
   double            Price(void)                         const { return this.m_price;           }
   double            Commission(void)                    const { return this.m_commission;      }
   double            Swap(void)                          const { return this.m_swap;            }
   double            Profit(void)                        const { return this.m_profit;          }
   double            Fee(void)                           const { return this.m_fee;             }
   double            SL(void)                            const { return this.m_sl;              }
   double            TP(void)                            const { return this.m_tp;              }
   string            Symbol(void)                        const { return this.m_symbol;          }
   string            Comment(void)                       const { return this.m_comment;         }
   string            ExternalID(void)                    const { return this.m_external_id;     }

   int               Digits(void)                        const { return this.m_digits;          }
   double            Point(void)                         const { return this.m_point;           }
   ulong             TicketTester(void)                  const { return this.m_ticket_tester;   }
   long              PosIDTester(void)                   const { return this.m_pos_id_tester;   }
   
//--- Compare two objects by the property specified in 'mode'
   virtual int       Compare(const CObject *node, const int mode=0) const
                       {
                        const CDeal *obj=node;
                        switch(mode)
                          {
                           case SORT_MODE_DEAL_TICKET          :  return(this.Ticket() > obj.Ticket()          ?  1  :  this.Ticket() < obj.Ticket()           ? -1  :  0);
                           case SORT_MODE_DEAL_ORDER           :  return(this.Order() > obj.Order()            ?  1  :  this.Order() < obj.Order()             ? -1  :  0);
                           case SORT_MODE_DEAL_TIME            :  return(this.Time() > obj.Time()              ?  1  :  this.Time() < obj.Time()               ? -1  :  0);
                           case SORT_MODE_DEAL_TIME_MSC        :  return(this.TimeMsc() > obj.TimeMsc()        ?  1  :  this.TimeMsc() < obj.TimeMsc()         ? -1  :  0);
                           case SORT_MODE_DEAL_TYPE            :  return(this.TypeDeal() > obj.TypeDeal()      ?  1  :  this.TypeDeal() < obj.TypeDeal()       ? -1  :  0);
                           case SORT_MODE_DEAL_ENTRY           :  return(this.Entry() > obj.Entry()            ?  1  :  this.Entry() < obj.Entry()             ? -1  :  0);
                           case SORT_MODE_DEAL_MAGIC           :  return(this.Magic() > obj.Magic()            ?  1  :  this.Magic() < obj.Magic()             ? -1  :  0);
                           case SORT_MODE_DEAL_REASON          :  return(this.Reason() > obj.Reason()          ?  1  :  this.Reason() < obj.Reason()           ? -1  :  0);
                           case SORT_MODE_DEAL_POSITION_ID     :  return(this.PositionID() > obj.PositionID()  ?  1  :  this.PositionID() < obj.PositionID()   ? -1  :  0);
                           case SORT_MODE_DEAL_VOLUME          :  return(this.Volume() > obj.Volume()          ?  1  :  this.Volume() < obj.Volume()           ? -1  :  0);
                           case SORT_MODE_DEAL_PRICE           :  return(this.Price() > obj.Price()            ?  1  :  this.Price() < obj.Price()             ? -1  :  0);
                           case SORT_MODE_DEAL_COMMISSION      :  return(this.Commission() > obj.Commission()  ?  1  :  this.Commission() < obj.Commission()   ? -1  :  0);
                           case SORT_MODE_DEAL_SWAP            :  return(this.Swap() > obj.Swap()              ?  1  :  this.Swap() < obj.Swap()               ? -1  :  0);
                           case SORT_MODE_DEAL_PROFIT          :  return(this.Profit() > obj.Profit()          ?  1  :  this.Profit() < obj.Profit()           ? -1  :  0);
                           case SORT_MODE_DEAL_FEE             :  return(this.Fee() > obj.Fee()                ?  1  :  this.Fee() < obj.Fee()                 ? -1  :  0);
                           case SORT_MODE_DEAL_SL              :  return(this.SL() > obj.SL()                  ?  1  :  this.SL() < obj.SL()                   ? -1  :  0);
                           case SORT_MODE_DEAL_TP              :  return(this.TP() > obj.TP()                  ?  1  :  this.TP() < obj.TP()                   ? -1  :  0);
                           case SORT_MODE_DEAL_SYMBOL          :  return(this.Symbol() > obj.Symbol()          ?  1  :  this.Symbol() < obj.Symbol()           ? -1  :  0);
                           case SORT_MODE_DEAL_COMMENT         :  return(this.Comment() > obj.Comment()        ?  1  :  this.Comment() < obj.Comment()         ? -1  :  0);
                           case SORT_MODE_DEAL_EXTERNAL_ID     :  return(this.ExternalID()  >obj.ExternalID()  ?  1  :  this.ExternalID()  <obj.ExternalID()   ? -1  :  0);
                           case SORT_MODE_DEAL_TICKET_TESTER   :  return(this.TicketTester()>obj.TicketTester()?  1  :  this.TicketTester()<obj.TicketTester() ? -1  :  0);
                           case SORT_MODE_DEAL_POS_ID_TESTER   :  return(this.PosIDTester() >obj.PosIDTester() ?  1  :  this.PosIDTester() <obj.PosIDTester()  ? -1  :  0);
                           default                             :  return(WRONG_VALUE);
                          }
                       }
   
//--- Constructors/destructor
                     CDeal(const ulong ticket, const string symbol) : m_ticket(ticket), m_symbol(symbol), m_ticket_tester(0), m_pos_id_tester(0)
                       { this.m_digits=(int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS); this.m_point=::SymbolInfoDouble(symbol, SYMBOL_POINT); }
                     CDeal(void) {}
                    ~CDeal(void) {}
  };

La clase prácticamente repetirá la estructura de transacción creada anteriormente. Además de las propiedades de la transacción, hemos añadido propiedades adicionales: Digits y Point del símbolo en el que se ha ejecutado la transacción. Esto simplificará la muestra de la descripción de la transacción, ya que estos datos se establecen en el constructor de la transacción justo al crear el objeto, lo cual evita la necesidad de obtener estas propiedades para cada transacción (si son necesarias) al acceder a ella.
Aquí también crearemos el método virtual Compare() para comparar dos objetos transacción: se utilizará al clasificar las listas de transacciones para encontrar la transacción requerida por la propiedad especificada.

Ahora vamos a crear la clase comercial del símbolo. La clase almacenará la lista de transacciones realizadas sobre el símbolo escrito en las propiedades del objeto, y desde ella estas transacciones serán solicitadas por el simulador para copiarlas. En general, esta clase supondrá la base para copiar las transacciones ejecutadas en la cuenta según símbolo en el simulador de estrategias:

//+------------------------------------------------------------------+
//|  Class for trading by symbol                                     |
//+------------------------------------------------------------------+
CDeal DealTmp; // Temporary deal object for searching by properties

class CSymbolTrade : public CObject
  {
private:
   int               m_index_next_deal;                  // Index of the next deal that has not yet been handled
   int               m_deals_processed;                  // Number of handled deals
protected:
   MqlTick           m_tick;                             // Tick structure
   CArrayObj         m_list_deals;                       // List of deals carried out by symbol
   CTrade            m_trade;                            // Trading class
   string            m_symbol;                           // Symbol name
public:
//--- Return the list of deals
   CArrayObj        *GetListDeals(void)                  { return(&this.m_list_deals);       }
   
//--- Set a symbol
   void              SetSymbol(const string symbol)      { this.m_symbol=symbol;             }
   
//--- (1) Set and (2) returns the number of handled deals
   void              SetNumProcessedDeals(const int num) { this.m_deals_processed=num;       }
   int               NumProcessedDeals(void)       const { return this.m_deals_processed;    }
   
//--- Add a deal to the deal array
   bool              AddDeal(CDeal *deal);
   
//--- Return the deal (1) by time in seconds, (2) by index in the list,
//--- (3) opening deal by position ID, (4) current deal in the list
   CDeal            *GetDealByTime(const datetime time);
   CDeal            *GetDealByIndex(const int index);
   CDeal            *GetDealInByPosID(const long pos_id);
   CDeal            *GetDealCurrent(void);
   
//--- Return (1) the number of deals in the list, (2) the index of the current deal in the list
   int               DealsTotal(void)              const { return this.m_list_deals.Total(); }
   int               DealCurrentIndex(void)        const { return this.m_index_next_deal;    }
   
//--- Return (1) symbol and (2) object description
   string            Symbol(void)                  const { return this.m_symbol;             }
   string            Description(void) const
                       {
                        return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() );
                       }

//--- Return the current (1) Bid and (2) Ask price, time in (3) seconds, (4) milliseconds
   double            Bid(void);
   double            Ask(void);
   datetime          Time(void);
   long              TimeMsc(void);
   
//--- Open (1) long, (2) short position, (3) close a position by ticket
   ulong             Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment);
   ulong             Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment);
   bool              ClosePos(const ulong ticket);

//--- Return the result of comparing the current time with the specified one
   bool              CheckTime(const datetime time)      { return(this.Time()>=time);        }
//--- Sets the index of the next deal
   void              SetNextDealIndex(void)              { this.m_index_next_deal++;         }
   
//--- OnTester handler. Returns the number of deals processed by the tester.
   double            OnTester(void)
                       {
                        ::PrintFormat("Symbol %s: Total deals: %d, number of processed deals: %d", this.Symbol(), this.DealsTotal(), this.NumProcessedDeals());
                        return this.m_deals_processed;
                       }

//--- Compares two objects to each other (comparison by symbol only)
   virtual int       Compare(const CObject *node, const int mode=0) const
                       {
                        const CSymbolTrade *obj=node;
                        return(this.Symbol()>obj.Symbol() ? 1 : this.Symbol()<obj.Symbol() ? -1 : 0);
                       }
//--- Constructors/destructor
                     CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {}
                     CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0)
                       {
                        this.m_trade.SetMarginMode();
                        this.m_trade.SetTypeFillingBySymbol(this.m_symbol);
                       }
                    ~CSymbolTrade(void) {}
  };

Veamos algunos métodos.

  • SetNumProcessedDeals() y NumProcessedDeals() establecen y devuelven el número de transacciones históricas ya procesadas por el simulador a partir de la lista de transacciones recibidas del archivo. Los métodos son necesarios para controlar que el procesamiento de las transacciones históricas sea correcto y obtener las estadísticas finales del número de transacciones procesadas por el simulador;
  • GetDealCurrent() devuelve el puntero a la transacción histórica actual que debe procesar el simulador y la etiqueta como procesada;
  • DealCurrentIndex() devuelve el índice de la transacción histórica seleccionada para ser procesada por el simulador en el momento actual;
  • SetNextDealIndex() establece el índice de la siguiente transacción que procesará el simulador una vez finalizado el procesamiento de la transacción histórica actual. Como todas las transacciones históricas de la lista están clasificadas por tiempo en milisegundos, este será el ajuste del índice de la siguiente transacción después de que el simulador termine de procesar la anterior. Así, seleccionaremos secuencialmente todas las transacciones de la historia que serán procesadas por el simulador en el momento que llegue la hora registrada en las propiedades de la transacción actualmente seleccionada;
  • CheckTime() comprueba el momento en el que llega en el simulador la hora registrada en las propiedades de la transacción histórica actual. La lógica es la siguiente: hay una transacción seleccionada que debe procesarse en el simulador. Mientras la hora en el simulador sea menor que la hora registrada en la transacción, no haremos nada, simplemente pasaremos al siguiente tick. Tan pronto como la hora en el simulador sea igual o mayor que la hora en la transacción seleccionada actualmente (la hora en el simulador puede no coincidir con la hora en la transacción, por lo que se comprobará que la hora sea "mayor que"), la transacción será procesada por el simulador dependiendo de su tipo y de la forma en que la posición es modificada por la transacción. A continuación, esta transacción se etiquetará como procesada, se establecerá el índice de la siguiente transacción y el proceso de tiempo de espera controlado por este método proseguirá, pero para la siguiente transacción:
  • Luego el manejador estándar OnTester() del asesor experto llamará al manejador OnTester(), mostrará en el diario de registro el nombre del símbolo, el número de transacciones históricas y procesadas por el simulador, y retornará el número de transacciones procesadas por el símbolo del objeto de transacción;

La clase tiene dos constructores, uno por defecto y otro paramétrico.

En el constructor paramétrico, en los parámetros formales, se transmitirá el nombre del símbolo del objeto comercial que se establece al crearse este; en el objeto comercial de la clase CTrade se establecerán el modo de cálculo del margen de acuerdo con los ajustes de la cuenta actual, y el tipo de orden por ejecución, de acuerdo con los ajustes del símbolo del objeto comercial

//--- Constructors/destructor
                     CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {}
                     CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0)
                       {
                        this.m_trade.SetMarginMode();
                        this.m_trade.SetTypeFillingBySymbol(this.m_symbol);
                       }

Método que añade una transacción al array de transacciones:

//+------------------------------------------------------------------+
//| CSymbolTrade::Add a trade to the trades array                    |
//+------------------------------------------------------------------+
bool CSymbolTrade::AddDeal(CDeal *deal)
  {
//--- If the list already contains a deal with the deal ticket passed to the method, return 'true'
   this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET);
   if(this.m_list_deals.Search(deal)>WRONG_VALUE)
      return true;
   
//--- Add a pointer to the deal to the list in sorting order by time in milliseconds
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   if(!this.m_list_deals.InsertSort(deal))
     {
      ::PrintFormat("%s: Failed to add deal", __FUNCTION__);
      return false;
     }
//--- All is successful
   return true;
  }

Al método se le transmitirá el puntero al objeto de transacción. Si en la lista ya hay una transacción con ese ticket, simplemente se retornará true. En caso contrario, la lista se clasificará según la hora de las transacciones en milisegundos, y la transacción se añadirá a la lista en el orden de clasificación según la hora en ms.

Método que retorna el puntero a un objeto de transacción según el tiempo en segundos:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the deal object by time in seconds          |
//+------------------------------------------------------------------+
CDeal* CSymbolTrade::GetDealByTime(const datetime time)
  {
   DealTmp.SetTime(time);
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   int index=this.m_list_deals.Search(&DealTmp);
   return this.m_list_deals.At(index);
  }

Al método se le transmitirá la hora buscada. Para el objeto de transacción temporal, estableceremos la hora transmitida al método; la lista se clasificará según el tiempo en milisegundos, y se buscará el índice de la transacción con una hora igual a la hora transmitida al método (establecida en el objeto temporal). A continuación, se devolverá el puntero a la transacción de la lista en el índice encontrado. Si la transacción con esa hora no está en la lista, el índice será igual a -1, y se devolverá NULL desde la lista.

Curiosamente, la transacción se buscará según la hora en segundos, pero clasificaremos la lista según el tiempo en milisegundos. Las pruebas han demostrado que si la lista se clasifica también en segundos, algunas transacciones no se encuentran en ella, aunque sin duda están ahí. Lo más probable es que esto se deba al hecho de que hay múltiples transacciones en un segundo con el tiempo en milisegundos. Y se retorna un puntero a una transacción procesada anteriormente, ya que varias transacciones tienen el mismo tiempo en segundos.

Método que retorna el puntero a una transacción de apertura según el identificador de la posición:

//+------------------------------------------------------------------+
//|CSymbolTrade::Return the opening trade by position ID             |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealInByPosID(const long pos_id)
  {
   int total=this.m_list_deals.Total();
   for(int i=0; i<total; i++)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL || deal.PositionID()!=pos_id)
         continue;
      if(deal.Entry()==DEAL_ENTRY_IN)
         return deal;
     }
   return NULL;
  }

Al método se transmitirá el identificador de la posición cuya transacción de apertura deba encontrarse. A continuación, en un ciclo a través de la lista de transacciones, obtendremos la transacción cuyo identificador de posición sea igual al transmitido al método y devolveremos el puntero a la transacción cuyo método de cambio de posición sea igual a "Entrada en el mercado" (DEAL_ENTRY_IN).

Método que devuelve el puntero al objeto de transacción según el índice de la lista:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the deal object by index in the list        |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealByIndex(const int index)
  {
   return this.m_list_deals.At(index);
  }

Simplemente devolveremos el puntero al objeto de la lista según el índice transmitido al método. Si el índice es incorrecto, se devolverá NULL.

Método que devuelve el puntero a la transacción a la que apunta el índice de la transacción actual:

//+------------------------------------------------------------------+
//| Return the deal pointed to by the current deal index             |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealCurrent(void)
  {
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   return this.GetDealByIndex(this.m_index_next_deal);
  }

La lista de transacciones se clasificará según el tiempo en milisegundos, y se devolverá el puntero a la transacción cuyo índice se escribe en la variable de clase m_index_next_deal.

Método que devuelve el precio Bid actual:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current Bid price                       |
//+------------------------------------------------------------------+
double CSymbolTrade::Bid(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.bid;
  }

Obtenemos los datos del último tick en la estructura de precios m_tick y devolvemos desde ella el precio Bid.

Método que devuelve el precio Ask actual:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current Ask price                       |
//+------------------------------------------------------------------+
double CSymbolTrade::Ask(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.ask;
  }

Obtenemos los datos del último tick en la estructura de precios m_tick y devolvemos el precio Ask a partir de ella.

Método que devuelve la hora actual en segundos:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current time in seconds                 |
//+------------------------------------------------------------------+
datetime CSymbolTrade::Time(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.time;
  }

Obtenemos los datos del último tick en la estructura de precios m_tick y devolvemos la hora a partir de ella.

Método que devuelve la hora actual en milisegundos:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current time in milliseconds            |
//+------------------------------------------------------------------+
long CSymbolTrade::TimeMsc(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.time_msc;
  }

Obtenemos los datos del último tick en la estructura de precios m_tick y devolvemos el tiempo en milisegundos a partir de ella.

Método que abre una posición larga:

//+------------------------------------------------------------------+
//| CSymbolTrade::Open a long position                               |
//+------------------------------------------------------------------+
ulong CSymbolTrade::Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Buy(volume, this.m_symbol, 0, sl, tp, comment))
     {
      return 0;
     }
   return this.m_trade.ResultOrder();
  }

Luego transmitiremos al método los parámetros de la posición larga que se va a abrir; en el objeto comercial, estableceremos el número mágico de la posición requerida y enviaremos una orden para abrir una posición larga con los parámetros especificados. Si se da un error al abrir una posición, se retornará cero; en caso de éxito, se retornará el ticket de la orden en base a la cual se ha abierto la posición.

Método que abre una posición corta:

//+------------------------------------------------------------------+
//| CSymbolTrade::Open a short position                              |
//+------------------------------------------------------------------+
ulong CSymbolTrade::Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Sell(volume, this.m_symbol, 0, sl, tp, comment))
     {
      return 0;
     }
   return this.m_trade.ResultOrder();
  }

Será similar al método anterior, pero se abrirá una posición corta.

Método que cierra una posición según el ticket:

//+------------------------------------------------------------------+
//| CSymbolTrade::Close position by ticket                           |
//+------------------------------------------------------------------+
bool CSymbolTrade::ClosePos(const ulong ticket)
  {
   return this.m_trade.PositionClose(ticket);
  }

Retornará el resultado de la llamada al método PositionClose() de un objeto comercial de la clase CTrade.

La clase comercial del símbolo ya está lista. Ahora vamos a implementarla en el asesor experto para trabajar con transacciones históricas guardadas en el archivo.


Analizaremos en el simulador la historia comercial desde un archivo

Primero pasaremos al archivo del asesor experto \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 e insertaremos un objeto temporal de la recién creada clase comercial del símbolo: será necesario para buscar el objeto requerido en la lista donde se almacenarán los punteros a dichos objetos:

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;

Tenemos un array de transacciones históricas a partir del cual podremos crear una lista de objetos comerciales, dentro de la cual habrá listas de transacciones pertenecientes al símbolo del objeto. El array de transacciones almacenará las estructuras que describen las transacciones. Como el objeto comercial contendrá listas de objetos de transacción, necesitaremos crear una función que cree un nuevo objeto de transacción y rellene las propiedades de la transacción a partir de los campos de la estructura que describe la transacción:

//+------------------------------------------------------------------+
//| Create a deal object from the structure                          |
//+------------------------------------------------------------------+
CDeal *CreateDeal(SDeal &deal_str)
  {
//--- If failed to create an object, inform of the error in the journal and return NULL
   CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol());
   if(deal==NULL)
     {
      PrintFormat("%s: Error. Failed to create deal object");
      return NULL;
     }
//--- fill in the deal properties from the structure fields
   deal.SetOrder(deal_str.order);               // Order the deal was based on
   deal.SetPositionID(deal_str.pos_id);         // Position ID
   deal.SetTimeMsc(deal_str.time_msc);          // Time in milliseconds
   deal.SetTime(deal_str.time);                 // Time
   deal.SetVolume(deal_str.volume);             // Volume
   deal.SetPrice(deal_str.price);               // Price
   deal.SetProfit(deal_str.profit);             // Profit
   deal.SetCommission(deal_str.commission);     // Deal commission
   deal.SetSwap(deal_str.swap);                 // Accumulated swap when closing
   deal.SetFee(deal_str.fee);                   // Fee for making a deal charged immediately after performing a deal
   deal.SetSL(deal_str.sl);                     // Stop Loss level
   deal.SetTP(deal_str.tp);                     // Take Profit level
   deal.SetType(deal_str.type);                 // Type
   deal.SetEntry(deal_str.entry);               // Position change method
   deal.SetReason(deal_str.reason);             // Deal execution reason or source
   deal.SetMagic(deal_str.magic);               // EA ID
   deal.SetComment(deal_str.Comment());         // Deal comment
   deal.SetExternalID(deal_str.ExternalID());   // Deal ID in an external trading system (on the exchange)
//--- Return the pointer to a created object
   return deal;
  }

Acto seguido, transmitiremos a la función la estructura de la transacción, crearemos un nuevo objeto de transacción y rellenaremos sus propiedades con los valores de los campos de la estructura.
La función retornará el puntero al objeto recién creado. Si se produce un error al crear el objeto, se devolverá NULL.

Ahora escribiremos una función que creará una lista de objetos comerciales de los símbolos:

//+------------------------------------------------------------------+
//| Create an array of used symbols                                  |
//+------------------------------------------------------------------+
bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
   bool res=true;                      // result
   int total=(int)array_deals.Size();  // total number of deals in the array
   
//--- if the deal array is empty, return 'false'
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
   
//--- in a loop through the deal array
   CSymbolTrade *SymbolTrade=NULL;
   for(int i=0; i<total; i++)
     {
      //--- get the next deal and, if it is neither buy nor sell, move on to the next one
      SDeal deal_str=array_deals[i];
      if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
         continue;
      
      //--- find a trading object in the list whose symbol is equal to the deal symbol
      string symbol=deal_str.Symbol();
      SymbTradeTmp.SetSymbol(symbol);
      list_symbols.Sort();
      int index=list_symbols.Search(&SymbTradeTmp);
      
      //--- if the index of the desired object in the list is -1, there is no such object in the list
      if(index==WRONG_VALUE)
        {
         //--- we create a new trading symbol object and, if creation fails,
         //--- add 'false' to the result and move on to the next deal
         SymbolTrade=new CSymbolTrade(symbol);
         if(SymbolTrade==NULL)
           {
            res &=false;
            continue;
           }
         //--- if failed to add a symbol trading object to the list,
         //--- delete the newly created object, add 'false' to the result
         //--- and we move on to the next deal
         if(!list_symbols.Add(SymbolTrade))
           {
            delete SymbolTrade;
            res &=false;
            continue;
           }
        }
      //--- otherwise, if the trading object already exists in the list, we get it by index
      else
        {
         SymbolTrade=list_symbols.At(index);
         if(SymbolTrade==NULL)
            continue;
        }
         
      //--- if the current deal is not yet in the list of deals of the symbol trading object
      if(SymbolTrade.GetDealByTime(deal_str.time)==NULL)
        {
         //--- create a deal object according to its sample structure
         CDeal *deal=CreateDeal(deal_str);
         if(deal==NULL)
           {
            res &=false;
            continue;
           }
         //--- add the result of adding the deal object to the list of deals of a symbol trading object to the result value
         res &=SymbolTrade.AddDeal(deal);
        }
     }
//--- return the final result of creating trading objects and adding deals to their lists
   return res;
  }

La lógica de la función se detalla en los comentarios. Después analizaremos cada transacción subsiguiente en un ciclo a través de la lista de transacciones históricas. Comprobaremos su símbolo y, si aún no existe ningún objeto comercial para este símbolo, crearemos un nuevo objeto comercial y lo guardaremos en la lista. Si ya existe uno, simplemente obtendremos de la lista el puntero al objeto comercial del símbolo de la transacción. A continuación, comprobaremos de la misma manera la presencia de dicha transacción en la lista de transacciones del objeto comercial y la añadiremos a la lista si está ausente. Como resultado de pasar en un ciclo por todas las transacciones históricas, obtendremos una lista de objetos comerciales por símbolos que contendrá listas de transacciones pertenecientes al símbolo del objeto.

La lista de objetos comerciales podrá visualizarse en el diario usando la función:

//+------------------------------------------------------------------+
//| Display a list of symbol trading objects in the journal          |
//+------------------------------------------------------------------+
void SymbolsArrayPrint(CArrayObj *list_symbols)
  {
   int total=list_symbols.Total();
   if(total==0)
      return;
   Print("Symbols used in trading:");
   for(int i=0; i<total; i++)
     {
      string index=StringFormat("% 3d", i+1);
      CSymbolTrade *obj=list_symbols.At(i);
      if(obj==NULL)
         continue;
      PrintFormat("%s. %s",index, obj.Description());
     }
  }

En un ciclo a través de la lista de objetos comerciales de los símbolos obtendremos el siguiente objeto y mostraremos su descripción en el diario. En el diario se verá más o menos así:

Symbols used in trading:
  1. AUDUSD trade object. Total deals: 218
  2. EURJPY trade object. Total deals: 116
  3. EURUSD trade object. Total deals: 524
  4. GBPUSD trade object. Total deals: 352
  5. NZDUSD trade object. Total deals: 178
  6. USDCAD trade object. Total deals: 22
  7. USDCHF trade object. Total deals: 250
  8. USDJPY trade object. Total deals: 142
  9. XAUUSD trade object. Total deals: 118

Ahora ya tenemos un objeto de la clase de transacción. Luego añadiremos una función que devolverá una descripción de la transacción:

//+------------------------------------------------------------------+
//| Return deal description                                          |
//+------------------------------------------------------------------+
string DealDescription(CDeal *deal, const int index)
  {
   string indexs=StringFormat("% 5d", index);
   if(deal.TypeDeal()!=DEAL_TYPE_BALANCE)
      return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                          indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                          deal.PositionID(), deal.Symbol(), deal.Magic(), deal.Digits(), deal.Price(),
                          TimeToString(deal.Time(), TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.Digits(), deal.SL(), deal.Digits(), deal.TP()));
   else
      return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                          indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                          deal.Profit(), AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.Time())));
  }

Esta función repetirá por entero la lógica de la propia función exacta que devuelve una descripción de la estructura de la transacción. Pero aquí transmitiremos a la función el puntero al objeto de transacción en lugar de la estructura de la transacción.

Vamos a dar forma ahora al manejador OnInit() hasta su conclusión lógica.

Para ello, añadiremos el procesamiento del inicio del asesor experto en el simulador; luego crearemos una lista de objetos comerciales y la dirección de cada símbolo utilizado en el comercio para cargar su historia y abrir las ventanas de los gráficos de estos símbolos en el simulador:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- If the EA is not running in the tester
   if(!MQLInfoInteger(MQL_TESTER))
     {
      //--- prepare a file with all historical deals
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- print all deals in the journal after loading them from the file 
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- get the first balance deal, create the message text and display it using Alert
      SDeal    deal=ExtArrayDeals[0];
      long     leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
      double   start_money=deal.profit;
      datetime first_time=deal.time;
      string   start_time=TimeToString(deal.time, TIME_DATE);
      string   message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage);
      
      //--- notify via alert of the recommended parameters of the strategy tester for starting the test
      Alert(message);
     }
//--- The EA has been launched in the tester
   else
     {
      //--- read data from the file into the array
      ulong file_size=0;
      ArrayResize(ExtArrayDeals, 0);
      if(!FileReadDealsToArray(ExtArrayDeals, file_size))
        {
         PrintFormat("Failed to read file \"%s\". Error %d", FILE_NAME, GetLastError());
         return(INIT_FAILED);
        }
         
      //--- report the number of bytes read from the file and writing the deals array in the journal.
      PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, ExtArrayDeals.Size());
     }
     
//--- Create a list of trading objects by symbols from the array of historical deals
   if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols))
     {
      Print("Errors found while creating symbol list");
      return(INIT_FAILED);
     }
//--- Print the created list of deals in the journal
   SymbolsArrayPrint(&ExtListSymbols);
   
//--- Access each symbol to start downloading historical data
//--- and opening charts of traded symbols in the strategy tester
   datetime array[];
   int total=ExtListSymbols.Total();

   for(int i=0; i<total; i++)
     {
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      CopyTime(obj.Symbol(), PERIOD_CURRENT, 0, 1, array);
     }
     
//--- All is successful
   return(INIT_SUCCEEDED);
  }

En el manejador OnDeinit(), el asesor experto eliminará los arrays y listas creadas:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- clear the created lists and arrays
   ExtListSymbols.Clear();
   ArrayFree(ExtArrayDeals);
  }

En el manejador OnTick() del asesor experto, en el simulador, procesaremos la lista de transacciones del archivo:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- work only in the strategy tester
   if(!MQLInfoInteger(MQL_TESTER))
      return;
      
//---  Handle the list of deals from the file in the tester
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }

Veamos más detenidamente esta función. En general, la lógica de procesamiento de las transacciones históricas se imaginó originalmente de la siguiente manera:

  1. obtenemos la hora de un tick,
  2. obtenemos la transacción con esa hora,
  3. procesamos la transacción en el simulador.

A primera vista, el esquema, aparentemente sencillo y lógico, fracasó por completo cuando se puso en práctica. Lo que ocurre es que en el simulador, la hora del tick no siempre coincide con la hora de la transacción. Incluso tomando el tiempo en milisegundos. Y al realizar las pruebas con todos los ticks, basadas en ticks reales tomados del mismo servidor, se perdían las transacciones. Conocemos la hora del tick, sabemos con certeza que hay una transacción a esta hora, pero el simulador no lo ve: no existe ningún tick con la misma hora que la transacción. Pero hay un tick con una hora anterior y un tick con una hora posterior al momento de la transacción. Como consecuencia, la lógica puede construirse no sobre ticks con su hora, sino sobre transacciones:

  1. Las transacciones se clasificarán en la lista según la hora de su aparición en milisegundos. Ahora estableceremos el índice de la primera transacción como el índice de la transacción actual,
  2. luego seleccionaremos una transacción según el índice de la transacción actual y obtendremos su hora;
  3. y esperaremos un tick con esa hora:
    1. si la hora del tick es menor que la hora de la transacción, esperaremos al siguiente tick,
    2. si la hora del tick es igual o mayor que la hora de la transacción, procesaremos la transacción, registraremos en ella el hecho de que ya ha sido procesada, y estableceremos el índice de la siguiente transacción como el índice de la actual;
  4. hasta que finalice la prueba, repetiremos desde el paso 2.

Este esquema nos permitirá esperar el momento de cada transacción posterior y ejecutarla en el simulador. Al mismo tiempo, no prestaremos atención al precio de la transacción: nos limitaremos a copiar las transacciones a medida que se produzcan. Incluso si la hora de tick en el simulador ya es un poco más tarde que en el comercio real, no pasará nada. La clave está en copiar las transacciones. El hecho de que la transacción ya haya sido procesada por el simulador supondrá un valor distinto a cero en la propiedad "ticket de la posición en el simulador" de la transacción. Si este valor es igual a cero, significará que esta transacción aún no se ha procesado en el simulador. Tras darse la ejecución de esta transacción en el simulador, en esta propiedad suya se introducirá el ticket de la posición a la que pertenece esta transacción en el simulador.

Ahora escribiremos una función que implementará la lógica descrita anteriormente:

//+------------------------------------------------------------------+
//| Trading by history                                               |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // number of trading objects in the list
   
//--- in a loop by all symbol trading objects
   for(int i=0; i<total; i++)
     {
      //--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- sort the deal by magic number and symbol
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- sort the deal by type (only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>0)
         continue;
      
      //--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- open a position by deal type
         double sl=0;
         double tp=0;
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- if a position is opened (we received its ticket)
         if(ticket>0)
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- get the position ID in the tester and write it to the properties of the deal object
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- in case of a market exit deal
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- get a deal a newly opened position is based on
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- get the position ticket in the tester from the properties of the opening deal
         //--- if the ticket is zero, then most likely the position in the tester is already closed
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- if the position is closed by ticket
         if(obj.ClosePos(ticket_tester))
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- if a ticket is now set in the deal object, then the deal has been successfully handled -
      //--- set the deal index in the list to the next deal
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

Este código copiará completamente la transacción original realizada en la cuenta cuyas transacciones están registradas en el archivo. Todas las posiciones se abrirán sin órdenes stop. Es decir, los valores de StopLoss y TakeProfit no se copiarán de las transacciones reales en los métodos de apertura de posiciones. Esto facilitará el seguimiento de las transacciones, ya que las transacciones de cierre también estarán en la lista, y el simulador las calculará independientemente de cómo se haya cerrado la posición: por StopLoss o TakeProfit.

Vamos a compilar el asesor experto y a ejecutarlo en un gráfico. Como resultado, el archivo HistoryDealsData.bin se creará en la carpeta común de los terminales cliente, en una ruta como "C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files", en la subcarpeta TradingByHistoryDeals, mostrándose a continuación en el gráfico una alerta con un mensaje sobre la configuración deseada del simulador:

Ahora vamos a ejecutar el asesor experto en el simulador seleccionando el rango de fechas especificado, el depósito inicial y el apalancamiento en la configuración del simulador:

Haremos la prueba con todos los símbolos y números mágicos negociados:

Resulta que todas las negociaciones nos han aportado menos 550 dólares. Me pregunto qué habría sucedido de establecer órdenes stop diferentes.

Vamos a comprobarlo.


Ajustamos las órdenes stop

Vamos a guardar el asesor experto en la misma carpeta \MQL5\Experts\TradingByHistoryDeals\ con el nuevo nombre TradingByHistoryDeals_SLTP.mq5.

A continuación, añadiremos la enumeración de los métodos de prueba y dividiremos los parámetros de entrada en grupos, añadiendo un grupo para el establecimiento de los parámetros de las órdenes stop, así como dos nuevas variables de nivel global para transmitir a través de ellas los valores de StopLoss y TakeProfit a los objetos comerciales:

//+------------------------------------------------------------------+
//|                                   TradingByHistoryDeals_SLTP.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "SymbolTrade.mqh"

enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,    /* Original trading                          */ 
   TESTING_MODE_SLTP,      /* Specified StopLoss and TakeProfit values  */ 
  };

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    group             "Strategy parameters"
input    string            InpTestedSymbol   =  "";                  /* The symbol being tested in the tester        */ 
input    long              InpTestedMagic    =  -1;                  /* The magic number being tested in the tester  */ 
sinput   bool              InpShowDataInLog  =  false;               /* Show collected data in the log               */ 

input    group             "Stops parameters"
input    ENUM_TESTING_MODE InpTestingMode    =  TESTING_MODE_ORIGIN; /* Testing Mode                                 */ 
input    int               InpStopLoss       =  300;                 /* StopLoss in points                           */ 
input    int               InpTakeProfit     =  500;                 /* TakeProfit in points                         */ 

//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;
int            ExtStopLoss;
int            ExtTakeProfit;


En el manejador OnInit(), ajustaremos y escribiremos en variables los valores de las órdenes stop establecidos por los parámetros de entrada:

int OnInit()
  {
//--- Adjust the stops
   ExtStopLoss  =(InpStopLoss<1   ? 0 : InpStopLoss);
   ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit);
   
//--- If the EA is not running in the tester

Además, añadiremos funciones que calcularán los valores correctos para los precios de StopLoss y TakeProfit relativos al StopLevel establecido para el símbolo:

//+------------------------------------------------------------------+
//| Return correct StopLoss relative to StopLevel                    |
//+------------------------------------------------------------------+
double CorrectStopLoss(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int stop_loss, const int spread_multiplier=2)
  {
   if(stop_loss==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))
      return 0;
   int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
   return
     (order_type==ORDER_TYPE_BUY ?
      NormalizeDouble(fmin(price-lv*pt, price-stop_loss*pt), dg) :
      NormalizeDouble(fmax(price+lv*pt, price+stop_loss*pt), dg)
     );
  }
//+------------------------------------------------------------------+
//| Return correct TakeProfit relative to StopLevel                  |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int take_profit, const int spread_multiplier=2)
  {
   if(take_profit==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))
      return 0;
   int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
   return
     (order_type==ORDER_TYPE_BUY ?
      NormalizeDouble(fmax(price+lv*pt, price+take_profit*pt), dg) :
      NormalizeDouble(fmin(price-lv*pt, price-take_profit*pt), dg)
     );
  }
//+------------------------------------------------------------------+
//| Return StopLevel in points                                       |
//+------------------------------------------------------------------+
int StopLevel(const string symbol_name, const int spread_multiplier)
  {
   int spread=(int)SymbolInfoInteger(symbol_name, SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(symbol_name, SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread*spread_multiplier : stop_level);
  }

Para establecer los niveles StopLoss y TakeProfit, el precio de la orden stop no deberá estar más cerca del precio actual a una distancia StopLevel. Si el StopLevel para un símbolo tiene un valor cero, entonces se utilizará un tamaño de StopLevel igual a dos (a veces tres) tamaños de dispersión establecidos para el símbolo. Estas funciones utilizarán un multiplicador doble de spread. Este valor se pasará en los parámetros formales de la función, y tendrá un valor por defecto de 2. Si es necesario cambiar el valor del multiplicador, deberemos transmitir un valor requerido diferente a las funciones al llamarlas. Las funciones devolverán los precios correctos para StopLoss y TakeProfit.

En la función TradeByHistory() escribiremos nuevos bloques de código, donde se tendrá en cuenta el modo comercial en el simulador y se establecerán los valores de StopLoss y TakeProfit en caso de que se seleccione la prueba con los valores de órdenes stop especificados. En el bloque de cierre de posiciones, estas deberán cerrarse solo cuando el tipo de prueba sea "comercio original". Si se selecciona la comprobación con los valores StopLoss y TakeProfit especificados, las transacciones de cierre deberán ignorarse: el propio simulador cerrará las posiciones de acuerdo con los valores StopLoss y TakeProfit establecidos. Lo único que deberemos hacer cuando al negociar con órdenes stop si se da el procesamiento de transacciones de cierre es marcarlas como procesadas y pasar a la siguiente transacción.

//+------------------------------------------------------------------+
//| Trading by history                                               |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // number of trading objects in the list
   
//--- in a loop by all symbol trading objects
   for(int i=0; i<total; i++)
     {
      //--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- sort the deal by magic number and symbol
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- sort the deal by type (only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>0)
         continue;
      
      //--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- set the sizes of stop orders depending on the stop setting method
         double sl=0;
         double tp=0;
         if(InpTestingMode==TESTING_MODE_SLTP)
           {
            ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
            sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
            tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
           }
         //--- open a position by deal type
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- if a position is opened (we received its ticket)
         if(ticket>0)
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- get the position ID in the tester and write it to the properties of the deal object
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- in case of a market exit deal
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- get a deal a newly opened position is based on
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- get the position ticket in the tester from the properties of the opening deal
         //--- if the ticket is zero, then most likely the position in the tester is already closed
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- if we reproduce the original trading history in the tester,
         if(InpTestingMode==TESTING_MODE_ORIGIN)
           {
            //--- if the position is closed by ticket
            if(obj.ClosePos(ticket_tester))
              {
               //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
               obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
               deal.SetTicketTester(ticket_tester);
              }
           }
         //--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped
         //--- accordingly, simply increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
         else
           {
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- if a ticket is now set in the deal object, then the deal has been successfully handled -
      //--- set the deal index in the list to the next deal
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

En el manejador OnTester() del asesor experto calcularemos y devolveremos el número total de transacciones procesadas en el simulador:

//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester(void)
  {
//--- calculate and return the total number of deals handled in the tester
   double ret=0.0;
   int total=ExtListSymbols.Total();
   for(int i=0; i<total; i++)
     {
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj!=NULL)
         ret+=obj.OnTester();
     }
   return(ret);
  }

Además, aquí, para cada objeto comercial de símbolo, se llamará a su propio manejador OnTester(), que imprimirá sus propios datos en el diario de registro. Al final de la prueba, obtendremos estos mensajes en el registro del simulador:

2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol AUDUSD: Total deals: 218, number of processed deals: 216
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol EURJPY: Total deals: 116, number of processed deals: 114
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol EURUSD: Total deals: 524, number of processed deals: 518
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol GBPUSD: Total deals: 352, number of processed deals: 350
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol NZDUSD: Total deals: 178, number of processed deals: 176
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDCAD: Total deals: 22, number of processed deals: 22
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDCHF: Total deals: 250, number of processed deals: 246
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDJPY: Total deals: 142, number of processed deals: 142
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol XAUUSD: Total deals: 118, number of processed deals: 118
2025.01.22 23:49:15.951 Core 1  final balance 3591.70 pips
2025.01.22 23:49:15.951 Core 1  OnTester result 1902

Vamos a compilar el asesor experto y a ejecutarlo con la misma configuración de prueba, pero especificando el tipo de prueba como "Specified StopLoss and TakeProfit values", estableciendo los tamaños de StopLoss y TakeProfit en 100 y 500 puntos respectivamente:

En la última prueba, al probar el comercio original, hemos obtenido unas pérdidas de 550 dólares. Ahora, sustituyendo el StopLoss de todas las posiciones por un valor de 100 pips y el TakeProfit por 500 pips, hemos obtenido un beneficio de 590 pips. Y eso con solo cambiar las órdenes stop sin mirar las particularidades de los diferentes símbolos negociados. Si elegimos diferentes tamaños de órdenes stop para cada uno de los símbolos negociados, es muy posible que el gráfico de prueba quede nivelado.


Conclusión

Hoy hemos hecho un pequeño experimento con una historia comercial del estilo "Qué pasaría si...". Creo que este tipo de experimentos pueden originar decisiones interesantes para cambiar la forma de comerciar. En el próximo artículo realizaremos otro experimento de este tipo: incluiremos varios trailings de posición. Será interesante.

Adjuntos al artículo encontrará los archivos de todos los asesor expertos y clases discutidos hoy. Podrá descargarlos, estudiarlos y experimentar por sí mismo con sus cuentas comerciales. Podrá descomprimir directamente la carpeta con el archivo en su directorio MQL5 del terminal de cliente: todos los archivos se colocarán en las subcarpetas requeridas.

Programas utilizados en el artículo:

#
Nombre
 Tipo Descripción
1
SymbolTrade.mqh
Biblioteca de clases
Biblioteca de clases de estructuras y transacciones, clase de símbolos comerciales
2
TradingByHistoryDeals.mq5
Asesor
Asesor experto para ver las transacciones y operaciones ejecutadas en la cuenta en el simulador
3
TradingByHistoryDeals_SLTP.mq5
Asesor
Asesor experto para ver y cambiar el StopLoss y el TakeProfit en el simulador de transacciones y operaciones realizadas en la cuenta.
4
MQL5.zip
Archivo
Archivo con los ficheros presentados anteriormente para desempaquetar en el directorio MQL5 del terminal cliente.


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

Archivos adjuntos |
SymbolTrade.mqh (53.86 KB)
MQL5.zip (22.3 KB)
fxsaber
fxsaber | 31 ene 2025 en 15:32
Artyom Trishkin #:

¿Cuál es el error?

Si el archivo es más pequeño que el array antes de la lectura, el tamaño del array no cambiará.

Puedes encontrarte con un error similar al utilizar ArrayCopy.
Artyom Trishkin
Artyom Trishkin | 31 ene 2025 en 15:34
fxsaber #:
Estás ignorando una buena característica

¿Cuál es la ventaja?

fxsaber
fxsaber | 31 ene 2025 en 15:36
Artyom Trishkin #:

¿Cuál es la ventaja?

Concisión y rapidez de ejecución (totalmente del lado de MQ).

fxsaber
fxsaber | 31 ene 2025 en 15:52
Artyom Trishkin #:

Muestre las estructuras estándar de autoimpresión y autorrelleno, por favor.

Casi estándar (campos MQ compartidos).
Artyom Trishkin
Artyom Trishkin | 31 ene 2025 en 16:11
fxsaber #:

En brevedad y rapidez de ejecución (totalmente del lado de MQ).

Gracias. Se echa de menos
Operar con el Calendario Económico MQL5 (Parte 3): Añadiendo filtros de divisa, importancia y tiempo Operar con el Calendario Económico MQL5 (Parte 3): Añadiendo filtros de divisa, importancia y tiempo
En este artículo, implementamos filtros en el panel del calendario económico MQL5 para refinar la visualización de eventos de noticias por divisa, importancia y hora. Primero establecemos criterios de filtrado para cada categoría y luego los integramos en el panel de control para mostrar solo los eventos relevantes. Por último, nos aseguramos de que cada filtro se actualice dinámicamente para proporcionar a los operadores información económica específica y en tiempo real.
Aprendizaje automático y Data Science (Parte 32): Mantener actualizados los modelos de IA, aprendizaje en línea Aprendizaje automático y Data Science (Parte 32): Mantener actualizados los modelos de IA, aprendizaje en línea
En el cambiante mundo del comercio, adaptarse a los cambios del mercado no es solo una opción, es una necesidad. Cada día surgen nuevos patrones y tendencias, lo que dificulta que incluso los modelos de aprendizaje automático más avanzados sigan siendo eficaces ante condiciones en constante evolución. En este artículo, exploraremos cómo mantener tus modelos relevantes y receptivos a los nuevos datos del mercado mediante el reentrenamiento automático.
Características del Wizard MQL5 que debe conocer (Parte 50): Awesome Oscillator Características del Wizard MQL5 que debe conocer (Parte 50): Awesome Oscillator
El Awesome Oscillator es otro indicador de Bill Williams que se utiliza para medir el impulso. Puede generar múltiples señales, por lo que las revisamos según un patrón, como en artículos anteriores, aprovechando las clases y el ensamblaje del Asistente MQL5 (Wizard MQL5).
Redes neuronales en el trading: Aprendizaje contextual aumentado por memoria (MacroHFT) Redes neuronales en el trading: Aprendizaje contextual aumentado por memoria (MacroHFT)
Hoy le propongo familiarizarse con el framework MacroHFT, que aplica el aprendizaje por refuerzo dependiente del contexto y la memoria para mejorar las decisiones en el comercio de criptodivisas de alta frecuencia utilizando datos macroeconómicos y agentes adaptativos.