English Русский 中文 Deutsch 日本語 Português
preview
Cómo construir un EA que opere automáticamente (Parte 11): Automatización (III)

Cómo construir un EA que opere automáticamente (Parte 11): Automatización (III)

MetaTrader 5Trading | 18 abril 2023, 09:58
501 1
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, Cómo construir un EA que opere automáticamente (Parte 10): Automatización (II), expliqué una forma de añadir un control de horario para que el EA se ejecute. Aunque todo el sistema del EA se ha construido de forma que favorezca la autonomía, antes de pasar a la última fase, en la que tendremos un EA 100% automático, debemos hacer algunos retoques y cambios menores en el código.

Durante la fase de automatización, no debemos modificar, crear o cambiar en modo alguno ninguna parte del código existente. Sólo debemos eliminar los puntos en los que había interacción del operador con el EA y, en lugar de esta interacción, poner algún tipo de disparador automático. Debe evitarse cualquier otro cambio en el código antiguo, a menos que sea necesario para poder realizar la automatización. Si este es el caso, significa que el código antiguo estaba mal planificado y es necesario rehacer toda la planificación para tener un sistema con las siguientes características:

  • Robustez: el sistema no debe contener fallos primarios que puedan comprometer la integridad de alguna parte del código;
  • Fiabilidad: un sistema fiable es aquel que ha sido sometido a varias situaciones potencialmente peligrosas y, aun así, ha funcionado bien, sin fallos;
  • Estabilidad: un sistema debe funcionar bien de forma continua, sin fallos inexplicables;
  • Escalabilidad: un sistema debe crecer sin problemas, sin necesidad de mucha programación de por medio;
  • Encapsulación: sólo las rutinas necesarias para su uso deben ser visibles fuera del lugar donde se está creando el código;
  • Velocidad: de nada sirve tener el mejor modelo si es lento debido a un código mal elaborado.

Algunas de estas características recuerdan al modelo de programación orientada a objetos. De hecho, este modelo es, con diferencia, el más adecuado para crear programas cuando necesitamos un buen nivel de seguridad. Desde el principio de esta serie, te habrás dado cuenta de que toda la programación se ha desarrollado centrándose en el uso de clases, es decir, en la programación orientada a objetos. Sé que al principio, este tipo de programación puede parecer bastante confuso y difícil de aprender, pero créeme, te beneficiarás mucho si te esfuerzas y aprendes a crear tu código como clases.

Comparto esta información para guiarte a ti, aspirante a programador profesional, sobre cómo crear tus códigos. Es importante que te acostumbres a trabajar con 3 carpetas, independientemente de la complejidad de tu código. Siempre debes trabajar en 3 pasos:

  1. En la primera etapa, que es la carpeta de desarrollo, se modificará, trabajará y probará todo el código. Cualquier nueva característica o cambio sólo debe hacerse en el código presente en dicha carpeta.
  2. Después de construir y probar el código previsto, se transporta a la segunda carpeta, la carpeta de trabajo. En esta carpeta, el código aún puede contener algunos fallos, pero NO debes modificarlo. Si necesitas trastear con el código de esta carpeta, debes trasladarlo de nuevo a la carpeta de desarrollo. Es importante señalar que si el cambio se hace sólo para corregir algunos fallos identificados sin hacer cambios más drásticos, el código presente en la carpeta de trabajo puede mantenerse allí, recibiendo sólo las correcciones necesarias.
  3. Finalmente, después de que el código se haya utilizado varias veces en diferentes situaciones y sin ningún cambio nuevo, pasará a una tercera y última carpeta, la carpeta estable. El código de esta carpeta ya ha demostrado ser impecable y muy útil y eficaz en la tarea para la que fue diseñado. No se debe añadir código nuevo a esta carpeta bajo ninguna circunstancia.

Siguiendo este proceso, acabarás creando una interesante base de datos de rutinas y procedimientos, que te permitirá programar cosas de forma rápida y segura. Esta práctica es especialmente valorada en el mercado financiero, donde nadie quiere utilizar código que no sea apropiado para el riesgo que ofrece el mercado. En una actividad en la que no se aprecian los errores RUN-TIME y todo sucede en tiempo real, es necesario que el código sea capaz de soportar imprevistos en cualquier momento.

Todo esto para llegar al punto central, que se presenta en la figura 01:

Figura 01

Figura 01 - Sistema de operación manual

Muchas personas no comprenden bien lo que están programando o creando. Esto se debe a que muchos de ellos no entienden realmente lo que está sucediendo dentro del sistema y terminan pensando que la plataforma debe proporcionar lo que el operador quiere hacer. Sin embargo, si observamos la figura 01, nos damos cuenta de que la plataforma no debería centrarse en proporcionar lo que el operador quiere. En lugar de eso, debe proporcionar formas de interactuar con el operador y, al mismo tiempo, comunicarse de forma estable, rápida y eficaz con el servidor comercial. Al mismo tiempo, la plataforma debe mantenerse en funcionamiento y dar soporte a las cosas que se utilizarán para interactuar con el operador.

Observa que ninguna de las flechas va más allá de su punto, lo que indica que se trata de un sistema de negociación manual. Observa que el EA es mantenido por la plataforma, y no al revés, y que el operador no contacta directamente con el EA. Aunque esto pueda sonar extraño, de hecho, el operador accede al EA a través de la plataforma, y la plataforma dirige al EA enviándole los eventos que el operador está creando o ejecutando. En respuesta, el EA envía a la plataforma las solicitudes que debe enviar al servidor de operaciones. Cuando el servidor responde, devuelve estas respuestas a la plataforma, que las envía al EA. El EA, después de analizar y procesar la respuesta del servidor, envía cierta información a la plataforma para que pueda mostrar al operador lo que está sucediendo o cuál fue el resultado de la solicitud que realizó el operador.

Mucha gente no lo ve así, y si hay un fallo en el EA, la plataforma no tendrá ningún problema. El problema está en el EA, pero los operadores menos experimentados pueden culpar erróneamente a la plataforma, diciendo que no está haciendo lo que ellos pretendían.

Como programador, a menos que participes en el desarrollo y mantenimiento de la plataforma, no deberías intentar cambiar su funcionamiento. En su lugar, debes asegurarte de que tu código responde adecuadamente a los requisitos de la plataforma.

Esto nos lleva a la siguiente pregunta: ¿por qué crear primero un EA manual, utilizarlo durante un tiempo y sólo después automatizarlo? La razón es exactamente ésta: crear una forma de probar realmente el código y crear exactamente lo que necesitamos, ni más ni menos.

Para automatizar adecuadamente el sistema sin tener que tocar ninguna línea del código que se utilizará como sistema de puntos de control y pedidos, necesitamos hacer algunas adiciones y pequeños cambios en este punto. De esta forma, el código creado hasta el artículo anterior se moverá a la carpeta de trabajo, el código creado en el artículo anterior se moverá a la carpeta estable, y el código presentado en este artículo se colocará en la carpeta de desarrollo. Así es como el proceso de desarrollo crece y se desarrolla para tener una codificación más rápida. Si algo va mal, siempre podemos volver dos versiones atrás donde las cosas funcionaban sin problemas.


Aplicación de los cambios

Lo primero que haremos es una modificación del sistema de control de tiempo. Esta modificación se ve en el código de abajo:

virtual const bool CtrlTimeIsPassed(void) final
                        {
                                datetime dt;
                                MqlDateTime mdt;
                                
                                TimeToStruct(TimeLocal(), mdt);
                                TimeCurrent(mdt);
                                dt = (mdt.hour * 3600) + (mdt.min * 60);
                                return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt));
                        }

La línea borrada ha sido sustituida por la línea resaltada. Pero, ¿por qué hemos hecho este cambio? Hay dos razones. En primer lugar, estamos sustituyendo dos llamadas por una. En la línea borrada, se hacía primero la llamada para conocer la hora y luego una segunda llamada para convertir la hora en una estructura. La segunda razón es que TimeLocal en realidad devuelve la hora del ordenador y no la hora vista en el ítem visión del mercado, como se muestra en la figura 02.

Figura 02

Figura 02 - Horario informado por el servidor en la última actualización.

Utilizar la hora del ordenador no es un problema, siempre que esté sincronizada a través de un servidor NTP (los que mantienen actualizada la hora oficial). Sin embargo, la mayoría de las veces, muchas personas no utilizan este tipo de servidores. Por lo tanto, puede ocurrir que el sistema de control horario permita a la EA entrar o salir antes de la hora correcta. En cualquier caso, el cambio era necesario para evitar este tipo de inconvenientes.

Aquí está el cambio realizado, no con el objetivo de cambiar radicalmente el código, sino para conseguir una mayor estabilidad, algo que el operador espera. Si el EA entra o sale antes o después de la hora programada, es posible que intentes entender por qué, pensando que se trata de un error en la plataforma o en su código. Sin embargo, en realidad sólo se trata de una falta de sincronización horaria entre tu ordenador y el servidor comercial. Este último utilizará con toda seguridad un servidor NTP para mantener la hora oficial, mientras que el ordenador utilizado para operar puede no estar utilizando ese tipo de servidor.

El siguiente cambio se produce en el sistema de órdenes, como puede verse a continuación:

                ulong ToServer(void)
                        {
                                MqlTradeCheckResult     TradeCheck;
                                MqlTradeResult          TradeResult;
                                bool bTmp;
                                
                                ResetLastError();
                                ZeroMemory(TradeCheck);
                                ZeroMemory(TradeResult);
                                bTmp = OrderCheck(m_TradeRequest, TradeCheck);
                                if (_LastError == ERR_SUCCESS) bTmp = OrderSend(m_TradeRequest, TradeResult);
                                if (_LastError != ERR_SUCCESS) MessageBox(StringFormat("Error Number: %d", GetLastError()), "Order System", MB_OK);
                                if (_LastError != ERR_SUCCESS) PrintFormat("Order System - Error Number: %d", _LastError);
                                return (_LastError == ERR_SUCCESS ? TradeResult.order : 0);
                        }

Este también es un cambio necesario para adecuar el sistema y que el EA pueda automatizarse sin demasiados problemas. En realidad, la línea borrada no se ha sustituido por la línea resaltada, no porque el código pueda ser más rápido o más estable, sino porque es necesario gestionar adecuadamente la aparición de un error. Sin embargo, cuando tenemos un EA automatizado, algunos tipos de fallos pueden ser ignorados, como se mencionó en artículos anteriores.

A menudo, la línea borrada lanza una ventana de mensaje informando del error, pero en algunos casos, el error es manejado adecuadamente por el código y la ventana no es necesaria. En estos casos, es más prudente imprimir un mensaje en el terminal, informando de la incidencia, para que el operador pueda tomar las medidas oportunas.

Recuerda que un EA 100% automatizado no puede esperar que el operador tome decisiones. Sin embargo, tampoco puede ejecutar tareas sin informar del tipo de problema que se ha producido. La modificación realizada en el código pretendía mejorar su agilidad, sin grandes cambios que requirieran una fase intensa de pruebas para detectar fallos.

Sin embargo, los próximos cambios requerirán pruebas más profundas, ya que modificarán el funcionamiento del sistema.


Preparando el camino para la automatización

Los cambios que haremos ahora nos permitirán crear un sistema totalmente automatizado. Sin ellos, tendríamos las manos atadas en el próximo artículo, donde mostraré cómo convertir un EA ya probado (espero que estés ejecutando pruebas para entender cómo funciona), en un sistema autónomo. Para hacer los cambios necesarios, tendremos que modificar y añadir algunas cosas. Vamos a empezar con la modificación. Lo que se modificará está en el fragmento de abajo:

//+------------------------------------------------------------------+
#include "C_ControlOfTime.mqh"
//+------------------------------------------------------------------+
#define def_MAX_LEVERAGE                10
#define def_ORDER_FINISH                false
//+------------------------------------------------------------------+
class C_Manager : public C_ControlOfTime

Estas dos definiciones ya no existirán, y dos nuevas variables aparecerán en su lugar, las cuales no pueden ser modificadas por el operador, pero pueden ser definidas por ti, el programador. ¿Por qué hacer este cambio? La razón es que al hacer este cambio, donde se cambian definiciones por variables, perderemos un poco en términos de velocidad. Aunque serán unos pocos ciclos de máquina, habrá una pequeña pérdida de rendimiento, ya que es considerablemente más rápido acceder a un valor constante que a una variable. Sin embargo, en compensación, ganaremos en reutilización de clases, y lo entenderás mejor en el próximo artículo. Créeme, la diferencia en facilidad y portabilidad compensa la pequeña pérdida de rendimiento que tendremos. Así, las dos líneas anteriores fueron reemplazadas por las siguientes:

class C_Manager : public C_ControlOfTime
{
        enum eErrUser {ERR_Unknown, ERR_Excommunicate};
        private :
                struct st00
                {
                        double  FinanceStop,
                                FinanceTake;
                        uint    Leverage,
                                MaxLeverage;
                        bool    IsDayTrade,
                                IsOrderFinish;                                          
                }m_InfosManager;

Presta mucha atención cuando trabajes con el código, porque no debes, como programador, modificar el valor de estas dos variables fuera del lugar donde serán inicializadas. Tenga mucho cuidado de no hacerlo. El lugar donde serán inicializadas es justo en el constructor de la clase, como se muestra en el siguiente fragmento:

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger, const bool IsOrderFinish, const uint MaxLeverage)
                        :C_ControlOfTime(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.IsOrderFinish    = IsOrderFinish;
                                m_InfosManager.MaxLeverage      = MaxLeverage;
                                m_InfosManager.FinanceStop      = FinanceStop;
                                m_InfosManager.FinanceTake      = FinanceTake;
                                m_InfosManager.Leverage         = Leverage;
                                m_InfosManager.IsDayTrade       = IsDayTrade;

Ahora, el constructor recibirá dos nuevos argumentos y estos inicializarán nuestras variables. Después de eso, cambiaremos los puntos donde las definiciones fueron instanciadas. Estos cambios se hicieron en los siguientes puntos:

inline int SetInfoPositions(void)
                        {
                                double v1, v2;
                                int tmp = m_Position.Leverage;
                                
                                m_Position.Leverage = (int)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal);
                                m_Position.IsBuy = ((ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE)) == POSITION_TYPE_BUY;
                                m_Position.TP = PositionGetDouble(POSITION_TP);
                                v1 = m_Position.SL = PositionGetDouble(POSITION_SL);
                                v2 = m_Position.PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
                                if (def_ORDER_FINISH) if (m_TicketPending > 0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                m_Position.EnableBreakEven = (def_ORDER_FINISH ? m_TicketPending == 0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                if (m_InfosManager.IsOrderFinish) if (m_TicketPending > 0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                m_Position.EnableBreakEven = (m_InfosManager.IsOrderFinish ? m_TicketPending == 0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage);

                                return m_Position.Leverage - tmp;
                        }
inline void TriggerBreakeven(void)
                        {
                                double price;
                                
                                if (PositionSelectByTicket(m_Position.Ticket))
                                        if (PositionGetDouble(POSITION_PROFIT) >= m_Trigger)
                                        {
                                                price = m_Position.PriceOpen + (GetTerminalInfos().PointPerTick * (m_Position.IsBuy ? 1 : -1));
                                                if (def_ORDER_FINISH)
                                                if (m_InfosManager.IsOrderFinish)
                                                {
                                                        if (m_TicketPending > 0) m_Position.EnableBreakEven = !ModifyPricePoints(m_TicketPending, price, 0, 0);
                                                }else m_Position.EnableBreakEven = !ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
                                        }
                        }
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                tmp = C_Orders::ToMarket(type, (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                        {
// ... Resto del código ...
                void UpdatePosition(const ulong ticket)
                        {
                                int ret;
                                double price;
                                
                                if ((ticket == 0) || (ticket != m_Position.Ticket) || (m_Position.Ticket == 0)) return;
                                if (PositionSelectByTicket(m_Position.Ticket))
                                {
                                        ret = SetInfoPositions();
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                price = m_Position.PriceOpen + (FinanceToPoints(m_InfosManager.FinanceStop, m_Position.Leverage) * (m_Position.IsBuy ? -1 : 1));
                                                if (m_TicketPending > 0) if (OrderSelect(m_TicketPending))
                                                {
                                                        price = OrderGetDouble(ORDER_PRICE_OPEN);
                                                        C_Orders::RemoveOrderPendent(m_TicketPending);
                                                }
                                                if (m_Position.Ticket > 0)      m_TicketPending = C_Orders::CreateOrder(m_Position.IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY, price, 0, 0, m_Position.Leverage, m_InfosManager.IsDayTrade);
                                        }
                                        m_StaticLeverage += (ret > 0 ? ret : 0);
                                }else
                                {
                                        ZeroMemory(m_Position);
                                        if ((def_ORDER_FINISH) && (m_TicketPending > 0))
                                        if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > 0))
                                        {
                                                RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                        }
                                }
                                ResetLastError();
                        }
inline void TriggerTrailingStop(void)
                        {
                                double price, v1;
                                
                                if ((m_Position.Ticket == 0) || (def_ORDER_FINISH ? m_TicketPending == 0 : m_Position.SL == 0)) return;
                                if ((m_Position.Ticket == 0) || (m_InfosManager.IsOrderFinish ? m_TicketPending == 0 : m_Position.SL == 0)) return;
                                if (m_Position.EnableBreakEven) TriggerBreakeven(); else
                                {
                                        price = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : (m_Position.IsBuy ? SYMBOL_ASK : SYMBOL_BID)));
                                        v1 = m_Position.SL;
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                                if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                        if (v1 > 0) if (MathAbs(price - v1) >= (m_Position.Gap * 2)) 
                                        {
                                                price = v1 + (m_Position.Gap * (m_Position.IsBuy ? 1 : -1));
                                                if (def_ORDER_FINISH)
                                                if (m_InfosManager.IsOrderFinish)
                                                        ModifyPricePoints(m_TicketPending, price, 0, 0);
                                                else    ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
                                        }
                                }
                        }

Todas y absolutamente todas las partes borradas han sido sustituidas por las partes resaltadas. De esta forma, hemos conseguido fomentar la tan soñada reutilización de la clase y su mejora en términos de usabilidad. Aunque no queda claro en este artículo, entenderás muy bien cómo se hará en el siguiente.

Además de los cambios ya realizados, todavía tenemos que hacer algunas adiciones para maximizar el acceso del sistema de automatización al sistema de envío de pedidos y así aumentar la reutilización de la clase. Para ello, primero tenemos que añadir la siguiente rutina:

inline void ClosePosition(void)
	{
		if (m_Position.Ticket > 0)
		{
			C_Orders::ClosePosition(m_Position.Ticket);
			ZeroMemory(m_Position.Ticket);
		}                               
	}

Esta rutina es necesaria en algunos modelos operativos, por lo que necesitamos incluirla en el código de la clase C_Manager. Sin embargo, es importante recordar que, al añadir esta rutina, el compilador generará varias advertencias al intentar compilar el código, como puede verse en la figura 03.

Figura 03

Figura 03 - Alertas del compilador

A diferencia de algunas advertencias del compilador que pueden ser ignoradas (aunque no es recomendable), este tipo de advertencias (figura 03) pueden ser potencialmente dañinas para el programa, y pueden hacer que el código generado no funcione correctamente.

Lo ideal es que, cuando notes que el compilador ha generado advertencias, intentes corregir el fallo que las está provocando. A veces, es algo sencillo de solucionar; otras veces, puede ser un poco más complicado, como cuando se produce un cambio de tipo y parte de los datos pueden perderse durante la conversión. De cualquier manera, es importante mirar por qué el compilador generó estas alertas, incluso si el código se compila y ejecuta.

Tener alertas del compilador es una señal de que algo no está bien en el código, ya que el compilador tiene dificultades para entender lo que se está programando. Si no puede entenderlo, no es posible generar código 100% fiable.

Algunas plataformas de programación permiten desactivar las alertas del compilador, pero personalmente desaconsejo esta práctica. Cuando se quiere tener un código 100% fiable, es mejor mantener todas las alertas activadas, excepto en casos específicos en los que se sabe que las alertas son falsos positivos. Con el tiempo, aprenderás que dejar la configuración por defecto de la plataforma de programación es la mejor manera de garantizar un código más fiable.

Para solucionar las alertas mencionadas anteriormente, hay dos opciones. La primera es sustituir las llamadas a la función ClosePosition que actualmente hacen referencia a la función presente en la clase C_Orders por la nueva función añadida en la clase C_Manager. Esta sería la mejor opción, ya que probaríamos la llamada presente en C_Manager. La otra opción sería informar al compilador de que las llamadas hacen referencia a la clase C_Orders.

Sin embargo, optaré por cambiar el código para utilizar la llamada de nueva creación. De esta forma, se solucionarán los puntos problemáticos que generan las alertas y el compilador entenderá lo que estamos intentando hacer.

                ~C_Manager()
                        {
                                if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate))
                                {
                                        if (m_TicketPending > 0) RemoveOrderPendent(m_TicketPending);
                                        if (m_Position.Ticket > 0) ClosePosition(m_Position.Ticket);
                                        ClosePosition();
                                        Print("EA was kicked off the chart for making a serious mistake.");
                                }
                        }

La parte fácil era resolverlo en el destructor, pero hay un punto un poco más complicado de resolver, que se puede ver a continuación:

                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                if (PositionSelectByTicket(m_Position.Ticket)) ClosePosition(m_Position.Ticket);
                                                ClosePosition();
                                                if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
                                                ZeroMemory(m_Position.Ticket);
                                                m_TicketPending = 0;
                                                ResetLastError();
                                        }else   SetUserError(ERR_Unknown);
                                }else m_Position.Ticket = (m_Position.Ticket == 0 ? m_TicketPending : m_Position.Ticket);
                                m_TicketPending = 0;
                                if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
                                CheckToleranceLevel();
                        }

Se han eliminado las partes borradas y hemos añadido la llamada a cerrar la posición. Sin embargo, si la entrada pendiente se convierte en una posición por cualquier motivo, tenemos que eliminar la posición, como se hacía en el código original. Sin embargo, la posición aún no ha sido capturada por la clase C_Manager. En este caso, le decimos al compilador que la llamada hará referencia a la clase C_Orders, como se muestra en resaltado.

Otro cambio que tenemos que hacer se muestra a continuación:

inline void EraseTicketPending(const ulong ticket)
                        {
                                if ((m_TicketPending == ticket) && (m_TicketPending > 0))
                                {
                                        if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); 
                                        else RemoveOrderPendent(m_TicketPending);
                                        m_TicketPending = 0;
                                }
                                ResetLastError();
                                m_TicketPending = (ticket == m_TicketPending ? 0 : m_TicketPending);
                        }

El código borrado, que era el original, ha sido sustituido por otro más complejo, pero que nos da una mayor capacidad para eliminar la orden pendiente o, si se ha convertido en posición, borrarla. Antes nos limitábamos a responder a un evento del que nos informaba la plataforma MetaTrader 5, haciendo que el valor indicado como entrada de la orden pendiente se pusiera a cero para poder enviar una nueva orden pendiente. Ahora haremos algo más que eso, ya que necesitaremos esta funcionalidad en un sistema 100% automatizado.

Con estos pequeños cambios, hemos conseguido un importante beneficio para todo el sistema, que se ha traducido en un aumento de la reutilización del código y de las pruebas.


Últimos retoques antes de la fase final

Antes de la fase final, podemos hacer algunas mejoras más que ayuden a aumentar la calidad del código. La primera de ellas se muestra en el siguiente fragmento:

                void UpdatePosition(const ulong ticket)
                        {
                                int ret;
                                double price;
                                
                                if ((ticket == 0) || (ticket != m_Position.Ticket) || (m_Position.Ticket == 0)) return;
                                if (PositionSelectByTicket(m_Position.Ticket))
                                {
                                        ret = SetInfoPositions();
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                price = m_Position.PriceOpen + (FinanceToPoints(m_InfosManager.FinanceStop, m_Position.Leverage) * (m_Position.IsBuy ? -1 : 1));
                                                if (m_TicketPending > 0) if (OrderSelect(m_TicketPending))
                                                {
                                                        price = OrderGetDouble(ORDER_PRICE_OPEN);
                                                        C_Orders::RemoveOrderPendent(m_TicketPending);
                                                        EraseTicketPending(m_TicketPending);
                                                }
                                                if (m_Position.Ticket > 0)      m_TicketPending = C_Orders::CreateOrder(m_Position.IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY, price, 0, 0, m_Position.Leverage, m_InfosManager.IsDayTrade);
                                        }
                                        m_StaticLeverage += (ret > 0 ? ret : 0);
                                }else
                                {
                                        ZeroMemory(m_Position);
                                        if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > 0))
                                        {
                                                RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                        }
                                        if (m_InfosManager.IsOrderFinish) EraseTicketPending(m_TicketPending);
                                }
                                ResetLastError();
                        }

Otro punto que puede beneficiarse de estas mejoras está en el siguiente código:

                ~C_Manager()
                        {
                                if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate))
                                {
                                        if (m_TicketPending > 0) RemoveOrderPendent(m_TicketPending);
                                        EraseTicketPending(m_TicketPending);
                                        ClosePosition();
                                        Print("EA was kicked off the chart for making a serious mistake.");
                                }
                        }

Y finalmente, un último punto que también se beneficia de la reutilización de código se muestra a continuación:

                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                ClosePosition();
                                                EraseTicketPending(m_TicketPending);
                                                if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                                ResetLastError();
                                        }else   SetUserError(ERR_Unknown);
                                }else m_Position.Ticket = (m_Position.Ticket == 0 ? m_TicketPending : m_Position.Ticket);
                                m_TicketPending = 0;
                                if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
                                CheckToleranceLevel();
                        }

Para finalizar las mejoras y cambios, es importante abordar un detalle que puede causar problemas. Supongamos que el EA tiene un volumen máximo fijado en 10 veces el volumen mínimo. Si el operador establece el volumen indicando un apalancamiento de 3 veces y el EA ejecuta tres operaciones, estaría cerca de alcanzar el volumen máximo permitido. Sin embargo, si envía una cuarta solicitud, esto violaría el volumen máximo permitido.

Puede parecer un defecto tonto y sin importancia, y muchos pueden considerarlo de baja peligrosidad. En cierto modo, estoy de acuerdo con esta idea, ya que nunca se aceptaría un quinto pedido. Sin embargo, la programación del EA indicaba un volumen de 10 veces, por lo que al aceptarse la cuarta solicitud, el EA habría ejecutado un volumen 12 veces mayor, superando en 2 veces el volumen máximo configurado. Esto ocurrió porque el operador configuró un apalancamiento de 3 veces. Pero, ¿y si hubiera indicado un apalancamiento de 9 veces, por ejemplo? En este caso, el operador esperaría que el EA ejecutara una sola operación.

Imagina la sorpresa y el susto que se llevaría alguien al ver que el EA abría una segunda operación, que excedía en 8 veces el volumen máximo que el programador había recibido instrucciones de limitar. A esa persona le podría incluso dar un infarto, dado el sobresalto que le provocaría.

Aunque es un fallo de poco potencial, no deja de ser un fallo que no se debe admitir, especialmente para un EA automático. Es importante señalar que para un EA manual, esto no sería un problema, ya que el propio operador se encargaría de no realizar otra entrada con el mismo nivel de apalancamiento. Sin embargo, en cualquier caso, es necesario implementar algún tipo de bloqueo en el EA. La razón principal es que esto debería estar resuelto antes del próximo artículo. No quiero, de ninguna manera, preocuparme por cualquier tipo de problema de esa naturaleza más adelante.

Para remediarlo, basta con añadir unas líneas de código, como puede verse a continuación:

//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {                               
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
//+------------------------------------------------------------------+

El código en verde evita el evento antes mencionado, pero es importante fijarse bien en todo el código resaltado. ¿Notas que es exactamente igual? Aquí podemos hacer dos cosas: crear una macro para acomodar todo el código o crear una función para reemplazar o acumular todo este código común o repetido en ambas funciones.

Hemos optado por crear una función para que sea sencillo y claro, ya que muchos lectores de estos artículos están empezando a programar y puede que no tengan mucha experiencia o conocimientos sobre programación. Esta nueva rutina se colocará en la cláusula privada, de modo que ningún otro código en EA necesite saber de su existencia. A continuación se muestra el código de la nueva función:

inline bool IsPossible(const bool IsPending)
	{
		if (!CtrlTimeIsPassed()) return false;
		if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return false;
		if ((IsPending) && (m_TicketPending > 0)) return false;
		if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
		{
			Print("Request denied, as it would violate the maximum volume allowed for the EA.");
			return false;
		}
                                
		return true;
	}

Entendamos lo que ocurre aquí. Todas las líneas de código que se repetían en los códigos de envío de órdenes están ahora reunidas en el código anterior. Sin embargo, hay una pequeña diferencia entre enviar una orden pendiente y una orden de mercado, y esa diferencia viene marcada por este punto concreto. Debemos comprobar si la orden procede de una orden pendiente o de una orden de mercado para diferenciarlas. Para diferenciar entre los dos tipos de órdenes, utilizamos un argumento que nos permite combinar todo el código en uno solo. Si hay algún tipo de imposibilidad de enviar la orden, el retorno será falso. Si la orden puede ser enviada, el retorno será verdadero.

Una vez hecho esto, el nuevo código de la función se puede ver a continuación:

//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                if (!IsPossible(true)) return;
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!IsPossible(false)) return;
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
//+------------------------------------------------------------------+

Observa como la llamada pasó a ser de acuerdo a cada caso y como las líneas borradas fueron removidas del código, pues ya no tiene sentido que existan allí. Así es como los programas se desarrollan y se vuelven seguros, fiables, estables y robustos, eliminando las partes duplicadas, analizando lo que se puede cambiar y mejorar, probando y reutilizando el código tanto como sea posible.

Al ver un código acabado, es frecuente tener la impresión de que nació así. Sin embargo, lo cierto es que han sido necesarios varios pasos para llegar a su forma final. Estos pasos implican pruebas y experimentos continuos, con el objetivo de reducir al máximo el número de fallos y posibles lagunas. El proceso es gradual y requiere dedicación y perseverancia para que el código alcance la calidad deseada.


Consideraciones finales

A pesar de todo lo que se ha dicho y mostrado hasta ahora, sigue existiendo una laguna considerablemente perjudicial para un sistema 100% automático que debemos cerrar. Aunque muchos están deseando que EA funcione de forma totalmente automática, no es aceptable que contenga una laguna o fallo en su funcionamiento. Algunos fallos menos graves pueden colarse en un sistema manual, pero no en un sistema 100% automático.

Como este tema es complicado de explicar, se abordará en el próximo artículo. El código hasta ahora está disponible en el apéndice. Para poner a prueba tus conocimientos, te dejo un reto: antes de leer el próximo artículo, ¿puedes identificar el fallo que sigue existiendo en EA y que impide automatizarlo con seguridad? Pista: El fallo está en la forma en que la clase C_Manager analiza el trabajo del EA.


Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/11293

Archivos adjuntos |
Luis Antonio Perdomo Martínez
Luis Antonio Perdomo Martínez | 21 abr. 2023 en 06:15
Si automatizado es mejor por que trabaja sólo la máquina y a un que uno estéd dormiendo está ganando. 
Experimentos con redes neuronales (Parte 3): Uso práctico Experimentos con redes neuronales (Parte 3): Uso práctico
Las redes neuronales lo son todo. Vamos a comprobar en la práctica si esto es así. MetaTrader 5 como herramienta autosuficiente para el uso de redes neuronales en el trading. Una explicación sencilla.
Cómo construir un EA que opere automáticamente (Parte 10): Automatización (II) Cómo construir un EA que opere automáticamente (Parte 10): Automatización (II)
La automatización no significa nada si no se puede controlar el horario. Ningún trabajador puede ser eficiente trabajando 24 horas al día. Sin embargo, muchos creen que un sistema automatizado debe trabajar 24 horas al día. Siempre es bueno tener formas de configurar una franja horaria para el Expert Advisor. En este artículo, vamos a discutir cómo agregar correctamente tal franja horaria.
Cómo construir un EA que opere automáticamente (Parte 12): Automatización (IV) Cómo construir un EA que opere automáticamente (Parte 12): Automatización (IV)
Si crees que los sistemas automatizados son sencillos, eso indica que aún no has entendido del todo lo necesario para crearlos. En este texto, hablaremos de un problema al que se enfrentan muchos Expert Advisors: la ejecución indiscriminada de órdenes, y de una posible solución a este problema.
Algoritmos de optimización de la población: Algoritmo de murciélago (Bat algorithm - BA) Algoritmos de optimización de la población: Algoritmo de murciélago (Bat algorithm - BA)
Hoy analizaremos el algoritmo de murciélago (Bat algorithm - BA), que posee una sorprendente convergencia en funciones suaves.