Implementación de un Expert Advisor tipo "arrastrar y soltar" semiautomático e interactivo basado en el riesgo predefinido y la relación R/R (riesgo/beneficio)

investeo | 1 abril, 2014


Introducción

Algunos operadores realizan todas sus operaciones de forma automática, y algunos hacen una mezcla de operaciones automáticas y manuales basadas ​​en las salidas de varios indicadores. Y como miembro de este último grupo, necesitaba una herramienta interactiva para poder evaluar de forma dinámica los niveles de riesgo y de beneficio, directamente a partir del gráfico.

Después de haber declarado el riesgo máximo sobre mi patrimonio, quería calcular los parámetros en tiempo real basados en el nivel Stop Loss que he colocado en el gráfico, y necesitaba ejecutar mi operación de trading directamente desde el EA basado en los niveles calculados de SL y TP.

En este artículo vamos a presentar una forma de implementación de un Expert Advisor con un riesgo de pérdida de patrimonio y relación R/R predefinidos. Se pueden modificar los parámetros de riesgo, R/R y el tamaño del lote durante la ejecución en el panel del EA. 


1. Requisitos

Los requisitos para el EA eran los siguientes:


2. Diseño

Debido a los requisitos del EA para mostrar y modificar los parámetros durante la ejecución, he decidido usar las clases CChartObject y sus derivadas para mostrar el GUI (interfaz gráfico del usuario) en la ventana del gráfico y manejar los eventos de entrada para la interacción con el usuario. Por tanto, el EA necesita un interfaz de usuario con etiquetas, botones y campos de edición.

Al principio, quería utilizar el objeto CChartObjectPanel para agrupar otros objetos en el panel, pero decidí intentar un planteamiento distinto, he diseñado una clase que contiene etiquetas, campos de edición y botones, y los muestra en una imagen de fondo. Se ha realizado la imagen de fondo del interfaz mediante el software GIMP. Los objetos generados por MQL5 son los campos de edición, las etiquetas rojas, que se actualizan en tiempo real, y los botones.

Simplemente he colocado objetos de etiquetas en el gráfico y registrado su posición, y he construido la clase CRRDialog que maneja todas las funciones que muestran las salidas calculadas, los parámetros recibidos de los campos de CChartObjectEdit y registra los estados de los botones. Los rectángulos coloreados del riesgo y beneficio son objetos de la clase CChartObjectRectangle y el puntero Stop Loss arrastrable es un objeto mapa de bit de la clase CChartObjectBitmap.


 

Figura 1. Captura de pantalla del gráfico del EA

Figura 1. Captura de pantalla del gráfico del EA

 


3. Implementación de la clase de diálogo del EA

La clase CRRDialog maneja todo el interfaz de usuario del EA. Contiene una cantidad de variables que se representan, objetos que se utilizan para mostrar las variables y métodos para obtener o definir los valores de las variables y actualizar el cuadro de diálogo.

Estoy utilizando el objeto CChartObjectBmpLabel para el fondo, objetos CChartObjectEdit para los campos de edición, objetos CChartObjectLabel para mostrar las etiquetas y objetos CChartObjectButton para los botones: 

class CRRDialog
  {
private:

   int               m_baseX;
   int               m_baseY;
   int               m_fontSize;
   
   string            m_font;
   string            m_dialogName;
   string            m_bgFileName;

   double            m_RRRatio;
   double            m_riskPercent;
   double            m_orderLots;
   double            m_SL;
   double            m_TP;
   double            m_maxAllowedLots;
   double            m_maxTicksLoss;
   double            m_orderEquityRisk;
   double            m_orderEquityReward;
   ENUM_ORDER_TYPE   m_orderType;

   CChartObjectBmpLabel m_bgDialog;

   CChartObjectEdit  m_riskRatioEdit;
   CChartObjectEdit  m_riskValueEdit;
   CChartObjectEdit  m_orderLotsEdit;

   CChartObjectLabel m_symbolNameLabel;
   CChartObjectLabel m_tickSizeLabel;
   CChartObjectLabel m_maxEquityLossLabel;
   CChartObjectLabel m_equityLabel;
   CChartObjectLabel m_profitValueLabel;
   CChartObjectLabel m_askLabel;
   CChartObjectLabel m_bidLabel;
   CChartObjectLabel m_tpLabel;
   CChartObjectLabel m_slLabel;
   CChartObjectLabel m_maxAllowedLotsLabel;
   CChartObjectLabel m_maxTicksLossLabel;
   CChartObjectLabel m_orderEquityRiskLabel;
   CChartObjectLabel m_orderEquityRewardLabel;
   CChartObjectLabel m_orderTypeLabel;

   CChartObjectButton m_switchOrderTypeButton;
   CChartObjectButton m_placeOrderButton;
   CChartObjectButton m_quitEAButton;

public:

   void              CRRDialog(); // CRRDialog constructor
   void             ~CRRDialog(); // CRRDialog destructor

   bool              CreateCRRDialog(int topX,int leftY);
   int               DeleteCRRDialog();
   void              Refresh();
   void              SetRRRatio(double RRRatio);
   void              SetRiskPercent(double riskPercent);
   double            GetRiskPercent();
   double            GetRRRRatio();
   void              SetSL(double sl);
   void              SetTP(double tp);
   double            GetSL();
   double            GetTP();
   void              SetMaxAllowedLots(double lots);
   void              SetMaxTicksLoss(double ticks);
   void              SetOrderType(ENUM_ORDER_TYPE);
   void              SwitchOrderType();
   void              ResetButtons();
   ENUM_ORDER_TYPE   GetOrderType();
   void              SetOrderLots(double orderLots);
   double            GetOrderLots();
   void              SetOrderEquityRisk(double equityRisk);
   void              SetOrderEquityReward(double equityReward);
  };

Puesto que los métodos para obtener/definir las variables son sencillos, me voy a centrar en los métodos CreateCRRDialog() y Refresh(). El método CreateCRRDialog() se utiliza para inicializar la imagen de fondo, las etiquetas y los campos de edición.

Para la inicialización de las etiquetas y los campos de edición uso: El método Create() con los parámetros de coordenadas para localizar el objeto en el gráfico, Font() y FontSize() para configurar la fuente y el método Description() para introducir el texto en la etiqueta.

Para los botones: Los parámetros adicionales del método Create() indican el tamaño del botón y el método BackColor() indica el color de fondo del botón. 

bool CRRDialog::CreateCRRDialog(int topX,int leftY)
  {
   bool isCreated=false;

   MqlTick current_tick;
   SymbolInfoTick(Symbol(),current_tick);

   m_baseX = topX;
   m_baseY = leftY;

   m_bgDialog.Create(0, m_dialogName, 0, topX, leftY);
   m_bgDialog.BmpFileOn(m_bgFileName);

   m_symbolNameLabel.Create(0, "symbolNameLabel", 0, m_baseX + 120, m_baseY + 40);
   m_symbolNameLabel.Font("Verdana");
   m_symbolNameLabel.FontSize(8);
   m_symbolNameLabel.Description(Symbol());

   m_tickSizeLabel.Create(0, "tickSizeLabel", 0, m_baseX + 120, m_baseY + 57);
   m_tickSizeLabel.Font("Verdana");
   m_tickSizeLabel.FontSize(8);
   m_tickSizeLabel.Description(DoubleToString(SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE), Digits()));

   m_riskRatioEdit.Create(0, "riskRatioEdit", 0, m_baseX + 120, m_baseY + 72, 35, 15);
   m_riskRatioEdit.Font("Verdana");
   m_riskRatioEdit.FontSize(8);
   m_riskRatioEdit.Description(DoubleToString(m_RRRatio, 2));

   m_riskValueEdit.Create(0, "riskValueEdit", 0, m_baseX + 120, m_baseY + 90, 35, 15);
   m_riskValueEdit.Font("Verdana");
   m_riskValueEdit.FontSize(8);
   m_riskValueEdit.Description(DoubleToString(m_riskPercent, 2));

   m_equityLabel.Create(0, "equityLabel", 0, m_baseX + 120, m_baseY + 107);
   m_equityLabel.Font("Verdana");
   m_equityLabel.FontSize(8);
   m_equityLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2));

   m_maxEquityLossLabel.Create(0, "maxEquityLossLabel", 0, m_baseX + 120, m_baseY + 122);
   m_maxEquityLossLabel.Font("Verdana");
   m_maxEquityLossLabel.FontSize(8);
   m_maxEquityLossLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY)*m_riskPercent/100.0,2));

   m_askLabel.Create(0, "askLabel", 0, m_baseX + 120, m_baseY + 145);
   m_askLabel.Font("Verdana");
   m_askLabel.FontSize(8);
   m_askLabel.Description("");

   m_bidLabel.Create(0, "bidLabel", 0, m_baseX + 120, m_baseY + 160);
   m_bidLabel.Font("Verdana");
   m_bidLabel.FontSize(8);
   m_bidLabel.Description("");

   m_slLabel.Create(0, "slLabel", 0, m_baseX + 120, m_baseY + 176);
   m_slLabel.Font("Verdana");
   m_slLabel.FontSize(8);
   m_slLabel.Description("");

   m_tpLabel.Create(0, "tpLabel", 0, m_baseX + 120, m_baseY + 191);
   m_tpLabel.Font("Verdana");
   m_tpLabel.FontSize(8);
   m_tpLabel.Description("");

   m_maxAllowedLotsLabel.Create(0, "maxAllowedLotsLabel", 0, m_baseX + 120, m_baseY + 208);
   m_maxAllowedLotsLabel.Font("Verdana");
   m_maxAllowedLotsLabel.FontSize(8);
   m_maxAllowedLotsLabel.Description("");

   m_maxTicksLossLabel.Create(0, "maxTicksLossLabel", 0, m_baseX + 120, m_baseY + 223);
   m_maxTicksLossLabel.Font("Verdana");
   m_maxTicksLossLabel.FontSize(8);
   m_maxTicksLossLabel.Description("");

   m_orderLotsEdit.Create(0, "orderLotsEdit", 0, m_baseX + 120, m_baseY + 238, 35, 15);
   m_orderLotsEdit.Font("Verdana");
   m_orderLotsEdit.FontSize(8);
   m_orderLotsEdit.Description("");

   m_orderEquityRiskLabel.Create(0, "orderEquityRiskLabel", 0, m_baseX + 120, m_baseY + 255);
   m_orderEquityRiskLabel.Font("Verdana");
   m_orderEquityRiskLabel.FontSize(8);
   m_orderEquityRiskLabel.Description("");

   m_orderEquityRewardLabel.Create(0, "orderEquityRewardLabel", 0, m_baseX + 120, m_baseY + 270);
   m_orderEquityRewardLabel.Font("Verdana");
   m_orderEquityRewardLabel.FontSize(8);
   m_orderEquityRewardLabel.Description("");

   m_switchOrderTypeButton.Create(0, "switchOrderTypeButton", 0, m_baseX + 20, m_baseY + 314, 160, 20);
   m_switchOrderTypeButton.Font("Verdana");
   m_switchOrderTypeButton.FontSize(8);
   m_switchOrderTypeButton.BackColor(LightBlue);

   m_placeOrderButton.Create(0, "placeOrderButton", 0, m_baseX + 20, m_baseY + 334, 160, 20);
   m_placeOrderButton.Font("Verdana");
   m_placeOrderButton.FontSize(8);
   m_placeOrderButton.BackColor(LightBlue);
   m_placeOrderButton.Description("Place Market Order");

   m_quitEAButton.Create(0, "quitEAButton", 0, m_baseX + 20, m_baseY + 354, 160, 20);
   m_quitEAButton.Font("Verdana");
   m_quitEAButton.FontSize(8);
   m_quitEAButton.BackColor(LightBlue);
   m_quitEAButton.Description("Quit");

   return isCreated;
  }

El método Refresh() actualiza todas las etiquetas y descripciones de los botones con las variables de CRRDialog y los niveles actuales de compra y venta, el patrimonio de la cuenta y los valores del riesgo de patrimonio: 

void CRRDialog::Refresh()
  {
   MqlTick current_tick;
   SymbolInfoTick(Symbol(),current_tick);

   m_equityLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2));
   m_maxEquityLossLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY)*
                                         StringToDouble(m_riskValueEdit.Description())/100.0,2));
   m_askLabel.Description(DoubleToString(current_tick.ask, Digits()));
   m_bidLabel.Description(DoubleToString(current_tick.bid, Digits()));
   m_slLabel.Description(DoubleToString(m_SL, Digits()));
   m_tpLabel.Description(DoubleToString(m_TP, Digits()));
   m_maxAllowedLotsLabel.Description(DoubleToString(m_maxAllowedLots,2));
   m_maxTicksLossLabel.Description(DoubleToString(m_maxTicksLoss,0));
   m_orderEquityRiskLabel.Description(DoubleToString(m_orderEquityRisk,2));
   m_orderEquityRewardLabel.Description(DoubleToString(m_orderEquityReward,2));

   if(m_orderType==ORDER_TYPE_BUY) m_switchOrderTypeButton.Description("Order Type: BUY");
   else if(m_orderType==ORDER_TYPE_SELL) m_switchOrderTypeButton.Description("Order Type: SELL");
  }


4. Eventos del gráfico

Puesto que el EA está diseñado para ser interactivo, debe ser capaz de manejar los eventos del gráfico.

Los eventos que se manejan incluyen:

Los eventos que se manejan son CHARTEVENT_OBJECT_CLICK para la selección del puntero y de los botones, CHARTEVENT_OBJECT_DRAG para arrastrar el puntero S/L, y CHARTEVENT_OBJECT_ENDEDIT después de que el trader haya actualizado los campos modificados.

Al principio, la implementación de la función OnChartEvent() ocupaba unas cuantas páginas de código, pero decidí dividirla en varios controladores de eventos, convirtiendo la función OnChartEvent() a un formato legible: 

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Check the event by pressing a mouse button
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      string clickedChartObject=sparam;

      if(clickedChartObject==slButtonID)
         SL_arrow.Selected(!SL_arrow.Selected());

      if(clickedChartObject==switchOrderTypeButtonID)
        {
         EA_switchOrderType();
        };

      if(clickedChartObject==placeOrderButtonID)
        {
         EA_placeOrder();
        }

      if(clickedChartObject==quitEAButtonID) ExpertRemove();

      ChartRedraw();
     }

   if(id==CHARTEVENT_OBJECT_DRAG)
     {
      // BUY 
      if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
        {
         EA_dragBuyHandle();
        };

      // SELL
      if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
        {
         EA_dragSellHandle();
        };
      ChartRedraw();
     }

   if(id==CHARTEVENT_OBJECT_ENDEDIT)
     {
      if((sparam==riskRatioEditID || sparam==riskValueEditID || sparam==orderLotsEditID) && orderPlaced==false)
        {
         EA_editParamsUpdate();
        }
     }
  }

Describiremos los controladores de eventos con más detalle en las próximas secciones. Cabe mencionar la técnica que he utilizado para seleccionar el objeto SL_arrow. Normalmente, para seleccionar un objeto del gráfico, hay que hacer un doble clic sobre el mismo. Pero se puede seleccionar haciendo un solo clic y llamando al método Selected() del objeto CChartObject o sus descendientes en la función OnChartEvent() dentro del controlador de evento CHARTEVENT_OBJECT_CLICK:

      if(clickedChartObject==slButtonID)
         SL_arrow.Selected(!SL_arrow.Selected());

Se selecciona o se anula la selección de un objeto según su estado anterior después de un clic. 


5. Clase extendida de gestión de dinero basada en CMoneyFixedRisk

Antes de poder describir los controladores ChartEvent, primero tengo que recorrer la clase de gestión de dinero.

Para la gestión de dinero, he reutilizado la clase CMoneyFixedRisk, proporcionada por MetaQuotes, e implementado la clase CMoneyFixedRiskExt.

Los métodos originales de la clase CMoneyFixedRisk devuelven la cantidad permitida del lote de la orden para un precio determinado, el nivel Stop Loss y el riesgo de patrimonio entre el tamaño mínimo y máximo del lote que permite el broker. He cambiado los métodos CheckOpenLong() y CheckOpenShort() para que devuelvan el tamaño de lote 0.0 si no se cumplen los requisitos del riesgo, y los he extendido con cuatro métodos: GetMaxSLPossible(), CalcMaxTicksLoss(), CalcOrderEquityRisk() y CalcOrderEquityReward():

class CMoneyFixedRiskExt : public CExpertMoney
  {
public:
   //---
   virtual double    CheckOpenLong(double price,double sl);
   virtual double    CheckOpenShort(double price,double sl);
   
   double GetMaxSLPossible(double price, ENUM_ORDER_TYPE orderType);
   double CalcMaxTicksLoss();
   double CalcOrderEquityRisk(double price, double sl, double lots);
   double CalcOrderEquityReward(double price, double sl, double lots, double rrratio);
  };

El método GetMaxSLPossible() el valor del precio de Stop Loss máximo para un riesgo de patrimonio determinado y el tamaño de trading permitido mínimo.

Por ejemplo, si el saldo de la cuenta es de 10.000 de la divisa de referencia de la cuenta y el riesgo es del 2%, podemos colocar la cantidad de 200 de la divisa de la cuenta a riesgo. Si el tamaño mínimo del lote de la operación es de 0.1 lote, este método devuelve el nivel de precio para la orden ORDER_TYPE_BUY ORDER_TYPE_SELL que cumpla con el valor de riesgo de patrimonio para la posición de 0.1 lote. Esto permite hacer una estimación del nivel Stop Loss máximo que nos podemos permitir para el tamaño mínimo del lote de la operación. Este es el precio que no podemos cruzar para un determinado nivel de riesgo de patrimonio. 

double CMoneyFixedRiskExt::GetMaxSLPossible(double price, ENUM_ORDER_TYPE orderType)
{
   double maxEquityLoss, tickValLoss, maxTicksLoss;
   double minvol=m_symbol.LotsMin();
   double orderTypeMultiplier;
   
   if(m_symbol==NULL) return(0.0);
   
   switch (orderType)
   {
   case ORDER_TYPE_SELL: orderTypeMultiplier = -1.0; break;
   case ORDER_TYPE_BUY: orderTypeMultiplier = 1.0; break;
   default: orderTypeMultiplier = 0.0;
   }
   
   maxEquityLoss = m_account.Balance()*m_percent/100.0; // max loss 
   tickValLoss = minvol*m_symbol.TickValueLoss(); // tick val loss
   maxTicksLoss = MathFloor(maxEquityLoss/tickValLoss);
 
   return (price - maxTicksLoss*m_symbol.TickSize()*orderTypeMultiplier);
}

El método CalcMaxTickLoss() devuelve el número máximo de ticks que podemos perder para un determinado riesgo y el tamaño mínimo permitido del lote.

Al principio, se calcula la perdida máxima de patrimonio como porcentaje del saldo actual, y después se calcula la pérdida del valor del tick para el cambio de un tick correspondiente al tamaño mínimo de lote permitido para un símbolo determinado. Entonces, se divide la pérdida máxima de patrimonio por la pérdida del valor del tick y se redondea  el resultado a un valor entero mediante la función MathFloor():

double CMoneyFixedRiskExt::CalcMaxTicksLoss()
{
   double maxEquityLoss, tickValLoss, maxTicksLoss;
   double minvol=m_symbol.LotsMin();
   
   if(m_symbol==NULL) return(0.0);
   
   maxEquityLoss = m_account.Balance()*m_percent/100.0; // max loss 
   tickValLoss = minvol*m_symbol.TickValueLoss(); // tick val loss
   maxTicksLoss = MathFloor(maxEquityLoss/tickValLoss);
   
   return (maxTicksLoss);
}

El método CalcOrderEquityRisk() devuelve el riesgo de patrimonio para un precio determinado, el nivel Stop Loss y la cantidad de lotes. Se calcula multiplicando el valor de pérdida del tick por el número de lotes y el precio, y después, se multiplica por la diferencia entre el precio actual y el nivel Stop Loss:

double CMoneyFixedRiskExt::CalcOrderEquityRisk(double price,double sl, double lots)
{
   double equityRisk;
   
   equityRisk = lots*m_symbol.TickValueLoss()*(MathAbs(price-sl)/m_symbol.TickSize()); 
   
   if (dbg) Print("calcEquityRisk: lots = " + DoubleToString(lots) +
                 " TickValueLoss = " + DoubleToString(m_symbol.TickValueLoss()) +
                 " risk = " + DoubleToString(equityRisk));
   
   return equityRisk;
}

 El método CalcOrderEquityReward() es similar al método CalcOrderEquityRisk() pero usa el método TickValueProfit() en lugar de TickValueLoss() y se multiplica el resultado por la relación riesgo/beneficio indicada:  

double CMoneyFixedRiskExt::CalcOrderEquityReward(double price,double sl, double lots, double rrratio)
{
   double equityReward; 
   equityReward = lots*m_symbol.TickValueProfit()*(MathAbs(price-sl)/m_symbol.TickSize())*rrratio; 
   
   if (dbg) Print("calcEquityReward: lots = " + DoubleToString(lots) + 
                   " TickValueProfit = " + DoubleToString(m_symbol.TickValueProfit()) +
                 " reward = " + DoubleToString(equityReward));
   return equityReward;
}

Estos métodos son suficientes para calcular los niveles máximos de Stop Loss y devolver el riesgo de patrimonio y el beneficio en tiempo real. Se usa el método CalcMaxTickLoss() para corregir el dibujo del rectángulo del riesgo; si el trader quiere colocar una operación que cruza el límite del número de ticks que puede perder, solo se dibuja el rectángulo hasta el número máximo de ticks que puede perder.

Ver esto directamente en el gráfico facilita bastante las cosas. Puede verlo en la demostración al final del artículo.


6. Implementación de los controladores de eventos del gráfico

Se desencadena el controlador EA_switchOrderType() después de recibir el evento CHARTEVENT_OBJECT_CLICK en el objeto m_switchOrderTypeButton. Alterna el tipo de orden entre ORDER_TYPE_BUY ORDER_TYPE_SELL, restablece el estado de los botones y las variables del cuadro de diálogo, y elimina los objetos del rectángulo de riesgo y beneficio del gráfico:

void EA_switchOrderType()
  {
   symbolInfo.RefreshRates();

   visualRRDialog.SwitchOrderType();
   visualRRDialog.ResetButtons();
   visualRRDialog.SetSL(0.0);
   visualRRDialog.SetTP(0.0);
   visualRRDialog.SetMaxAllowedLots(0.0);
   visualRRDialog.SetOrderLots(0.0);
   visualRRDialog.SetMaxTicksLoss(0);
   visualRRDialog.SetOrderEquityRisk(0.0);
   visualRRDialog.SetOrderEquityReward(0.0);

   if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY) SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Ask());
   else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL) SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Bid());
   SL_arrow.SetInteger(OBJPROP_TIME,0,TimeCurrent());

   rectReward.Delete();
   rectRisk.Delete();

   visualRRDialog.Refresh();

  }

Se desencadena el controlador EA_dragBuyHandle() después de arrastrar y soltar el objeto SL_arrow en el gráfico. Primero lee los parámetros de precio y de tiempo del punto en el cual se suelta el objeto SL_arrow a partir del gráfico, y establece el nivel de precio de nuestra operación a un Stop Loss hipotético.

A continuación, calcula la cantidad de lotes que podemos abrir para un riesgo determinado sobre el patrimonio. Si el valor Stop Loss no puede garantizar el objetivo de riesgo para el lote de trading más bajo posible para este símbolo, se mueve automáticamente hasta el máximo nivel SL posible. Esto ayuda a evaluar la cantidad de espacio de la que disponemos para un riesgo determinado.

Después de calcular el riesgo y el beneficio, se actualizan los objetos del rectángulo en el gráfico.

void EA_dragBuyHandle()
  {
   SL_arrow.GetDouble(OBJPROP_PRICE,0,SL_price);
   SL_arrow.GetInteger(OBJPROP_TIME,0,startTime);

   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();

// BUY
   double allowedLots=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);
   Print("Allowed lots = "+DoubleToString(allowedLots,2));
   double lowestSLAllowed=MM.GetMaxSLPossible(symbolInfo.Ask(),ORDER_TYPE_BUY);

   if(SL_price<lowestSLAllowed)
     {
      SL_price=lowestSLAllowed;
      ObjectSetDouble(0,slButtonID,OBJPROP_PRICE,lowestSLAllowed);
     }

   visualRRDialog.SetSL(SL_price);
   visualRRDialog.SetTP(symbolInfo.Ask()+(symbolInfo.Ask()-SL_price)*visualRRDialog.GetRRRRatio());

   if(visualRRDialog.GetTP()<SL_price)
     {
      visualRRDialog.SetSL(0.0);
      visualRRDialog.SetTP(0.0);
      SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Ask());
      rectReward.Delete();
      rectRisk.Delete();
      return;
     }

   double lotSize=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);

   visualRRDialog.SetMaxAllowedLots(lotSize);
   visualRRDialog.SetOrderLots(lotSize);
   visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());
   visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Ask(), SL_price, lotSize));
   visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Ask(), 
                                       SL_price, lotSize, visualRRDialog.GetRRRRatio()));
   visualRRDialog.Refresh();

   rectUpdate(visualRRDialog.GetOrderType());

  }

Se desencadena EA_dragSellHandle() para la configuración de la orden de venta.

Los cálculos se basan en el precio symbolInfo.Bid(), y se dibujan los rectángulos en consecuencia, es decir, que el beneficio en la zona marcada con el color verde es inferior al nivel actual del precio. 

void EA_dragSellHandle()
  {
   SL_arrow.GetDouble(OBJPROP_PRICE,0,SL_price);
   SL_arrow.GetInteger(OBJPROP_TIME,0,startTime);

   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();

   double allowedLots=MM.CheckOpenShort(symbolInfo.Bid(),SL_price);
   Print("Allowed lots = "+DoubleToString(allowedLots,2));
   double maxSLAllowed=MM.GetMaxSLPossible(symbolInfo.Bid(),ORDER_TYPE_SELL);

   if(SL_price>maxSLAllowed)
     {
      SL_price=maxSLAllowed;
      SL_arrow.SetDouble(OBJPROP_PRICE,0,maxSLAllowed);
     }

   visualRRDialog.SetSL(SL_price);
   visualRRDialog.SetTP(symbolInfo.Bid()-(SL_price-symbolInfo.Bid())*visualRRDialog.GetRRRRatio());

   if(visualRRDialog.GetTP()>SL_price)
     {
      visualRRDialog.SetSL(0.0);
      visualRRDialog.SetTP(0.0);
      SL_arrow.SetDouble(OBJPROP_PRICE,symbolInfo.Bid());
      rectReward.Delete();
      rectRisk.Delete();
      return;
     }

   double lotSize=MM.CheckOpenShort(symbolInfo.Bid(),SL_price);

   visualRRDialog.SetMaxAllowedLots(lotSize);
   visualRRDialog.SetOrderLots(lotSize);
   visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());
   visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Bid(), SL_price, lotSize));
   visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Bid(),
                                       SL_price, lotSize, visualRRDialog.GetRRRRatio()));
   visualRRDialog.Refresh();

   rectUpdate(visualRRDialog.GetOrderType());

  }

Se desencadena EA_placeOrder() después de pulsar el objeto m_placeOrderButton. Coloca una orden de mercado de compra o de venta para los niveles calculados de SL y TP y el tamaño indicado del lote.

Tenga en cuenta lo fácil que es colocar órdenes de mercado mediante la clase CExpertTrade. 

bool EA_placeOrder()
  {
   symbolInfo.RefreshRates();
   visualRRDialog.ResetButtons();

   if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
      orderPlaced=trade.Buy(visualRRDialog.GetOrderLots(),symbolInfo.Ask(),
                            visualRRDialog.GetSL(),visualRRDialog.GetTP(),TimeToString(TimeCurrent()));
   else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
      orderPlaced=trade.Sell(visualRRDialog.GetOrderLots(),symbolInfo.Bid(),
                            visualRRDialog.GetSL(),visualRRDialog.GetTP(),TimeToString(TimeCurrent()));

   return orderPlaced;
  }

Se desencadena el controlador EA_editParamsUpdate() al pulsar la tecla "Enter" después de modificar alguno de los campos de edición: riskRatioEdit, riskValueEdit y orderLotsEdit.

Cuando esto sucede, hay que volver a calcular el tamaño de lote permitido, el nivel TP, la pérdida máxima del tick, el riesgo de patrimonio y el beneficio:

void EA_editParamsUpdate()
  {
   MM.Percent(visualRRDialog.GetRiskPercent());

   SL_arrow.GetDouble(OBJPROP_PRICE, 0, SL_price);
   SL_arrow.GetInteger(OBJPROP_TIME, 0, startTime);

   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();

   double allowedLots=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);

   double lowestSLAllowed=MM.GetMaxSLPossible(symbolInfo.Ask(),ORDER_TYPE_BUY);
   if(SL_price<lowestSLAllowed)
     {
      SL_price=lowestSLAllowed;
      ObjectSetDouble(0,slButtonID,OBJPROP_PRICE,lowestSLAllowed);
     }

   visualRRDialog.SetSL(SL_price);
   visualRRDialog.SetTP(symbolInfo.Ask()+(symbolInfo.Ask()-SL_price)*visualRRDialog.GetRRRRatio());

   visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());
   visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Ask(), 
                                     SL_price, visualRRDialog.GetOrderLots()));
   visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Ask(), SL_price, 
                                       visualRRDialog.GetOrderLots(), visualRRDialog.GetRRRRatio()));
   visualRRDialog.Refresh();
   rectUpdate(visualRRDialog.GetOrderType());

   ChartRedraw();
  }

Se llama a EA_onTick() cada vez que llega un nuevo tick. Se llevan a cabo los cálculos solo si la orden no se ha colocado aún y que ya se había elegido el nivel Stop Loss mediante el arrastre del puntero SL_arrow.

Una vez la orden colocada, ya no hacen falta ni el riesgo, ni el beneficio, ni el nivel TP, y tampoco hace falta volver a dibujar el riesgo y el beneficio.  

void EA_onTick()
  {
   if(SL_price!=0.0 && orderPlaced==false)
     {
      double lotSize=0.0;
      SL_price=visualRRDialog.GetSL();
      symbolInfo.RefreshRates();

      if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
         lotSize=MM.CheckOpenLong(symbolInfo.Ask(),SL_price);
      else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
         lotSize=MM.CheckOpenShort(symbolInfo.Ask(),SL_price);

      visualRRDialog.SetMaxAllowedLots(lotSize);
      if(visualRRDialog.GetOrderLots()>lotSize) visualRRDialog.SetOrderLots(lotSize);

      visualRRDialog.SetMaxTicksLoss(MM.CalcMaxTicksLoss());

      if(visualRRDialog.GetOrderType()==ORDER_TYPE_BUY)
        {
         visualRRDialog.SetTP(symbolInfo.Ask()+(symbolInfo.Ask()-SL_price)*visualRRDialog.GetRRRRatio());
         visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(symbolInfo.Ask(), 
                                           SL_price, visualRRDialog.GetOrderLots()));
         visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Ask(), SL_price, 
                                             visualRRDialog.GetOrderLots(), visualRRDialog.GetRRRRatio()));
        }
      else if(visualRRDialog.GetOrderType()==ORDER_TYPE_SELL)
        {
         visualRRDialog.SetTP(symbolInfo.Bid()-(SL_price-symbolInfo.Bid())*visualRRDialog.GetRRRRatio());
         visualRRDialog.SetOrderEquityRisk(MM.CalcOrderEquityRisk(
                                           symbolInfo.Bid(), SL_price, visualRRDialog.GetOrderLots()));
         visualRRDialog.SetOrderEquityReward(MM.CalcOrderEquityReward(symbolInfo.Bid(), SL_price, 
                                             visualRRDialog.GetOrderLots(), visualRRDialog.GetRRRRatio()));
        }
      visualRRDialog.Refresh();
      rectUpdate(visualRRDialog.GetOrderType());
     }

   ChartRedraw(0);
  }

La función rectUpdate() se encarga de redibujar los rectángulos de riesgo y beneficio en color. Los puntos de control son el tiempo de inicio del objeto SL_arrow, el valor del precio de Compra o Venta actual dependiendo del tipo de orden, y los niveles SL y TP. El rectángulo rosa claro muestra el rango de precios entre el precio actual y el nivel SL y el verde claro muestra el rango de precios entre el precio actual y el nivel TP.

Ambos rectángulos representan una herramienta muy útil para observar el impacto de la relación riesgo/beneficio sobre los niveles de precios de SL y TP y ayudan a ajustar el riesgo antes de realizar la operación.

void rectUpdate(ENUM_ORDER_TYPE orderType)
  {
   symbolInfo.RefreshRates();
   currentTime=TimeCurrent();
   SL_arrow.GetInteger(OBJPROP_TIME,0,startTime);

   if(orderType==ORDER_TYPE_BUY)
     {
      rectReward.Create(0,rewardRectID,0,startTime,symbolInfo.Ask(),currentTime,symbolInfo.Ask()+
                       (symbolInfo.Ask()-visualRRDialog.GetSL())*visualRRDialog.GetRRRRatio());
      rectReward.Color(LightGreen);
      rectReward.Background(true);

      rectRisk.Create(0,riskRectID,0,startTime,visualRRDialog.GetSL(),currentTime,symbolInfo.Ask());
      rectRisk.Color(LightPink);
      rectRisk.Background(true);
     }
   else if(orderType==ORDER_TYPE_SELL)
     {
      rectReward.Create(0,rewardRectID,0,startTime,symbolInfo.Bid(),currentTime,symbolInfo.Bid()-
                        (visualRRDialog.GetSL()-symbolInfo.Bid())*visualRRDialog.GetRRRRatio());
      rectReward.Color(LightGreen);
      rectReward.Background(true);

      rectRisk.Create(0,riskRectID,0,startTime,visualRRDialog.GetSL(),currentTime,symbolInfo.Bid());
      rectRisk.Color(LightPink);
      rectRisk.Background(true);
     }
  }

 

7. Demostración

Se recomienda observar la siguiente demostración de funcionamiento del Expert Advisor en plena acción. Estoy realizando una orden de compra después del gran rebote, justo después de la apertura del mercado el lunes 01/11/2010.

Para una mejor visualización, ponga el vídeo en pantalla completa y la calidad en 480p. Los comentarios están incluidos en el vídeo.

 

Conclusión

En este artículo, he introducido el modo de implementar un Expert Advisor interactivo para el trading manual, basado en el riesgo predefinido y la relación riesgo/beneficio

He explicado cómo se utilizan las clases estándar para mostrar el contenido en el gráfico y cómo se manejan los eventos del gráfico para introducir nuevos datos y manejar los objetos "arrastrar y soltar" Espero que los conceptos que he presentado le sirvan como base a la implementación de otras herramientas gráficas en MQL

Todos los archivos fuente y los mapas de bits están adjuntos al artículo