Depuración de programas en MQL5

Mykola Demko | 19 marzo, 2014

Introducción

Este artículo va dirigido a los programadores que ya conocen el lenguaje, pero que aún no han asimilado suficiententemente bien el desarrollo de programas. En el presente artículo se aclaran las cuestiones principales con las que debe lidiar el desarrollador a la hora de depurar un programa. ¿Qué es la depuración?

La depuración de un programa es la etapa del desarrollo en la que se detectan y eliminan los errores de ejecución de un programa. Durante el proceso de depuración, el desarrollador analiza concienzudamente el programa y aclara cualquier posible problema. Los datos para el análisis se obtienen mediante la observación de las variables y el proceso de ejecución del programa (qué funciones son llamadas y cuándo).

Existen dos tecnologías de depuración, que se complementan mútuamente:

Bien, supongamos que usted conoce MQL5: qué son las variables, para qué sirven las estructuras y mucho más. Pero aún no ha desarrollado por sí mismo un programa. Lo primero que deberá realizar es la compilación. De hecho, es la primera etapa de la depuración.


1. Compilación

La Compilación es la transferencia de un programa compuesto de un lenguaje fuente, de un lenguaje de programación de alto nivel a uno de menor nivel.

Querría destacar que el compilador MetaEditor traduce los programas a byte-code, y no a un lenguaje de máquina (puede leer sobre ello con más detalle en el enlace). Esto da la posibilidad de crear programas encriptados, a prueba de intrusiones. La ventaja de este tipo de traducción es que el byte-code puede ser iniciado tanto como en la versión de 32 bits de un sistema operativo, como en la de 64.

Pero nos estamos desviando del tema. Así pues, la compilación es la primera etapa de la depuración. Después de pulsar el botón F7 (o bien el botón "Compilar"), el MetaEditor 5 le informará de todos los errores que ha cometido durante la escritura del código. Dentro de la ventana "Instrumentos", en la pestaña "Errores" puede encontrar la descripción del error y la línea del código donde se encuentra. Si selecciona con el cursor la línea de la descripción y pulsa Enter, podrá pasar directamente al error.

El compilador sólo puede dar dos tipos de errores:

Los errores de sintaxis normalmente se cometen por falta de atención. Por ejmplo, al declarar una variable, resulta muy sencillo confundir "," y ";":

int a; b; // son declaraciones incorrectas

Como resultado de una declaración así, el compilador dará error. Una declaración correcta tendrá el aspecto siguiente:

int a, b; // son declaraciones correctas

O bien así:

int a; int b; // son declaraciones correctas

Es mejor no ignorar las advertencias (mucha gente no se las toma muy en serio). Si durante el proceso de compilación no se han detectado errores, ni han aparecido advertencias, entonces el programa se creará correctamente, aunque no sea un hecho que vaya a salir igual que pensamos en un principio.

Las advertencias son sólo la punta del iceberg del trabajo que han llevado a cabo los desarrolladores de MQL5 sobre la sistematización de las erratas típicas de los programadores .

Por ejemplo, usted quiere realizar una comparación entre dos variables:

if(a==b) { } // si a es igual a b entonces ...

Pero, por falta de atención, o al cometer una errata, podría escribir "=" en lugar de "==". En este caso, el compilador interpreta el código de la siguiente manera:

if(a=b) { } // asignamos b a a, si a es verdad entonces ... (en MQL5 a diferencia de MQL4 esto es posible)

Como podemos ver, este tipo de errata puede conducir a cambios radicales en el funcionamiento del programa. Por eso, en este punto, el compilador dará una advertencia.

Resumiendo: la compilación es el primer paso de la depuración. No hay que despreciar en absoluto las advertencias del compilador.

Dib. 1. Información sobre la depuración en la etapa de compilación.

Dib. 1. Información sobre la depuración en la etapa de compilación.


2. Depurador

La segunda etapa de la depuración es la utilización del Depurador (botón funcional F5). El depurador inicia el programa en el modo de emulación, ejecutándolo paso por paso. El uso del depurador es una novedad de MetaEditor 5, en el cuarto MetaEditor no existe. Por eso los programadores que pasan del lenguaje MQL4 al MQL5 no tienen ninguna práctica usándolo.

El interfaz del depurador tiene tres botones principales y tres auxiliares:

Esta sería una breve descripción del interfaz del depurador. Pero, ¿cómo usarlo? La depuración del programa comienza desde la línea en la que el programador haya establecido la función especial de depuración DebugBreak(), o bien desde un punto de parada, que se puede establecer con la ayuda del botón F9 o con el botón del panel de instrumentos:

Dib. 2. Distribución de los puntos de parada.

Dib. 2. Distribución de los puntos de parada.

Sin la distribución de los puntos de parada, el depurador simplemente ejecutará el programa y le comunicará que la depuración se ha realizado con éxito, pero no podrá ver nada. Al usar DebugBreak podría saltarse algunas partes del código que no le interesen y comenzar después una ejecución del programa paso por paso desde el lugar que considere problemático.

Y bien, ya hemos inciado la depuración, hemos puesto el DebugBreak en el lugar necesario y estamos examinando la ejecución del programa. ¿Y qué hay que hacer después? ¿Cómo nos va a ayudar esto a comprender qué sucede con el programa?

En primer lugar, fíjese en la parte izquierda de la ventana del depurador. Allí se muestra el nombre de la función y el número de la línea del código en el que se encuentra usted ahora. En segundo lugar, eche un vistazo a la parte derecha de la ventana. Por ahora no hay nada, pero en el campo "Expresión", usted puede introducir el nombre de cualquier variable que le interese. Introduzca el nombre de la variable y podrá ver su valor en un momento dado, en el campo "Valor".

Igualmente se puede añadir la variable utilizando la combinación de teclas [Shift+F9], seleccionándola previamente del menú de contexto, como se muestra más abajo:

Dib. 3. Cómo añadir la observación de una variable durante la depuración.

Dib. 3. Cómo añadir la observación de una variable durante la depuración.

De esta forma podrá relizar un seguimiento de en qué línea de código se encuentra y vigilar el valor de las variables más importantes para usted. Si analizamos todo esto en su conjunto, usted, en conclusión, podrá evaluar si el programa funciona correctamente o no.

No hace falta preocuparse sobre si la variable que le interesa se declara como local y usted aún no ha alcanzado la función en la que se declara la misma. Mientras usted se encuentre fuera de la zona de visibilidad de dicha variable, esta tendrá el valor de "Unknown identifier" - identificador desconocido. Esto significa que la variable no se ha declarado, y no va a provocar un error del depurador. Además, cuando llegue a la zona de visibilidad de la variable, podrá ver su valor y tipo.

Dib. 4. Proceso de depuración. Observación del valor de las variables.

Dib. 4. Proceso de depuración. Observación del valor de las variables.

Estas son las posibilidades básicas del depurador. En la sección "Simulador" se hablará sobre lo que no se puede hacer en el depurador.


3. Perfilado

Un complemento muy importante para el depurador es el perfilador del código. Se podría decir que es la última etapa de la depuración de un programa, su optimización.

El perfilador se invoca en el menú MetaEditor 5 con ayuda del botón "Comenzar perfilado". No se trata de un análisis paso a paso del programa, como en el depurador, sino más bien se trata de su ejecución. Si el programa es un indicador o un asesor, entonces el perfilador funcionará hasta que el programa sea descargado. Se puede realizar la descarga tanto eliminando el indicador o el asesor del gráfico, como con la ayuda del botón "Detener perfilado".

El perfilado proporciona una estadística muy importante: cuántas veces ha sido llamada cada función y cuánto tiempo se ha invertido en su ejecución. Es posible que la estadística en tanto por ciento le resulte un poco confusa. Es necesario entender que la estadística no entiende de subrutinas anidadas, por eso la suma de todos los tantos por ciento será muy superior al 100%.

Aún así, el perfilador sigue siendo un instrumento potente para la optimización de programas, ya que en él se puede ver claramente qué función merece la pena optimizar en cuanto a su velocidad y dónde merece la pena ahorrar memoria.

Dib. 5. Resultados del trabajo con el perfilador.

Dib. 5. Resultados del trabajo con el perfilador.


4. Interactividad

De todas formas, considero que el instrumento principal en la depuración es el uso de la funciones de representación (display) de mensajes - Print y Comment. Lo primero, son muy fáciles de usar, y lo segundo, los programadores que han pasado a MQL5 desde la versión anterior deben acostumbrase a ellas.

La función "Print" envía el parámetro transmitido a un log file y la pestaña de instrumentos "Expertos" en forma de línea de texto. A la izquierda del texto se muestra la hora del envío y el nombre del programa que ha llamado la función. Normalmente, durante el proceso de depuración, la función se usa para determinar qué valores contienen las variables.

Además de los valores de las variables, a veces es necesario conocer la consecutividad de las llamadas de las mismas funciones. Con estos fines es cómodo usar las macros "__FUNCTION__" y "__FUNCSIG__". Su peculiaridad reside en que la primera retorna en forma de línea el nombre de la función de la que es llamada, y la segunda, como complemento, muestra la lista de los parámetros de la función llamada.

El uso de macros tiene aproximadamente este aspecto:

//+------------------------------------------------------------------+
//| Ejemplo de muestra de la información para la depuración          |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
   Print(__FUNCSIG__); // muestra de la información para la depuración 
//--- aquí hay parte de un código de la función misma
  }

Prefiero usar la macro "__FUNCSIG__", dado que me permite ver la diferencia entre funciones sobrecargadas (funciones que tienen el mismo nombre pero cuyos parámetros se diferencian).

Con frecuencia es necesario saltarse una cierta cantidad de llamadas o directamente concentrarse en la llamada concreta de una función. Con este cometido, la función "Print" puede ser protegida por una condición. Por ejemplo, llamar la función imprimir sólo tras la 1013era iteración:

//+------------------------------------------------------------------+
//| Ejemplo de muestra de la información de la depuración            |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
//--- declaración del contador estático
   static int cnt=0;
//--- condiciones para la llamada de una función
   if(cnt==1013)
      Print(__FUNCSIG__," a=",a); // muestra de la información para la depuración
//--- aumentamos el contador
   cnt++;
//--- aquí hay parte de un código para la función misma
  }

Se pueden llevar a cabo las mismas manipulaciones con la función "Comment", que muestra los comentarios en la esquina superior izquierda del gráfico. Esto constituye una gran ventaja, dado que no es necesario cambiar a ninguna parte durante la depuración. Sin embargo, al usar esta función, cada nuevo comentario entrante borra el anterior, lo que podría considerarse una desventaja (aunque a veces resulte cómodo).

Para solucionar esta desventaja de los comentario, se puede usar un método consistente en la escritura de una nueva línea en adición a la variable. Primero se declara (normalmente a un nivel global) e inicia una variable tipo string con un valor vacío. Después, cada línea nueva de texto se coloca al principio, añadiéndole el símbolo de transferencia de línea, mientras que el valor previo de la variable se agrega al final.

string com=""; // declaración de la variable global para el guardado de información para la depuración
//+------------------------------------------------------------------+
//| Ejemplo de muestra de la información para la depuración          |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
//--- declaración del contador estático
   static int cnt=0;
//--- almacenando información para la depuración en la variable global
   com=(__FUNCSIG__+" cnt="+(string)cnt+"\n")+com;
   Comment(com); // muestra de la información para la depuración
//--- aumentamos el contador
   cnt++;
//--- aquí hay parte de un código de la función misma
  }

Ahora realizaremos una aproximación ya a otro método para ver con detalle el contenido de un programa: imprimir en un archivo. Las funciones "Print" y "Comment" no siempre sirven para volúmenes grandes de información o para la impresión de alta velocidad. La primera porque no siempre tiene tiempo de mostrar los cambios (porque las llamadas a veces adelantan a su propia representación en el display, llevando a la confusión), la segunda porque funciona todavía más lento, y además, los comentarios no se pueden releer o examinar detalladamente.

Imprimir en un archivo es el método más cómodo de representación cuando hay que comprobar la consecutividad de las llamadas y registrar grandes cantidades de datos, aunque con un pequeño incoveniente. Consiste en que la impresión se usa, no después de cada iteración, sino sólo al final del archivo, mientras que en cada iteración se usa el guardado de información en la variable string, según el principio explicado más arriba (con la única diferencia de que la información nueva se agrega, no al principio, sino al final de la variable).

string com=""; // declaración de la variable global para el almacenado de datos para la depuración
//+------------------------------------------------------------------+
//| Finalización del programa                                        |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- salvando la información en un archivo al finalizar el programa
   WriteFile();
  }
//+------------------------------------------------------------------+
//| Ejemplo de muestra de la información para la depuración          |
//+------------------------------------------------------------------+
void myfunc(int a)
  {
//--- declaración del contador estático
   static int cnt=0;
//--- almacenando información para la depuración en la variable global
   com+=__FUNCSIG__+" cnt="+(string)cnt+"\n";
//--- aumentamos el contador
   cnt++;
//--- aquí hay parte de un código de la función misma
  }
//+------------------------------------------------------------------+
//| Guardado de datos en el archivo                                  |
//+------------------------------------------------------------------+
void WriteFile(string name="Depuración")
  {
//--- apertura de archivo
   ResetLastError();
   int han=FileOpen(name+".txt",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
//--- comprobando si se ha abierto el archivo
   if(han!=INVALID_HANDLE)
     {
      FileWrite(han,com); // imprimiendo datos
      FileClose(han);     // cierre de archivo
     }
   else
      Print("File open failed "+name+".txt, error",GetLastError());
  }

La función "WriteFile" se invoca en "OnDeinit", de esta forma se graban en el archivo todos los cambios acontecidos en el programa.

Observación importante: si su registro en muy grande, entonces será más lógico guardarlo en varias variables. La mejor manera de hacer esto es poner los contenidos de la variable de texto en una célula de matriz tipo string con la consecuente puesta a cero la variable com (la preparación para la siguiente etapa del trabajo, por así decirlo).

Es necesario hacer esto aproximadamente cada 1-2 millones de líneas (entradas únicas). Así, en primer lugar, evitará la pérdida de datos que puede causar el desbordamiento de la variable (algo que, a pesar de mis esfuerzos, aún no he logrado, los desarrolladores han trabajado muy bien en el tipo string), y en segundo lugar, y esto es lo más importante, podrá mostrar la información en varios archivos, lo que le evitará las dificultades provocadas por la apertura de archivos enormes en el redactor para su visualización.

Para no tener que estar pendiente de la cantidad de líneas guardadas, se pueden usar las separaciones de funciones, para trabajar con los archivos en tres partes. La primera parte es la apertura del archivo, la segunda, la grabación en el archivo en cada iteración, y la tercera parte, el cierre del archivo.

//--- abrir archivo
int han=FileOpen("Depuración.txt",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
//--- escritura de datos
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
if(han!=INVALID_HANDLE) FileWrite(han,com);
//--- cierre de archivos
if(han!=INVALID_HANDLE) FileClose(han);

Sin embargo, hay que usar este método con cuidado. Si la ejecución de su programa termina en error (por ejemplo, como resultado de una división entre cero), entonces podrá obtener un archivo abierto pendiente, ya imposible de manejar y que puede afectar al funcionamiento del sistema operativo.

Le recomiendo encarecidamente que no use en cada iteración del ciclo completo apertura-grabación-cierre. Mi experiencia personal me dice que su disco duro dejará de funcionar en unos cuantos meses.


5. Simulador

Al depurar asesores, con frecuencia surgen situaciones según las cuales se debe llevar a cabo una comprobación del funcionamiento de tal o cual condición. Pero el depurador (del que hablamos más arriba) inicia el asesor sólo en el modo de tiempo real, con lo que podemos estar esperando a que se dé esta condición durante mucho, mucho tiempo.

En realidad, las condiciones comerciales específicas se dan muy raramente. Sin embargo, sabemos que tienen lugar, aunque esperar durante meses que tengan lugar las circunstancias concretas que usted necesita es algo absurdo. ¿Qué podemos hacer, entonces?

En nuestra ayuda acude aquí el probador de estrategias. Para la depuración se usan las mismas funciones "Print" y "Comment". Los comentarios siempre ocupan el primer lugar para evaluar la situación, y ya para un análisis más detallado se usa la función "Print". El probador guarda la información mostrada en el registro del probador (un directorio por separado para cada agente de testado).

Para iniciar el agente en el lugar adecuado, suelo localizar la hora (donde ocurre el fallo, en mi opinión), establezco la fecha necesaria en el probador y lo inicio en el modo de visualización en todos los ticks.

Quiero hacer notar que este método de depuración lo he tomado de MetaTrader 4, donde resultaba casi el único método para depurar un programa durante su ejecución.

Dib.6. Ejecución de la depuración con ayuda del probador de estrategias.

Dib.6. Ejecución de la depuración con ayuda del probador de estrategias.


6. Depuración en OOP

La programación orientada a objetos, aparecida en MQL5, ha dejado también su huella en la depuración. Durante la depuración, es posible orientarse fácilmente en el programa, usando sólo los nombres de las funciones. Pero en OOP, con frecuencia surge la necesidad de saber desde qué objeto se llama tal o cual método. Esto es especialmente importante, cuando los objetos están diseñados verticalmente (utilizando la herencia). Aquí son de mucha ayuda las plantillas (introducidas hace poco en MQL5).

Gracias a las funciones de plantilla se puede obtener el tipo de índice, en forma de un valor del tipo string.

template<typename T> string GetTypeName(const T &t) { return(typename(T)); }

Para la depuración, yo uso esta propiedad de la siguiente forma:

//+------------------------------------------------------------------+
//| La clase básica contiene la variable para                        |
//| el almacenamiento del tipo                                       |
//+------------------------------------------------------------------+
class CFirst
  {
public:
   string            m_typename; // variable para el guardado del tipo
   //--- llenando la variable con el tipo propio en el constructor
                     CFirst(void) { m_typename=GetTypeName(this); }
                    ~CFirst(void) { }
  };
//+------------------------------------------------------------------+
//| La clase derivada cambia el valor de la variable de la clase base|
//+------------------------------------------------------------------+
class CSecond : public CFirst
  {
public:
   //--- llenando la variable con el tipo propio en el constructor
                     CSecond(void) { m_typename=GetTypeName(this); }
                    ~CSecond(void) {  }
  };

La clase básica contiene la variable para el almacenamiento de su propio tipo (se inicia en el constructor de cada objeto). La clase derivada igualmente usa el valor de esta variable para el almacenamiento de el que ya sería su propio tipo. De esa forma, al llamar una macro, simplemente añado al final la variable m_typename y obtengo, no sólo el nombre de la función invocada, sino también el tipo de objeto que esta función llama.

Se puede mostrar el índice mismo para reconocer con mayor precisión los objetos, eso permitirá diferenciar los objetos por el número. Dentro del objeto se hace de esta forma:

Print((string)this); // imprimir el número del índice dentro de la clase

Y afuera así:

Print((string)GetPointer(pointer)); // imprimir el número del índice fuera de la clase

Dentro de cada clase, igualmente, se puede usar la variable para almacenar los nombres del objeto mismo. Así, cuando se cree un objeto, su nombre se podrá transmitir en calidad de parámetro del constructor. En este caso, usted podrá, no sólo separar los objetos por su número, sino también comprender qué indica cada uno de esos objetos (ya que usted mismo les pondrá nombre). Este método se puede realizar de manera análoga al del llenado de la variable m_typename.


7. Rastreamiento

Todos los métodos enumerados más arriba se complementan unos a otros y son muy importantes en la depuración. Pero existe otro método, no muy extendido, el rastreamiento.

Debido a su complijidad y laboriosidad, casi nadie usa este método, así que podría llamársele el último bastión. Si se encuentra en un callejón sin salida, un momento en el que no entiende qué está sucediendo, el rastreamiento puede acudir en su ayuda.

Este método le ayudará a aclararse con la estructura del programa, con qué motivo se llama qué función, los objetos y las secuencias de las llamadas. Gracias a él usted podrá entender rápidamente qué funciona en el programa de manera diferente a como pensaba. Además, el método proporciona una buena panorámica del proyecto.

El rastreamiento se lleva a cabo de la forma siguiente. Creamos dos macros:

//--- abriendo substitución  
#define zx Print(__FUNCSIG__+"{");
//--- cerrando substitución
#define xz Print("};");

Estas son las macros de apertura zx y cierre xz respectivamente. Después las colocamos en los cuerpos de las funciones que hay que rastrear:

//+------------------------------------------------------------------+
//| Ejemplo de rastreamiento de una función                          |
//+------------------------------------------------------------------+
void myfunc(int a,int b)
  {
   zx
//--- aquí hay parte de un código de la función misma
   if(a!=b) { xz return; } // salida en mitad de la función
//--- aquí hay parte de un código de la función misma
   xz return;
  }

Además, si en la función se da la salida según las condiciones, entonces, antes de cada return en la zona protegida, ponemos un xz de cierre. Esto nos permitirá no destruir la estructura del rastreamiento.

Quisiera añadir que la macro descrita más arriba se ha expuesto para que el ejemplo sea más sencillo, para el rastreamiento es mejor usar la impresión en archivo. Asímismo, al usar la grabación en archivo, yo suelo emplear un truco. Para que la estructura del rastreamiento se vea mejor, al grabar envuelvo los nombres de las funciones en la construcción sintáctica siguiente:

if() {...}

Al archivo resultante se le añade la extensión ".mqh", lo que nos da la posibilidad de abrirlo en el MetaEditor y utilizar el estilizador [Ctrl+,] para tener una estructura más visual del rastreamiento.

El código completo para el rastreamiento se puede ver más abajo:

string com=""; // declaración de la variable global para el almacenado de datos para la depuración
//--- abriendo substitución
#define zx com+="if("+__FUNCSIG__+"){\n";
//--- cerrando substitución
#define xz com+="};\n"; 
//+------------------------------------------------------------------+
//| Finalización del programa                                        |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- //--- salvando la información en un archivo al finalizar el programa
   WriteFile();
  }
//+------------------------------------------------------------------+
//| Ejemplo de rastreamiento de una función                          |
//+------------------------------------------------------------------+
void myfunc(int a,int b)
  {
   zx
//--- aquí hay parte de un código de la función misma
   if(a!=b) { xz return; } // salida en mitad de la función
//--- aquí hay parte de un código de la función misma
   xz return;
  }
//+------------------------------------------------------------------+
//| Guardado de datos en el archivo                                  |
//+------------------------------------------------------------------+
void WriteFile(string name="Rastreamiento")
  {
//--- apertura de archivo
   ResetLastError();
   int han=FileOpen(name+".mqh",FILE_WRITE|FILE_TXT|FILE_ANSI," ");
//--- comprobando si se ha abierto el archivo
   if(han!=INVALID_HANDLE)
     {
      FileWrite(han,com); // imprimiendo datos
      FileClose(han);     // cierre del archivo
     }
   else
      Print("File open failed "+name+".mqh, error",GetLastError());
  }

Para comenzar el rastreamiento desde un lugar determinado hay que completar las macros con las condiciones:

bool trace=0; //variable para proteger el rastreamiento con la condición
//--- abriendo substitución
#define zx if(trace) com+="if("+__FUNCSIG__+"){\n";
//--- cerrando substitución
#define xz if(trace) com+="};\n";

En este caso, usted será capaz de activar o desactivar el rastreamiento, después de asignar el valor "true" o "false" en la variable "trace" tras un evento determinado o en un lugar concreto.

Si el rastreamiento ya no es necesario, pero puede serlo de nuevo en el futuro, o en ese momento no hay tiempo para limpiar la fuente, entonces se lo puede desconectar cambiando los valores de las macros por otros vacíos:

//--- substitución de valores vacíos
#define zx
#define xz

El archivo del asesor estándar, con los cambios necesarios para el rastreamiento, se incluye más abajo. Puede ver los resultados del rastreamiento después de iniciar el asesor en el gráfico, en el directorio Files (se creará el archivo rastreamiento.mqh). Aquí tenemos parte del texto resultante:

if(int OnInit()){
};
if(void OnTick()){
if(void CheckForOpen()){
};
};
if(void OnTick()){
if(void CheckForOpen()){
};
};
if(void OnTick()){
if(void CheckForOpen()){
};
};
//--- ...

Dese cuenta de que al principio, en el archivo creado no resulta obvia la estructura de subrutinas anidadas, pero tras usar el estilizador de código usted podrá ver su estructura al completo. Aquí tenemos el texto del archivo resultante, después de utilizar el estilizador:

if(int OnInit())
  {
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
if(void OnTick())
  {
   if(void CheckForOpen())
     {
     };
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
if(void OnTick())
  {
   if(void CheckForOpen())
     {
     };
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
if(void OnTick())
  {
   if(void CheckForOpen())
     {
     };
  };
//--- ...

Se trata simplemente de un truco mío, no de una forma establecida sobre cómo hacer el rastreamiento. Cada uno puede hacer el rastreamiento como le resulte más cómodo. Lo más importante es que el rastreamiento revele la estructura de llamadas de las funciones.


Una observación importante sobre el depurado

Si durante el depurado usted introduce cambios en el código, entonces aplique el envolvimiento de llamadas de funciones directas de MQL5. Se hace de la forma que sigue:

//+------------------------------------------------------------------+
//| Ejemplo de envolvimiento de las funciones estándar,              |
//| en una función shell                                             |
//+------------------------------------------------------------------+
void DebugPrint(string text) { Print(text); }

Esto le permitirá limpiar el código de una manera muy sencilla cuando la depuración esté completa:

Y lo mismo en lo que respecta a las variables usadas durante la depuración. Por eso, intente usar variables y funciones declaradas de manera global, esto le evitará buscar las construcciones extraviadas en las profundidades de su programa.


Conclusión

La depuración es una parte importante en el trabajo de un programador. Alguien que no sepa llevar a cabo la depuración de programas no se puede llamar programador. La depuración más importante siempre tiene lugar en la propia cabeza del programador. En el artículo sólo he mostrado los métodos usados en la depuración. Pero sin una comprensión adecuada de los principios de funcionamiento del programa desarrollado, estos métodos no tendrán utilidad alguna.

¡Le deseo éxito en futuras depuraciones!