Desarrollando un EA comercial desde cero (Parte 19): Un nuevo sistema de órdenes (II)

Daniel Jose | 13 julio, 2022

1.0 - Introducción

En el artículo anterior Desarrollando un EA comercial desde cero (Parte 18), hemos realizado varias correcciones, cambios y ajustes en el sistema de órdenes hasta crear un sistema que nos permite operar de forma diferenciada entre una cuenta de NETTING y una cuenta de HEDGING, ya que hay diferencias entre operar con una o la otra. Para el tipo NETTING, el sistema de negociación crea un precio medio por lo que solo tendremos una única posición abierta en un activo. Para el tipo HEDGING, podemos tener varias posiciones abiertas, cada una con sus propios límites específicos, pudiendo ser compradas y vendidas al mismo tiempo en el mismo activo, y esto solo es posible en una cuenta de tipo HEDGING. Esta es la base que muchos entenderían como operar usando opciones binarias.

Pero ahora por fin ha llegado el momento de subir la apuesta y hacer que el sistema de órdenes sea totalmente visual, de manera que podremos prescindir del cuadro de mensajes para analizar cuáles son los valores de cada posición, y podremos hacerlo simplemente observando el nuevo sistema de órdenes. Esto nos permitirá ajustar varias cosas a la vez que será fácil saber cuáles son los límites de pérdidas y ganancias de una posición OCO o de una orden OCO pendiente, ya que el propio EA nos informará de ello en tiempo real, sin que tengamos que hacer cálculos para ello.

Si bien esta será la primera parte de esta implementación, no partiremos de cero, sino que modificaremos el sistema existente añadiendo aún más objetos y eventos al gráfico del activo que estamos negociando.


2.0 - Planificación

La planificación del sistema que se utilizará aquí no es algo muy difícil, ya que modificaremos un sistema existente, solo cambiaremos el sistema de presentación de órdenes en el gráfico. Esta es la idea básica, parece algo bastante fácil de hacer, pero en la práctica exige mucha creatividad, ya que tendremos que manipular y modelar los datos para que la plataforma MetaTrader 5 se encargue del trabajo pesado por nosotros.

Hay varias formas de modelar los datos, y cada una tiene sus pros y sus contras.

Si crees que esto no es fácil de lograr, mira el siguiente fragmento en la clase C_HLineTrade:

inline void SetLineOrder(ulong ticket, double price, eHLineTrade hl, bool select)
{
        string sz0 = def_NameHLineTrade + (string)hl + (string)ticket, sz1;
                                
        ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, 0, 0, 0);

//... Código restante de la clase .... 

Este fragmento resaltado muestra exactamente eso, podemos crear tantas líneas horizontales como queramos, y ellas recibirán eventos de forma totalmente independiente, pero lo único que tenemos que hacer es implementar los eventos basados en el nombre que tendrá cada una de las líneas, ya que los nombres serán únicos, la plataforma MetaTrader 5 se encargará del resto por nosotros, así que el resultado será efectivamente como se ve a continuación:


Pero aunque parezca algo ideal, este modelado no es suficiente para lo que realmente necesitamos. La idea es esta y efectivamente se puede implementar, pero el modelado que tiene el EA en este momento no es el ideal para que se pueda tener un número ilimitado de objetos basados en el mismo nombre. Tenemos que hacer cambios y estos cambios implican modificar el código de forma bastante profunda.

Bien, empecemos a implementar esta nueva forma de modelar los datos, pero solo cambiaremos lo necesario para que eso ocurra, de manera que no se desestabilizará el código, este deberá seguir funcionando lo más estable posible, y todo el trabajo lo tiene que hacer la plataforma MetaTrader 5, solo diremos cómo debe entender la plataforma nuestro modelado.


3.0 - Implementación

La primera alteración es el cambio de la clase C_HLineTrade a una nueva clase C_ObjectsTrade, esta nueva clase será capaz de dar soporte a lo que queremos, una forma de tener un número ilimitado de objetos enlazados entre sí.

Empecemos por ver las definiciones iniciales, que se pueden ver en el siguiente fragmento

class C_ObjectsTrade
{
//+------------------------------------------------------------------+
#define def_NameObjectsTrade 	"SMD_OT"
#define def_SeparatorInfo       '*'
#define def_IndicatorTicket0    1
//+------------------------------------------------------------------+
        protected:
                enum eIndicatorTrade {IT_NULL, IT_STOP= 65, IT_TAKE, IT_PRICE};
//+------------------------------------------------------------------+

// ... Código restante de la clase

Aquí tenemos la base inicial que realmente vamos a implementar, en el futuro esto se ampliará, pero de momento quiero mantener el sistema estable mientras se modifica y tiene un nuevo modelado de los datos.

Pues bien, aún dentro de los límites de la declaración protected tenemos las siguientes funciones:

inline double GetLimitsTake(void) const { return m_Limits.TakeProfit; }
//+------------------------------------------------------------------+
inline double GetLimitsStop(void) const { return m_Limits.StopLoss; }
//+------------------------------------------------------------------+
inline bool GetLimitsIsBuy(void) const { return m_Limits.IsBuy; }
//+------------------------------------------------------------------+
inline void SetLimits(double take, double stop, bool isbuy)
{
        m_Limits.IsBuy = isbuy;
        m_Limits.TakeProfit = (m_Limits.TakeProfit < 0 ? take : (isbuy ? (m_Limits.TakeProfit > take ? m_Limits.TakeProfit : take) : (take > m_Limits.TakeProfit ? m_Limits.TakeProfit : take)));
        m_Limits.StopLoss = (m_Limits.StopLoss < 0 ? stop : (isbuy ? (m_Limits.StopLoss < stop ? m_Limits.StopLoss : stop) : (stop < m_Limits.StopLoss ? m_Limits.StopLoss : stop)));
}
//+------------------------------------------------------------------+
inline int GetBaseFinanceLeveRange(void) const { return m_BaseFinance.Leverange; }
//+------------------------------------------------------------------+
inline int GetBaseFinanceIsDayTrade(void) const { return m_BaseFinance.IsDayTrade; }
//+------------------------------------------------------------------+
inline int GetBaseFinanceTakeProfit(void) const { return m_BaseFinance.FinanceTake; }
//+------------------------------------------------------------------+
inline int GetBaseFinanceStopLoss(void) const { return m_BaseFinance.FinanceStop; }

Estas funciones, de momento, son solo una medida de seguridad para otro esquema que se hará en el futuro. Pero aunque los datos y el análisis realizado por ellos se puede hacer en otro lugar, es bueno dejar algunas cosas en el nivel más bajo posible de la cadena de herencia, y aunque los valores de retorno serán utilizados solo por las clases que heredan, no quiero permitir esto directamente, es decir, no busco que la clase heredera simplemente acceda a los valores que están dentro de esta clase objeto C_ObjectsTrade, esto rompería la idea de encapsulación de la clase objeto, dificultando futuras modificaciones, o correcciones de errores, donde una clase heredera modifique el valor de la clase base sin que este cambio se haga a través de una llamada a un procedimiento.

Como quiero reducir la superposición de llamadas tanto como sea posible, todas las funciones se declaran como inline, esto aumenta el ejecutable un poco, pero tenemos el beneficio de un sistema ligeramente más seguro.

Ahora llegamos a las declaraciones privadas.

//+------------------------------------------------------------------+
        private :
                string  m_SelectObj;
                struct st00
                {
                        double  TakeProfit,
                                StopLoss;
                        bool    IsBuy;
                }m_Limits;
                struct st01
                {
                        int     FinanceTake,
                                FinanceStop,
                                Leverange;
                        bool    IsDayTrade;
                }m_BaseFinance;
//+------------------------------------------------------------------+
                string MountName(ulong ticket, eIndicatorTrade it)
                {
                        return StringFormat("%s%c%c%c%d", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket);
                }
//+------------------------------------------------------------------+

La gran cuestión aquí es el fragmento resaltado, este modelará los nombres de los objetos, por el momento estoy manteniendo lo básico aún presente en el sistema, como dije, primero creamos y modificamos el modelado, manteniendo el sistema estable, luego agregamos los nuevos objetos, solo que esto se hará de manera bastante simple, rápida y manteniendo toda la estabilidad ya lograda.

Pues bien, aunque el código ha sufrido muchos más cambios que los aquí mostrados, a partir de este momento me centraré solo en las nuevas funciones o en los puntos en los que el cambio ha sido extremadamente drástico respecto a los códigos anteriores.

Así que siguiendo esta idea, la primera función que se ve es la que se muestra a continuación:

inline string CreateIndicatorTrade(ulong ticket, eIndicatorTrade it, bool select)
{
        string sz0 = MountName(ticket, it);
                                
        ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, 0, 0, 0);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_COLOR, (it == IT_PRICE ? clrBlue : (it == IT_STOP ? clrFireBrick : clrForestGreen)));
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_WIDTH, 1);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_STYLE, STYLE_DASHDOT);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTABLE, select);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTED, false);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_BACK, true);
        ObjectSetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, (string)ticket + " "+StringSubstr(EnumToString(it), 3, 10));
                                
        return sz0;
}

Ella creará, en este momento solo una línea horizontal, pero miren allí el código de generación del nombre, observen también que los colores ahora serán definidos dentro del código y no, por el usuario, pero esto fue una decisión mía.

Lo siguiente es sobrecargar esta misma función, y esto se ve justo debajo.

inline string CreateIndicatorTrade(ulong ticket, double price, eIndicatorTrade it, bool select)
{
        if (price <= 0)
        {
                RemoveIndicatorTrade(ticket, it);
                return NULL;
        }
        string sz0 = CreateIndicatorTrade(ticket, it, select);
        ObjectMove(Terminal.Get_ID(), sz0, 0, 0, price);
                                
        return sz0;
}

No confundas las dos funciones, porque aunque parezcan iguales, son diferentes. Además, el uso de la sobrecarga es algo bastante común, es decir, cuando creamos una función simple y luego añadimos nuevos parámetros en ella, con el fin de acumular un tipo específico de modelado, si esto no se hiciera a través de la sobrecarga, tendríamos que repetir a veces la misma secuencia de código, y esto es peligroso desde el punto de vista de que nos olvidamos de declarar algo, no es muy práctico, por lo que sobrecargamos la función para hacer una sola llamada en lugar de hacer varias llamadas.

Una cosa que merece ser mencionada aquí es la parte que está resaltada en esta segunda versión, de alguna manera no necesita ser creada aquí, podríamos hacerlo en otra parte, pero cuando intentamos crear algún objeto que está con el precio en cero, este en realidad debería ser destruido.

Para ver realmente un momento en el que esto ocurre, ve el fragmento que aparece a continuación:

class C_Router : public C_ObjectsTrade
{

// ... Código interno de la clase ....

                void UpdatePosition(int iAdjust = -1)
                        {

// ... Código interno de la función ...

                                for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
                                {
                                        ul = PositionGetInteger(POSITION_TICKET);
                                        m_bContainsPosition = true;
                                        CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false);
                                        CreateIndicatorTrade(ul, take = PositionGetDouble(POSITION_TP), IT_TAKE, true);
                                        CreateIndicatorTrade(ul, stop = PositionGetDouble(POSITION_SL), IT_STOP, true);

// ... Resto del código ...

Cada vez que el EA reciba un evento OnTrade, ejecutará la función de arriba, y se intentará crear el indicador en los puntos resaltados, pero si el usuario elimina el límite, este se hará cero, por lo que al realizar la llamada, en realidad eliminará el indicador que existe en el gráfico, ahorrándonos tener objetos inútiles en memoria, así tenemos cierta ganancia en algunos momentos, ya que la prueba se hará justo en el momento de la creación.

Pero aún nos queda el tema de la sobrecarga, que quizás algunos no entiendan muy bien cómo se usa en el código real, pero para entenderlo, mira los dos fragmentos de código de abajo:

class C_OrderView : public C_Router
{
        private  :
//+------------------------------------------------------------------+
        public   :
//+------------------------------------------------------------------+
                void InitBaseFinance(int nContracts, int FinanceTake, int FinanceStop, bool b1)
                        {                       
                                SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1);
                                CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false);
                                CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false);
                                CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false);
                        }
//+------------------------------------------------------------------+

// ... Resto del código ....
class C_Router : public C_ObjectsTrade
{

// ... Código de la clase ...

                void UpdatePosition(int iAdjust = -1)
                        {
// ... Código de la función ....
                                for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
                                {
                                        ul = PositionGetInteger(POSITION_TICKET);
                                        m_bContainsPosition = true;
                                        CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false);

// ... Resto del código ...

Vean que en ambos casos tenemos el mismo nombre de la función que se está utilizando y forman parte de la misma clase, C_ObjectsTrade, pero aun así el compilador puede distinguir entre ellos y el hecho es el número de parámetros, pero si te fijas bien verán que la única diferencia es un parámetro extra, el precio, pero podría tener varios más, vean que es mucho más sencillo utilizar una sola llamada para copiar todo el código que está presente en una de las versiones sobrecargadas, por lo que tenemos al final un código más limpio y un mantenimiento más sencillo.

Bien, pero volvamos a la clase C_ObjectsTrade, y la siguiente función que debemos entender se ve a continuación:

bool GetInfosOrder(const string &sparam, ulong &ticket, double &price, eIndicatorTrade &it)
{
        string szRet[];
        char szInfo[];
                                
        if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false;
        if (szRet[0] != def_NameObjectsTrade) return false;
        StringToCharArray(szRet[1], szInfo);
        it = (eIndicatorTrade)szInfo[0];
        ticket = (ulong) StringToInteger(szRet[2]);
        price = ObjectGetDouble(Terminal.Get_ID(), sparam, OBJPROP_PRICE);
                                
        return true;
}

Este es, de hecho, el corazón, la mente y el cuerpo de todo nuestro nuevo sistema, aunque parece bastante simple y modesto, hace un trabajo que es extremadamente importante para que todo el EA llegue a funcionar como nuestro nuevo sistema de modelado requiere.

Presta mucha atención al código resaltado, fíjate en la función StringSplit, si no existiera en MQL5, tendríamos que codificarla, pero afortunadamente está presente en MQL5, así que vamos a usar y abusar de esta rutina. Bueno, lo que hace es descomponer el nombre del objeto en datos relevantes para nosotros, cuando se crea el nombre del objeto, se modela de una manera muy específica, y, por esta razón, también podemos deshacer este modelado codificador, por lo que lo que la función StringFormat hace el StringSplit lo deshará.

Vean, el resto de la rutina es justamente eso, capturar los datos presentes en el nombre del objeto, de manera que podemos probarlo y utilizarlo más tarde, o sea, MetaTrader 5 genera los datos por nosotros, los desglosamos para saber qué ha pasado, y luego le decimos a MetaTrader 5 qué acción debe tomar. Nuestro trabajo va a ser este, hacer que MetaTrader 5 trabaje por nosotros, no quiero crear una plataforma desde CERO, sino modelar una interfaz y un EA desde CERO, y para ello tenemos que aprovechar cada uno de los soportes mínimos que nos da MetaTrader 5 antes de buscar una solución externa.

Pues bien, de la misma manera que hicimos arriba, haremos algo muy parecido en el código de abajo:

inline void RemoveAllsIndicatorTrade(bool bFull)
{
        string sz0, szRet[];
        int i0 = StringLen(def_NameObjectsTrade);
                                
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        for (int c0 = ObjectsTotal(Terminal.Get_ID(), -1, -1); c0 >= 0; c0--)
        {
                sz0 = ObjectName(Terminal.Get_ID(), c0, -1, -1);
                if (StringSubstr(sz0, 0, i0) == def_NameObjectsTrade)
                {
                        if (!bFull)
                        {
                                StringSplit(sz0, def_SeparatorInfo, szRet);
                                if (StringToInteger(szRet[2]) == def_IndicatorTicket0) continue;
                        }
                }else continue;                                         
                ObjectDelete(Terminal.Get_ID(), sz0);
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
}

Cada vez que apartamos una línea del gráfico, ya sea una posición que se va a cerrar o un límite que se va a retirar, tenemos que eliminar el objeto correspondiente, al igual que en el momento en que se elimina el EA del gráfico, tenemos que eliminar los objetos, pero también tenemos un conjunto de líneas que no deben ser eliminadas sin que realmente sea necesario, que es el Ticket0, este no debe ser eliminado a menos que sea extremadamente necesario. Entonces para evitar esto tenemos el código resaltado, sin este código, tendríamos que estar recreando todo el tiempo este Ticket0, porque este ticket es muy importante en otro punto del código, que mostraré un poco más adelante.

Otras veces tenemos que apartar algo bastante específico, en cuyo caso tenemos otra rutina de eliminación de objetos, y esto se ve a continuación.

inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it != NULL) && (it != IT_PRICE))
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, it));
        else
        {
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_PRICE));
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_TAKE));
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_STOP));
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
}

La siguiente rutina que apareció se puede ver a continuación:

inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy)
{
        double ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal());
        ObjectMove(Terminal.Get_ID(), MountName(ticket, it), 0, 0, price);
        if (it == IT_PRICE)
        {
                ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_TAKE), 0, 0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad))));
                ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_STOP), 0, 0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad)));
        }
}

Esta posicionará los objetos en el eje de precios, pero no te apegues demasiado a ella, ya que pronto dejará de existir, por varias razones, entre ellas hay una que ya fue trabajada y explicada en otro artículo de esta misma serie: Múltiples indicadores en un gráfico (Parte 05): convirtamos el MetaTrader 5 en un sistema RAD (I), en este en un punto determinado hay una tabla que muestra los objetos que pueden utilizar coordenadas cartesianas para posicionarse, y estas coordenadas son de tipo X e Y, las coordenadas del precio y del tiempo. A pesar de ser útil en algunos casos, no es algo muy viable cuando vamos a posicionar elementos que tienen que ser posicionados en puntos específicos de la pantalla, aunque es más rápido desarrollar cosas usando coordenadas de precio y tiempo, son mucho más complicadas de trabajar que el sistema de tipo X e Y.

El cambio se hará en otro momento, esta vez el deseo es crear un sistema alternativo al que se está utilizando hasta ahora.

A continuación tenemos una última rutina relevante dentro de la clase C_ObjectsTrade, y que se ve en el siguiente código:

inline double GetDisplacement(const bool IsBuy, const double Vol, eIndicatorTrade it) const
{
        int i0 = (it == IT_TAKE ? m_BaseFinance.FinanceTake : m_BaseFinance.FinanceStop),
            i1 = (it == IT_TAKE ? (IsBuy ? 1 : -1) : (IsBuy ? -1 : 1));
        return (Terminal.AdjustPrice(i0 * (Vol / m_BaseFinance.Leverange) * Terminal.GetAdjustToTrade() / Vol) * i1);
}

Esta rutina hará una conversión entre los valores indicados en el Chart Trader para la orden que quedará pendiente o para una posición que se abrirá como de mercado.

Todos estos cambios fueron hechos de manera que la rutina C_HLineTrade fuera transformada en C_ObjectsTrade, pero al hacer estos cambios, también fue necesario hacer otros cambios, pero la clase que realmente cambió fue la clase C_ViewOrder, varias partes de esta clase simplemente dejaron de existir, porque no tenía sentido que existieran, si bien las rutinas que permanecieron sufrieron cambios, y las que merecen ser destacadas se ven a continuación.

La primera es la rutina de inicialización de los datos procedentes del Chart Trader

void InitBaseFinance(int nContracts, int FinanceTake, int FinanceStop, bool b1)
{                       
        SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1);
        CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false);
        CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false);
        CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false);
}

los puntos resaltados son donde realmente se crea el Ticket0, y este ticket se utiliza para posicionar una orden pendiente mediante el ratón y el teclado (SHIFT) para comprar (CTRL) para vender. Antes se creaban líneas en este punto que luego se utilizaban para indicar donde se posicionaría la orden, pero ahora, la cosa es mucho más sencilla, por lo que de la misma manera que vemos una orden que se va a posicionar, también veremos una posición pendiente o abierta, lo que significa que siempre estaremos probando el sistema, es como si estuvieras ensamblando un vehículo y, en cada momento, estuvieras probando los frenos, para que, cuando realmente tengas que usarlo, sepas cómo se va a comportar.

El gran problema de muchos códigos es que, a veces, se crea una función, y, solo sabremos si funciona o no, en el momento en que se utilice realmente. Pero, al hacer esto, el sistema siempre estará siendo probado, aunque no utilicemos todas las funcionalidades, siempre estarán siendo probadas, ya que la reutilización del código se produce en muchos puntos.

La última rutina que mencionaré en este artículo se ve a continuación, es la que hará el posicionamiento de la orden pendiente, vean que quedó extremadamente más compacta en relación a la misma rutina vista en artículos pasados.

inline void MoveTo(uint Key)
{
        static double local = 0;
        datetime dt;
        bool    bEClick, bKeyBuy, bKeySell, bCheck;
        double  take = 0, stop = 0, price;
                                
        bEClick  = (Key & 0x01) == 0x01;    //Clic izquierdo
        bKeyBuy  = (Key & 0x04) == 0x04;    //SHIFT presionado
        bKeySell = (Key & 0x08) == 0x08;    //CTRL presionado                          
        Mouse.GetPositionDP(dt, price);
        if (bKeyBuy != bKeySell)
        {
                Mouse.Hide();
                bCheck = CheckLimits(price);
        } else Mouse.Show();
        PositionAxlePrice((bKeyBuy != bKeySell ? price : 0), def_IndicatorTicket0, IT_PRICE, (bCheck ? 0 : GetBaseFinanceTakeProfit()), (bCheck ? 0 : GetBaseFinanceStopLoss()), GetBaseFinanceLeveRange(), bKeyBuy);
        if((bEClick) && (bKeyBuy != bKeySell) && (local == 0)) CreateOrderPendent(bKeyBuy, local = price);
        local = (local != price ? 0 : local);
}

Y la razón es que ahora el sistema tendrá una nueva norma, por lo que la rutina «se ha adelgazado» y se ha vuelto más compacta.


4.0 - Conclusión

Aquí presenté los cambios que en realidad se utilizarán en el próximo artículo, todo esto tenía el propósito de facilitarlos o presentar las cosas ya completamente diferentes de un momento a otro. El deseo aquí es que todo el mundo acompañe el ritmo y aprenda cómo hacer para programar un EA que se utilizará para ayudarle en las operaciones y no simplemente presentar un sistema ya listo y terminado. Quiero mostrar que hay problemas por resolver y cuál fue el camino que tomé para resolver las cuestiones y problemas que surgen durante el desarrollo, espero que entiendan esto, porque si la idea era crear un sistema y presentarlo listo, sería mejor que lo hiciera y vendiera la idea después, pero esta no es mi intención...