Desarrollando un EA comercial desde cero (Parte 11): Sistema de órdenes cruzadas

Daniel Jose | 25 mayo, 2022

Introducción

Hay una clase de activos que les hace la vida muy difícil a los comerciantes, estos son los activos de contratos futuros, y ¿por qué le hacen la vida difícil al comerciante? Al tener una fecha de vencimiento, después de ella se crea un nuevo contrato que comienza a negociarse. Básicamente, cuando un activo expira, debe terminar cualquier estudio sobre él, guardar todo como un template e importar este modelo al nuevo contrato para mantener el estudio. Esto es algo de rutina para los que operan con este tipo de activos, pero incluso ellos, los contratos futuros, tienen algún tipo de historial y utilizando este historial podemos mantener el estudio de forma continua.

Pero a los comerciantes profesionales les gusta analizar cierta información pasada, y esto nos obliga a tener un segundo gráfico en la pantalla. Ahora bien, esto resulta innecesario cuando utilizamos los medios adecuados para eludir este tipo de cosas, y uno de estos recursos es el uso de un sistema de órdenes cruzadas.


Planificación

En el artículo que inició esta serie hablé de este tipo de órdenes, pero no promoví su implementación, porque quería mostrar una serie de cosas y comenzar un sistema más completo que fuera utilizado en la plataforma MetaTrader 5. Aquí mostraré cómo implementar esa funcionalidad.

Para entender la motivación de la creación de tal funcionalidad, echemos un vistazo a las dos imágenes siguientes:

           

La imagen de la izquierda es un típico contrato de futuros, en este caso, del MINI DÓLAR FUTURO, que comenzó hace unos días, como se puede notar en el gráfico, pero aun así el gráfico de la derecha refleja este contrato y contiene datos extra que en realidad son los valores de los contratos ya vencidos, por lo que el gráfico de la derecha es un gráfico de historial, cuando vamos a analizar antiguos puntos de soporte o resistencia damos preferencia en utilizar precisamente el gráfico de la derecha. Pero hay un problema cuando vamos a comerciar, y esto se puede ver a continuación:

          

Veamos que en el CHART TRADE se indica el activo que se está negociando, incluso cuando utilizamos el historial, el CHART TRADE informa que el envío de la orden se puede hacer, y esto se demuestra por la caja de herramientas, veamos que en la imagen de la izquierda tenemos sólo la orden creada allí, en el contrato actual, pero en la imagen de la derecha vemos sólo la orden que se indica en la caja de mensajes, y ninguna orden se puede ver en el gráfico.

Puede que nos estemos imaginando que se trata simplemente de una cuestión de visualización, pero no, la cosa es mucho más compleja, eso es precisamente lo que voy a enseñar a resolver aquí en este artículo.

Un detalle importante: Aquí mostraré como crear las reglas para poder utilizar el historial para operar, y en este caso estas reglas estarán orientadas a operar el Mini Dólar ( WDO ) y el Mini Índice ( WIN ) que forman parte de la bolsa de Brasil (B3). El correcto entendimiento permitirá adaptar las reglas para tener la misma operación para cualquier tipo de contrato de futuros de cualquier bolsa del mundo.

El sistema en sí no se limita a un activo u otro, todo es cuestión de adaptar los puntos adecuados del código, si esto se hace correctamente, tendremos un EA con el que no tendremos que preocuparnos de si el activo está o no cerca del vencimiento y cuál será el siguiente contrato, el propio EA lo hará por nosotros, cambiando el contrato por el correcto en cuanto sea necesario.


Cómo entender las reglas del juego

Los contratos de futuros WDO (Mini Dólar), WIN (Mini Índice), DOL (Dólar Futuro) e IND (Índice Futuro) siguen reglas muy específicas en cuanto a la emisión de sus vencimientos y la nomenclatura del contrato, pero veamos primero cómo saber la fecha de vencimiento del contrato, esto se puede ver en la imagen de abajo:


Observemos las zonas resaltadas, en AZUL tenemos un resumen de la fecha de vencimiento del contrato, y en rojo tenemos el momento exacto en el que el contrato dejará de existir al no ser negociado por ningún sistema. Saber esto es muy importante porque sin saber consultar estos momentos estaríamos en el limbo sólo preguntando cuándo se producirá el vencimiento.

Pues bien, a pesar de que la fecha de vencimiento y el momento del mismo se especifican en el propio contrato como se puede ver, no ocurre lo mismo con el nombre, pero afortunadamente esto se puede deducir en base a unas reglas conocidas por todo el mercado de trading y la regla es muy estricta. En el caso de los contratos de futuros de Dólar e Índice tenemos la siguiente regla de nomenclatura:

Las 3 primeras letras indican el tipo de contrato:

Código Contrato
WIN Mini contrato de futuros del índice ibovespa 
IND Contrato de futuros del índice ibovespa
WDO Mini contrato de futuros del dólar
DOL Contrato de futuros del dólar

Luego de este código tenemos una letra que indica el mes de vencimiento del contrato:

Mes de vencimiento Letra que denota WDO y DOL Letra que denota WIN e IND 
Enero F  
Febrero  G  G
Marzo  H  
Abril  J  J
Mayo  K  
Junio  M  M
Julio  N  
Agosto  Q  Q
Septiembre  U  
Octubre  V  V
Noviembre  X  
Diciembre  Z  Z

Y para terminar tenemos dos dígitos que representan el año de vencimiento del contrato, entonces como ejemplo para un contrato futuro del dólar con vencimiento en abril de 2022 tenemos el siguiente código: DOLJ22. Y este sería el contrato que se debería negociar hasta principios de mayo. Cuando comience el mes de mayo este contrato estará vencido, ya la regla para el WIN e IND es un poco diferente, el contrato vence el miércoles más cercano al día 15 del mes indicado, o sea, la regla es aparentemente mucho más compleja, pero notarán que el EA podrá manejarlo muy bien, y siempre informa el contrato correcto.


Implementación

Nuestro EA ya tiene los puntos necesarios para recibir las reglas de nomenclatura, solo tendremos que hacer algunos ajustes en cuanto al sistema de envío de órdenes, pero empecemos el trabajo, lo primero es en la clase objeto C_Terminal, agreguemos el siguiente código a esta clase:

void CurrentSymbol(void)
{
        MqlDateTime mdt1;
        string sz0, sz1;
        datetime dt = TimeLocal();
            
        sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3);                           
        if ((sz0 != "WDO") && (sz0 != "DOL") && (sz0 != "WIN") && (sz0 != "IND")) return;
        sz1 = ((sz0 == "WDO") || (sz0 == "DOL") ? "FGHJKMNQUVXZ" : "GJMQVZ");
        TimeToStruct(TimeLocal(), mdt1);
        for (int i0 = 0, i1 = mdt1.year - 2000;;)
        {
                m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1);
                if (i0 < StringLen(sz1)) i0++; else
                {
                        i0 = 0;
                        i1++;
                }
                if (macroGetDate(dt) < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_EXPIRATION_TIME))) break;
        }
}

Este código de arriba utiliza las reglas descritas anteriormente para generar el nombre del activo, y para asegurarnos de que siempre vamos a utilizar un contrato actual hacemos una prueba, y esto lo hace la línea resaltada, es decir el activo es válido por la propia plataforma de comercio y el EA utilizará el nombre generado, si se quiere operar con otros contratos de futuros lo único que hay que hacer es adaptar este código anterior para que se genere correctamente el nombre, esto va en función de cada caso, pero el código no se limita sólo a los activos que están ligados a él, se puede utilizar para reflejar cualquier tipo de contrato de futuros, siempre que se cree la regla correcta.

Ahora viene la parte de los detalles de la orden. Pues bien, si utilizamos el sistema en este punto del desarrollo tendremos el siguiente comportamiento:


Me refiero a que ya se puede tener un modo de órdenes cruzadas, pero aún no está completamente implementado, falta la indicación de la orden en la pantalla gráfica, esto no es algo tan difícil de hacer, como muchos de ustedes ya se estarán imaginando, lo único que tenemos que hacer es indicarlo por medio de líneas horizontales, pero eso no es todo, cuando usamos una orden cruzada perdemos algunas cosas que MetaTrader 5 nos proporciona, y necesitamos implementar esta lógica para que el sistema de órdenes siga funcionando y sea igualmente seguro, estable y confiable, de lo contrario podríamos tener problemas al usar órdenes cruzadas.

Pues bien, mirando desde este punto de vista, la cosa ya no parece tan sencilla, de hecho no lo es tanto, ya que tendremos que crear toda la lógica que inicialmente hacía la plataforma MetaTrader, así que lo primero que hay que hacer es olvidarse del sistema interno de MetaTrader, no nos servirá de apoyo a partir de este momento que empecemos a utilizar el sistema de órdenes cruzadas.

A partir de ahora quien dictará las reglas será el ticket de la orden, pero esto tiene varios costos, uno de los más grandes es que no sabemos cuántas órdenes colocará un determinado tráder en su gráfico, y limitar la cantidad no será algo que le agrade, por lo que necesitamos hacer algo para que él pueda usar el sistema de la misma manera que lo usaría si estuviera usando el soporte provisto por MetaTrader, este es nuestro primer problema a resolver.


La clase C_HLineTrade

Para solucionar este problema vamos a crear una nueva clase, la clase C_HLineTrade, que sustituirá al sistema que proporciona MetaTrader 5 para mostrar las órdenes en el gráfico. Así que veamos esta clase con calma, empezando por su declaración:

class C_HLineTrade
{
#define def_NameHLineTrade "*HLTSMD*"
        protected:
                enum eHLineTrade {HL_PRICE, HL_STOP, HL_TAKE};
        private :
                color   m_corPrice,
                        m_corStop,
                        m_corTake;
                string  m_SelectObj;

Observemos que hemos definido algunas cosas, y esto se utilizará mucho a lo largo del código. Por lo que habrá que prestar mucha atención a los cambios, que no serán pocos, de hecho serán muchos. Lo siguiente es declarar el constructor y el destructor de la clase:

C_HLineTrade() : m_SelectObj("")
{
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false);
        RemoveAllsLines();
};
//+------------------------------------------------------------------+  
~C_HLineTrade()
{
        RemoveAllsLines();
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, true);
};

El constructor impedirá que las líneas originales sean visibles, el destructor las obligará a verse de nuevo en el gráfico. Tenemos una rutina común en ambas rutinas así que veámosla a continuación.

void RemoveAllsLines(void)
{
        string sz0;
        int i0 = StringLen(def_NameHLineTrade);
                                
        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_NameHLineTrade) ObjectDelete(Terminal.Get_ID(), sz0);
        }
}

La línea resaltada probará si el objeto, en este caso una línea horizontal, es uno de los objetos usados por la clase, y si lo es borrará el objeto, nótese que no sabemos cuántos objetos tenemos, pero el sistema probará objeto por objeto para limpiar todos los que fueron creados por la clase. La siguiente rutina destacada de la clase se ve a continuación:

inline void SetLineOrder(ulong ticket, double price, eHLineTrade hl, bool select)
{
        string sz0 = def_NameHLineTrade + (string)hl + (string)ticket, sz1;
                                
        if (price <= 0)
        {
                ObjectDelete(Terminal.Get_ID(), sz0);
                return;
        }
        if (!ObjectGetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, 0, sz1))
        {
                ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, 0, 0, 0);
                ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_COLOR, (hl == HL_PRICE ? m_corPrice : (hl == HL_STOP ? m_corStop : m_corTake)));
                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(hl), 3, 10));
        }
        ObjectSetDouble(Terminal.Get_ID(), sz0, OBJPROP_PRICE, price);
}

Para esta rutina no importa cuántos objetos se crearán, si existe o no cuando es llamada, y se asegura de que la línea se creará y se posicionará en el lugar indicado. Esta línea es la que será el indicador de reemplazo de la línea originalmente utilizada por MetaTrader.

La cosa aquí es ser funcional y no bonito o simpático, por eso las filas no se seleccionan cuando se crean, se puede cambiar ese comportamiento, pero estoy usando el sistema de mensajería de MetaTrader 5 para posicionar las filas, así que para poder moverlas, habría que indicarlo explícitamente, y para indicar qué fila se está poniendo tenemos otra rutina que se ve a continuación:

inline void Select(const string &sparam)
{
        int i0 = StringLen(def_NameHLineTrade);
                                
        if (m_SelectObj != "") ObjectSetInteger(Terminal.Get_ID(), m_SelectObj, OBJPROP_SELECTED, false);
        m_SelectObj = "";
        if (StringSubstr(sparam, 0, i0) == def_NameHLineTrade)
        {
                if (ObjectGetInteger(Terminal.Get_ID(), sparam, OBJPROP_SELECTABLE))
                {
                        ObjectSetInteger(Terminal.Get_ID(), sparam, OBJPROP_SELECTED, true);
                        m_SelectObj = sparam;
                };
        }
}

Esta rutina mantendrá la línea resaltada, y cuando se seleccione otra, eliminará la selección de la anterior, esto se hace de forma muy sencilla, pero sólo se manipularán realmente las líneas manejadas por la clase. Hay una última rutina de esta clase que merece ser destacada, y se ve a continuación:

bool GetNewInfosOrder(const string &sparam, ulong &ticket, double &price, eHLineTrade &hl)
{
        int i0 = StringLen(def_NameHLineTrade);
                                
        if (StringSubstr(sparam, 0, i0) == def_NameHLineTrade)
        {
                hl = (eHLineTrade) StringToInteger(StringSubstr(sparam, i0, 1));
                ticket = (ulong)StringToInteger(StringSubstr(sparam, i0 + 1, StringLen(sparam)));
                price = ObjectGetDouble(Terminal.Get_ID(), sparam, OBJPROP_PRICE);
                return true;
        }
        return false;
}

Esta rutina es quizás la más importante de esta clase, ya que no sabemos cuántas líneas hay en el gráfico, necesitamos saber qué línea está siendo manipulada por el usuário, esta rutina hace justamente eso, le dice al sistema cuáles son los datos de la línea que está siendo manipulada por el usuario.

Bueno, pero esto es sólo una pequeña parte de todo lo que tenemos que hacer, el sistema no está ni siquiera cerca de ser funcional, así que vamos a ir al siguiente paso, la adición y modificación de las rutinas de la clase C_Router, que es la clase responsable del enrutamiento de la orden. Esta clase recibe por herencia la funcionalidad que acabamos de construir en la clase C_HLineTrade y que se puede ver en el siguiente fragmento:

#include "C_HLineTrade.mqh"
//+------------------------------------------------------------------+
class C_Router : public C_HLineTrade


La nueva clase C_Router

En la clase C_Router original, existía la limitación de tener una sola orden abierta, pero a partir de ahora ya no tendremos esta limitación, esto nos trae cambios drásticos en la clase C_Router.

El primer cambio, y este es muy drástico, es en la rutina de actualización de esta clase, veamos cómo quedó la nueva rutina:

void UpdatePosition(void)
{
        static int memPositions = 0, memOrder = 0;
        ulong ul;
        int p, o;
                                
        p = PositionsTotal() - 1;
        o = OrdersTotal() - 1;
        if ((memPositions != p) || (memOrder != o))
        {
                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
                RemoveAllsLines();
                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
                memOrder = o;
                memPositions = p;
        };
        for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                ul = PositionGetInteger(POSITION_TICKET);
                SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);
                SetLineOrder(ul, PositionGetDouble(POSITION_TP), HL_TAKE, true);
                SetLineOrder(ul, PositionGetDouble(POSITION_SL), HL_STOP, true);
        }
        for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
        {
                SetLineOrder(ul, OrderGetDouble(ORDER_PRICE_OPEN), HL_PRICE, true);
                SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true);
                SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true);
        }
};

Antes esta rutina solo capturaba los datos de la única posición abierta y la guardaba en su observatorio, pero ahora la cosa explota delante de nosotros, esta rutina pondrá absolutamente todas las posiciones abiertas o pendientes en el gráfico. Esto es definitivamente el reemplazo del sistema que se encuentra en MetaTrader, la cosa aquí es seria, esta rutina tiene que ser muy bien entendida, porque si falla pondrá en juego todo el sistema de órdenes cruzadas. Así que antes de operar en una cuenta real, pongamos este sistema en una cuenta demo para ser probado, sólo por cosas como lo que sucede en esta rutina, PROBEMOS y PROBEMOS de nuevo hasta que estemos absolutamente seguros de que la cosa está funcionando de la manera que esperamos, ajustemos lo que sea necesario porque la forma en que este sistema funciona es ligeramente diferente de la forma en que MetaTrader 5 funciona.

Observemos las líneas resaltadas, prestemos mucha atención a ellas y respondamos con sinceridad: ¿Tenemos alguna idea de lo que realmente hacen? Estas dos líneas están ahí por una razón que se verá cuando hable de la clase C_OrderView más adelante en este artículo, pero sin estas dos líneas el código es muy inestable, y funciona de una manera extraña, pero salvo la parte resaltada, el resto de la rutina es muy sencilla, crea a través de la clase objeto C_HLineTrade, cada una de las líneas. En este caso hay una sola línea que no se puede seleccionar, y esto se indica fácilmente como se muestra en el fragmento:

SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);

En otras palabras, el sistema se ha vuelto muy simple y sencillo, esta rutina es llamada por el EA durante un evento en OnTrade como se puede ver en el siguiente fragmento:

C_TemplateChart Chart;

// ... Código do EA ...

void OnTrade()
{
        Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, Chart.UpdateRoof(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY]);
        Chart.UpdatePosition();
}

// ... Restante do código do EA ...

El código resaltado promoverá la actualización del sistema de órdenes en la pantalla, pero hay una cosa, nótese que estamos usando la clase C_TemplateChart para esto, es porque el sistema pasó por un cambio radical en la estructura de clases, la nueva estructura de clases se puede ver a continuación:

El hecho de que sea así hace que el flujo de mensajes dentro del EA sea direccionado, siempre que tengamos cualquier duda de cómo llega el flujo de mensajes a una clase en particular, miremos este gráfico de herencia de clases. La única clase que se trata como pública es la clase objeto C_Terminal, todas las demás se tratan por herencia entre clases, y ninguna y absolutamente ninguna variable es pública en este sistema.

Como el sistema ahora no analiza sólo una orden, es necesario entender otra cosa: ¡¿Cómo entender el resultado de las operaciones?! ¿Por qué es importante? Bueno, cuando sólo se tiene una posición abierta el sistema es sencillo de entender, pero a medida que aumenta la cantidad de posiciones abiertas, hay que entender lo que está pasando. Pues bien, la rutina que informa esto se ve a continuación:

void OnTick()
{
        Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, Chart.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]);
}

Básicamente no cambió, pero la rutina resaltada SÍ, así que veamos el código de esta rutina resaltada:

inline double CheckPosition(void)
{
        double Res = 0, last, sl;
        ulong ticket;
                        
        last = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_LAST);
        for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                ticket = PositionGetInteger(POSITION_TICKET);
                Res += PositionGetDouble(POSITION_PROFIT);
                sl = PositionGetDouble(POSITION_SL);
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                {
                        if (last < sl) ClosePosition(ticket);
                }else
                {
                        if ((last > sl) && (sl > 0)) ClosePosition(ticket);
                }
        }
        return Res;
};

La rutina tiene 3 partes resaltadas, la parte amarilla es la que informa el resultado de las posiciones abiertas, ya las partes verdes harán un chequeo en la posición en caso de que se salte el stop loss por una gran volatilidad, si esto sucede se debe cerrar la posición lo antes posible. Así que esta rutina no devuelve el resultado de una sola posición, excepto si está con una sola posición abierta en un activo específico.

Pero además de esas rutinas tenemos otras que ayudan y permiten que el sistema siga funcionando cuando utilizamos un modelo de orden cruzado, y también merecen ser destacadas, dos de ellas se pueden ver a continuación:

bool ModifyOrderPendent(const ulong Ticket, const double Price, const double Take, const double Stop, const bool DayTrade = true)
{
        if (Ticket == 0) return false;
        ZeroMemory(TradeRequest);
        ZeroMemory(TradeResult);
        TradeRequest.action     = TRADE_ACTION_MODIFY;
        TradeRequest.order      = Ticket;
        TradeRequest.price      = NormalizeDouble(Price, Terminal.GetDigits());
        TradeRequest.sl         = NormalizeDouble(Stop, Terminal.GetDigits());
        TradeRequest.tp         = NormalizeDouble(Take, Terminal.GetDigits());
        TradeRequest.type_time  = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
        TradeRequest.expiration = 0;
        return OrderSend(TradeRequest, TradeResult);
};
//+------------------------------------------------------------------+
bool ModifyPosition(const ulong Ticket, const double Take, const double Stop)
{
        ZeroMemory(TradeRequest);
        ZeroMemory(TradeResult);
        if (!PositionSelectByTicket(Ticket)) return false;
        TradeRequest.action     = TRADE_ACTION_SLTP;
        TradeRequest.position   = Ticket;
        TradeRequest.symbol     = PositionGetString(POSITION_SYMBOL);
        TradeRequest.tp         = NormalizeDouble(Take, Terminal.GetDigits());
        TradeRequest.sl         = NormalizeDouble(Stop, Terminal.GetDigits());
        return OrderSend(TradeRequest, TradeResult);
};

La primera se encarga de modificar una orden que aún está abierta, ya la otra modifica una posición abierta, aunque parecen ser iguales, no lo son, y por último tenemos una rutina igualmente importante para el sistema, se ve a continuación:

bool RemoveOrderPendent(ulong Ticket)
{
        ZeroMemory(TradeRequest);
        ZeroMemory(TradeResult);
        TradeRequest.action     = TRADE_ACTION_REMOVE;
        TradeRequest.order      = Ticket;       
        return OrderSend(TradeRequest, TradeResult);
};

Con esta última rutina cerramos la clase C_Router, es así como se implementa el sistema básico necesario para suprimir el soporte que nos daba MetaTrader, ahora por el sistema de órdenes cruzadas ya no podremos contar con este soporte. Sin embargo el sistema aún no está terminado en este punto, tenemos más cosas que añadir antes de que el sistema sea realmente funcional, sin embargo hasta el momento si hay una orden se verá como se muestra a continuación, es importante que esto esté sucediendo, para que el siguiente paso se pueda ejecutar con éxito.


Observemos con atención la imagen de arriba, en el cuadro de mensajes tenemos la orden abierta y se indica cuál es el activo en el que está abierta, en el CHART TRADE se indica el activo negociado, observemos que es el mismo que se está indicando en el cuadro de mensajes, ahora miremos qué activo se está mostrando en el gráfico, esto lo podemos ver en el título de la ventana del gráfico... es completamente diferente, no es el activo en el gráfico, es el historial del mini índice, lo que significa que no estamos utilizando el sistema de MetaTrader 5 para apoyarnos, estamos utilizando el sistema de órdenes cruzadas, que se describe en este artículo. Hasta el momento solo tenemos esta funcionalidad que nos permite mostrar donde se posiciona la orden, pero esto no es suficiente para tener un sistema funcional que nos permita operar a través del sistema de órdenes cruzadas, necesitamos algunas cosas más, en el caso de los eventos de movimiento de órdenes, y esto se logrará en otra clase.


Una nueva funcionalidad en la clase C_OrderView

Aunque la clase objeto C_OrderView es capaz de hacer varias cosas, todavía no es capaz de manipular los datos de una orden abierta o pendiente, no obstante cuando le añadimos un sistema de mensajes empezamos a tener otras posibilidades de uso. Esta es la única adición que haremos a la clase en este momento. La rutina se puede ver completa a continuación:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong           ticket;
        double          price, pp, pt, ps;
        eHLineTrade     hl;
        
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        MoveTo((int)lparam, (int)dparam, (uint)sparam);
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (GetNewInfosOrder(sparam, ticket, price, hl))
                        {
                                if (OrderSelect(ticket))
                                {
                                        switch (hl)
                                        {
                                                case HL_PRICE:
                                                        RemoveOrderPendent(ticket);
                                                        break;
                                                case HL_STOP:
                                                        ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), OrderGetDouble(ORDER_TP), 0);
                                                        break;
                                                case HL_TAKE:
                                                        ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), 0, OrderGetDouble(ORDER_SL));
                                                        break;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        switch (hl)
                                        {
                                                case HL_PRICE:
                                                        ClosePosition(ticket);
                                                        break;
                                                case HL_STOP:
                                                        ModifyPosition(ticket, OrderGetDouble(ORDER_TP), 0);
                                                        break;
                                                case HL_TAKE:
                                                        ModifyPosition(ticket, 0, OrderGetDouble(ORDER_SL));
                                                        break;
                                        }
                                }
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        C_HLineTrade::Select(sparam);
                        break;
                case CHARTEVENT_OBJECT_DRAG:
                        if (GetNewInfosOrder(sparam, ticket, price, hl))
                        {
                                price = AdjustPrice(price);
                                if (OrderSelect(ticket)) switch(hl)
                                {
                                        case HL_PRICE:
                                                pp = price - OrderGetDouble(ORDER_PRICE_OPEN);
                                                pt = OrderGetDouble(ORDER_TP);
                                                ps = OrderGetDouble(ORDER_SL);
                                                if (!ModifyOrderPendent(ticket, price, (pt > 0 ? pt + pp : 0), (ps > 0 ? ps + pp : 0))) UpdatePosition();
                                                break;
                                        case HL_STOP:
                                                if (!ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), OrderGetDouble(ORDER_TP), price)) UpdatePosition();
                                                break;
                                        case HL_TAKE:
                                                if (!ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), price, OrderGetDouble(ORDER_SL))) UpdatePosition();
                                                break;
                                }
                                if (PositionSelectByTicket(ticket)) switch (hl)
                                {
                                        case HL_PRICE:
                                                UpdatePosition();
                                                break;
                                        case HL_STOP:
                                                ModifyPosition(ticket, PositionGetDouble(POSITION_TP), price);
                                                break;
                                        case HL_TAKE:
                                                ModifyPosition(ticket, price, PositionGetDouble(POSITION_SL));
                                                break;
                                }
                        };
                break;
        }
}

Este código completa el sistema de órdenes cruzadas, con esto tenemos las posibilidades aumentadas hasta el punto de poder hacer casi las mismas cosas que eran posibles antes de implementar el sistema de órdenes cruzadas. Básicamente no hay mucho anormal en esta función, pero hay un tipo de evento que no es muy común, es el CHARTEVENT_OBJECT_DELETE. Aquí, cuando el usuario elimina una fila, esto se verá reflejado en el gráfico y en el sistema de órdenes, así que tengamos especial cuidado cuando empecemos a eliminar líneas de nuestro gráfico, eso sí, no debemos preocuparnos cuando eliminemos el EA del gráfico, ya que las órdenes continuarán intactas como se muestra en la siguiente animación:


Pero, si el EA está en el gráfico, debemos tener el máximo cuidado al eliminar las filas de nuestro gráfico, sobre todo las que están ocultas en la lista de objetos, porque de lo contrario, veamos a continuación lo que ocurre en el sistema de órdenes cuando se eliminan las filas que fueron creadas por el sistema de órdenes cruzadas.

Y para terminar la presentación del sistema, veamos lo que ocurre con una orden cuando arrastramos las líneas de precio, esto se puede ver justo debajo, recuerda lo siguiente, la línea que se va a arrastrar debe estar seleccionada, si no está seleccionada no será posible moverla, el cambio de precio solo se producirá cuando se suelte la línea en el gráfico, antes el precio se mantendrá en la misma posición que antes.


Si tenemos dificultades para saber cuando una línea está seleccionada o no, hagamos el cambio en el código de selección, los cambios están resaltados en el código, en la versión que se adjunta, este cambio ya vendrá implementado.

inline void Select(const string &sparam)
{
        int i0 = StringLen(def_NameHLineTrade);
                
        if (m_SelectObj != "")
        {
                ObjectSetInteger(Terminal.Get_ID(), m_SelectObj, OBJPROP_SELECTED, false);
                ObjectSetInteger(Terminal.Get_ID(), m_SelectObj, OBJPROP_WIDTH, 1);
        }
        m_SelectObj = "";
        if (StringSubstr(sparam, 0, i0) == def_NameHLineTrade)
        {
                if (ObjectGetInteger(Terminal.Get_ID(), sparam, OBJPROP_SELECTABLE))
                {
                        ObjectSetInteger(Terminal.Get_ID(), sparam, OBJPROP_SELECTED, true);
                        ObjectSetInteger(Terminal.Get_ID(), sparam, OBJPROP_WIDTH, 2);
                        m_SelectObj = sparam;
                };
        }
}

El resultado de esta modificación en el código puede verse en la siguiente animación.


Conclusión

Bueno, aquí he mostrado cómo crear un sistema de órdenes cruzadas en MetaTrader. Espero que este sistema sea útil para todos aquellos que puedan utilizar estos conocimientos, pero recordemos lo siguiente, antes de empezar a operar en una cuenta real con este sistema, probémoslo lo máximo posible, en muchos escenarios de mercado diferentes, ya que a pesar de estar implementado en la plataforma MetaTrader, este sistema no tiene casi ningún soporte por parte de la plataforma en cuanto a tratamiento de errores, por lo que, si por casualidad se producen, tendríamos que actuar rápidamente, para no tener grandes pérdidas. Pero probándolo en diferentes escenarios, podremos notar donde aparecen los problemas, en forma de movimientos, cantidad máxima de órdenes que nuestro ordenador será capaz de soportar, el spread máximo aceptable para el sistema de análisis, el nivel de volatilidad aceptable para las órdenes abiertas, ya que a mayor cantidad de órdenes abiertas e información a analizar, mayor es la posibilidad de que ocurra algo malo, ya que cada una de las órdenes es analizada a cada tick recibido por el sistema, y esto puede ser un problema cuando hay muchas órdenes abiertas al mismo tiempo.

El consejo es que no hay que confiar en este sistema antes de probarlo en una cuenta de prueba en el mayor número posible de escenarios, incluso si el código parece ser perfecto, no cuenta con ningún tipo de análisis de errores.

Adjunto el código de todo el EA hasta el momento.