Aprendiendo MQL5 de principiante a profesional (Parte VI): Fundamentos del desarrollo de asesores expertos
Introducción
Por fin hemos llegado a la fase de creación de asesores expertos (EAs). En cierto modo, hemos cruzado el Rubicón.
Para aprovechar al máximo este artículo, ya debes estar familiarizado con los siguientes conceptos:
- Variables (locales y globales)
- Funciones y sus parámetros (tanto por referencia como por valor)
- Matrices (incluida una comprensión básica de las matrices en serie)
- Operadores principales, incluidos los operadores lógicos, aritméticos, condicionales (si, cambio, ternarios) y de bucle (principalmente 'for', pero también es útil estar familiarizado con 'while' y 'do...while').
Desde la perspectiva de un programador, los Asesores Expertos no son mucho más complejos que los indicadores que analizamos en el artículo anterior de esta serie. La lógica comercial también implica verificar ciertas condiciones y, si se cumplen, realizar una acción (normalmente enviar una orden comercial al servidor). La clave es comprender la estructura de las órdenes comerciales, conocer las funciones para enviar esas órdenes y poder acceder a los datos necesarios para operar.
¡Importante! Todos los asesores expertos que aparecen en este artículo tienen como único objetivo ilustrar los principios de programación y no están destinados al comercio real ni a generar beneficios. Si planeas utilizar este código en cuentas reales, es probable que tengas que perfeccionar los algoritmos de toma de decisiones. De lo contrario, corres el riesgo de incurrir en pérdidas.
De hecho, el código de los EAs que se proporciona aquí no es adecuado para el trading real, incluso si se mejora la lógica de entrada. El primer ejemplo no incluye ningún tipo de gestión de errores: ni para el envío de la solicitud ni para la respuesta del servidor. Esto se hace intencionadamente para simplificar el código y facilitar su comprensión, pero limita el uso del EA en la creación rápida de prototipos o en la prueba de la lógica básica de la estrategia. El segundo EA incluye un poco más de validación... Sin embargo, ni siquiera eso es suficiente para publicar en el mercado o para realizar operaciones en tiempo real de forma fiable, donde los problemas deben resolverse a medida que surgen (si es que pueden resolverse, en lugar de simplemente notificarse y cerrarse).
En el próximo artículo se abordará un EA completamente funcional que técnicamente podría aceptarse para su publicación en el Market. Ese EA incluirá las validaciones necesarias y una lógica un poco más compleja que la que cubriremos aquí. En este artículo, me centro en los fundamentos de la automatización del trading. Crearemos dos EAs: uno sin indicadores y otro que utilice el indicador de media móvil integrado. El primero operará utilizando órdenes pendientes, mientras que el segundo ejecutará las operaciones al precio de mercado.
Plantilla de asesor experto
Cada asesor experto comienza con la creación de una plantilla en blanco, generalmente utilizando el Asistente MQL5 (Figura 1).

Figura 1. Asistente de MQL (MQL Wizard): Primera pantalla
El Asistente MQL ofrece dos opciones principales: Crear un asesor experto a partir de una plantilla (opción superior) o generar una versión más avanzada y estructurada. Para los programadores principiantes, recomiendo encarecidamente elegir la primera opción, es decir, la plantilla.
La versión generada avanzada está orientada a objetos y dividida en varios archivos. Sin herramientas adicionales ni experiencia, puede resultar bastante complicado de entender y aún más difícil de personalizar para adaptarlo a su propia lógica de trading. Por eso sugiero utilizar esta versión solo después de haber adquirido una sólida comprensión de los conceptos y prácticas de la programación orientada a objetos (Object-Oriented Programming, OOP). Revisar el código generado puede resultar instructivo en términos de «ver lo que es posible», pero recuerde que solo es una de las muchas implementaciones posibles, optimizada para la generación automática. Para cuando estés listo para comprender completamente las complejidades de su estructura de clases, probablemente prefieras escribir tus propias plantillas de código. Naturalmente, editar tu propio código es mucho más fácil que descifrar el de otra persona. Y lo más probable es que tus propias plantillas sean tan buenas, si no mejores, que las que proporciona el Asistente.
La mayoría de las funciones opcionales que el Asistente puede añadir (figuras 2 y 3) no son estrictamente necesarias, pero suelen ser muy útiles. Por ejemplo, las funciones de la tercera pantalla del Asistente (Figura 2) le permiten gestionar eventos desencadenados durante las operaciones bursátiles, como cuando el servidor recibe una señal, se abre una posición, etc. (OnTrade y OnTradeTransaction), así como eventos de temporizador (OnTimer), interacciones con gráficos como pulsaciones de botones o creación de objetos (OnChartEvent) y actualizaciones del libro de órdenes (OnBookEvent).

Figura 2. Creación de un asesor experto: Tercera pantalla del Asistente (funciones adicionales del EA).
También hay funciones especiales que se utilizan exclusivamente dentro del probador de estrategias, pero no durante el funcionamiento normal (Figura 3). Estos son principalmente útiles para versiones de demostración que solo funcionan en el entorno de pruebas y no en cuentas reales. En ocasiones, es posible que necesite registros más detallados durante las pruebas o que desee obtener datos de fuentes alternativas. Personalmente, utilizo estas funciones muy raramente, pero pueden ser muy útiles en el contexto adecuado.

Figura 3. Creación de un asesor experto: Cuarta pantalla del Asistente (funciones para el funcionamiento exclusivo del Probador)
Algunas de las funciones de la figura 2 se tratarán con más detalle en futuros artículos, mientras que las de la figura 3 las dejaremos para que las explore usted mismo.
Al crear un EA utilizando el Asistente, el archivo generado siempre incluye al menos tres funciones (Ejemplo 1):
//+------------------------------------------------------------------+ //| FirstExpert.mq5 | //| Oleg Fedorov (aka certain) | //| mailto:coder.fedorov@gmail.com | //+------------------------------------------------------------------+ #property copyright "Oleg Fedorov (aka certain)" #property link "mailto:coder.fedorov@gmail.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
Ejemplo 1. Plantilla mínima del EA creada por el Asistente.
- OnInit: Una función habitual en el desarrollo de indicadores, utilizada para la configuración inicial. Se ejecuta una vez al inicio del programa.
- OnDeinit: También es probable que le resulte familiar, esta función se invoca cuando el Asesor Experto se detiene. Está destinado a la limpieza: eliminar objetos gráficos creados por EA, cerrar archivos, liberar recursos de indicadores y realizar otras tareas de finalización.
- OnTick: Se ejecuta en cada tick (similar a OnCalculate en los indicadores). Aquí es donde se ejecuta la lógica central de los EAs.
Entre estos, solo OnTick es obligatorio para un asesor experto. Puede omitir OnInit y OnDeinit si su EA es simple.
Términos clave del trading automatizado en MetaTrader 5
Al desarrollar sistemas de trading automatizado en MetaTrader 5, hay ciertos términos y conceptos que todo programador debe comprender. Estos términos están asociados con la lógica comercial y los nombres de las funciones. Vamos a explorarlos.
Order: Mensaje enviado al servidor en el que se indica la intención de comprar o vender un instrumento específico a un precio determinado.
Cada cambio en la negociación, ya sea colocar una orden de mercado o modificar un nivel de Stop Loss, se realiza a través de órdenes de negociación. Las órdenes pueden ser de ejecución inmediata (por ejemplo, compra/venta al precio de mercado) o pendientes, lo que significa que la operación se ejecutará en el futuro cuando se cumplan las condiciones de precio (como las órdenes stop o limitadas).
Las órdenes pendientes incluyen Stop Loss, Take Profit, Buy/Sell Stop, Buy/Sell Limit y Buy/Sell Stop Limit. Algunas funciones relacionadas con la gestión de pedidos son OrderSend (envía un pedido al servidor) y OrderGetInteger (devuelve parámetros enteros del pedido, como el número de ticket o la hora de creación).
Deal: La ejecución real de una orden.
Una operación en MetaTrader 5 está esencialmente relacionada con el historial. Es el momento en el que se ejecuta una orden. No se puede influir directamente en una operación, ya que esta se lleva a cabo en el servidor una vez que se ejecuta una orden. Sin embargo, puede recuperar información histórica sobre las operaciones, como el precio y la hora de ejecución. Funciones de ejemplo: HistoryDealGetDouble (obtiene un parámetro doble de una transacción, como el precio), HistoryDealsTotal (devuelve el número total de transacciones en el historial).
Position: el estado resultante de su cartera tras una o varias operaciones con un símbolo específico.
MetaTrader 5 se diseñó originalmente para que cada símbolo pudiera tener solo una posición. Sin embargo, el comportamiento real depende del tipo de cuenta. En las cuentas de compensación, todas las operaciones actualizan una única posición. En las cuentas de cobertura, cada operación crea su propia posición (a menos que se trate de una orden stop). Esto puede dar lugar a múltiples posiciones sobre el mismo símbolo e incluso en direcciones opuestas. En tales casos, es posible que tenga que calcular manualmente la exposición neta larga y corta. Las posiciones se pueden modificar mediante órdenes de negociación: se pueden cerrar total o parcialmente, o ajustar en términos de niveles de Stop Loss y Take Profit. Algunos ejemplos de funciones que funcionan con posiciones son: PositionSelectByTicket (seleccionar una posición por su ticket) y PositionGetString (obtener parámetros de cadena como el nombre del símbolo o el comentario del usuario).
Cada operación es el resultado de la ejecución de una orden de negociación, y cada posición refleja el efecto acumulativo de una o más operaciones.
Event: Cualquier suceso significativo que se produzca en el entorno del programa.
Enviar una solicitud de intercambio es un evento. El servidor que acepta la solicitud, un usuario que hace clic en el gráfico, un cambio en la escala del gráfico, un nuevo tick... todos estos también son eventos. Algunos de ellos se gestionan mediante funciones de controlador estándar que comienzan por On, como OnInit (activada por el evento Init) u OnTick (para eventos Tick).
Otros tipos de eventos se procesan utilizando identificadores constantes. Esto significa que primero debe activarse una de las funciones del controlador de eventos globales. Por ejemplo, OnChartEvent se invoca para cualquier evento de gráfico. Dentro de esta función, se determina el tipo exacto de evento comparando la variable del código del evento con constantes conocidas. Los parámetros que se pasan a estas funciones ayudan a identificar los detalles del evento. Este artículo no entrará en detalles sobre estos eventos más pequeños.
Principios básicos del trading automatizado en MetaTrader 5
Echemos un vistazo general a cómo funciona realmente el trading en MetaTrader 5. Comenzaremos con un dato importante.
El software del terminal que se ejecuta en su ordenador y el software del servidor que ejecuta las operaciones con su dinero son dos programas independientes. Se comunican exclusivamente a través de la red (normalmente Internet).
Por lo tanto, cuando haces clic en «Buy» o «Sell», se produce la siguiente secuencia de eventos:
- Su terminal genera un paquete de datos especial, rellenando una estructura especial MqlTradeRequest.
- Esta estructura rellenada se envía al servidor utilizando OrderSend (modo sincrónico) u OrderSendAsync (modo asincrónico), formando una orden de negociación order.
- El servidor recibe el paquete y comprueba si cumple todos los requisitos: Si hay precios coincidentes, si tiene saldo suficiente, etc.
- Si todo está bien, la orden se coloca en la cola de órdenes junto con las órdenes de otros operadores, a la espera de ser ejecutada.
- Se envía un mensaje de confirmación a su terminal.
- Si el mercado alcanza el nivel de precio solicitado, el servidor ejecuta la operación y registra el evento en su registro.
- El servidor envía los resultados a tu terminal.
- El terminal recibe este resultado en forma de una estructura MqlTradeResult y genera los eventos correspondientes, como Trade y TradeTransaction.
- El terminal comprueba si hay errores en el lado del servidor (lo hace comprobando el campo «retcode» de la estructura MqlTradeResult).
- Si todo está bien, el terminal actualiza sus variables internas, las entradas del registro y el gráfico.
- Como resultado, aparecerá una nueva posición (o una actualizada) en su cartera para el instrumento correspondiente.
Todo este proceso se puede visualizar como un diagrama simplificado, tal y como se muestra en la figura 4:

Figura 4. Diagrama del proceso de tramitación de órdenes comerciales
Modo de transmisión de datos asincrónicos
You may have noticed that during the interaction between the terminal and the server, the terminal must communicate over the network at least twice: once to send data and again to receive a response. Cuando se utiliza la función OrderSend, este proceso es esencialmente sincrónico, es decir, el EA espera una respuesta, ocupando recursos del sistema como el ancho de banda de Internet y, potencialmente, tiempo de CPU.
Sin embargo, desde el punto de vista del procesador, las operaciones de red son muy lentas. Un script de trading suele ejecutarse en unos pocos cientos de microsegundos (por ejemplo, 200 μs = 2e-4 segundos), pero la transmisión de datos en red se mide normalmente en milisegundos (por ejemplo, 20 ms = 2e-2 segundos), lo que es al menos 100 veces más lento. Añade el tiempo de procesamiento del servidor y, en ocasiones, retrasos inesperados debido a tareas de mantenimiento o problemas técnicos, etc. En el peor de los casos, el intervalo entre el envío de una solicitud de operación y la recepción de una respuesta podría prolongarse hasta varios segundos o incluso minutos. Si varios EA están esperando sin hacer nada durante este tiempo, se desperdiciarán muchos recursos de la CPU de forma improductiva.
Para solucionar esta ineficiencia, MetaTrader ofrece un modo de negociación especial asincrónico. La palabra «asincrónico» significa que el EA puede enviar una solicitud de operación y continuar realizando otras tareas (dormir, ejecutar cálculos o cualquier otra cosa) sin esperar una respuesta. Cuando finalmente llega la respuesta del servidor, el terminal genera un evento TradeTransaction (y luego un evento Trade). El EA puede entonces «despertarse» y procesar la respuesta para tomar nuevas decisiones de trading. Las ventajas de este enfoque son evidentes.
Tanto en el modo sincrónico como en el asincrónico, los errores comerciales se gestionan mediante la función OnTradeTransaction. Esto no hace que el código sea más complejo, simplemente se traslada parte de la lógica de OnTick a OnTradeTransaction. Y si trasladas este código a funciones separadas, llamarlo y transferirlo no causará ningún problema. Por lo tanto, la elección entre los modos de negociación sincrónico y asincrónico depende totalmente de sus preferencias y de la tarea que se vaya a realizar. Las estructuras de datos utilizadas en ambos modos siguen siendo las mismas.
Empezando a operar
Supongamos que queremos crear un Asesor Experto para el mercado FOREX que busque barras internas. Como recordatorio, una barra interior es una vela cuyo máximo y mínimo se encuentran completamente dentro del rango de la vela anterior (más grande). El EA operará una vez por vela y, cuando detecte el patrón de barra interior, colocará simultáneamente dos órdenes pendientes:
- Una orden de compra stop unos pocos puntos (configurables) por encima del máximo de la vela más grande.
- Una orden de venta stop a la misma distancia por debajo del mínimo de esa vela.
- Cada pedido tendrá una validez de dos barras. Si no se activa dentro de este periodo, se eliminará.
- El Stop Loss para ambas órdenes se colocará en el punto medio de la vela más grande.
- El Take Profit se fijará en 7/8 del rango de la vela más grande.
- El volumen de negociación será el lote mínimo permitido.
Esta versión inicial de la EA evitará condiciones adicionales para que el código sea más fácil de entender. Crearemos un marco básico que se pueda ampliar más adelante. Comenzaremos creando la plantilla EA utilizando el asistente, dejando todas las casillas sin marcar en la tercera ventana (ya que en esta versión no gestionaremos las respuestas del servidor). El código resultante será similar al del Ejemplo 1. Para que el EA sea configurable y optimizable, definiremos cuatro parámetros de entrada: distancia desde el máximo/mínimo para colocar órdenes pendientes (inp_PipsToExtremum), distancia para colocar Stop Loss y Take Profit (inp_StopCoefficient e inp_TakeCoefficient), y el número de barras tras las cuales se eliminarán las órdenes no activadas (inp_BarsForOrderExpired). Además, declararemos un número mágico para el EA, lo que nos ayudará a distinguir «nuestras» órdenes de las realizadas por otros EA o manualmente.
//--- declare and initialize input parameters input int inp_PipsToExtremum = 2; input double inp_TakeCoeffcient = 0.875; input double inp_StopCoeffcient = 0.5; input int inp_BarsForOrderExpired = 2; //--- declare and initialize global variables #define EXPERT_MAGIC 11223344
Escenario 2. Descripción de los parámetros de entrada del EA y el número mágico
Solo un recordatorio: El código del Ejemplo 2 debe colocarse en la parte superior del archivo EA, inmediatamente después de las directivas #property.
El resto del código de este ejemplo se colocará dentro de la función OnTick. Dejaremos todas las demás funciones vacías por ahora. Aquí está el código que debe colocarse en el cuerpo de OnTick:
/**************************************************************** * Please note: this Expert Advisor uses standard functions * * to access price/time data. Therefore, it's convenient to * * work with series as arrays (time and prices). * ****************************************************************/ string symbolName = Symbol(); ENUM_TIMEFRAMES period = PERIOD_CURRENT; //--- Define a new candlestick (Operations only at the start of a new candlestick) static datetime timePreviousBar = 0; // Time of the previous candlestick datetime timeCurrentBar; // Time of the current candlestick // Get the time of the current candlestick using the standard function timeCurrentBar = iTime( symbolName, // Symbol name period, // Period 0 // Candlestick index (remember it's series) ); if(timeCurrentBar==timePreviousBar) { // If the time of the current and previous candlesticks match return; // Exit the function and do nothing } // Otherwise the current candlestick becomes the previous one, // so as not to trade on the next tick timePreviousBar = timeCurrentBar; //--- Prepare data for trading double volume=SymbolInfoDouble(symbolName,SYMBOL_VOLUME_MIN); // Volume (lots) - get minimum allowed volume // Candlestick extrema double high[],low[]; // Declare arrays // Declare that arrays are series ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); // Fill arrays with values of first two closed candlesticks // (start copying with index 1 // as we only need closed candlesticks; use 2 values) CopyHigh(symbolName,period,1,2,high); CopyLow(symbolName,period,1,2,low); double lengthPreviousBar; // The range of the "long" bar MqlTradeRequest request; // Request structure MqlTradeResult result; // Server response structure if( // If the first closed bar is inside high[0]<high[1] && low[0]>low[1] ) { // Calculate the range lengthPreviousBar=high[1]-low[1]; // Timeseries have right-to-left indexing //--- Prepare data for a buy order request.action =TRADE_ACTION_PENDING; // order type (pending) request.symbol =symbolName; // symbol name request.volume =volume; // volume deal request.type =ORDER_TYPE_BUY_STOP; // order action (buy) request.price =high[1] + inp_PipsToExtremum*Point(); // buy price // Optional parameters request.deviation =5; // acceptable deviation from the price request.magic =EXPERT_MAGIC; // EA's magic number request.type_time =ORDER_TIME_SPECIFIED; // Parameter is required to set the lifetime request.expiration =timeCurrentBar+ PeriodSeconds()*inp_BarsForOrderExpired; // Order lifetime request.sl =high[1]-lengthPreviousBar*inp_StopCoeffcient; // Stop Loss request.tp =high[1]+lengthPreviousBar*inp_TakeCoeffcient; // Take Profit //--- Send a buy order to the server OrderSend(request,result); // For asynchronous mode you need to use OrderSendAsync(request,result); //--- Clear the request and response structures for reuse ZeroMemory(request); ZeroMemory(result); //--- Prepare data for a sell order. Parameers are the same as in the previous function. request.action =TRADE_ACTION_PENDING; // order type (pending) request.symbol =symbolName; // symbol name request.volume =volume; // volume request.type =ORDER_TYPE_SELL_STOP; // order action (sell) request.price =low[1] - inp_PipsToExtremum*Point(); // sell price // Optional parameters request.deviation =5; // acceptable deviation from the price request.magic =EXPERT_MAGIC; // EA's magic number request.type_time =ORDER_TIME_SPECIFIED; // Parameter is required to set the lifetime request.expiration =timeCurrentBar+ PeriodSeconds()*inp_BarsForOrderExpired; // Order lifetime request.sl =low[1]+lengthPreviousBar*inp_StopCoeffcient; // Stop Loss request.tp =low[1]-lengthPreviousBar*inp_TakeCoeffcient; // Take Profit //--- Send a sell order to the server OrderSend(request,result); }
Ejemplo 3. La función OnTick de este EA contiene toda la lógica de trading.
La función estándar Point devuelve el tamaño del punto para el gráfico actual. Por ejemplo, si el bróker ofrece cotizaciones de cinco dígitos, un punto para el EURUSD será 0.00001 y para el USDJPY será 0.001. Las funciones iTime, iHigh e iLow permiten recuperar la hora, el precio más alto y el precio más bajo de una vela específica (por su índice de derecha a izquierda, siendo 0 la barra actual). En este ejemplo, solo utilizamos iTime para comprobar si hay una nueva barra recuperando la hora actual. Para obtener los valores altos y bajos, utilizamos las funciones de copia de matrices CopyHigh y CopyLow.
El código se divide en dos secciones principales: comprobación de una nueva barra y negociación (que comienza con una fase de preparación). El bloque de negociación se divide a su vez en dos segmentos casi idénticos: uno para la compra y otro para la venta. Es evidente que la estructura y la lógica de envío de órdenes son tan similares en ambos casos que tendría sentido refactorizarlas en una función independiente, que rellenaría los campos comunes y se ramificaría solo para aspectos específicos como el tipo de orden y los precios de ejecución (precio, tp, sl). Sin embargo, en este ejemplo, la compacidad y la reutilización del código se sacrificaron intencionadamente en aras de la claridad y la legibilidad.
Cada etapa del proceso se marca con un comentario utilizando //---, mientras que los comentarios dentro de cada etapa se escriben en estilo sencillo sin guiones. La lógica de negociación consta de dos partes principales: rellenar la estructura de la solicitud y enviarla. Al rellenar la estructura de la solicitud, solo los cinco primeros campos son obligatorios.
Es importante tener en cuenta que si tiene intención de utilizar el tiempo de caducidad del pedido, tal y como se muestra en este ejemplo, debe rellenar ambos campos request.type_time y request.expiration. Si deja el primer campo sin configurar, el segundo se ignorará de forma predeterminada.
Para probar cómo funciona este Asesor Experto, puede ejecutarlo en cualquier marco temporal en una cuenta demo (funciona incluso en gráficos por minutos, aunque el rendimiento real depende del spread del símbolo elegido). Como alternativa, pulse <Ctrl>+<F5> en MetaEditor para iniciar una prueba retrospectiva utilizando datos históricos en el Probador de estrategias. El código fuente completo se puede encontrar en el archivo adjunto TrendPendings.mq5.
Utilizando indicadores estándar
El Asesor Experto del Ejemplo 3 no utilizó ningún indicador, pero eso no siempre será así. Para las estrategias basadas en indicadores estándar, existen dos enfoques principales: utilizar funciones de indicador integradas o trabajar con clases de indicador. Cubriremos ambos métodos, comenzando por las funciones integradas.
Supongamos que queremos crear un Asesor Experto basado en una media móvil simple. Como recordatorio, el objetivo de este artículo no es crear una estrategia rentable, sino demostrar la lógica fundamental del trading. Por lo tanto, mantendré el código lo más sencillo y legible posible. Basándonos en eso, definiremos nuestras reglas de negociación de la siguiente manera:
- Al igual que en el EA anterior, las operaciones solo se considerarán cuando se forme una nueva barra;
- Para comprar, la vela anterior debe cerrar por encima de la media móvil.
- Para vender, la vela anterior debe cerrar por debajo de la media móvil.
- Como filtro, utilizaremos la pendiente de la media móvil: si la media sube desde la penúltima barra hasta la última barra cerrada, compramos; si baja, vendemos.
- Como filtro, utilizaremos la pendiente de la media móvil: si la media sube desde la penúltima barra hasta la última barra cerrada, compramos; si baja, vendemos.
- Se coloca un Stop Loss protector en el máximo (para señales de venta) o en el mínimo (para señales de compra) de la vela de la señal.
- Solo se permite una posición abierta por símbolo, incluso en cuentas de cobertura; si aparece una señal pero ya hay una posición abierta, la omitimos.
La figura 5 ilustra el principio de filtrado utilizado en este EA.

Figura 5. Principio de filtrado de velas utilizado en un EA basado en medias móviles.
En este EA, seguiré esforzándome por mantener el código lo más limpio y comprensible posible. Sin embargo, comenzaré a incorporar más comprobaciones de errores para acercar la implementación a los estándares del mundo real.
Todos los parámetros de la media móvil, junto con la desviación máxima permitida del precio de la orden solicitada, se añadirán a los parámetros de entrada. Además, declararemos una variable global para el identificador del indicador (lo explicaré en un momento) y también definiremos el número mágico del EA.
//--- declare and initialize global variables #define EXPERT_MAGIC 3345677 input int inp_maPeriod = 3; // MA period input int inp_maShift = 0; // Shift input ENUM_MA_METHOD inp_maMethod = MODE_SMA; // Calculation mode input ENUM_APPLIED_PRICE inp_maAppliedPrice = PRICE_CLOSE; // Applied price input int inp_deviation = 5; // Max price deviation from the request price in points //--- MA indicator handle int g_maHandle;
Ejemplo 4. Variables globales de EA para operar utilizando medias móviles.
Antes de utilizar cualquier indicador en un EA u otro indicador, debemos hacer tres cosas:
-
Inicializar el indicador y obtener un identificador para él. Esto se suele hacer utilizando funciones indicadoras integradas (como iMA para la media móvil) o iCustom para indicadores personalizados definidos por el usuario. Esta inicialización se suele llevar a cabo dentro de la función OnInit.
En programación, un identificador es como una referencia o puntero a un recurso con el que tu código puede interactuar. Piénsalo como un número de ticket que te permite acceder al indicador y solicitar sus datos siempre que lo necesites. - Antes de poder utilizar los valores del indicador, es necesario obtener los datos más recientes. Esto se suele hacer creando una matriz por cada búfer de indicador y rellenando estas matrices con la función CopyBuffer.
- Utilice los datos de las matrices rellenadas.
- Para evitar fugas de memoria o el uso innecesario de recursos, es importante liberar el identificador del indicador cuando finaliza el programa. Esto se hace en la función OnDeinit utilizando IndicatorRelease.
En este EA en particular, las funciones OnInit y OnDeinit son bastante sencillas y no contienen ninguna lógica inusual o compleja:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Before an action that could potentially cause an error, reset // built-in _LastError variable to default // (assuming there's no error yet) ResetLastError(); //--- The standard iMA function returns the indicator handle g_maHandle = iMA( _Symbol, // Symbol PERIOD_CURRENT, // Chart period inp_maPeriod, // MA period inp_maShift, // MA shift inp_maMethod, // MA calculation method inp_maAppliedPrice // Applied price ); // inp_maAppliedPrice in general case can be // either a price type as in this example, // (from ENUM_APPLIED_PRICE), // or a handle of another indicator //--- if the handle is not created if(g_maHandle==INVALID_HANDLE) { //--- report failure and output error code PrintFormat("Failed to crate iMA indicator handle for the pair %s/%s, error code is %d", _Symbol, EnumToString(_Period), GetLastError() // Output error code ); //--- If an error occurs, terminate the EA early return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release resources occupied by the indicator if(g_maHandle!=INVALID_HANDLE) IndicatorRelease(g_maHandle); }
Ejemplo 5. Inicialización y desinicialización de indicadores en un Asesor Experto
Un matiz sobre el que me gustaría llamar su atención es el uso del par de funciones ResetLastError y GetLastError dentro de la función de inicialización. El primero restablece la variable del sistema _LastError a un estado «sin error», mientras que el segundo permite recuperar el código del error más reciente, si se ha producido alguno.
Aparte de eso, todo es bastante sencillo. Las funciones de inicialización para indicadores (incluido iMA) devuelven un identificador de indicador válido o una constante especial INVALID_HANDLE si no se ha podido obtener el identificador. Este mecanismo nos permite detectar cuándo algo ha salido mal y gestionar el error como corresponde; en nuestro caso, mostrando un mensaje de error. Si OnInit devuelve INIT_FAILED, el Asesor Experto (o indicador) no se iniciará. Y, de hecho, si no conseguimos una referencia válida al indicador de media móvil, lo único que podemos hacer es detener la ejecución.
En cuanto a la función OnTick, la analizaremos paso a paso. La primera parte consiste en la declaración e inicialización de variables.
//--- Declare and initialize variables MqlTradeRequest requestMakePosition; // Request structure for opening a new position MqlTradeRequest requestClosePosition; // Request structure for closing an existing position MqlTradeResult result; // Structure for receiving the server's response MqlTradeCheckResult checkResult; // Structure for validating the request before sending bool positionExists = false; // Flag indicating if a position exists bool tradingNeeds = false; // Flag indicating whether trading is allowed ENUM_POSITION_TYPE positionType; // Type of currently open position ENUM_POSITION_TYPE tradingType; // Desired position type (used for comparison) ENUM_ORDER_TYPE orderType; // Desired order type double requestPrice=0; // Entry price for the future position /* The MqlRates structure contains all candle data: open, close, high, and low prices, tick volume, real volume, spread, and time. In this example, I decided to demonstrate how to fill the entire structure at once, instead of retrieving each value separately. */ MqlRates rates[]; // Array of price data used for evaluating trading conditions double maValues[]; // Array of MA values // Declare data arrays as series ArraySetAsSeries(rates,true); ArraySetAsSeries(maValues,true);
Ejemplo 6. Variables locales de la función OnTick
Tenemos la misma comprobación de si ha aparecido una barra:
//--- Check whether there's a new bar static datetime previousTime = iTime(_Symbol,PERIOD_CURRENT,0); datetime currentTime = iTime(_Symbol,PERIOD_CURRENT,0); if(previousTime==currentTime) { return; } previousTime=currentTime;
Ejemplo 7. Comprobando si es una barra nueva
A continuación, obtenemos todos los datos que necesitamos utilizando funciones especiales. Aquí asumimos que puede haber errores, por ejemplo, que el terminal no haya tenido tiempo de cargar los datos necesarios, por lo que procesamos estos posibles errores utilizando el operador de ramificación.
//--- Prepare data for processing // Copy the quotes of two bars, starting from the first one if(CopyRates(_Symbol,PERIOD_CURRENT,1,2,rates)<=0) { PrintFormat("Data error for symbol %s, error code is %d", _Symbol, GetLastError()); return; } // Copy the values of the moving average indicator buffer if(CopyBuffer(g_maHandle,0,1,2,maValues)<=0) { PrintFormat("Error getting indicator data, error code is %d", GetLastError()); return; }
Ejemplo 8. Copiar los datos actuales del indicador y las cotizaciones a matrices locales
Y ahora seleccionamos una posición abierta para el símbolo actual utilizando la función estándar PositionSelect. Decidimos que solo puede haber una posición para un símbolo, por lo que no debería haber ningún problema, pero seguimos pensando detenidamente en lo que podría salir mal... Como mínimo, debemos comprobar que la posición ha sido abierta por nuestro EA:
//--- Determine if there is an open position if(PositionSelect(_Symbol)) { // Set the open position flag - for further processing positionExists = true; // Save the type of the open position positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // Check if the position has been opened by our EA requestClosePosition.magic = PositionGetInteger(POSITION_MAGIC); // I didn't create a separate variable for // the existing position magic number if(requestClosePosition.magic!= EXPERT_MAGIC) { // Some other EA started trading our symbol. Let it do so... return; } // if(requestClosePosition.magic!= EXPERT_MAGIC) } // if(PositionSelect(_Symbol))
Ejemplo 9. Obtener datos de la posición actual
Ahora podemos comprobar las condiciones comerciales. En este ejemplo, guardaré los resultados de la comprobación en variables separadas y luego utilizaré estas variables para tomar la decisión final: comprar o vender. En soluciones grandes y complejas, este enfoque se justifica, en primer lugar, por su flexibilidad y, en segundo lugar, por el hecho de que el código final resulta más corto. Aquí utilicé esta técnica principalmente por la segunda razón: dado que el algoritmo final no es muy claro, intento mejorar la claridad de diferentes maneras.
//--- Check trading conditions, if( // Conditions for BUY rates[0].close>maValues[0] // If the first candlestick closed above MA && maValues[0]>maValues[1] // and the MA slope is upwards ) { // Set the trade flag tradingNeeds = true; // and inform the EA about the direction (here - BUY) tradingType = POSITION_TYPE_BUY; // to check the direction of the open direction orderType = ORDER_TYPE_BUY; // to trade in the right direction // calculate the deal price requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_ASK); } else if( // conditions for SELL rates[0].close<maValues[0] && maValues[0]<maValues[1] ) { tradingNeeds = true; tradingType = POSITION_TYPE_SELL; orderType = ORDER_TYPE_SELL; requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_BID); }
Ejemplo 10. Comprobación de las condiciones comerciales
El código que se muestra a continuación determina si una operación debe ejecutarse en el momento actual. La decisión se basa en tres preguntas clave:
- ¿Hay alguna configuración de trading? En otras palabras, ¿ha cerrado la vela por encima de la media móvil? Esta condición se gestiona mediante la variable tradingNeeds. Si la respuesta es «no» (tradingNeeds == false), no se debe realizar ninguna operación.
- ¿Ya hay una vacante disponible? Esto se comprueba utilizando la variable positionExists. Si no hay ninguna posición abierta, siga adelante y opere. Si es así, pase a la siguiente comprobación.
- ¿La posición existente está alineada con la nueva señal comercial o es opuesta a ella? Esto se determina comparando tradingType y positionType. Si son iguales, la posición se alinea con la nueva señal, por lo que no se abre ninguna nueva operación. Si difieren, la posición actual está en la dirección opuesta y debe cerrarse antes de abrir una nueva.
Esta lógica de decisión se visualiza en el diagrama de flujo (Figura 6).

Figura 6. Diagrama de flujo que representa los principales puntos de decisión del algoritmo de negociación.
En MQL5, tanto cerrar como abrir una posición implican el envío de una orden de mercado. El enfoque es el mismo que el que ya conoces: completar una estructura de solicitud comercial y enviarla al servidor. Y la diferencia entre estas dos estructuras es que, al cerrar una posición, es necesario especificar su ticket y copiar con precisión los parámetros de la posición existente en la solicitud. Al abrir una nueva posición no hay nada que copiar, por lo que somos más libres, y no hay ningún ticket de posición antigua, por lo que no hay necesidad de pasar nada.
En comparación con el ejemplo anterior con órdenes pendientes, el código aquí difiere en dos campos:
- El campo de acción, que anteriormente contenía el valor TRADE_ACTION_PENDING, ahora contiene TRADE_ACTION_DEAL.
- El campo de tipo, que ahora representa una orden de mercado directa (ORDER_TYPE_BUY o ORDER_TYPE_SELL) en lugar de una orden pendiente.
Para facilitar el seguimiento de la correspondencia entre los fragmentos de código y el diagrama de flujo de la Figura 6, el código de ejemplo ha sido codificado por colores de acuerdo con la lógica de ramificación que se muestra en el diagrama.
Hay dos diferencias más notables con respecto al Ejemplo 3. Antes de enviar una solicitud de operación, la estructura se valida mediante OrderCheck. Esto permite que el programa detecte campos completados incorrectamente y proporcione un código de retorno (retcode) y una explicación textual (comentario). Después de enviar la solicitud, verificamos si el servidor la aceptó. Si ocurre un error, el programa lo informará con un mensaje relevante.
// If the setup is to trade if(tradingNeeds) { // If there is a position if(positionExists) { // And it is opposite to the desired direction of trade if(positionType != tradingType) { //--- Close the position //--- Clear all participating structures, otherwise you may get an "invalid request" error ZeroMemory(requestClosePosition); ZeroMemory(checkResult); ZeroMemory(result); //--- set operation parameters // Get position ticket requestClosePosition.position = PositionGetInteger(POSITION_TICKET); // Closing a position is just a trade requestClosePosition.action = TRADE_ACTION_DEAL; // position type is opposite to current trading direction, // therefore, for the closing deal, we can use the current order type requestClosePosition.type = orderType; // Current price requestClosePosition.price = requestPrice; // Operation volume must match the current position volume requestClosePosition.volume = PositionGetDouble(POSITION_VOLUME); // Set acceptable deviation from the current price requestClosePosition.deviation = inp_deviation; // Symbol requestClosePosition.symbol = Symbol(); // Position magic number requestClosePosition.magic = EXPERT_MAGIC; if(!OrderCheck(requestClosePosition,checkResult)) { // If the structure is filled incorrectly, display a message PrintFormat("Error when checking an order to close position: %d - %s",checkResult.retcode, checkResult.comment); } else { // Send order if(!OrderSend(requestClosePosition,result)) { // If position closing failed, report PrintFormat("Error closing position: %d - %s",result.retcode,result.comment); } // if(!OrderSend) } // else (!OrderCheck) } // if(positionType != tradingType) else { // Position opened in the same direction as the trade signal. Do not trade return; } // else(positionType != tradingType) } // if(positionExists) //--- Open a new position //--- Clear all participating structures, otherwise you may get an "invalid request" error ZeroMemory(result); ZeroMemory(checkResult); ZeroMemory(requestMakePosition); // Fill the request structure requestMakePosition.action = TRADE_ACTION_DEAL; requestMakePosition.symbol = Symbol(); requestMakePosition.volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); requestMakePosition.type = orderType; // While waiting for position to close, the price could have changed requestMakePosition.price = orderType == ORDER_TYPE_BUY ? SymbolInfoDouble(_Symbol,SYMBOL_ASK) : SymbolInfoDouble(_Symbol,SYMBOL_BID) ; requestMakePosition.sl = orderType == ORDER_TYPE_BUY ? rates[0].low : rates[0].high; requestMakePosition.deviation = inp_deviation; requestMakePosition.magic = EXPERT_MAGIC; if(!OrderCheck(requestMakePosition,checkResult)) { // If the structure check fails, report a check error PrintFormat("Error when checking a new position order: %d - %s",checkResult.retcode, checkResult.comment); } else { if(!OrderSend(requestMakePosition,result)) { // If position opening failed, report an error PrintFormat("Error opening position: %d - %s",result.retcode,result.comment); } // if(!OrderSend(requestMakePosition // Trading completed, reset flag just in case tradingNeeds = false; } // else (!OrderCheck(requestMakePosition)) } // if(tradingNeeds)
Ejemplo 11. El código comercial principal (la mayor parte del espacio se dedica a rellenar la estructura y comprobar si hay errores).
El código fuente completo de este ejemplo está incluido en el archivo adjunto: MADeals.mq5.
Uso de clases indicadoras de la biblioteca estándar
Las clases para los indicadores estándar se encuentran en la carpeta <Include\Indicators>. Puede incluirlos todos a la vez importando el archivo <Include\Indicators\Indicators.mqh> (tenga en cuenta la «s» al final del nombre del archivo), o cargarlos por grupos, por ejemplo, «Trend.mqh», «Oscillators.mqh», «Volumes.mqh» o «BillWilliams.mqh». También hay archivos separados para incluir clases de acceso a series de tiempo ("TimeSeries.mqh") y una clase para trabajar con indicadores personalizados ("Custom.mqh").
Los archivos restantes en esa carpeta son módulos útiles y probablemente serán de poca utilidad para aquellos que no estén familiarizados con la programación orientada a objetos. Cada archivo "funcional" de la carpeta generalmente contiene varias clases relacionadas. Estas clases normalmente se nombran siguiendo una convención consistente: el prefijo C seguido del mismo nombre utilizado en la función de creación del indicador. Por ejemplo, la clase para trabajar con promedios móviles se llama CiMA y se puede encontrar en "Trend.mqh".
Trabajar con estas clases es muy similar a trabajar con las funciones indicadoras nativas de MQL5. Las principales diferencias incluyen las llamadas a los métodos y sus nombres. En la primera etapa (creación), llamamos al método Create y pasamos los parámetros necesarios para el indicador. En la segunda etapa (obtención de los datos) utilizamos el método Refresh, normalmente sin parámetros. Si es necesario, puede especificar qué períodos de tiempo desea actualizar, por ejemplo: (OBJ_PERIOD_D1 | OBJ_PERIOD_H1). Durante el uso, utilizamos el método GetData, generalmente con dos parámetros: el número de búfer y el índice de vela (tenga en cuenta que la indexación sigue el modelo de series de tiempo, aumentando de derecha a izquierda).
En el Ejemplo 12, proporciono un Asesor Experto mínimo que utiliza la clase CiMA. Este EA simplemente genera el valor del promedio móvil en la primera vela cerrada. Si desea ver cómo se puede utilizar este enfoque basado en clases en una estrategia comercial real, copie el Asesor Experto de la sección anterior (MADeals.mq5) en un nuevo archivo y reemplace las líneas apropiadas con las del Ejemplo 12.
#include <Indicators\Indicators.mqh> CiMA g_ma; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create the indicator g_ma.Create(_Symbol,PERIOD_CURRENT,3,0,MODE_SMA,PRICE_CLOSE); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Comment(""); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Get data g_ma.Refresh(); //--- Use Comment( NormalizeDouble( g_ma.GetData(0,1), _Digits ) ); } //+------------------------------------------------------------------+
Ejemplo 12. Uso de la clase CiMA (indicador de media móvil)
Conclusión
Después de leer este artículo, ahora debería poder escribir Asesores Expertos (EAs) simples para la creación rápida de prototipos de cualquier estrategia comercial sencilla, ya sea basada únicamente en datos de velas o incorporando indicadores estándar que extraen sus señales a través de buffers de indicadores (en lugar de representación gráfica). Espero que el tema no haya parecido demasiado complejo. Pero si así fuera, quizá valga la pena volver a revisar el material de artículos anteriores para lograr una comprensión más clara.
En el próximo artículo planeo presentar un Asesor Experto que está técnicamente listo para su publicación en el Market. Este EA incluirá incluso más comprobaciones de validación que el segundo ejemplo de este artículo. Estas comprobaciones harán que el EA sea más sólido y fiable. Su estructura también será ligeramente diferente. La función OnTick ya no servirá como el único centro de la lógica. Aparecerán funciones adicionales para organizar mejor el código. Lo más importante es que el EA tendrá la capacidad de gestionar errores en la colocación de órdenes (como recotizaciones). Para lograr esto, reestructuraremos OnTick para que se pueda acceder directamente a cada "etapa" de la operación del EA (por ejemplo, colocar una operación, esperar una nueva barra, calcular el tamaño del lote...), sin tener que pasar por otras etapas. También utilizaremos el evento TradeTransaction para rastrear las respuestas del servidor. El resultado será una plantilla organizada funcionalmente y fácilmente modificable que podrá usar para construir sus propios EA de cualquier complejidad, aún sin profundizar en OOP, pero completamente operativa y lista para producción.
Lista de artículos anteriores de la serie:
- Aprendiendo MQL5 de principiante a profesional (Parte I): Comenzamos a programar
- Aprendiendo MQL5 de principiante a profesional (Parte II): Tipos de datos básicos y uso de variables
- Aprendiendo MQL5 de principiante a profesional (Parte III): Tipos de datos complejos y archivos de inclusión
- Aprendiendo MQL5 de principiante a profesional (Parte IV): Sobre arrays, funciones y variables globales del terminal
- Aprendiendo MQL5 de principiante a profesional (Parte V): Operadores básicos para redirigir el flujo de comandos (Este artículo también analiza los principios de la construcción de indicadores personalizados)
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/15727
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Características del Wizard MQL5 que debe conocer (Parte 47): Aprendizaje por refuerzo con diferencia temporal
Redes neuronales en el trading: Sistema multiagente con validación conceptual (FinCon)
Observador de Connexus (Parte 8): Cómo agregar un observador de solicitudes
Del básico al intermedio: Estructuras (VI)
- 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
excelente artículo claro y muchas cosas se explican - muchas gracias. ¡Especialmente al final cómo utilizar indicadores a través de clases! Genial! consideraré probar prototipos en mi desarrollo de TS simples.
Muchas gracias.
Esperamos con impaciencia su próxima parte.