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

Cómo construir un EA que opere automáticamente (Parte 03): Nuevas funciones

MetaTrader 5Trading | 24 enero 2023, 09:28
962 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, Cómo construir un EA que opere automáticamente (Parte 02): Inicio de la codificación, comenzamos a desarrollar el sistema de órdenes para ser utilizado en el EA automático. Sin embargo, solo construimos una de las funciones o procedimientos necesarios.

Normalmente, un EA automático requiere varias cosas adicionales dentro del sistema de órdenes para ser completamente construido. Además, hay personas que prefieren utilizar varios EAs operando con diferentes configuraciones pero con el mismo activo.

Para cuentas de tipo NETTING, no se recomienda hacer esto. La razón es que el servidor de negociación crea el llamado precio medio de posición. Esto puede causar una situación de concurrencia, donde un EA está tratando de vender y otro está tratando de comprar. Esto hará que ambos EAs se disparen, causando la destrucción de su patrimonio en poco tiempo. Sin embargo, esta situación de concurrencia no ocurrirá si la cuenta es de tipo HEDGING. En este caso, un EA puede estar vendiendo mientras que otro puede estar comprando sin que una orden cancele a la otra.

Por esta razón, algunas personas incluso operan el mismo activo que el EA, pero para evitar la situación de concurrencia, es necesario utilizar una cuenta HEDGING. En caso de duda, nunca debes colocar dos EAs operando el mismo activo (al menos en la misma corredora) y tampoco debes realizar operaciones en el activo mientras el EA automático esté funcionando.

Con este aviso dado, podemos agregar las demás funciones necesarias en un EA automático, lo cual cubrirá más del 90% de los casos.


¿Por qué necesitamos nuevos procedimientos?

Normalmente, cuando un EA automático realiza una operación, él en gran medida de los casos entrará y saldrá del mercado, es decir, raramente un EA automático colocará una orden en el libro de órdenes. Sin embargo, hay situaciones en las que colocar una orden en el libro se vuelve necesario y el procedimiento para hacerlo es uno de los más complejos. Por esta razón, el artículo anterior se dedicó solo a implementar este procedimiento. Pero solo ese procedimiento no es suficiente para un EA automático. Como se mencionó anteriormente, en gran parte de las veces él entrará y saldrá del mercado. Por lo tanto, necesitamos al menos dos procedimientos adicionales:

  • Uno para enviar órdenes de compra o venta a precio de mercado;
  • Uno para poder cambiar el precio de una orden.

Sólo estos dos procedimientos, junto con el procedimiento visto en el artículo anterior, son los que realmente necesitarás en un EA automático. Ahora, entendamos por qué solo necesitaremos agregar estos dos y no más. Bien, cuando un EA automático abre una posición, ya sea de venta o compra, a menudo lo hace a precio de mercado con un volumen predefinido por el operador.

Y cuando necesita cerrar la posición, puede hacerlo de dos maneras: la primera es a precio de mercado, realizando una operación en la dirección opuesta y con el mismo volumen que esté abierto, lo que cerrará la posición. Es cierto que este tipo de operación es efectivo en el caso de una cuenta de tipo NETTING. Para una cuenta de tipo HEDGING, este procedimiento no cerrará la posición de hecho. En este caso, necesitamos una operación y un requerimiento de cierre específico.

A pesar de que, al abrir una operación en la dirección opuesta, con el mismo volumen, en una cuenta HEDGING, te permitirá bloquear el precio; esto no indicaría de hecho que la posición ha sido cerrada. Solo el precio estaría bloqueado, sin generar ganancias o pérdidas. Por esta razón, agregaremos una forma para que el EA cierre la posición, necesitando implementar un tercer procedimiento. Pero recuerda: para una cuenta de tipo NETTING, puedes simplemente enviar una orden de mercado, con el mismo volumen, pero en la dirección opuesta y la posición se cerrará.

Entendamos el hecho de necesitar tener un procedimiento para modificar el precio de la orden. Hay algunos modelos operativos en los que el EA funciona de la siguiente manera: él abre una posición de mercado y de inmediato envía una orden pendiente como STOP LOSS. Esta orden irá al libro de ofertas y permanecerá allí todo el tiempo hasta que la posición sea cerrada. En este caso, el EA no enviará realmente una orden de cierre, ya sea en la dirección opuesta o cualquier otro tipo de requerimiento, simplemente administrará la orden presente en el libro de forma tal que tenga una orden de cierre siempre activa.

Esto funciona muy bien para cuentas de tipo NETTING, pero para cuentas de tipo HEDGING, este sistema no funcionará como se espera, ya que en este caso, la orden en el libro simplemente haría lo que expliqué anteriormente sobre la forma de cerrar una operación. Pero volviendo al punto, este mismo procedimiento que administrará la orden presente en el libro también sirve para mover los puntos de take profit y stop loss, y esto en una orden de tipo OCO (orden cancela orden).

Normalmente, un EA automático no utiliza órdenes de tipo OCO, simplemente trabaja con una orden de mercado y una orden que permanecerá en el libro, pero para el caso de cuentas HEDGING, este mecanismo puede ser hecho utilizando la orden OCO, solo se configuraría el punto de stop loss, o si el programador desea, puede simplemente entrar al mercado y mantener al EA observando el mercado de alguna forma, y tan pronto como se alcanza un cierto punto o nivel de precio, el EA envía una orden de cierre.

Todo esto que se explicó anteriormente es sólo para mostrar que existe más de una manera de hacer el mismo tipo de cosa, en este caso, de CERRAR UNA POSICIÓN. La apertura de la posición es la parte fácil del proceso, porque durante el cierre ya hay que tener en cuenta:

  • Posibles momentos de alta volatilidad en que las órdenes pueden tener movimientos bruscos (en el caso de las OCO), mientras que las órdenes del libro de ofertas, salvo las de tipo STOP LIMIT, pueden dispararse fuera del punto deseado pero NUNCA «saltarán».
  • Problemas de conexión, en los que el EA puede pasar un tiempo sin poder acceder al servidor;
  • Problemas de liquidez, en los que una orden puede estar ahí, esperando a ser ejecutada, pero el volumen no es suficiente para que realmente se ejecute;
  • Condición de concurrencia, en la que el EA se dispara, comenzando a ejecutar órdenes al azar.

Todos estos puntos deben ser considerados y observados al crear un EA automático. Existen otros puntos, como el hecho de que algunos programadores agreguen horarios para que el EA realmente pueda operar. En cuanto a esto, soy bastante sincero e honesto. Esto es completamente INÚTIL y una completa BURRADA. A pesar de que se muestra en otro artículo cómo hacerlo, no es algo que recomiende, y voy a explicar el motivo.

Piensa en lo siguiente: no sabes operar en el mercado, arreglas un EA para hacerlo de forma automática, colocas un horario para que él opere. Estupendo, ahora piensas: puedo ir a hacer otra cosa, tratar de otras actividades... [sonido de bocina]... MAL. NUNCA, y voy a repetirlo, NUNCA debes dejar un EA, incluso automático, operando SIN SUPERVISIÓN. NUNCA. Mientras esté encendido, tú, o alguien de tu confianza, deben estar allí con él, observando lo que hace.

Dejar el EA encendido sin supervisión es darle la bienvenida a problemas, por lo que agregar métodos o gatillos de disparo por horario, en los que el EA comenzará y finalizará sus actividades, es el mayor signo de burrada que alguien puede desear poner en un EA automático. Por favor, no lo hagas. Si quieres que el EA opere para ti, está bien, enciéndelo y quédate allí observando, cuando tengas que salir, apágalo y ve a hacer lo que necesitas hacer, pero no lo dejes encendido operando DIOS sabrá cómo. No lo hagas, ya que los resultados pueden lastimarte mucho.

Implementación de las funciones necesarias 

Dado que el procedimiento para ejecutar una orden de mercado se parece mucho al utilizado para enviar una orden pendiente, se hace posible crear un procedimiento común, de manera que se completen todos los campos que tendrán el mismo tipo de llenado, quedando solo los puntos pendientes de cada tipo de requerimiento, para ser llenados localmente. Entonces veamos cómo quedó esta función de llenado común, se puede ver a continuación:

inline void CommonData(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double Desloc;
                                
                                ZeroMemory(m_TradeRequest);
				m_TradeRequest.magic		= m_Infos.MagicNumber;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.stoplimit        = 0;
                                m_TradeRequest.expiration       = 0;
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                        }

Observen que todo esto, que es parte del procedimiento común, estaba presente en la función de creación de una orden pendiente, vista en el artículo anterior, y, por lo tanto, está ahora aquí, pero también he añadido un pequeño extra, que no existía antes, mas puede ser bastante útil, si estás trabajando con una cuenta HEDGING, o planeas crear un EA automático, que solo observará las órdenes que él mismo creó, el llamado número mágico. Normalmente no hago uso de este número, pero si lo vas a hacer, ya tendrás el sistema implementado para soportarlo.

Con esto, veamos cómo quedó el nuevo procedimiento encargado de enviar una orden pendiente, este procedimiento se ve en el código a continuación:

                ulong CreateOrder(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double  bid, ask, Desloc;                               
                                
                                Price = AdjustPrice(Price);
                                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                    (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));                              
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action           = TRADE_ACTION_PENDING;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.type             = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                            (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                                
                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

Todas las partes tachadas anteriormente, fueron retiradas del código, ya que estos campos están siendo llenados por esta función común, todo lo que realmente necesitamos hacer es ajustar estos dos valores y el sistema de órdenes seguirá creando una orden pendiente, como se vio en el artículo anterior.

Bueno, entonces veamos lo que realmente necesitamos programar para tener un sistema de órdenes capaz de enviar requerimientos de ejecución a precio de mercado. El código necesario se ve a continuación:

                ulong ToMarket(const ENUM_ORDER_TYPE type, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                CommonData(type, SymbolInfoDouble(_Symbol, (type == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID)), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_DEAL;
                                m_TradeRequest.type     = type;

                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

Observen lo sencillo que es codificar una orden de mercado, todo lo que necesitamos cambiar en comparación con una orden pendiente son estos dos puntos. De esta manera, aseguramos que el servidor siempre recibirá datos compatibles, ya que el único cambio será en el tipo de solicitud.

La forma de trabajar, tanto analizando la respuesta como la forma de llamar a los procedimientos para colocar una orden pendiente o ejecutar una operación al mercado, es prácticamente igual, la única diferencia real para quien llama a los procedimientos es que, cuando se ejecuta una orden de mercado, no necesitará informar el precio, ya que la clase llenará este valor de forma correcta, mientras que, en una orden pendiente, deberá informar el valor. Además de esto, todo el trabajo será igual.

Ahora veamos algo que ha cambiado en el sistema. Ya que se ha agregado un valor para ser utilizado como número mágico, necesitamos hacer que la clase reciba este valor y así pueda utilizarlo. Esto es y debe ser hecho en el constructor de la clase, de esta manera, ahora nuestra clase tendrá que recibir un parámetro en su llamada. Vé cómo quedó el constructor en el código a continuación:

                C_Orders(const ulong magic = 0)
                        {
                                m_Infos.MagicNumber     = magic;
                                m_Infos.nDigits         = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_Infos.VolMinimal      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_Infos.VolStep         = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_Infos.PointPerTick    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_Infos.ValuePerPoint   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_Infos.AdjustToTrade   = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
                                m_Infos.PlotLast        = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);
                        };

Entendamos lo que está sucediendo en este código anterior. Cuando declaramos un valor por defecto como se está haciendo aquí, no necesitamos informarlo en el momento en que la clase sea creada, lo que hace que este constructor sea como si fuera un constructor por defecto (aquellos que no reciben ningún tipo de argumento).

Pero si deseas forzar al usuario de la clase, es decir, al programador, a informar qué valores deben usarse durante la fase de creación de la clase, deberás eliminar el valor del parámetro. Así, cuando el compilador intente generar el código, notará que falta algo y pedirá que indiques qué valores deben utilizarse. Sin embargo, esto solo funcionará si la clase no tiene otro constructor y este es un constructor predeterminado. Esto es un pequeño detalle que deja a muchas personas sin entender por qué a veces tienen que informar algunos valores y en otros casos no.

Como puedes ver, la programación puede ser muy interesante. En muchos casos, lo que realmente hacemos es intentar crear una solución utilizando el menor esfuerzo, lo que reduce la cantidad de cosas que deben ser programadas y probadas. Sin embargo, no terminamos nuestro trabajo en la clase C_Orders, recuerda que todavía necesitamos crear otra función, necesariamente, y una que puede ser opcional, pero aún así será creada debido al hecho de que, cuando se opera en una cuenta HEDGING, las cosas deben ser hechas de manera diferente a una cuenta NETTING. Entonces vamos al siguiente procedimiento, y este puede verse a continuación:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action   = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order    = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

El procedimiento anterior es extremadamente importante, incluso más importante que el próximo que veremos. La razón es que este procedimiento es responsable de manipular las posiciones de precio, ya sea las que están en el libro de oferta, en el caso de una orden pendiente, o los límites de una posición abierta. La función anterior es tan poderosa que puede crear o eliminar los límites de una orden o posición. Pero para que no te quedes atrás sin entender cómo funciona, vamos a desglosar su código interno, de esta manera entenderás cómo trabajar y utilizar esta función de manera correcta.

Para facilitar la explicación y, por lo tanto, la comprensión, vamos a dividir las cosas en partes, así que presta mucha atención para no perderte en la explicación.

Empecemos entendiendo lo siguiente: Cuando envías un pedido, ya sea para colocar una orden pendiente en el libro o para enviar una orden de mercado, recibirás un valor de retorno y si no ha ocurrido ningún tipo de error en la solicitud, este valor será diferente de cero. Sin embargo, este valor no es algo que debas ignorar, el valor devuelto por las funciones de creación de la orden o posición debe ser almacenado con bastante cariño y cuidado, ya que representa el ticket de la orden o posición.

Este ticket, que es el billete que sirve como una especie de pase, te dará diversas posibilidades, entre ellas la de poder manipular las órdenes o posiciones que están en el servidor de mercado. Entonces, ese valor que obtienes al ejecutar una operación a precio de mercado, o al intentar colocar una orden en el libro, y es devuelto por las funciones que realizan ese procedimiento, sirve, en realidad, cuando es diferente de cero, como un pasaporte para que el EA pueda comunicarse con el servidor para poder trabajar o manipular los precios utilizando justamente ese valor, ese billete (ticket).

Cada orden o posición tiene un billete único, así que cuida de este número y no intentes crearlo al azar, hay formas de obtenerlo si no sabes cuál es su valor o lo has perdido. Pero de una manera u otra, esto tomará tiempo del EA, por esa razón, debes evitar perder este número.

Bueno, primero supongamos que tenemos una orden y queremos eliminar o modificar los valores de límite (take profit o stop loss) de la orden. No confundas orden con posición. Cuando digo orden, me refiero a una posible y futura posición, normalmente las órdenes se encuentran en el libro, mientras que la posición es cuando de hecho se ha ejecutado una orden.. En este caso, deberás informar el ticket de la orden y los nuevos valores de precio, presta atención a esto, ahora son valores de precio, ya no informarás el valor financiero (el valor monetario relacionado con las operaciones de compra y venta). El valor esperado ahora es el valor de «cara» o el valor que ve en el gráfico, por eso no debes trabajar de cualquier manera en esta función, de lo contrario, tu solicitud será rechazada.

Entendido esto, puedes colocar virtualmente cualquier valor de take profit y stop loss en una orden, pero en realidad, esto no es del todo correcto. Si tienes una orden de compra, el valor de stop loss no puede ser mayor que el precio de apertura de la posición y el valor de take profit, de la misma manera, no puede ser menor que el precio de apertura de la posición. Si intentas hacer esto, el servidor devolverá un error.

Ahora presta mucha atención a esto: el valor de take profit y stop loss en una orden debe seguir estos criterios, pero en el caso de una posición el valor de stop loss, en el caso de una posición comprada, puede ser mayor que el precio de apertura de la posición, y en este caso el stop loss se convierte en un stop gain, es decir, ya tendrás algún beneficio si se activa la orden de stop loss. Pero hablaremos más sobre esto más adelante. Por ahora, entiende que, en el caso de una orden, el stop loss debe estar en el lado opuesto al que esperas, es decir, en la compra, el stop loss debe estar debajo del precio de apertura, en la venta, debe estar por encima. En el caso de una posición, el stop loss puede estar en cualquier lugar.

La explicación anterior solo cubre los límites de la orden o posición, pero si observas la función anterior, verás que también puedes manipular el precio de apertura de una posición mientras aún es una orden. Ahora viene un detalle, cuando lo hagas, debes moverlo junto con la orden de stop loss y la orden de take profit, si no lo haces, en algún momento, el servidor negará tu solicitud de cambio de precio de apertura.

Para entender realmente esta cuestión, vamos a crear un pequeño programa EA para probar estos casos. Crea un nuevo archivo EA y luego copia y pega el siguiente código en ese archivo abierto. Una vez hecho esto, compila el código y ponlo en un gráfico, y vamos a la explicación:

#property copyright "Daniel Jose"
#property description "This one is an automatic Expert Advisor"
#property description "for demonstration. To understand how to"
#property description "develop yours in order to use a particular"
#property description "operational, see the articles where there"
#property description "is an explanation of how to proceed."
#property version   "1.03"
#property link      "https://www.mql5.com/pt/articles/11226"
//+------------------------------------------------------------------+
#include <Generic Auto Trader\C_Orders.mqh>
//+------------------------------------------------------------------+
C_Orders *orders;
//+------------------------------------------------------------------+
input int       user01   = 1;           //Fator de alavancagem
input int       user02   = 100;         //Take Profit ( FINANCEIRO )
input int       user03   = 75;          //Stop Loss ( FINANCEIRO )
input bool      user04   = true;        //Day Trade ?
input double    user05   = 84.00;       //Preço de entrada...
//+------------------------------------------------------------------+
int OnInit()
{
        orders = new C_Orders(1234456789);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        delete orders;
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
#define KEY_UP                  38
#define KEY_DOWN                40
#define KEY_NUM_1               97
#define KEY_NUM_2               98
#define KEY_NUM_3               99
#define KEY_NUM_7               103
#define KEY_NUM_8               104
#define KEY_NUM_9               105

        static ulong sticket = 0;
        int key = (int)lparam;
        
        switch (id)
        {
                case CHARTEVENT_KEYDOWN:
                        switch (key)
                        {
                                case KEY_UP:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        break;
                                case KEY_DOWN:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        break;
                                case KEY_NUM_1:
                                case KEY_NUM_7:
                                        if (sticket > 0) ModifyStop(key == KEY_NUM_7, sticket);
                                        break;
                                case KEY_NUM_2:
                                case KEY_NUM_8:
                                        if (sticket > 0) ModifyPrice(key == KEY_NUM_8, sticket);
                                        break;
                                case KEY_NUM_3:
                                case KEY_NUM_9:
                                        if (sticket > 0) ModifyTake(key == KEY_NUM_9, sticket);
                                        break;
                        }
                        break;
        }
}
//+------------------------------------------------------------------+
void ModifyPrice(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        s = OrderGetDouble(ORDER_SL);
        t = OrderGetDouble(ORDER_TP);
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+
void ModifyTake(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN);
        s = OrderGetDouble(ORDER_SL);
        t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+
void ModifyStop(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN);
        s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        t = OrderGetDouble(ORDER_TP);
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+

No te preocupes si este código te parece complicado a primera vista, solo es para mostrar una cosa que puede suceder y necesitas entender para poder usarlo a tu favor en el futuro.

Este código en sí, se parece mucho al visto en el artículo anterior, pero aquí podemos hacer algo más, podemos manipular las órdenes modificando el valor de take profit, stop loss y punto de apertura. La única desventaja, todavía presente en el código, es que una vez que hayas colocado una orden en el gráfico no sirve simplemente eliminarla para poder colocar otra, puedes dejar la orden en el gráfico, esto no importa, pero al usar este EA para crear una orden pendiente (y de momento solo funciona para ordenes pendientes), podrás cambiar el punto de precio que indica dónde estará la orden usando el teclado numérico, ese en la esquina derecha del teclado físico.

Esto se hace usando este manejador de eventos. Ten en cuenta que cada una de las teclas sirve para una cosa, como subir el stop loss o bajar el precio de la orden, haz esto, mientras observas, en la pestaña de comercio de la caja de herramientas, el resultado de la manipulación, haciendo esto, aprenderás mucho.

Aquellos que tal vez no se sientan totalmente confiados en ejecutar este código en la plataforma (a pesar de que él sea inofensivo, excepto el hecho de que seas totalmente imprudente) pueden ver en el video de abajo, que es la demostración de lo que este código hace en la práctica.



Demostración del código anterior.

Lo que deberías haber notado es que cuando movemos el stop loss o el take profit, tenemos un movimiento adecuado e incluso esperado, pero si movemos el precio de apertura, tanto el take profit como el stop loss se quedan parados... ¿pero por qué sucede esto? La razón es que, para el servidor de negociación, lo que realmente estás haciendo es moviendo una orden que posiblemente sea el stop de otra operación abierta.

Recuerda que, al principio de este artículo, se dijo que una de las formas de parar una operación abierta sería colocar una orden en el libro y moverla poco a poco. Bueno, es precisamente eso lo que estaría sucediendo aquí. Es decir, para el servidor, el precio a ser movido es solo aquel que se informa. No ve la orden OCO como una entidad única. Para él, la orden OCO sería como si fueran puntos de precio diferentes. Pero una vez que se alcance uno de los límites, el servidor enviará un evento que destruirá el precio donde la posición está abierta. Al hacer esto, ambas órdenes, take profit y stop loss, dejarán de existir ya que el billete al que están vinculadas será eliminado del sistema, quedando disponible para un uso futuro.

Entonces necesitarás hacer que, en caso de que exista una orden OCO generada por el EA, al mover el precio de apertura, tanto el take profit como el stop loss se muevan juntos. Para ello, bastaría hacer la siguiente modificación en el código anterior:

void ModifyPrice(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

Ahora, al agregar estos puntos en el código, tan pronto como el precio de apertura se mueva, tanto el precio de take profit como el de stop loss también se moverán, siempre manteniendo la misma distancia del punto de apertura.

La cosa funciona de la misma forma en el caso de las posiciones, pero en este caso, obviamente, no podemos mover el precio de apertura de la posición. Ahora bien, para mover el take profit y el stop loss, utilizamos el mismo método y la misma función, pero en este caso, solo el requerimiento será diferente, como se muestra en el fragmento a continuación:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
// ... Código para movimentar ordens ...
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

Es decir, es de esta forma, cómo logras hacer el llamado breakeven y trailing stop utilizando precisamente el fragmento mostrado anteriormente. La única cosa que se necesita hacer es observar el valor que servirá como gatillo para disparar el movimiento, conocido como breakeven. Una vez que se ha disparado el gatillo, capturas el precio de apertura de la posición y lo colocas como precio de stop loss, luego ejecutas la llamada de modificación del precio de la posición y el resultado será el breakeven.

El trailing stop funciona casi de la misma forma, solo que en este caso el movimiento ocurrirá cuando el gatillo, ya sea una distancia que el precio se ha movido o cualquier otra cosa, se dispare. Cuando esto suceda, tomás el nuevo valor que se utilizará como stop loss y llamás la función anteriormente mencionada. Es algo extremadamente simple y fácil de hacer.

Este tema de los gatillos, tanto para breakeven como para trailing stop, lo discutiré con más detalle más adelante, cuando muestre la forma de desarrollar estos gatillos para que el EA pueda operar de forma automática. Sin embargo, para aquellos que han estado estudiando y son entusiastas de este tema, deben estar pensando y buscando formas de hacer estos gatillos incluso antes de entrar en detalles. Si este es tu caso: ¡Felicidades! Ya estás en el camino correcto.

Ahora volvamos al procedimiento de modificación de precios, ya que allí hay algo que todavía no he mencionado y es importante que sepas cómo y por qué está allí. Para facilitar la explicación, prestaremos atención al fragmento de código que se ve a continuación:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order  = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
// Parte responsável pelo trabalho em posições ...
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

Hay momentos en los que puede ser necesario eliminar una orden del libro, es decir, cerrarla o eliminarla. Y existe el peligro de que durante la ejecución, hayas hecho algo que haga que el EA genere un precio igual a cero. Créeme, sucede, y es algo bastante común si piensas en el caso de un EA automático. Entonces el EA envía una solicitud para que se modifique el precio de la orden, pero debido a un error, este precio se envía como cero.

En estos casos, el servidor de negociación negará la orden, pero el EA puede quedarse insistiendo de forma casi loca en que el precio de apertura debe ser cero, y si no se hace algo, puede entrar en un bucle, lo cual es algo extremadamente desagradable en una cuenta de producción (cuenta real). Para evitar que el EA se quede insistiendo en algo que no será aceptado por el servidor, se incluyó la siguiente idea: Si el EA envía un precio de apertura de posición que es igual a cero, la orden deberá ser cerrada por el servidor. Y es precisamente eso lo que hace este código aquí.

Manda un mensaje al servidor de negociación de que la orden informada debe ser cerrada, dejando de permanecer en el libro. Cuando esto suceda, la orden dejará de estar allí viva en el libro, y puedes ser informado al respecto, pero aquí no se incluye un código para esto, ya que hay otros usos igualmente útiles para este tipo de cosa, no solo evitar que el EA siga insistiendo con algo, sino simplemente para eliminar una orden del libro.

Pero todavía no hemos terminado el artículo, todavía queda un último procedimiento, aunque este es casi opcional, pero en algunos casos, realmente se usa, así que ya que estoy aquí, abriendo la caja negra de cómo funciona el sistema de órdenes, necesitamos ver más un procedimiento, y este se ve a continuación:

                bool ClosePosition(const ulong ticket, const uint partial = 0)
                        {
                                double v1 = partial * m_Infos.VolMinimal, Vol;
                                bool IsBuy;
                                
                                if (!PositionSelectByTicket(ticket)) return false;
                                IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
                                Vol = PositionGetDouble(POSITION_VOLUME);
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action    = TRADE_ACTION_DEAL;
                                m_TradeRequest.type      = (IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY);
                                m_TradeRequest.price     = SymbolInfoDouble(_Symbol, (IsBuy ? SYMBOL_BID : SYMBOL_ASK));
                                m_TradeRequest.position  = ticket;
                                m_TradeRequest.symbol    = _Symbol;
                                m_TradeRequest.volume    = ((v1 == 0) || (v1 > Vol) ? Vol : v1);
                                m_TradeRequest.deviation = 1000;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

El procedimiento anterior hace que muchas personas sueñen en grande, imaginando cosas y viendo estrellas. Al ver este procedimiento, debes estar pensando que sirve para cerrar una posición. ¿Por qué alguien estaría soñando y delirando al ver este procedimiento?

Tranquilo, querido lector, todavía no lo has entendido, estás manifestando tu prejuicio al ver el nombre del procedimiento. Pero vamos a profundizar un poco más, vamos a analizar el código y entender por qué muchos sueñan en grande. Obsérvese que aquí, en este procedimiento, hay algunos cálculos, ¿pero por qué tiene cálculos? La razón es permitir hacer las conocidas salidas parciales. Entendamos cómo realmente sucede esto. Supongamos que tienes una posición abierta con un volumen de 300, entonces, si el volumen mínimo negociable es de 100, podrás salir con 100, 200 o 300.

Pero para esto, deberás informar un valor, y este es por defecto cero, es decir, le dice a la función que la posición se cerrará completamente, pero esto solo sucederá si y solo si lo mantienes como predeterminado. Pero hay un detalle aquí: NO, y de nuevo NO, debes informar el valor del volumen, debes informar el desapalancamiento que se hará, es decir, si tienes un volumen de 300 y el volumen mínimo es de 100, esto significa que estás apalancado en 3x, para hacer una parcial, en este caso, debes informar un valor que puede ser 1 y 2, si se informa 0, 3 o un valor mayor que tu apalancamiento, la posición se cerrará completamente, esto se informa en este punto aquí.

Sin embargo, hay algunas alternativas a esto. Por ejemplo, en el caso específico de B3 (Bolsa de Brasil), los activos (acciones de empresas) se negocian en lotes de 100, pero existe el mercado fraccionario, en el que puedes negociar de 1 en 1. En este caso, si el EA se está ejecutando en el fraccionario, el valor a informar, en el mismo ejemplo de los 300, podría ir de 1 a 299, y aún así la posición no se cerrará completamente, quedando un residuo abierto.

¿Ahora entienden por qué muchos sueñan al mirar este procedimiento? La forma de trabajar aquí dependerá, por supuesto, del activo, del tipo de mercado y del interés del operador. Pero de una manera u otra, si estás trabajando en una cuenta de tipo HEDGING, con toda certeza necesitarás esta función anterior, sin ella, las posiciones se quedarán allí acumulando, tomando tiempo y recursos del EA para analizar cosas que ya podrían haber sido cerradas.

Para finalizar este artículo, abordando completamente el tema del sistema de órdenes, vamos a ver cómo debería ser el código del EA (Expert Advisor) para remover esa limitación de no poder colocar otras órdenes una vez creada una, y el problema de no poder manipular los datos de una posición. Para resolver estos problemas, tendremos que hacer algunos cambios en su código, pero al hacerlo, ya podrás divertirte y hacer mucho más. Tal vez esto te haga sentir un poco más emocionado con lo que se puede hacer con un código relativamente simple y que está siendo expuesto de una forma que puede ser entendida, incluso por aquellos con poco conocimiento en programación.

Bien, para corregir en parte el EA, tendremos que modificar el código responsable de manejar los eventos del gráfico, el nuevo código se ve a continuación:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
#define KEY_UP                  38
#define KEY_DOWN                40
#define KEY_NUM_1               97
#define KEY_NUM_2               98
#define KEY_NUM_3               99
#define KEY_NUM_7               103
#define KEY_NUM_8               104
#define KEY_NUM_9               105

        static ulong sticket = 0;
        ulong ul0;
        int key = (int)lparam;
        
        switch (id)
        {
                case CHARTEVENT_KEYDOWN:
                        switch (key)
                        {
                                case KEY_UP:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        ul0 = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        sticket = (ul0 > 0 ? ul0 : sticket);
                                        break;
                                case KEY_DOWN:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        ul0 = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        sticket = (ul0 > 0 ? ul0 : sticket);
                                        break;
                                case KEY_NUM_1:
                                case KEY_NUM_7:
                                        if (sticket > 0) ModifyStop(key == KEY_NUM_7, sticket);
                                        break;
                                case KEY_NUM_2:
                                case KEY_NUM_8:
                                        if (sticket > 0) ModifyPrice(key == KEY_NUM_8, sticket);
                                        break;
                                case KEY_NUM_3:
                                case KEY_NUM_9:
                                        if (sticket > 0) ModifyTake(key == KEY_NUM_9, sticket);
                                        break;
                        }
                        break;
        }
}

Lo que se hizo para modificar, y así dar un poco más de posibilidades, fue quitar las partes tachadas, y agregar una nueva variable, esta recibirá el valor del ticket, devuelto por la clase de órdenes, si el valor es diferente de cero, el nuevo ticket será almacenado en una variable estática, esta mantendrá el valor por todo el tiempo, hasta que un nuevo valor entre en el lugar del anterior. Bueno, esto ya te permitirá manipular muchas más cosas, y si por casualidad se ejecuta una orden, y no has sobrescrito el valor abriendo una nueva orden, podrás manipular la información de límite de la orden.

Ahora, como tarea adicional, para verificar si realmente has aprendido a trabajar con el sistema de órdenes (hasta que se publique el próximo artículo) y dar una respuesta, intenta que el sistema de órdenes abra una posición de mercado y utilice el valor del ticket, de manera que se pueda manipular los límites de la posición abierta a precio de mercado. Haz esto, sin ver el próximo artículo, de hecho, servirá para que te muestres a ti mismo si estás o no logrando seguir las explicaciones.

Muy bien, pero ahora veremos cómo quedó el código que cambia los precios, ya que el código del precio de apertura no sufrirá cambios. Podemos saltar el caso en el que una orden se convierte en posición, pero veremos el de take profit, que está a continuación:

void ModifyTake(bool IsUp, const ulong ticket)
{
        double p = 0, s, t;
        
        if (!OrderSelect(ticket)) return;
        if (OrderSelect(ticket))
        {
                p = OrderGetDouble(ORDER_PRICE_OPEN);
                s = OrderGetDouble(ORDER_SL);
                t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        }else if (PositionSelectByTicket(ticket))
        {
                s = PositionGetDouble(POSITION_SL);
                t = PositionGetDouble(POSITION_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        }else return;
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

El código tachado ya no existe, y por ello ganamos la posibilidad de un nuevo código, que nos permite manipular el valor del take profit de una posición. Lo mismo es válido para el código stop loss, que se ve justo debajo:

void ModifyStop(bool IsUp, const ulong ticket)
{
        double p = 0, s, t;
        
        if (!OrderSelect(ticket)) return;
        if (OrderSelect(ticket))
        {
                p = OrderGetDouble(ORDER_PRICE_OPEN);
                s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
                t = OrderGetDouble(ORDER_TP);
        }else if (PositionSelectByTicket(ticket))
        {
                s = PositionGetDouble(POSITION_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
                t = PositionGetDouble(POSITION_TP);
        }else return;
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

Emplea este EA como una herramienta de aprendizaje; úsalo y abusa de él en las cuentas demo; explora al máximo lo que se está mostrando aquí en estos 3 primeros artículos, porque, en este momento exacto, considero que el sistema de órdenes concluido. En el próximo artículo te voy a mostrar cómo inicializar el EA, con el fin de captar alguna información, los problemas y las posibles soluciones involucrados en esta inicialización, pero estas preguntas no son realmente parte del sistema de órdenes, éste termina aquí, ya que se implementó todo lo que realmente necesitamos para hacer que el EA sea automático.


Conclusión

A pesar de lo visto en estos tres primeros artículos, todavía estamos lejos de tener un EA automático. Los detalles presentados son a menudo ignorados o desconocidos por muchos, lo cual es peligroso. Sin embargo, todavía estamos comenzando a hablar de los sistemas automatizados y los peligros de usarlos sin los conocimientos adecuados.

En el apéndice, se proporciona el código visto hasta ahora para que puedas estudiarlo y aprender a hacerlo funcionar. Espero verte en el próximo artículo.

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

Archivos adjuntos |
Cómo construir un EA que opere automáticamente (Parte 04): Gatillos manuales (I) Cómo construir un EA que opere automáticamente (Parte 04): Gatillos manuales (I)
Aprenda a crear un EA que opere automáticamente de forma sencilla y segura.
DoEasy. Elementos de control (Parte 21): Elemento de control SplitContainer. Separador de paneles DoEasy. Elementos de control (Parte 21): Elemento de control SplitContainer. Separador de paneles
En este artículo, crearemos una clase de objeto auxiliar de separador de paneles para el control SplitContainer.
Cómo construir un EA que opere automáticamente (Parte 05): Gatillos manuales (II) Cómo construir un EA que opere automáticamente (Parte 05): Gatillos manuales (II)
Aprenda a crear un EA que opere automáticamente de forma sencilla y segura. Al final del artículo anterior, pensé que sería apropiado permitir el uso del EA de forma manual, al menos durante un tiempo.
Aprendiendo a diseñar un sistema de trading con Alligator Aprendiendo a diseñar un sistema de trading con Alligator
Bienvenidos a un nuevo artículo de nuestra serie dedicada a la creación de sistemas comerciales basados en indicadores técnicos populares. Hoy analizaremos el indicador Alligator y crearemos sistemas comerciales basados en él.