English Русский 中文 Deutsch 日本語 Português
preview
Desarrollamos un asesor experto multidivisa (Parte 16): Efecto de diferentes historias de cotizaciones en los resultados de las pruebas

Desarrollamos un asesor experto multidivisa (Parte 16): Efecto de diferentes historias de cotizaciones en los resultados de las pruebas

MetaTrader 5Probador | 17 diciembre 2024, 11:46
247 0
Yuriy Bykov
Yuriy Bykov

Introducción

Como siempre, recordemos que en el último artículo empezamos a preparar el asesor experto multidivisa en desarrollo para negociar en una cuenta real. Como parte del proceso de preparación, hemos añadido soporte para diferentes nombres de instrumentos comerciales, la finalización automática de las transacciones si deseamos cambiar la configuración de las estrategias comerciales y la reanudación correcta del asesor experto después de un reinicio por diversas razones.

Las actividades de preparación no terminan aquí. Hemos esbozado algunos pasos necesarios más, a los que regresaremos más adelante. Hoy analizaremos un aspecto muy importante: cómo garantizar resultados similares del asesor experto desarrollado en diferentes brókeres. Es bien sabido que las cotizaciones de los instrumentos comerciales en distintos brókeres, aunque similares, no son idénticas. Por lo tanto, al probar y optimizar las cotizaciones simples, seleccionamos los parámetros óptimos para ellas. Obviamente, esperamos que cuando empecemos a negociar con otras cotizaciones, sus diferencias con las cotizaciones usadas para las pruebas sean insignificantes y, en consecuencia, las diferencias en los resultados de las transacciones también lo sean. 

No obstante, se trata de una cuestión demasiado importante para dejarla sin un examen detallado. Así que hoy analizaremos el comportamiento de nuestro asesor experto cuando este se pone a prueba con las cotizaciones de diferentes brókeres.


Comparando los resultados

En primer lugar, ejecutaremos nuestro asesor experto con las cotizaciones del servidor MetaQuotes-Demo. La primera ejecución la hemos hecho con el gestor de riesgos activado. Sin embargo, de cara al futuro, podemos decir que con otras cotizaciones el gestor de riesgos ha terminado de negociar mucho antes del final del intervalo de prueba, así que lo desactivaremos para obtener una imagen completa. De este modo podemos garantizar una comparación más justa de los resultados. Esto es lo que tenemos:


Fig. 1. Resultados de las pruebas con las cotizaciones del servidor MetaQuotes-Demo sin gestor de riesgos

Ahora conectaremos el terminal al servidor real de otro bróker y empezaremos a probar de nuevo el asesor experto con los mismos parámetros:

Fig. 2. Resultados de la prueba con cotizaciones de servidores reales de otro bróker sin gestor de riesgos

Y aquí vemos un giro inesperado. La cuenta se ha vaciado totalmente durante el primer año incompleto de los dos años de funcionamiento. Vamos a intentar comprender las razones de este comportamiento, para saber si es posible corregir esta situación de alguna manera.


Buscando la causa

Vamos a guardar los informes de los simuladores para las pasadas realizadas como archivos XML, a abrirlos y encontrar el lugar donde comienza la lista de los transacciones realizadas. Luego colocaremos las ventanas de los informes abiertos de forma que podamos ver simultáneamente las partes superiores de las listas de transacciones de ambos informes:

Fig. 3. Partes superiores de las listas de transacciones realizadas por el asesor experto durante las pruebas con cotizaciones de diferentes servidores

Incluso por las primeras líneas de los informes, podemos ver que las posiciones se han abierto en diferentes momentos. Por ello, las diferencias en las cotizaciones para los mismos momentos de tiempo en distintos servidores, si las ha habido, probablemente no hayan tenido una influencia tan destructiva como las distintas horas de apertura.

Veamos dónde definen nuestras estrategias los momentos de apertura de posiciones. Debemos buscar esto en el archivo que implementa la clase de instancia única de la estrategia comercial SimpleVolumesStrategy.mqh. Si miramos en el código, podemos encontrar rápidamente el método SignalForOpen() que retorna la señal de apertura:

//+------------------------------------------------------------------+
//| Signal for opening pending orders                                |
//+------------------------------------------------------------------+
int CSimpleVolumesStrategy::SignalForOpen() {
// By default, there is no signal
   int signal = 0;

// Copy volume values from the indicator buffer to the receiving array
   int res = CopyBuffer(m_iVolumesHandle, 0, 0, m_signalPeriod, m_volumes);

// If the required amount of numbers have been copied
   if(res == m_signalPeriod) {
      // Calculate their average value
      double avrVolume = ArrayAverage(m_volumes);

      // If the current volume exceeds the specified level, then
      if(m_volumes[0] > avrVolume * (1 + m_signalDeviation + m_ordersTotal * m_signaAddlDeviation)) {
         // if the opening price of the candle is less than the current (closing) price, then 
         if(iOpen(m_symbol, m_timeframe, 0) < iClose(m_symbol, m_timeframe, 0)) {
            signal = 1; // buy signal
         } else {
            signal = -1; // otherwise, sell signal
         }
      }
   }

   return signal;
}

Podemos ver que la señal de apertura está determinada por los valores de los volúmenes de ticks para el instrumento comercial actual. Los precios (actuales o pasados) no participan en la formación de la señal de apertura. Siendo más concretos, su participación está presente ya después de determinar que una posición deba abrirse y afecta solo a la dirección de la apertura. Por consiguiente, parece que la explicación se encuentra en las fuertes diferencias de los valores de volumen de los ticks recibidos de distintos servidores.

Esto es muy posible, porque para que los diferentes brókeres coincidan visualmente con los gráficos de precios de velas, será suficiente dar solo cuatro ticks correctos por minuto para construir los precios Open, Close, High y Low en la vela M1 de periodo más bajo. No importa cuántos ticks intermedios haya en los que el precio haya estado dentro de los límites especificados entre Low y High. Así, el número de ticks almacenados en la historia y su distribución temporal dentro de una misma vela dependerán exclusivamente del bróker, que es libre de fijar los parámetros que más le convengan. No debemos olvidar que incluso en un mismo bróker los servidores para las cuentas demo y para las cuentas reales pueden no mostrar exactamente la misma imagen.

Si realmente es eso, quizá podamos salvar ese obstáculo. Pero para poner en práctica una solución de este tipo, deberemos asegurarnos primero de identificar correctamente la causa de las discrepancias observadas; así, nuestros esfuerzos no serán baldíos.


Trazando el camino

Para comprobar nuestra hipótesis, necesitaremos las siguientes herramientas:

  • Almacenamiento de la historia. Vamos a añadir a nuestro asesor experto la posibilidad de guardar la historia de transacciones, es decir, las posiciones de apertura y cierre, al final de la pasada en el simulador. El almacenamiento puede realizarse en un archivo o en una base de datos. Como esta herramienta solo se utilizará por ahora como herramienta auxiliar, probablemente sea más fácil utilizar el almacenamiento en un archivo. Si en el futuro queremos recurrir a ella de forma más permanente, podremos ampliarla ofreciendo la posibilidad de guardar la historia en una base de datos.

  • Reproducción del trading. Vamos a crear un nuevo asesor experto que no contendrá ninguna regla de apertura de posiciones en su interior, sino que solo reproducirá las transacciones de apertura y cierre leyéndolas de la historia guardada por otro asesor experto. Como hemos decidido guardar la historia en un archivo por ahora, este asesor experto aceptará el nombre del archivo con la historia de transacciones como parámetro de entrada, y luego leerá y ejecutará las transacciones registradas en él.

Una vez creadas estas herramientas, primero ejecutaremos nuestro EA en el simulador usando las cotizaciones del servidor MetaQuotes-Demo y guardaremos la historia de transacciones de esta pasada en un archivo. Esta será la primera pasada. A continuación, ejecutaremos un nuevo asesor experto en el simulador para reproducir el trading con las cotizaciones de otro servidor, utilizando el archivo de historia almacenado. Esta será la segunda pasada. Si las diferencias en los resultados comerciales obtenidos anteriormente se deben efectivamente a que los datos de volumen de ticks son demasiado distintos, y los precios en sí son aproximadamente los mismos, en la segunda pasada deberíamos obtener resultados similares a los de la primera.


Almacenamiento de la historia

El almacenamiento de la historia puede realizarse de muchas maneras. Podemos, por ejemplo, añadir un método a la clase CVirtualAdvisor que será llamado desde el manejador de eventos OnTester(). Pero este método nos obligará a extender una clase existente, añadiendo funcionalidad de la que en realidad podríamos prescindir. Así que, para resolver este problema en particular, mejor implementaremos una clase CExpertHistory aparte. No necesitamos crear varios objetos de esta clase, así que podremos hacerla estática, es decir, que contenga solo propiedades y métodos estáticos.

El método público principal de la clase será solo uno: Export(), los demás métodos cumplirán una función auxiliar. Luego transmitiremos dos parámetros al método Export(): el nombre del archivo para grabar la historia y la bandera de uso de la carpeta compartida de datos del terminal. El nombre de archivo por defecto puede ser una cadena vacía. En este caso, usaremos el método de ayuda GetHistoryFileName() para generar el nombre del archivo. Usando el indicador de escritura en una carpeta compartida podremos elegir dónde se guardará el archivo de la historia: en la carpeta de datos compartida o en la carpeta de datos local del terminal. Por defecto, el valor se establecerá para guardar en la carpeta compartida, ya que la apertura de la carpeta local del agente de pruebas resulta más complicada cuando se ejecuta en el simulador.

Como propiedades de la clase necesitaremos un símbolo delimitador especificado al abrir un archivo CSV para su escritura, el propio manejador del archivo abierto para que pueda ser utilizado en métodos auxiliares, y un array con los nombres de las columnas de los datos que se guardan.

//+------------------------------------------------------------------+
//| Export trade history to file                                     |
//+------------------------------------------------------------------+
class CExpertHistory {
private:
   static string     s_sep;            // Separator character
   static int        s_file;           // File handle for writing
   static string     s_columnNames[];  // Array of column names

   // Write deal history to file
   static void       WriteDealsHistory();

   // Write one row of deal history to file 
   static void       WriteDealsHistoryRow(const string &fields[]);

   // Get the first deal date
   static datetime   GetStartDate();

   // Form a file name
   static string     GetHistoryFileName();

public:
   // Export deal history
   static void       Export(
      string exportFileName = "",   // File name for export. If empty, the name is generated
      int commonFlag = FILE_COMMON  // Save the file in shared data folder
   );
};

// Static class variables
string CExpertHistory::s_sep = ",";
int    CExpertHistory::s_file;
string CExpertHistory::s_columnNames[] = {"DATE", "TICKET", "TYPE",
                                          "SYMBOL", "VOLUME", "ENTRY", "PRICE",
                                          "STOPLOSS", "TAKEPROFIT", "PROFIT",
                                          "COMMISSION", "FEE", "SWAP",
                                          "MAGIC", "COMMENT"
                                         };

En el método principal Export(), crearemos y abriremos un archivo para escribir con el nombre especificado o generado. Si el archivo puede abrirse, llamaremos al método de registro de la historia de transacciones y cerraremos el archivo.

//+------------------------------------------------------------------+
//| Export deal history                                              |
//+------------------------------------------------------------------+
void CExpertHistory::Export(string exportFileName = "", int commonFlag = FILE_COMMON) {
   // If the file name is not specified, then generate it
   if(exportFileName == "") {
      exportFileName = GetHistoryFileName();
   }

   // Open the file for writing in the desired data folder
   s_file = FileOpen(exportFileName, commonFlag | FILE_WRITE | FILE_CSV | FILE_ANSI, s_sep);

   // If the file is open,
   if(s_file > 0) {
      // Set the deal history
      WriteDealsHistory();

      // Close the file
      FileClose(s_file);
   } else {
      PrintFormat(__FUNCTION__" | ERROR: Can't open file [%s]. Last error: %d",  exportFileName, GetLastError());
   }
}

En el método GetHistoryFileName(), el nombre del archivo estará compuesto por diversos fragmentos. En primer lugar, añadiremos al inicio el nombre del EA y su versión, si se especifica en la constante __VERSION__. En segundo lugar, añadiremos la fecha de inicio y fin de la historia de transacciones. Luego determinaremos la fecha de inicio según la fecha de la primera transacción de la historia llamando al método GetStartDate(). La fecha de finalización vendrá determinada por la hora actual, ya que la exportación de la historia se realizará una vez finalizada la pasada en el simulador. Es decir, la hora actual en el momento de la llamada al método de almacenamiento de la historia será exactamente la hora de finalización de la prueba. En tercer lugar, añadiremos al nombre del archivo los valores de algunas características de la pasada: balance inicial, balance final, reducción y ratio de Sharpe.

Si el nombre es demasiado largo, lo acortaremos a una longitud válida y añadiremos la extensión ".history.csv".

//+------------------------------------------------------------------+
//| Form the file name                                               |
//+------------------------------------------------------------------+
string CExpertHistory::GetHistoryFileName() {
   // Take the EA name
   string fileName = MQLInfoString(MQL_PROGRAM_NAME);

   // If a version is specified, add it
#ifdef __VERSION__
   fileName += "." + __VERSION__;
#endif

   fileName += " ";

   // Add the history start and end date
   fileName += "[" + TimeToString(GetStartDate(), TIME_DATE);
   fileName += " - " + TimeToString(TimeCurrent(), TIME_DATE) + "]";

   fileName += " ";

   // Add some statistical characteristics
   fileName += "[" + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT) + TesterStatistics(STAT_PROFIT), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_EQUITY_DD_RELATIVE), 0);
   fileName += ", " + DoubleToString(TesterStatistics(STAT_SHARPE_RATIO), 2);
   fileName += "]";

   // If the name is too long, shorten it
   if(StringLen(fileName) > 255 - 13) {
      fileName = StringSubstr(fileName, 0, 255 - 13);
   }

   // Add extension
   fileName += ".history.csv";

   return fileName;
}

En el método para escribir la historia en un archivo, primero escribiremos el encabezado, es decir, una cadena con los nombres de las columnas de datos. A continuación, seleccionaremos toda la historia disponible y empezaremos a enumerar todas las transacciones. Para cada transacción, obtendremos sus propiedades. Si se trata de una transacción de apertura o de balance, formaremos un array con los valores de todas las propiedades de la transacción y lo transmitiremos al método WriteDealsHistoryRow() para escribir una transacción individual.

//+------------------------------------------------------------------+
//| Write deal history to file                                       |
//+------------------------------------------------------------------+
void CExpertHistory::WriteDealsHistory() {
   // Write a header with column names
   WriteDealsHistoryRow(s_columnNames);

   // Variables for each deal properties
   uint     total;
   ulong    ticket = 0;
   long     entry;
   double   price;
   double   sl, tp;
   double   profit, commission, fee, swap;
   double   volume;
   datetime time;
   string   symbol;
   long     type, magic;
   string   comment;

   // Take the entire history
   HistorySelect(0, TimeCurrent());
   total = HistoryDealsTotal();

   // For all deals
   for(uint i = 0; i < total; i++) {
      // If the deal is successfully selected,
      if((ticket = HistoryDealGetTicket(i)) > 0) {
         // Get the values of its properties
         time  = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
         type  = HistoryDealGetInteger(ticket, DEAL_TYPE);
         symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
         volume = HistoryDealGetDouble(ticket, DEAL_VOLUME);
         entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
         price = HistoryDealGetDouble(ticket, DEAL_PRICE);
         sl = HistoryDealGetDouble(ticket, DEAL_SL);
         tp = HistoryDealGetDouble(ticket, DEAL_TP);
         profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
         commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION);
         fee = HistoryDealGetDouble(ticket, DEAL_FEE);
         swap = HistoryDealGetDouble(ticket, DEAL_SWAP);
         magic = HistoryDealGetInteger(ticket, DEAL_MAGIC);
         comment = HistoryDealGetString(ticket, DEAL_COMMENT);

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL || type == DEAL_TYPE_BALANCE) {
            // Replace the separator characters in the comment with a space
            StringReplace(comment, s_sep, " ");

            // Form an array of values for writing one deal to the file string
            string fields[] = {TimeToString(time, TIME_DATE | TIME_MINUTES | TIME_SECONDS),
                               IntegerToString(ticket), IntegerToString(type), symbol, DoubleToString(volume), IntegerToString(entry),
                               DoubleToString(price, 5), DoubleToString(sl, 5), DoubleToString(tp, 5), DoubleToString(profit),
                               DoubleToString(commission), DoubleToString(fee), DoubleToString(swap), IntegerToString(magic), comment
                              };

            // Set the values of a single deal to the file
            WriteDealsHistoryRow(fields);
         }
      }
   }
}

En el método WriteDealsHistoryRow(), simplemente concatenaremos todos los valores del array transmitido en una única cadena mediante el delimitador especificado y escribiremos en un archivo CSV abierto. Aquí usaremos la nueva macro JOIN, que hemos añadido a nuestra colección de macros en el archivo Macros.mqh, para realizar la conexión.

//+------------------------------------------------------------------+
//| Write one row of deal history to the file                        |
//+------------------------------------------------------------------+
void CExpertHistory::WriteDealsHistoryRow(const string &fields[]) {
   // Row to be set
   string row = "";

   // Concatenate all array values into one row using a separator
   JOIN(fields, row, ",");

   // Write a row to the file
   FileWrite(s_file, row);
}

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

Ahora no queda mucho por hacer: conectaremos este archivo al archivo del EA y añadiremos una llamada al método CExpertHistory::Export() en el manejador de eventos OnTester():

...

#include "ExpertHistory.mqh"

...

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

Después guardaremos los cambios realizados en el archivo SimpleVolumesExpert.mq5 en la carpeta actual.

Vamos a probar el EA. Cuando este termine, se creará un archivo con el nombre

SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv

Del nombre se desprende que la historia de transacciones abarca dos años (2021 y 2022), el balance inicial de la cuenta era de 10 000$ y el balance final de 34 518$. Durante el intervalo de prueba, la máxima pérdida relativa de fondos ha sido de 1 294$, el ratio de Sharpe de 3,75. Si abrimos el archivo resultante en Excel, veremos lo siguiente:

Fig. 4. Resultados de la carga de la historia de transacciones en un archivo CSV

Los datos parecen bastante correctos. Ahora procederemos a la segunda etapa: vamos a escribir un asesor experto que sea capaz de reproducir el trading en otra cuenta utilizando este archivo CSV.


Reproducción comercial

Empezaremos a implementar el nuevo asesor experto creando una estrategia comercial. De hecho, seguir las instrucciones de otras personas sobre cuándo y qué posiciones abrir también puede denominarse estrategia comercial. Si una fuente de señales es fiable, ¿por qué no usarla? Así que vamos a crear una nueva clase CHistoryStrategy, heredándola de CVirtualStrategy. De los métodos necesitaremos implementar en esta el constructor, el método de procesamiento de ticks y el método de conversión a una cadena. Aunque no necesitaremos el último, su presencia es obligatoria debido a la herencia, ya que este método es abstracto en la clase padre.

Como veremos más adelante, solo tenemos que añadir las siguientes propiedades a la nueva clase:

  • m_symbols — array de nombres de símbolos (instrumentos comerciales);
  • m_history — array bidimensional para leer la historia de transacciones del archivo (N filas * 15 columnas);
  • m_totalDeals — número de transacciones en la historia;
  • m_currentDeal — número actual de la transacción;
  • m_symbolInfo — objeto para obtener información sobre las propiedades de los símbolos.
Los valores iniciales de estas propiedades se establecerán en el constructor.
//+------------------------------------------------------------------+
//| Trading strategy for reproducing the history of deals            |
//+------------------------------------------------------------------+
class CHistoryStrategy : public CVirtualStrategy {
protected:
   string            m_symbols[];            // Symbols (trading instruments)
   string            m_history[][15];        // Array of deal history (N rows * 15 columns)
   int               m_totalDeals;           // Number of deals in history
   int               m_currentDeal;          // Current deal index

   CSymbolInfo       m_symbolInfo;           // Object for getting information about the symbol properties

public:
                     CHistoryStrategy(string p_params);        // Constructor
   virtual void      Tick() override;        // OnTick event handler
   virtual string    operator~() override;   // Convert object to string
};

El constructor de la estrategia deberá tomar un argumento, la cadena de inicialización. Este requisito también se deriva de la herencia. La cadena de inicialización deberá contener todos los valores necesarios; el constructor los leerá de la cadena de inicialización y los utilizará según sea necesario. En el caso de esta sencilla estrategia, solo necesitaremos transmitir un valor en la línea de inicialización: el nombre del archivo con la historia. Todos los demás datos para el funcionamiento de la estrategia se obtendrán del archivo con la historia. Entonces el constructor podría implementarse así:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CHistoryStrategy::CHistoryStrategy(string p_params) {
   m_params = p_params;

// Read the file name from the parameters
   string fileName = ReadString(p_params);

// If the name is read, then
   if(IsValid()) {
      // Attempting to open a file in the data folder
      int f = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ',');

      // If failed to open a file, then try to open the file from the shared folder
      if(f == INVALID_HANDLE) {
         f = FileOpen(fileName, FILE_COMMON | FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ',');
      }

      // If this does not work, report an error and exit
      if(f == INVALID_HANDLE) {
         SetInvalid(__FUNCTION__,
                    StringFormat("ERROR: Can't open file %s from common folder %s, error code: %d",
                                 fileName, TerminalInfoString(TERMINAL_COMMONDATA_PATH), GetLastError()));
         return;
      }

      // Read the file up to the header string (usually it comes first)
      while(!FileIsEnding(f)) {
         string s = FileReadString(f);
         // If we find a header string, read the names of all columns without saving them
         if(s == "DATE") {
            FORI(14, FileReadString(f));
            break;
         }
      }

      // Read the remaining rows until the end of the file
      while(!FileIsEnding(f)) {
         // If the array for storing the read history is filled, increase its size
         if(m_totalDeals == ArraySize(m_history)) {

            ArrayResize(m_history, ArraySize(m_history) + 10000, 100000);
         }

         // Read 15 values from the next file string into the array string
         FORI(15, m_history[m_totalDeals][i] = FileReadString(f));

         // If the deal symbol is not empty,
         if(m_history[m_totalDeals][SYMBOL] != "") {
            // Add it to the symbol array if there is no such symbol there yet
            ADD(m_symbols, m_history[m_totalDeals][SYMBOL]);
         }

         // Increase the counter of read deals
         m_totalDeals++;
      }

      // Close the file
      FileClose(f);

      PrintFormat(__FUNCTION__" | OK: Found %d rows in %s", m_totalDeals, fileName);

      // If there are read deals except for the very first one (account top-up), then
      if(m_totalDeals > 1) {
         // Set the exact size for the history array
         ArrayResize(m_history, m_totalDeals);

         // Current time
         datetime ct = TimeCurrent();

         PrintFormat(__FUNCTION__" |\n"
                     "Start time in tester:  %s\n"
                     "Start time in history: %s",
                     TimeToString(ct, TIME_DATE), m_history[0][DATE]);

         // If the test start date is greater than the history start date, then report an error
         if(StringToTime(m_history[0][DATE]) < ct) {
            SetInvalid(__FUNCTION__,
                       StringFormat("ERROR: For this history file [%s] set start date less than %s",
                                    fileName, m_history[0][DATE]));
         }
      }

      // Create virtual positions for each symbol
      CVirtualReceiver::Get(GetPointer(this), m_orders, ArraySize(m_symbols));

      // Register the event handler for a new bar on the minimum timeframe
      FOREACH(m_symbols, IsNewBar(m_symbols[i], PERIOD_M1));
   }
}

En él, leeremos el nombre del archivo de la cadena de inicialización e intentaremos abrirlo. Si hemos podido abrir el archivo desde la carpeta de datos local o compartida, leeremos su contenido, llenando el array m_history con él. A medida que leamos, también rellenaremos el array de nombres de símbolos m_symbols: en cuanto encontremos un nuevo nombre, lo añadiremos inmediatamente a este array. La macro ADD() se encargará de este trabajo.

Al mismo tiempo, contaremos el número de registros sobre transacciones leídas en la propiedad m_totalDeals, utilizándola como índice de la primera dimensión del array m_history, en el que deberemos registrar la información sobre la siguiente transacción. Una vez leído todo el contenido del archivo, lo cerraremos.

A continuación, comprobaremos si la fecha de inicio de la prueba es mayor que la fecha de inicio de la historia. No podemos permitir tal situación, porque en este caso no podremos simular una parte de las transacciones desde el principio de la historia. Y esto podría dar lugar a resultados distorsionados en las pruebas. Por ello, permitiremos que el constructor cree un objeto útil solo cuando la historia de transacciones no comience antes de la fecha de inicio de la prueba.

El punto clave del constructor será asignar posiciones virtuales rigurosamente según el número de nombres de símbolos diferentes que se encuentren en la historia. Como la tarea de la estrategia es proporcionar el volumen de posición abierta necesario para cada símbolo, podemos hacerlo utilizando una sola posición virtual por símbolo.

El método de procesamiento de ticks solo funcionará con el array de transacciones leídas. Dado que en un mismo momento temporal podemos tener la apertura/cierre de varios símbolos a la vez, organizaremos un ciclo que procese todas las líneas de la historia de transacciones cuyo tiempo no sea superior al tiempo actual. Los registros de transacciones restantes se procesarán en los siguientes ticks, cuando aumente el tiempo actual y tengamos nuevas transacciones cuyo tiempo ya haya llegado.

Si se detecta al menos una transacción que necesite ser procesada, primero buscaremos su símbolo y el índice de ese símbolo en el array m_symbols. Usando este índice determinaremos qué posición virtual del array m_orders es responsable del símbolo dado. Si el índice no se encuentra por alguna razón (si el funcionamiento es correcto esto no debería ocurrir hasta ahora), simplemente omitiremos esta transacción. También omitiremos las transacciones que reflejen movimientos de balance en la cuenta.

Ahora viene la parte divertida. Necesitamos procesar las transacciones leídas. Existen dos casos posibles: no hay ninguna posición virtual abierta para este símbolo o hay una posición virtual abierta.

En el primer caso, todo es sencillo, pues abriremos una posición en la dirección de la transacción con el volumen de la misma. En el segundo caso, es posible que tengamos que aumentar o reducir el tamaño de la posición actual en este símbolo, con el añadido de que podría ser necesario reducir la posición hasta tal punto que la posición abierta cambie de dirección.

Para simplificar los cálculos, procederemos del siguiente modo:

  • Primero convertiremos el volumen de la nueva transacción al formato "con señal". Es decir, si estaba en la dirección SELL, haremos que su volumen sea negativo.
  • Luego obtendremos el volumen de una transacción abierta del mismo símbolo de la misma manera que en una transacción nueva. El método CVirtualOrder::Volume() retornará directamente el volumen en formato "con signo".
  • Sumaremos el volumen de una posición ya abierta al volumen de una nueva transacción. Obtendremos el nuevo volumen que deberá permanecer abierto tras considerar la nueva transacción. Este volumen también estará en formato "con signo".
  • Luego cerraremos la posición virtual abierta.
  • Si el nuevo volumen no es igual a cero, abriremos una nueva posición virtual para este símbolo. Su dirección se determinará según el signo del nuevo volumen (positivo - BUY, negativo - SELL), y como el volumen, se transmitirá el módulo del nuevo volumen al método de apertura de una posición virtual.

Después de este procedimiento, incrementaremos el contador de transacciones procesadas de la historia y pasaremos a la siguiente iteración del ciclo. Si en ese momento no hay más transacciones para procesar o las transacciones de la historia han finalizado, el procesamiento de ticks habrá terminado.

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CHistoryStrategy::Tick() override {
//---
   while(m_currentDeal < m_totalDeals && StringToTime(m_history[m_currentDeal][DATE]) <= TimeCurrent()) {
      // Deal symbol
      string symbol = m_history[m_currentDeal][SYMBOL];
      
      // Find the index of the current deal symbol in the array of symbols
      int index;
      FIND(m_symbols, symbol, index);

      // If not found, then skip the current deal
      if(index == -1) {
         m_currentDeal++;
         continue;
      }
      
      // Deal type
      ENUM_DEAL_TYPE type = (ENUM_DEAL_TYPE) StringToInteger(m_history[m_currentDeal][TYPE]);

      // Current deal volume
      double volume = NormalizeDouble(StringToDouble(m_history[m_currentDeal][VOLUME]), 2);

      // If this is a top-up/withdrawal, skip the deal
      if(volume == 0) {
         m_currentDeal++;
         continue;
      }

      // Report information about the read deal
      PrintFormat(__FUNCTION__" | Process deal #%d: %s %.2f %s",
                  m_currentDeal, (type == DEAL_TYPE_BUY ? "BUY" : (type == DEAL_TYPE_SELL ? "SELL" : EnumToString(type))),
                  volume, symbol);

      // If this is a sell deal, then make the volume negative
      if(type == DEAL_TYPE_SELL) {
         volume *= -1;
      }

      // If the virtual position for the current deal symbol is open,
      if(m_orders[index].IsOpen()) {
         // Add its volume to the volume of the current trade
         volume += m_orders[index].Volume();
         
         // Close the virtual position
         m_orders[index].Close();
      }

      // If the volume for the current symbol is not 0,
      if(MathAbs(volume) > 0.00001) {
         // Open a virtual position of the required volume and direction
         m_orders[index].Open(symbol, (volume > 0 ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), MathAbs(volume));
      }

      // Increase the counter of handled deals
      m_currentDeal++;
   }
}

Guardaremos el código obtenido en el archivo HistoryStrategy.mqh de la carpeta actual.

Ahora vamos a crear un archivo de EA basado en el SimpleVolumesExpert.mq5 ya existente. Para obtener el resultado deseado, tendremos que añadir al asesor experto un parámetro de entrada donde podremos especificar el nombre del archivo de historia.

input group "::: Testing the deal history"
input string historyFileName_    = "";    // File with history

La parte del código responsable de cargar las cadenas de inicialización de la estrategia desde la base de datos es ahora innecesaria, así que la eliminaremos.

En la línea de inicialización del asesor experto necesitaremos escribir la creación de una única instancia de la estrategia de la clase CHistoryStrategy con el nombre del archivo de la historia que se le transmite como argumento:

// Prepare the initialization string for an EA with a group of several strategies
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        class CHistoryStrategy(\"%s\")\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            historyFileName_, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_, rmMaxOverallProfitDate_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "HistoryReceiver", useOnlyNewBars_
                         );

Con esto concluiremos los cambios en el archivo del EA, por lo que podremos guardarlo con el nombre HistoryReceiverExpert.mq5 en la carpeta actual.

Ahora tenemos un EA de trabajo que puede reproducir la historia de las transacciones. De hecho, sus capacidades son algo más amplias. Podemos ver fácilmente cómo serán los resultados de las transacciones al utilizar el aumento del volumen de posiciones abiertas con el crecimiento del balance de la cuenta, a pesar de que en la historia las transacciones se registran a partir del cálculo de transacciones de balance fijo. Asimismo, podemos aplicar distintos parámetros del gestor de riesgos para evaluar su impacto en las transacciones, aunque la historia de transacciones se haya registrado con otros parámetros en el gestor de riesgos (o incluso con el gestor de riesgos desactivado). Tras realizar una pasada en el simulador, la historia de transacciones se guardará automáticamente en un nuevo archivo.

Pero si todavía no necesitamos todas estas características adicionales, no queremos usar el gestor de riesgos y no nos gustan un montón de parámetros de entrada no usados relacionados con él, podremos crear una nueva clase de EA que no contendrá nada "superfluo". En esta clase podremos al mismo tiempo deshacernos del guardado de estados, la interfaz para dibujar posiciones virtuales en los gráficos y otras cosas que no se usan mucho todavía.

Una implementación de una clase de este tipo podría tener el aspecto siguiente:

//+------------------------------------------------------------------+
//| Trade history replay EA class                                    |
//+------------------------------------------------------------------+
class CVirtualHistoryAdvisor : public CAdvisor {
protected:
   CVirtualReceiver *m_receiver;       // Receiver object that brings positions to the market
   bool              m_useOnlyNewBar;  // Handle only new bar ticks
   datetime          m_fromDate;       // Test start time

public:
   CVirtualHistoryAdvisor(string p_param);   // Constructor
   ~CVirtualHistoryAdvisor();                // Destructor

   virtual void      Tick() override;        // OnTick event handler
   virtual double    Tester() override;      // OnTester event handler

   virtual string    operator~() override;   // Convert object to string
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualHistoryAdvisor::CVirtualHistoryAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the file name from the initialization string
   string fileName = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      if(!MQLInfoInteger(MQL_TESTER)) {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "ERROR: This expert can run only in tester");
         return;
      }

      if(fileName == "") {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "ERROR: Set file name with deals history in ");
         return;
      }

      string strategyParams = StringFormat("class CHistoryStrategy(\"%s\")", fileName);

      CREATE(CHistoryStrategy, strategy, strategyParams);

      Add(strategy);

      // Initialize the receiver with the static receiver
      m_receiver = CVirtualReceiver::Instance(65677);

      // Save the work (test) start time
      m_fromDate = TimeCurrent();
   }
}

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualHistoryAdvisor::~CVirtualHistoryAdvisor() {
   if(!!m_receiver)     delete m_receiver;      // Remove the recipient
   DestroyNewBar();           // Remove the new bar tracking objects 
}


//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualHistoryAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Start handling in strategies
   CAdvisor::Tick();

// Receiver handles virtual positions
   m_receiver.Tick();

// Adjusting market volumes
   m_receiver.Correct();
}

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

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// Fixed balance for trading from settings
   double fixedBalance = CMoney::FixedBalance();

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = fixedBalance * 0.1 / MathMax(1, balanceDrawdown);

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

// If it is not specified, then take the initial balance (although this will give a distorted result)
   if(fixedBalance < 1) {
      fixedBalance = TesterStatistics(STAT_INITIAL_DEPOSIT);
      balanceDrawdown = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
      coeff = 0.1 / balanceDrawdown;
      fittedProfit = fixedBalance * MathPow(1 + profit * coeff / fixedBalance, 1 / totalYears);
   }

   return fittedProfit;
}

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualHistoryAdvisor::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}
//+------------------------------------------------------------------+

Un asesor experto de esta clase admitirá solo dos parámetros en la línea de inicialización: el nombre del archivo de historia y la bandera de trabajo solo en la apertura de la barra de minutos. Guardaremos este código en el archivo VirtualHistoryAdvisor.mqh de la carpeta actual.

El archivo de EA que usa esta clase también se puede acortar un poco en comparación con la variante anterior:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Testing the deal history"
input string historyFileName_    = "";    // File with history
input group "::: Money management"
sinput double fixedBalance_      = 10000; // - Used deposit (0 - use all) in the account currency
input  double scale_             = 1.00;  // - Group scaling multiplier

input group "::: Other parameters"
input bool     useOnlyNewBars_   = true;  // - Work only at bar opening

datetime fromDate = TimeCurrent();        // Operation start time

CVirtualHistoryAdvisor     *expert;       // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Set parameters in the money management class
   CMoney::DepoPart(scale_);
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for the deal history replay EA
   string expertParams = StringFormat(
                            "class CVirtualHistoryAdvisor(\"%s\",%f,%d)",
                            historyFileName_, useOnlyNewBars_
                         );

// Create an EA handling virtual positions
   expert = NEW(expertParams);

// If the EA is not created, then return an error
   if(!expert) return INIT_FAILED;

// Successful initialization
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   expert.Tick();
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if(!!expert) delete expert;
}

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

Luego guardaremos este código en el archivo SimpleHistoryReceiverExpert.mq5 en la carpeta actual.


Resultados de las pruebas

Después iniciaremos uno de los asesores expertos creados, especificando el nombre correcto del archivo con la historia de transacciones guardado. En primer lugar, lo iniciaremos en el mismo servidor de cotizaciones que utilizamos para obtener la historia (MetaQuotes-Demo). Los resultados obtenidos en las pruebas han coincidido totalmente con los resultados iniciales. Debemos admitir que se trata de un resultado hasta cierto punto inesperado, lo cual indica que hemos implementado correctamente el objetivo previsto. 

Ahora vamos a ver lo que sucede cuando se ejecuta el EA en un servidor diferente:


Fig. 5. Resultados de la reproducción de la historia de transacciones con las cotizaciones del servidor real de otro bróker

El gráfico de la curva de balance es casi indistinguible del gráfico con los resultados iniciales de la negociación en MetaQuotes-Demo. Sin embargo, los valores numéricos son ligeramente distintos. Vamos a echar otro vistazo a los valores de origen para comparar:


Fig. 6. Resultados de las pruebas iniciales con las cotizaciones del servidor MetaQuotes-Demo

Se observa una ligera disminución del beneficio medio anual total y normalizado, del ratio de Sharpe y un ligero aumento del reducción. Sin embargo, estos resultados no se comparan con la pérdida de todo el depósito que hemos padecido inicialmente al ejecutar el asesor experto con las cotizaciones del servidor real de otro bróker. Esto resulta muy alentador y descubre una nueva capa de tareas que quizá debamos resolver mientras llevamos el asesor experto desarrollado al comercio real.


Conclusión

Es hora de resumir los nuevos resultados provisionales. Hemos podido demostrar que, para una determinada estrategia comercial utilizada, el cambio del servidor de cotizaciones puede tener consecuencias muy tristes. Pero una vez comprendidas los motivos de tal comportamiento, hemos podido demostrar que si dejamos la lógica de las señales de apertura de posiciones en el servidor con las cotizaciones originales, y transferimos únicamente las operaciones de apertura y cierre de posiciones al nuevo servidor, los resultados de las operaciones volverán a ser comparables.

Para ello, hemos desarrollado dos nuevas herramientas de trabajo que permiten guardar la historia de transacciones después de las pasadas del simulador y, a continuación, volver a realizar las transacciones basándonos en la historia registrada. Pero estas herramientas solo pueden usarse en el simulador. En el comercio real, carecen de sentido. Sin embargo, ahora podemos emprender con confianza la aplicación de dicha separación de funciones entre asesores expertos para el comercio real, ya que los resultados de las pruebas confirman la validez de este enfoque.

Tendremos que dividir el EA en dos EA separados. El primero tomará decisiones sobre la apertura de posiciones y las abrirá, trabajando sobre el servidor de cotizaciones que nos parezca más conveniente. No obstante, simultáneamente con el comercio, tendrá que asegurarse de que la lista de posiciones abiertas se genere en una forma que pueda ser obtenida por el segundo asesor experto. El segundo asesor experto funcionará en otro terminal conectado a otro servidor de cotizaciones si es necesario. Este mantendrá constantemente el volumen de posiciones abiertas correspondiente a los valores emitidos a los primeros asesores expertos. Este esquema de trabajo ayudará a evitar la limitación que señalábamos al principio de este artículo.

Podríamos ir aún más lejos. El esquema de trabajo mencionado presupone implícitamente que ambos terminales deberán funcionar en la misma computadora, pero eso no es necesario en absoluto. Estos terminales bien pueden funcionar en diferentes computadoras, lo principal es que el primer asesor experto sea capaz de transmitir la información sobre las posiciones al segundo asesor experto a través de algunos canales. Está claro que esta no es la manera de lograr que funcionen con éxito las estrategias comerciales para las que resulta esencial observar el momento exacto y el precio de apertura de una posición. Pero inicialmente nos centraremos en utilizar otras estrategias para las que no es necesaria una mayor precisión en las entradas. Por consiguiente, los retrasos en los canales de comunicación no deberían suponer un obstáculo a la hora de organizar un plan de trabajo de este tipo.

Pero no nos adelantemos demasiado, vamos a continuar avanzando sistemáticamente en la dirección elegida en los próximos artículos.

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


Contenido del archivo

#
 Nombre
Versión  Descripción   Cambios recientes
 MQL5/Experts/Article.15330
1 Advisor.mqh 1.04. Clase básica del experto Parte 10
2 Database.mqh 1.03 Clase para trabajar con las bases de datos Parte 13
3 ExpertHistory.mqh 1.00 Clase para exportar la historia de transacciones a un archivo Parte 16
4 Factorable.mqh 1.01 Clase base de objetos creados a partir de una cadena Parte 10
5 HistoryReceiverExpert.mq5 1.00 Asesor experto para reproducir la historia de transacciones con el gestor de riesgos Parte 16  
6 HistoryStrategy.mqh  1.00 Clase de estrategia comercial para reproducir la historia de transacciones  Parte 16
7 Interface.mqh 1.00 Clase básica de visualización de diversos objetos Parte 4
8 Macros.mqh 1.02 Macros útiles para las operaciones con arrays Parte 16  
9 Money.mqh 1.01  Curso básico de gestión de capital Parte 12
10 NewBarEvent.mqh 1.00  Clase de definición de una nueva barra para un símbolo concreto  Parte 8
11 Receptor.mqh 1.04.  Clase básica de transferencia de volúmenes abiertos a posiciones de mercado  Parte 12
12 SimpleHistoryReceiverExpert.mq5 1.00 Asesor experto simplificado para reproducir la historia de transacciones   Parte 16
13 SimpleVolumesExpert.mq5 1.19 Asesor experto para el trabajo paralelo de varios grupos de estrategias modelo. Los parámetros deberán cargarse desde la base de datos de optimización. Parte 16
14 SimpleVolumesStrategy.mqh 1.09  Clase de estrategia comerciales usando volúmenes de ticks Parte 15
15 Strategy.mqh 1.04.  Clase básica de estrategia comercial Parte 10
16 TesterHandler.mqh  1.02 Clase para gestionar los eventos de optimización  Parte 13 
17 VirtualAdvisor.mqh  1.06  Clase del EA que trabaja con posiciones (órdenes) virtuales Parte 15
18 VirtualChartOrder.mqh  1.00  Clase de posición virtual gráfica Parte 4  
19 VirtualFactory.mqh 1.04.  Clase de fábrica de objetos  Parte 16
20 VirtualHistoryAdvisor.mqh 1.00  Clase experta para reproducir la historia de transacciones  Parte 16
21 VirtualInterface.mqh  1.00  Clase de GUI del asesor  Parte 4  
22 OrdenVirtual.mqh 1.04.  Clase de órdenes y posiciones virtuales  Parte 8
23 VirtualReceiver.mqh 1.03  Clase de transferencia de volúmenes abiertos a posiciones de mercado (receptor)  Parte 12
24 VirtualRiskManager.mqh  1.02  Clase de gestión de riesgos (gestor de riesgos)  Parte 15
25 EstrategiaVirtual.mqh 1.05  Clase de estrategia comercial con posiciones virtuales  Parte 15
26 VirtualStrategyGroup.mqh  1.00  Clase de grupo o grupos de estrategias comerciales Parte 11 
27 VirtualSymbolReceiver.mqh  1.00 Clase de receptor simbólico  Parte 3
MQL5/Files 
1 SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv    Historia de transacciones del asesor experto SimpleVolumesExpert.mq5, obtenida tras la exportación. Puede usarse para reproducir en el simulador transacciones con los asesores expertos SimpleHistoryReceiverExpert.mq5 o HistoryReceiverExpert.mq5  

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

Archivos adjuntos |
MQL5-2.zip (163.78 KB)
La teoría del caos en el trading (Parte 2): Continuamos la inmersión La teoría del caos en el trading (Parte 2): Continuamos la inmersión
Continuamos nuestra inmersión en la teoría del caos en los mercados financieros: hoy analizaremos su aplicabilidad al análisis de divisas y otros activos.
Algoritmo de optimización de reacciones químicas (CRO) (Parte I): Química de procesos en la optimización Algoritmo de optimización de reacciones químicas (CRO) (Parte I): Química de procesos en la optimización
En la primera parte de este artículo, nos sumergiremos en el mundo de las reacciones químicas y descubriremos un nuevo enfoque de la optimización. La optimización de reacciones químicas (Chemical Reaction Optimization, CRO) utiliza principios derivados de las leyes de la termodinámica para lograr resultados eficientes. Desvelaremos los secretos de la descomposición, la síntesis y otros procesos químicos que se convirtieron en la base de este innovador método.
Del básico al intermedio: Variables (I) Del básico al intermedio: Variables (I)
Muchos programadores principiantes tienen muchas dificultades para comprender por qué sus códigos no funcionan como esperan. Existen muchos detalles que hacen que un código sea realmente funcional. No se trata simplemente de escribir toda una serie de funciones y operaciones para que un código funcione. ¿Qué tal si aprendemos de la manera correcta cómo se crea un código real en lugar de copiar y pegar fragmentos de código encontrados aquí y allá? El contenido expuesto aquí tiene como objetivo, pura y simplemente, la didáctica. En ningún caso debe considerarse como una aplicación cuya finalidad no sea el aprendizaje y el estudio de los conceptos mostrados.
Visualización de transacciones en un gráfico (Parte 2): Visualización gráfica de datos Visualización de transacciones en un gráfico (Parte 2): Visualización gráfica de datos
Aquí vamos a desarrollar un script desde cero que simplifica la descarga de pantallas de impresión de operaciones para analizar las entradas de operaciones. Toda la información necesaria sobre una operación debe mostrarse cómodamente en un gráfico con la posibilidad de dibujar distintos plazos.