Cómo hacer más fácil la detección y recuperación de errores en el código de un asesor experto

Roman Kramar | 11 marzo, 2016


Introducción

El desarrollo de asesores expertos para el trading en el lenguaje MQL4 no es nada fácil desde varios puntos de vista:

  • En primer lugar, la construcción de un algoritmo para un sistema de trading más o menos difícil ya es un problema en sí mismo, ya que es necesario tener en cuenta muchos detalles, desde las peculiaridades del algoritmo de un asesor experto hasta el entorno específico de MetaTrader 4;
  • en segundo lugar, incluso la presencia de un algoritmo de un asesor experto no elimina las dificultades que aparecen cuando se transfiere el algoritmo desarrollado al lenguaje de programación MQL4.

Debemos hacer justicia con la plataforma de trading MetaTrader 4: la existencia del lenguaje de programación para escribir asesores expertos de trading es ya un gran paso adelante comparado con las alternativas disponibles anteriormente. El compilador es de gran ayuda a la hora de escribir asesores expertos de forma correcta. Inmediatamente después de hacer clic en 'Compile', este informará sobre todos los errores sintácticos en nuestro código. Si trabajamos con un lenguaje interpretado, dichos errores los encontraremos solo durante el funcionamiento del asesor experto, y esto incrementaría la dificultad y el periodo de desarrollo. No obstante, salvo los errores sintácticos, cualquier asesor experto puede contener errores lógicos. Ahora vamos a analizar dichos errores.



Errores al usar funciones integradas

Aunque ningún asesor experto puede prescindir del uso de las funciones integradas, vamos a intentar facilitar el trabajo al analizar los errores devueltos por dichas funciones. En primer lugar, vamos a ver los resultados del funcionamiento de las funciones directamente relacionados con las operaciones de trading, ya que ignorar los errores en dichas funciones puede llevar a resultados críticos en un asesor expertos. No obstante, hay otros argumentos que también hacen referencia a otras funciones integradas.

Por desgracia, al usar las opciones de MQL4 no podemos escribir una biblioteca generalizada para el procesamiento de todas las condiciones de error posibles. En cada caso necesitamos procesar los errores de forma independiente. Aunque a pesar de ello hay buenas noticias: no necesitamos procesar todos los errores, ya que muchos de ellos simplemente se eliminan en la etapa de desarrollo del asesor experto. Pero para hacer esto deben ser detectados a tiempo. Como ejemplo, vamos a ver dos errores típicos en los asesores expertos en MQL4:

1) Error 130 — ERR_INVALID_STOPS
2) Error 146 — ERR_TRADE_CONTEXT_BUSY

Uno de los casos, cuando aparece el primer error, es un intento de que un asesor experto coloque una orden pendiente demasiado cerca del mercado. Su existencia puede echar a perder las características del asesor experto en algunos casos. Por ejemplo, supongamos que el asesor experto ha abierto una posición rentable y que corta el beneficio cada 150 puntos. Si en el siguiente intento aparece el error 130 y el precio vuelve al nivel stop anterior, esto puede privarnos de nuestro beneficio legítimo. A pesar de dicha posibilidad, este error puede eliminarse desde el principio, insertando en el código del asesor experto una función que tenga en cuenta la distancia mínima aceptable entre un precio y las órdenes stop.

El segundo error 'trade context is busy' no puede eliminarse por completo. Cuando varios asesores expertos operan en un terminal, podemos afrontar la situación, cuando uno de los asesores expertos intenta abrir una posición y cuando el segundo está haciendo lo mismo. Por tanto, este error debe ser siempre procesado.

De ahí que siempre necesitemos saber si cualquier de las funciones integradas devuelve el error durante la operación del asesor experto. Esto puede lograrse usando la siguiente función adicional simple:

#include <stderror.mqh>
#include <stdlib.mqh>
 
void logError(string functionName, string msg, int errorCode = -1)
  {
    Print("ERROR: in " + functionName + "()");
    Print("ERROR: " + msg );
    
    int err = GetLastError();
    if(errorCode != -1) 
        err = errorCode;
        
    if(err != ERR_NO_ERROR) 
      {
        Print("ERROR: code=" + err + " - " + ErrorDescription( err ));
      }    
  }

En el caso más simple, debe usarse de la forma siguiente:


void openLongTrade()
  {
    int ticket = OrderSend(Symbol(), OP_BUY, 1.0, Ask + 5, 5, 0, 0);
    if(ticket == -1) 
        logError("openLongTrade", "could not open order");
  }

El primer parámetro de la función logError() muestra el nombre de la función en la que se ha detectado el error, que en nuestro caso es la función openLongTrade(). Si nuestro asesor experto llama a la función OrderSend() en varios lugares, tendremos la oportunidad de determinar exactamente en qué caso se ha producido el error. El segundo parámetro proporciona la descripción del error y permite conocer en qué lugar exacto dentro de la función openLongTrade se detectó el error. Esta puede ser una descripción breve o una detallada que incluya los valores de todos los parámetros pasados a la función integrada.

Prefiero la última variante, ya que si se produce un error, uno puede inmediatamente obtener toda la información necesaria para el análisis. Supongamos que antes de llamar a OrderSend() el precio actual se movió drásticamente desde el último precio conocido. Como resultado de ello se producirá un error y aparecerán las siguientes líneas en el archivo de registro del asesor experto:

 ERROR: in openLongTrade()
 ERROR: could not open order
 ERROR: code=138 - requote

Es decir, quedará claro:

  1. en qué función se produjo el error;
  2. a qué se refiere este (en nuestro caso al intento de abrir una posición);
  3. qué error se produjo exactamente (código del error y su descripción).

Ahora vamos a ver el tercer parámetro, opcional, de la función logError(). Es necesario cuando queremos procesar un cierto tipo de error y se incluirán otros errores en un archivo de registro, como antes:

void updateStopLoss(double newStopLoss)
  {
    bool modified = OrderModify(OrderTicket(), OrderOpenPrice(), 
                newStopLoss, OrderTakeProfit(), OrderExpiration());
    
    if(!modified)
      {
        int errorCode = GetLastError();
        if(errorCode != ERR_NO_RESULT ) 
            logError("updateStopLoss", "failed to modify order", errorCode);
      }
  }

Aquí en la función updateStopLoss() se llama a la función OrderModify(). Esta función es ligeramente distinta a OrderSend() en términos del procesamiento de errores. Si ningún parámetro de una orden modificada se diferencia de sus parámetros actuales, la función devolverá el error ERR_NO_RESULT. Si dicha situación es aceptable en nuestro asesor experto, debemos ignorar este error. Para ello analizamos el valor devuelto por GetLastError(). Si se produjo un error con el código ERR_NO_RESULT, no escribimos nada en el archivo de registro.

No obstante, si se produjo otro error, debemos informar sobre ello al igual que hicimos antes. Exactamente para esta finalidad guardamos el resultado de la función GetLastError() en una variable intermedia y lo pasamos usando el tercer parámetro en la función logError(). En realidad, la función integrada GetLastError() pone a cero automáticamente el último código de error después de su llamada. Si no pasamos el código del erro a logError(), el archivo de registro del asesor experto contendrá un error con el código 0 y la descripción "no error".

Deben hacerse acciones similares cuando se procesan otros errores, por ejemplo, las recotizaciones. La idea principal es procesar solo los errores que necesitan ser procesados y pasar los otros a la función logError(). En este caso sabremos si se ha producido un error inesperado durante la operación del asesor experto. Después de analizar el archivo de registro, sabremos si este error requiere un procesamiento independiente o si puede ser eliminado mejorando el código del asesor experto. Dicho método nos facilita la vida y reduce el tiempo necesario para solucionar los errores.



Diagnóstico de errores lógicos

Los errores lógicos en el código de un asesor experto pueden ser bastante problemáticos. La ausencia de una opción para abordarlos en el asesor experto hace que combatirlos sea una tarea bastante molesta. La herramienta principal para el diagnóstico de dichos errores en el momento actual es la función integrada Print(). Usándola podemos imprimir los valores actuales de importantes variables y registrar el flujo de operaciones del asesor experto. Cuando depuramos un asesor experto durante las pruebas con visualización, la función integrada Comment() puede ser también de ayuda. Como norma, cuando se ha confirmado el trabajo erróneo de un asesor experto, tenemos que añadir una llamada temporal de la función Print() y registrar el estado interno del asesor experto en los lugares en los que se asume que ha aparecido el error.

Pero para la detección de situaciones de error difíciles, algunas veces necesitamos añadir docenas de dichas llamadas de diagnóstico de la función Print(). Y después de la detección y recuperación del problema, las llamadas de la función deben borrarse o comentarse para no sobrecargar el archivo de registro del asesor experto y no ralentizar las pruebas. La situación es aún peor si el código del asesor experto ya incluye la función Print() para el registro periódico de diferentes estados. Entonces, las llamadas temporales a la función Print() no pueden borrarse mediante una simple búsqueda de 'Print' en el código del asesor experto. Es necesario pensar si debemos borrar una función o no.

Por ejemplo, al registrar errores de las funciones OrderSend(), OrderModify() y OrderClose() es útil escribir en un archivo de registro el valor actual de las variables Bid y Ask. Esto facilita encontrar las causas de los errores como ERR_INVALID_STOPS y ERR_OFF_QUOTES.

Para escribir esta información de diagnóstico en un archivo de registro, recomiendo usar la siguiente función adicional:

void logInfo(string msg)
  {
    Print("INFO: " + msg);
  }

ya que:

  • en primer lugar, ahora dicha función no será confundida con 'Print' durante la búsqueda;
  • en segundo lugar, esta función tiene una de las peculiaridades más útiles que discutiremos más tarde.

Lleva mucho tiempo añadir y borrar llamadas de diagnóstico temporales de la función Print(). Por ello, recomiendo un método más que es eficiente en la detección de errores lógicos en un código y nos ayuda a ahorrar tiempo. Vamos a analizar la siguiente función simple:

void openLongTrade(double stopLoss)
  {
    int ticket = OrderSend(Symbol(), OP_BUY, 1.0, Ask, 5, stopLoss, 0);
    if(ticket == -1) 
        logError("openLongTrade", "could not open order");
  }

En este caso, mientras que abrimos una posición larga, está claro que en una operación correcta de un asesor experto, el valor del parámetro stopLoss no puede ser mayor ni igual al precio Bid actual. Es decir, corrige la declaración en la que cuando se llama a la función openLongTrade(), la condición según la cual stopLoss < Bid siempre se cumple. Como sabemos, ya en la etapa de escritura de la función analizada podemos usarla de la forma siguiente:


void openLongTrade( double stopLoss )
  {
    assert("openLongTrade", stopLoss < Bid, "stopLoss < Bid");
    
    int ticket = OrderSend(Symbol(), OP_BUY, 1.0, Ask, 5, stopLoss, 0);
    if(ticket == -1) 
        logError("openLongTrade", "could not open order");
  }

Es decir, insertamos nuestra declaración en el código usando la nueva función adicional assert(). La función en sí misma es muy simple:


void assert(string functionName, bool assertion, string description = "")
  {
    if(!assertion) 
        Print("ASSERT: in " + functionName + "() - " + description);
  }

El primera parámetro de la función es el nombre de la función en la que se comprueba nuestra condición (por analogía con la función logError()). El segundo parámetro muestra los resultados de esta comprobación de la condición. Y el tercer parámetro indica su descripción. Como resultado, si no se cumple una condición esperada, el archivo de registro del asesor experto contendrá la siguiente información:

  1. el nombre de la función en la que no se cumplió la condición;
  2. la descripción de esta condición.

Como descripción, podemos visualizar, por ejemplo, la propia condición o una descripción más detallada que contenga los valores de las variables controladas en el momento de comprobar la condición si esto puede ayudar a encontrar las causas del error.

Por supuesto, el ejemplo que hemos visto se ha simplificado al máximo. Pero espero que refleje la idea con bastante claridad. A medida que aumenta la funcionalidad del asesor experto, vemos claramente cómo debe trabajar y qué condiciones y parámetros de entrada son aceptables y cuáles no. Insertándolos en el código de un asesor experto usando la función assert() obtenemos una información útil sobre el lugar en el que se ha roto la lógica de funcionamiento del asesor experto. Además, eliminamos parcialmente la necesidad de añadir y borrar llamadas temporales de la función Print(), ya que la función assert() genera mensajes de diagnóstico en el archivo de registro del asesor experto en el momento de detectar las discrepancias en las condiciones esperadas.

Un método útil más es usar esta función antes de cada operación de división. En realidad, este o aquel error lógico puede a veces dar como resultado una división por cero. En tales casos, el asesor experto deja de funcionar y aparece una única línea en el archivo de registro: 'zero divide'. Y si la operación de división se usa muchas veces en el código, puede ser bastante difícil detectar el lugar en el que se produjo el error. Y aquí la función assert() puede ser muy útil. Simplemente necesitamos insertar la comprobación correspondiente antes de cada operación de división:

assert("buildChannel", distance > 0, "distance > 0");
double slope = delta / distance;

Y ahora, en caso de una división por cero, miramos en el archivo de registro para encontrar exactamente el error.



Analizar el archivo de registro del asesor experto para la detección de errores

Las funciones recomendadas para el registro de errores ayudan a encontrarlos fácilmente en el archivo de registro. Tan solo necesitamos abrir dicho archivo en modo texto y buscar las palabras "ERROR:" y "ASSERT:". No obstante, algunas veces nos encontramos con situaciones en las que durante el desarrollo omitimos los resultados de la llamada de esta o aquella función integrada. Algunas veces se omite la división por cero. ¿Cómo podemos detectar los mensajes sobre estos errores entre miles de líneas que contienen la información sobre las posiciones abiertas, cerradas y modificadas? Si se encuentra con dicho problema, le recomiendo la solución siguiente.

Abra Microsoft Excel y descargue el archivo de registro de funcionamiento del asesor experto como archivo CSV, indicando que como separador debe usarse uno o varios espacios. Ahora habilite "Autofiltro". Esto le permitirá buscar en las opciones de filtrado en dos columnas adyacentes (sabrá fácilmente qué columnas), para saber si el archivo de registro contiene errores de escritura por el terminal o no. Todas las entradas generadas por las funciones logInfo(), logError() y assert(), comienzan por el prefijo ("INFO:", "ERROR:" and "ASSERT:"). Las entradas del terminal sobre los errores pueden verse también fácilmente entre varias variantes de entradas típicas sobre el trabajo con otras.

Esta tarea puede solucionarse de una forma más elegante. Por ejemplo, si ya está familiarizado con las herramientas para el procesamiento de archivos de texto, puede escribir un pequeño script que mostrará solo aquellas líneas del archivo de registro del asesor experto que hagan referencia a los errores de funcionamiento del asesor experto. Recomiendo encarecidamente ejecutar cada asesor experto en el probador durante un largo periodo de tiempo y luego analizar el archivo de registro de su funcionamiento usando los métodos descritos. Esto permitirá detectar la mayoría de errores posibles antes de ejecutar su asesor experto en una cuenta demostración. Después de esto, el mismo análisis de vez en cuando del archivo de registro del funcionamiento le ayudará a detectar a tiempo los errores específicos del funcionamiento del asesor experto en las cuentas y en tiempo real.



Conclusión

Las funciones adicionales y los métodos simples descritos permiten simplificar el proceso de detección y recuperación de errores en un asesor experto escrito en el lenguaje de programación MQL4. Para su comodidad, las funciones descritas anteriormente se incluyen en los archivos adjuntos. Tan solo tiene que añadir el archivo a su asesor experto usando el directorio #include. Espero que los métodos descritos sean usados con éxito por los operadores que se preocupan por la solidez y exactitud de los asesores expertos.