
Desarrollamos un asesor experto multidivisa (Parte 16): Efecto de diferentes historias de cotizaciones en los resultados de las pruebas
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.
//+------------------------------------------------------------------+ //| 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





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso