English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Desarrollando un EA comercial desde cero (Parte 18): Un nuevo sistema de órdenes (I)

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

MetaTrader 5Trading | 8 julio 2022, 08:49
796 0
Daniel Jose
Daniel Jose

1.0 - Introducción

Desde que este EA comenzó a tener su desarrollo documentado en artículos, y esto comenzó en el artículo Desarrollando un EA comercial desde cero, ha sufrido muchos cambios y mejoras, sin embargo ha mantenido el mismo modelo de sistema de órdenes en el gráfico, algo muy sencillo y funcional, pero no adecuado en muchas de las veces que estamos operando realmente.

Existe la posibilidad de añadir cosas al sistema original, de forma que se tiene cierta información sobre las órdenes, tanto las abiertas como las pendientes, pero en cierto modo esto transformará el código en un Frankenstein que, con el paso del tiempo, convierte el proceso de mejora en una auténtica pesadilla. Y por mucho que tengamos una metodología, ésta se pierde con el tiempo a medida que el código se hace extremadamente grande y complejo.

Pues bien, necesitamos crear un sistema completamente diferente en lo que se refiere al modelo de órdenes que se está utilizando, pero que, al mismo tiempo, sea sencillo de entender por el operador, y que nos dé toda la información que necesitamos para operar con seguridad.


1.0.1 - Aviso extremadamente importante

El sistema que voy a describir en este artículo es el que utiliza el modo de negociación registrado en la cuenta como posiciones de cobertura del tipo ACCOUNT_MARGIN_MODE_RETAIL_NETTING, ya que en éstas tenemos sólo y únicamente una única posición abierta por activo. Si está utilizando un sistema de tráding registrado en la cuenta como ACCOUNT_MARGIN_MODE_RETAIL_HEDGING este artículo no añadirá nada al EA, ya que en este caso es posible tener varias posiciones abiertas al mismo tiempo sin que una interfiera con la otra, por lo que todas las modificaciones pueden ser descartadas o eliminadas del código final.

El modo ACCOUNT_MARGIN_MODE_RETAIL_HEDGING es el más comúnmente usado cuando se quiere tener un EA funcionando de forma automática, abriendo y cerrando posiciones sin tu intervención, mientras tú operas el mismo activo que el EA estará operando, o sea, el hecho de que el EA esté vendiendo no influirá en nada en una operación que tú ejecutes comprando, las posiciones en este caso serán independientes.

Por esta razón, dentro del artículo todas las partes añadidas o modificadas en los códigos están bien resaltadas, y las modificaciones o adiciones se harán lenta y gradualmente, de manera que si se necesita o se quiere eliminar las modificaciones y adiciones será fácil encontrar dónde se hicieron.

No obstante se puedan eliminar las modificaciones, hay una parte en la que voy a probar el sistema para que, aunque tú mantengas los cambios que se van a producir aquí, puedas utilizar este EA en cualquier tipo de cuenta, ya sea del tipo NETTING o HEDGING, porque el EA tendrá una prueba para comprobar y ajustarse a un modelo u otro.


2.0 - Planificación

Lo primero que hay que hacer, de hecho, es entender qué pasa con las órdenes que se van añadiendo al sistema y que se van ejecutando a medida que se van alcanzando los precios en los que ellas se fijaron, puede que muchos no lo sepan, o mejor dicho, nunca se han parado a pensar en ello y, por eso, no han hecho ninguna prueba para entenderlo, y de esa manera implementar un sistema que sea adecuado a la vez que siga siendo fiable.

Para entender lo que realmente sucede, utilicemos un ejemplo sencillo, tú tienes una posición abierta, digamos una posición de compra, en un activo, con un volumen inicial de, por ejemplo, 1 lote, entonces colocas una nueva orden de compra, con un volumen de 2 lotes a un precio un poco más alto. Hasta este punto todo está bien, nada demasiado extravagante, pero tan pronto como esos 2 lotes sean comprados, algo sucederá, y ahí es exactamente donde radica el problema.

Cuando se compren los dos lotes, ahora tendrás 3 lotes, pero el sistema de negociación actualizará su precio inicial a un precio medio. Hasta aquí todo bien, es algo perfectamente entendido por todos los operadores del mercado, pero la cuestión es la siguiente: ¿Cuál será el valor de stop y de take de la nueva posición?

Una gran parte de los operadores no tienen ni idea de lo que estoy hablando, pero deberían pensar en esto. Si utiliza el sistema de órdenes OCO en todas las operaciones, notarás que ocurre algo muy curioso e interesante cada vez que abres o cierras una posición con una cantidad parcial del volumen total que puedes operar.

Bueno, en el artículo sobre las órdenes cruzadas, presenté una forma para que tengas los niveles de take y stop directamente en un gráfico sin necesidad del sistema estándar de MetaTrader 5, es muy cierto que ese método funciona de manera casi idéntica al sistema de MetaTrader 5, ya que fue pensado para tener una funcionalidad muy cercana a la que se encuentra en la plataforma, pero con las debidas proporciones. Sin embargo al hacer algunas pruebas, he comprobado que cuando tenemos una orden OCO abierta y una orden pendiente, una OCO también es capturada por el sistema de tráding, debido a que el precio ha alcanzado el valor especificado en la orden, además de generarse un precio medio, tenemos la modificación de los valores de take y de stop por los valores indicados en la última orden OCO capturada, lo que significa, dependiendo de cómo esté configurada, que el EA la cerrará inmediatamente después de que el sistema de tráding informe de un nuevo valor de take o stop.

Esto sucede debido a la siguiente prueba presente en EA:

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;
};

Las líneas resaltadas son responsables de esta prueba y del cierre si el precio sale del canal que está limitado entre el valor de take y el del stop.

Es importante saber esto, porque no se trata de un fallo del EA, ni de un problema o defecto del sistema de tráding, el verdadero problema es que la mayoría de las veces no se presta la atención necesaria a lo que está ocurriendo y este tipo de cosas se van ignorando hasta que ya no es posible ignorar el hecho, debido a las constantes pérdidas, o a las salidas realizadas fuera de tiempo.

Si operas de forma que no se haga precio medio, no verás que ocurra este tipo de cosas, pero hay un rango muy grande de operaciones en las que hacer precio medio es adecuado e incluso necesario, y en esos casos si utilizas el sistema sin conocer los detalles que he descrito anteriormente, puedes acabar saliendo de la posición antes de lo deseado, aunque hayas modificado previamente la orden OCO de una posición abierta de forma adecuada, en cuanto se captura una orden OCO pendiente, los valores de los límites (cada vez que hable de límites me referiré al take y al stop) anteriores serán modificados a los indicados en la orden OCO pendiente recién capturada.

La forma de corregir, o mejor evitar esto, es no utilizar órdenes OCO, al menos cuando ya se tiene una posición abierta, todas las demás órdenes emitidas al sistema de negociación deben ser de tipo simple, sin que se establezcan los valores de take y stop.

Básicamente es esto, pero cuando un EA está en el gráfico, está ahí para ayudarnos, facilitándonos la vida al máximo, si no es así, no tendría sentido tener todo el trabajo en programar un EA, y luego no utilizarlo.


3.0 - Implementación del nuevo sistema


Básicamente, para implementar y asegurar que el sistema funcionará como esperamos, es decir, que EA nos ayudará y evitará que cometamos errores, tenemos que hacer algunos pequeños cambios en el código.

En principio no es algo muy complicado, pero estos cambios nos garantizarán que nunca corramos el riesgo de que una orden OCO entre en un momento no deseado y provoque un verdadero lío en nuestra negociación.

Empecemos por hacer los siguientes cambios:


3.0.1 - Modificación de la clase C_Router

La clase C_Router es la encargada de analizar y enviarnos las órdenes, por lo que añadimos una variable privada en ella, y cuando se detecte una posición abierta, en el activo del que se encarga el EA, esta variable nos almacenará esta información, y cada vez que el EA quiera saber si hay una posición abierta, nos informará de ello.

Esta implementación se puede ver en el fragmento de abajo, a pesar de todo, este fragmento sufrirá cambios pronto en este artículo, pero quiero mostrar las cosas poco a poco, para que entiendas como fue el proceso de modificación.

//+------------------------------------------------------------------+
inline bool ExistPosition(void) const { return m_bContainsPosition; }
//+------------------------------------------------------------------+
void UpdatePosition(void)
{
        static int memPositions = 0, memOrder = 0;
        ulong ul;
        int p, o;
                                
        p = PositionsTotal();
        o = OrdersTotal();
        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;
                m_bContainsPosition = false;
        };
        for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                ul = PositionGetInteger(POSITION_TICKET);
                m_bContainsPosition = true;
                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);
        }
};
//+------------------------------------------------------------------+

Las líneas resaltadas son las adiciones necesarias para ejecutar todas las pruebas que se harán más adelante, en algunos puntos del código de nuestro EA.

En cierto modo podríamos hacer todas las pruebas y ajustes sólo en la clase C_Router, pero esto no será suficiente, como explicaré más adelante, pero sigamos haciendo las modificaciones. Después de crear la prueba vista anteriormente, añadiremos un constructor, para inicializar correctamente la variable recién añadida.

C_Router() : m_bContainsPosition(false) {}

Ahora modificamos la función de colocación de órdenes pendientes de la siguiente manera:

ulong CreateOrderPendent(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true)
{
        double last = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_LAST);
                                
        ZeroMemory(TradeRequest);
        ZeroMemory(TradeResult);
        TradeRequest.action             = TRADE_ACTION_PENDING;
        TradeRequest.symbol             = Terminal.GetSymbol();
        TradeRequest.volume             = Volume;
        TradeRequest.type               = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
        TradeRequest.price              = NormalizeDouble(Price, Terminal.GetDigits());
        TradeRequest.sl                 = NormalizeDouble((m_bContainsPosition ? 0 : Stop), Terminal.GetDigits());
        TradeRequest.tp                 = NormalizeDouble((m_bContainsPosition ? 0 : Take), Terminal.GetDigits());
        TradeRequest.type_time          = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
        TradeRequest.stoplimit          = 0;
        TradeRequest.expiration         = 0;
        TradeRequest.type_filling       = ORDER_FILLING_RETURN;
        TradeRequest.deviation          = 1000;
        TradeRequest.comment            = "Order Generated by Experts Advisor.";
        if (!Send()) return 0;
                                                                
        return TradeResult.order;
};

Las partes resaltadas son las modificaciones a realizar.

Ahora volvamos al código de update para hacer una nueva modificación sobre el mismo, recordemos que es llamado por la función OnTrade, y esta es llamada cada vez que se modifican las órdenes, y esto se puede ver en el fragmento de abajo:

void UpdatePosition(void)
{
        static int memPositions = 0, memOrder = 0;
        ulong ul;
        int p, o;
                                
        p = PositionsTotal();
        o = OrdersTotal();
        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;
                m_bContainsPosition = false;
        };
        for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                ul = PositionGetInteger(POSITION_TICKET);
                m_bContainsPosition = true;
                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())
        {
                if (m_bContainsPosition)
                {
                        ModifyOrderPendent(ul, OrderGetDouble(ORDER_PRICE_OPEN), 0, 0);
                        (OrderSelect(ul) ? 0 : 0);
                }
                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);
        }
};

Lo que estamos haciendo ahora es asegurarnos de que el usuario no transforme una orden pendiente simple en una orden pendiente de tipo OCO, y esto cuando ya hay una posición abierta, es decir si el abre la caja de herramientas e intenta editar los valores de take y de stop. Entonces, cuando el usuario intenta hacer esto, el servidor de negociación nos informa a través de la función OnTrade, por lo que el EA sabrá del cambio inmediatamente y deshará el cambio realizado por el usuario, asegurando la fiabilidad del sistema.

Pero hay otro punto que también debemos modificar, y es el de las órdenes de mercado, esta es una modificación muy sencilla de hacer, ya que no es necesario hacer muchas cosas en lo que a pruebas se refiere, por lo que el nuevo código de la función será el que se muestra a continuación:

ulong ExecuteOrderInMarket(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true)
{
        ZeroMemory(TradeRequest);
        ZeroMemory(TradeResult);
        TradeRequest.action             = TRADE_ACTION_DEAL;
        TradeRequest.symbol             = Terminal.GetSymbol();
        TradeRequest.volume             = Volume;
        TradeRequest.type               = (IsBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
        TradeRequest.price              = NormalizeDouble(Price, Terminal.GetDigits());
        TradeRequest.sl                 = NormalizeDouble((m_bContainsPosition ? 0 : Stop), Terminal.GetDigits());
        TradeRequest.tp                 = NormalizeDouble((m_bContainsPosition ? 0 : Take), Terminal.GetDigits());
        TradeRequest.type_time          = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
        TradeRequest.stoplimit          = 0;
        TradeRequest.expiration         = 0;
        TradeRequest.type_filling       = ORDER_FILLING_RETURN;
        TradeRequest.deviation          = 1000;
        TradeRequest.comment            = "[ Order Market ] Generated by Experts Advisor.";
        if (!Send()) return 0;
                                                
        return TradeResult.order;
};

Aunque pueda parecer extraño, estas modificaciones ya garantizan un nivel de seguridad adecuado, al menos aceptable, para que no pasemos una orden OCO, pendiente o de mercado, cuando ya hay una posición abierta en el activo con el que el EA se está ejecutando, de ese modo el EA ya estará de hecho ocupándose del sistema de envío de órdenes.

Bueno, todo es demasiado bonito y maravilloso para ser verdad, ¿no? Puedes pensar que esto ya te garantizará un buen margen de seguridad, pero no es exactamente así, estas modificaciones garantizarán que una orden OCO no se quede pendiente o no entre en el mercado cuando tengamos una posición abierta. Pero hay un fallo mortal en estas modificaciones, y si no se corrige adecuadamente, este fallo puede darte un gran dolor de cabeza y una tremenda pérdida, y puede hacer que tu cuenta quiebre, o que el bróker te cierre la posición por falta de margen.

Fíjate que no hay una comprobación de si la orden pendiente está o no dentro de los límites de la posición abierta, y esto es mortal, porque tal y como está el sistema, cuando añades una orden pendiente OCO fuera de los límites de la posición abierta, el EA no permitirá que esta orden sea del tipo OCO, o sea, la orden no tendrá límites, será una orden sin límites de ganancias o pérdidas, por lo que cuando se cierre la posición y entre esta orden pendiente, tendrás que ajustar lo antes posible sus límites, en caso de que te olvides de hacerlo, corres el riesgo de tener una posición abierta sin límites.

Y para ajustar estos límites tendrás que recurrir al buzón de mensajes, abrir la orden y editar los valores de los límites, pero esto se arreglará pronto, primero vamos a arreglar el problema actual.

Por lo tanto, debemos modificar esta forma de cómo el EA está cuidando las órdenes pendientes, ya que si el usuario quiere crear una orden sin dichos límites, el EA simplemente lo entenderá como algo normal, pero si el usuario llega a crear una orden limitada, el EA deberá ajustar la orden de la forma adecuada, poniendo límites si la orden se coloca fuera de los límites de la posición abierta, o quitando los límites de la orden pendiente si la orden se coloca dentro de los límites de la posición abierta.


3.0.2 - Trabajando dentro de los límites

Lo primero que vamos a hacer es crear límites de verificación, esto es bastante sencillo de hacer, al menos la parte conceptual, sin embargo hay que prestar mucha atención a los detalles, porque hay 2 posibles casos, que se pueden ampliar a más, pero para entenderlo, sólo hay que saber cómo se procede en estos 2.


El primer caso se ve justo arriba, en este caso tenemos una orden pendiente fuera de los límites, el límite en este caso es la región en degradé, o sea, tenemos una posición abierta, que no importa si es de compra o de venta, pero tenemos un punto de límite superior, que al ser alcanzado o ultrapasado por el tiempo hará que la posición se cierre, en este caso la orden pendiente puede haber sido configurada por el usuario como una orden OCO, y el EA debe aceptar la forma en que la orden es configurada por el usuario, ya sea una orden simple o una orden OCO, el EA no debe interferir de ninguna manera en este caso.

Ya el segundo caso se ve a continuación, en este caso la orden pendiente se encuentra dentro del área limitada por la posición abierta, aquí el EA debe eliminar los límites que puedan estar o sean configurados por el usuario.


Obsérvese que no importa hasta donde llegue el límite, si estamos comprando o vendiendo, si la orden pendiente entra en esta región, el EA debe eliminar los valores del límite de la orden pendiente, pero si salimos de esta región, el EA debe dejar la orden tal y como la configure el usuario.

Una vez definido esto, necesitamos crear algunas variables, que se muestran a continuación:

class C_Router : public C_HLineTrade
{
        protected:
                MqlTradeRequest TradeRequest;
                MqlTradeResult  TradeResult;
        private  :
                bool            m_bContainsPosition;
                struct st00
                {
                        double  TakeProfit,
                                StopLoss;
                        bool    IsBuy;
                }m_Limits;

// ... Restante do código

Ahora tenemos una forma de comprobar los límites durante la fase en la que se produce un evento que desencadena la orden OnTrade, así que de nuevo modificamos la función de actualización de la clase C_Router

// Restante do código....

//+------------------------------------------------------------------+
#define macro_MAX(A, B) (A > B ? A : B)
#define macro_MIN(A, B) (A < B ? A : B)
void UpdatePosition(void)
{
        static int      memPositions = 0, memOrder = 0;
        ulong           ul;
        int             p, o;
        double          price;
        bool            bTest;
                                
        p = PositionsTotal();
        o = OrdersTotal();
        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;
                m_bContainsPosition = false;
                m_Limits.StopLoss = -1;
                m_Limits.TakeProfit = -1;
                m_Limits.IsBuy = false;
        };
        for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                ul = PositionGetInteger(POSITION_TICKET);
                m_bContainsPosition = true;
                SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);
                SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true);
                SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true);
                m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
        }
        for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
        {
                price = OrderGetDouble(ORDER_PRICE_OPEN);
                if ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1)) bTest = false; else
                {
                        bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price));
                        bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price));
                        bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price)));
                }
                if ((m_bContainsPosition) && (bTest))
                {
                        ModifyOrderPendent(ul, price, 0, 0);
                        (OrderSelect(ul) ? 0 : 0);
                }
                SetLineOrder(ul, price, HL_PRICE, true);
                SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true);
                SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true);
        }
};
#undef macro_MAX
#undef macro_MIN
//+------------------------------------------------------------------+

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

Ahora la clase tratará las posiciones pendientes de manera que se diferencíen cuando están o no dentro de un área que no puede tener órdenes pendientes del tipo OCO, fíjate que en cada cambio de estado en el sistema de órdenes, esta función será llamada, lo primero que hará, es inicializar las variables de manera adecuada.

m_Limits.StopLoss = -1;
m_Limits.TakeProfit = -1;
m_Limits.IsBuy = false;

Una vez hecho esto, comprobaremos si hay o no una posición abierta, y esto se puede hacer en cualquier momento. Desde el momento en que tengamos una posición abierta, esta delimitará la región en la que no podemos tener órdenes pendientes del tipo OCO, y esto se consigue en este punto

SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true);
SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true);
m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;

Ahora podemos probar cada una de las órdenes pendientes para comprobar si están o no dentro de la región delimitada. Un detalle, aquí tenemos que saber si la orden es de compra o de venta, ya que puede ser que no tengamos una Take, pero sí un stop, y en este caso necesitamos saber de qué va la posición, para entenderlo ve las imágenes de abajo:

 

Si «apostamos» a vender, el stop marca el techo...

 

Si «apostamos» a comprar, el stop marca el piso...

Es decir, en un caso las órdenes pendientes pueden ser del tipo OCO si están por encima de un máximo establecido, en otros casos deben estar por debajo de un mínimo establecido. Pero también puede ocurrir otro caso en el que las órdenes pendientes también pueden ser de tipo OCO, esto se muestra a continuación:

 

Órdenes pendientes fuera de los límites... caso típico...

Así que para comprobarlo utilizamos el siguiente fragmento

price = OrderGetDouble(ORDER_PRICE_OPEN);
if ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1)) bTest = false; else
{
        bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price));
        bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price));
        bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price)));
}

Esto probará si hay alguna posición abierta, si no, el EA deberá respetar la configuración de la orden hecha por el usuario, en caso de que haya alguna posición, probaremos las cosas en el siguiente orden:

  1. Si estamos vendiendo, el precio al que se posicionará la orden pendiente debe ser mayor que el valor del stop de la posición abierta;
  2. Si estamos comprando, el precio al que se posicionará la orden pendiente debe ser inferior al valor del stop de la posición abierta;
  3. En caso de que el sistema siga aceptando la orden como una orden de tipo OCO, hacemos una última prueba que será verificar si la orden está fuera de los límites de la posición

Hecho esto, tendremos una seguridad de que la orden pendiente se puede dejar o no de la forma en que el usuario la configuró, y la vida sigue... pero hay una última adición antes de pasar al siguiente paso, en realidad una última prueba, que mencioné al principio del artículo y que está en el fragmento de abajo:

void UpdatePosition(void)
{

// ... Código interno ...

        for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                ul = PositionGetInteger(POSITION_TICKET);
                m_bContainsPosition = true;
                SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);
                SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true);
                SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true);
                m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
        }
        if (AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
                m_bContainsPosition = false;
        for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
        {
                price = OrderGetDouble(ORDER_PRICE_OPEN);
                if (m_bContainsPosition)
                {
                        if ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1)) bTest = false; else
                        {
                                bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price));
                                bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price));
                                bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price)));
                        }
                        if (bTest)
                        {
                                ModifyOrderPendent(ul, price, 0, 0);
                                (OrderSelect(ul) ? 0 : 0);
                        }
                }
                SetLineOrder(ul, price, HL_PRICE, true);
                SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true);
                SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true);
        }
};

En la parte resaltada se verificará si el tipo de cuenta es de HEDGING, y si es así aunque la variable haya indicado que tendremos que trabajar con límites, en este punto nos indicará que no será necesario trabajar con límites, por lo que el EA ignorará cualquier tipo de limitación que pudiera darse, y aceptará las órdenes tal y como fueron configuradas por el usuario, es una prueba muy sencilla, pero es necesario que se haga en este punto para garantizar que tendremos un correcto funcionamiento de todo el sistema.


3.1.0 - Ajuste del sistema de posicionamiento

Aunque gran parte de los problemas se han resuelto haciendo ajustes a la clase de objeto C_Router, todavía no tenemos un sistema adecuado, tenemos otro problema que resolver, y es el de corregir el sistema de posicionamiento del ratón, este es otro paso igualmente importante, pero que tiene varias implicaciones, ya que tenemos que definir cómo debe funcionar el sistema presente en la clase C_OrderView.

La gran pregunta, y esto dependerá de cómo se quiera operar realmente, es si la clase C_OrderView creará o no límites para las órdenes pendientes cuando se salgan de los límites de una posición abierta.

Aunque es tentador hacer esto siempre, hay cosas que se deben tener en cuenta a la hora de tomar esta decisión, pero «vamos por partes», como diría Jack el destripador, básicamente el único cambio real que tendremos que hacer a la clase C_OrderView se muestra en el código de abajo:


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;    //Clique esquerdo
        bKeyBuy  = (Key & 0x04) == 0x04;    //SHIFT Pressionada
        bKeySell = (Key & 0x08) == 0x08;    //CTRL Pressionada                          
        Mouse.GetPositionDP(dt, price);
        if (bKeyBuy != bKeySell)
        {
                Mouse.Hide();
                bCheck = CheckLimits(price);
        } else Mouse.Show();
        ObjectMove(Terminal.Get_ID(), m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? price : 0));
        ObjectMove(Terminal.Get_ID(), m_Infos.szHLineTake, 0, 0, take = (bCheck ? 0 : price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1))));
        ObjectMove(Terminal.Get_ID(), m_Infos.szHLineStop, 0, 0, stop = (bCheck ? 0 : price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1))));
        if((bEClick) && (bKeyBuy != bKeySell) && (local == 0)) CreateOrderPendent(bKeyBuy, m_Infos.Volume, local = price, take, stop, m_Infos.IsDayTrade);
        local = (local != price ? 0 : local);                           
        ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE));
        ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE));
        ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE));
};

¡¿Pero eso es todo?! Sí, eso es todo lo que tenemos que hacer, todo el resto de la lógica está dentro de la clase C_Router, y lo que no se ha modificado lo ejecuta el propio sistema de mensajería de MetaTrader 5, ya que cuando se produce un cambio en la lista de órdenes, ya sean las pendientes o las posiciones, se llama a la rutina OnTrade, y cuando esto ocurra se disparará la rutina de actualización desde dentro de la clase C_Router que hará los ajustes necesarios. Pero hay un código que aparece en esta rutina y que quizás te vuelvas loco buscando dónde está, en realidad está dentro de la clase C_Router, se ve justo debajo:

#define macro_MAX(A, B) (A > B ? A : B)
#define macro_MIN(A, B) (A < B ? A : B)
inline bool CheckLimits(const double price)
{
        bool bTest = false;
                                
        if ((!m_bContainsPosition) || ((m_Limits.StopLoss == -1) && (m_Limits.TakeProfit == -1))) return bTest;
        bTest = ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price));
        if (m_Limits.TakeProfit == 0)
        {
                bTest = (bTest ? bTest : (!m_Limits.IsBuy) && (m_Limits.StopLoss > price));
                bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price));
        }
        return bTest;
};
#undef macro_MAX
#undef macro_MIN

Este código es exactamente lo que había dentro de la rutina update de la clase C_Router, él se eliminó de allí, y en su lugar se añadió una llamada al mismo...


3.2.0 - Limitar o no limitar, he ahí el dilema

Nuestro trabajo está casi terminado, pero hay una última cuestión que decidir, y es quizás la más complicada en este momento. Si has seguido y entendido el contenido hasta este punto, te habrás dado cuenta de que el sistema funciona muy bien, pero cada vez que una orden pendiente que está configurada como orden OCO entra en los límites de una posición abierta, la orden pierde los límites que estaban configurados en ella. Esto siempre ocurrirá.

Pero si por casualidad el operador cambia los límites de la posición abierta, o si se saca de esos límites la orden que era OCO, que ahora es una orden simple, ella seguirá siendo una orden simple, por lo que tenemos un problema potencial en ese punto.

El gran detalle es: ¡¿Cómo debe proceder el EA?! Debería notificar al operador que acaba de aparecer una simple orden en el activo... ¿O simplemente debería poner los límites de la orden y convertirla así en una orden OCO?

Esta pregunta es algo de extrema relevancia si realmente quieres que el EA te ayude en las operaciones. Efectivamente es prudente que el EA emita un aviso informando de lo sucedido, pero si estás en un activo, en un momento de gran volatilidad también es prudente hacer que el EA cree automáticamente algún tipo de límite para la orden, y que no se quede ahí suelto, y pueda darnos grandes pérdidas hasta que nos demos cuenta de lo que está pasando.

Así que para resolver este problema, el sistema pasó por una última modificación, pero como expliqué anteriormente, deberías pensar seriamente en cómo abordar realmente este problema. Ahora veamos cómo he implementado una posible solución.

Lo primero que se ha hecho es añadir una nueva variable para que el operador le diga a EA cuál será el procedimiento, esto se puede ver a continuación:

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

input group "Chart Trader"
input int       user20   = 1;              //Fator de alavancagem
input int       user21   = 100;            //Take Profit ( FINANCEIRO )
input int       user22   = 75;             //Stop Loss ( FINANCEIRO )
input color     user23   = clrBlue;        //Cor da linha de Preço
input color     user24   = clrForestGreen; //Cor da linha Take Profit
input color     user25   = clrFireBrick;   //Cor da linha Stop
input bool      user26   = true;           //Day Trade ?
input bool      user27   = true;           //Sempre criar limites de ordens soltas

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

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

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

Ahora volvamos a la clase C_Router y añadamos 3 nuevas rutinas a la misma. Estas pueden verse a continuación:

//+------------------------------------------------------------------+
void SetFinance(const int Contracts, const int Take, const int Stop)
{
        m_Limits.Contract = Contracts;
        m_Limits.FinanceTake = Take;
        m_Limits.FinanceStop = Stop;
}
//+------------------------------------------------------------------+
inline double GetDisplacementTake(const bool IsBuy, const double Vol) const
{
        return (Terminal.AdjustPrice(m_Limits.FinanceTake * (Vol / m_Limits.Contract) * Terminal.GetAdjustToTrade() / Vol) * (IsBuy ? 1 : -1));
}
//+------------------------------------------------------------------+
inline double GetDisplacementStop(const bool IsBuy, const double Vol) const
{
        return (Terminal.AdjustPrice(m_Limits.FinanceStop * (Vol / m_Limits.Contract) * Terminal.GetAdjustToTrade() / Vol) * (IsBuy ? -1 : 1));
}
//+------------------------------------------------------------------+

Estas rutinas mantendrán los valores que se informan en el Chart Trader, como se puede ver en la imagen inferior, pero también corregirán de forma proporcional el valor que debemos utilizar como límites en las órdenes pendientes del tipo OCO.

Es decir, ya tenemos de dónde sacar los valores que utilizaremos para que EA pueda configurar mínimamente una orden OCO cuando quede suelta una orden pendiente, pero como estarás sospechando, tendremos que hacer un nuevo cambio en el código update de la clase C_Router, y estos cambios los puedes ver a continuación:

void UpdatePosition(bool bAdjust)
{
        static int      memPositions = 0, memOrder = 0;
        ulong           ul;
        int             p, o;
        long            info;
        double          price, stop, take, vol;
        bool            bIsBuy, bTest;
                        
        p = PositionsTotal();
        o = OrdersTotal();
        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;
                m_bContainsPosition = false;
                m_Limits.StopLoss = -1;
                m_Limits.TakeProfit = -1;
                m_Limits.IsBuy = false;
        };
        for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                ul = PositionGetInteger(POSITION_TICKET);
                m_bContainsPosition = true;
                SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);
                SetLineOrder(ul, take = PositionGetDouble(POSITION_TP), HL_TAKE, true);
                SetLineOrder(ul, stop = PositionGetDouble(POSITION_SL), HL_STOP, true);
                m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
                m_Limits.TakeProfit = (m_Limits.TakeProfit < 0 ? take : (m_Limits.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 : (m_Limits.IsBuy ? (m_Limits.StopLoss < stop ? m_Limits.StopLoss : stop) : (stop < m_Limits.StopLoss ? m_Limits.StopLoss : stop)));
        }
        if ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
                m_bContainsPosition = false;
        for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
        {
                price = OrderGetDouble(ORDER_PRICE_OPEN);
                take = OrderGetDouble(ORDER_TP);
                stop = OrderGetDouble(ORDER_SL);
		bTest = CheckLimits(price);
                if ((take == 0) && (stop == 0) && (bAdjust) && (!bTest))
                {
                        info = OrderGetInteger(ORDER_TYPE);
                        vol = OrderGetDouble(ORDER_VOLUME_CURRENT);
                        bIsBuy = ((info == ORDER_TYPE_BUY_LIMIT) || (info == ORDER_TYPE_BUY_STOP) || (info == ORDER_TYPE_BUY_STOP_LIMIT) || (info == ORDER_TYPE_BUY));
                        take = price + GetDisplacementTake(bIsBuy, vol);
                        stop = price + GetDisplacementStop(bIsBuy, vol);
                        ModifyOrderPendent(ul, price, take, stop);
                }
                if ((take != 0) && (stop != 0) && (bTest))
                        ModifyOrderPendent(ul, price, take = 0, stop = 0);
                SetLineOrder(ul, price, HL_PRICE, true);
                SetLineOrder(ul, take, HL_TAKE, true);
                SetLineOrder(ul, stop, HL_STOP, true);
        }
};

Todas las líneas resaltadas probarán si la orden está suelta, y si el EA debe o no interferir en esta, si el EA debe interferir, se hará un cálculo sobre el valor financiero informado en el Chart Trader y sobre la base del volumen que hay en la orden pendiente. Después de esto, la orden simple recibirá los límites calculados con base en la información recopilada, y se crearán las líneas que informan de las posiciones límite de la orden haciendo así una orden pendiente simple en una orden pendiente OCO.


Conclusión

A pesar de todos los esfuerzos por probar el sistema para verificar si estaba o no reconociendo la cuenta como una de tipo HEGDE, no pude ser efectivo en este punto, el EA siempre informaba que la cuenta estaba en modo NETTING, aunque la plataforma MetaTrader 5 informaba si estaba en una cuenta HEDGE... así que debo tener cuidado porque aunque está funcionando como se desea, incluso en una cuenta HEDGE las órdenes pendientes se están ajustando como si la cuenta fuera una cuenta NETTING ...

En el video se ve claramente lo que acabo de describir, pero se puede notar que el uso del sistema es muy interesante.



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

Archivos adjuntos |
Desarrollando un EA comercial desde cero (Parte 19): Un nuevo sistema de órdenes (II) Desarrollando un EA comercial desde cero (Parte 19): Un nuevo sistema de órdenes (II)
Aquí vamos a desarrollar un sistema gráfico de órdenes, del tipo «vea lo que está pasando». Cabe decir que 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.
Desarrollando un EA comercial desde cero (Parte 17): Acceso a los datos en la web (III) Desarrollando un EA comercial desde cero (Parte 17): Acceso a los datos en la web (III)
En este artículo continuaremos a aprender cómo obtener datos de la web para utilizarlos en un EA. Así que pongamos manos a la obra, o más bien a empezar a codificar un sistema alternativo.
Redes neuronales: así de sencillo (Parte 14): Clusterización de datos Redes neuronales: así de sencillo (Parte 14): Clusterización de datos
Lo confieso: ha pasado más de un año desde que publiqué el último artículo. En tanto tiempo, me ha sido posible repensar mucho, desarrollar nuevos enfoques. Y en este nuevo artículo, me gustaría alejarme un poco del método anteriormente usado de aprendizaje supervisado, y sugerir una pequeña inmersión en los algoritmos de aprendizaje no supervisado. En particular, vamos a analizar uno de los algoritmos de clusterización, las k-medias.
Desarrollando un EA de trading desde cero (Parte 16): Acceso a los datos en la Web (II) Desarrollando un EA de trading desde cero (Parte 16): Acceso a los datos en la Web (II)
Saber cómo introducir los datos de la Web en un EA no es tan obvio, o mejor dicho, no es tan simple que puede hacerse sin conocer y entender realmente todas las características que están presentes en MetaTrader 5.