Creando un EA gradador multiplataforma

Roman Klymenko | 3 junio, 2019

Introducción

Para nadie en este sitio web es un secreto que el lenguaje MQL5 es la mejor opción a la hora de crear asesores comerciales propios. Por desgracia, no todos los brókeres permiten crear cuentas disponibles en MetaTrader 5. E incluso si usted opera con un bróker que permite hacerlo, es totalmente posible que se vea obligado en el futuro a pasarse a un bróker que solo tiene la posibilidad de trabajar con MetaTrader 4. ¿Y qué hacemos en este caso con todos los asesores que hemos creado en el lenguaje MQL5? ¿Gastar una barbaridad de tiempo en modificarlos para MQL4? ¿No sería mejor crear directamente un asesor que pueda funcionar tanto con MetaTrader 5 como con MetaTrader 4?

En este artículo, vamos a intentar implementar un asesor de este tipo, al tiempo que comprobamos si puede funcionar un sistema comercial basado en la creación de una cuadrícula de órdenes.

Algunas palabras sobre la compilación condicional

La compilación condicional nos ayudará a crear un asesor que funcione tanto en MetaTrader 4, como en MetaTrader 5. La sintaxis que valos a utilizar es la siguiente:

   #ifdef __MQL5__ 
      // MQL5 code
   #else 
      // MQL4 code
   #endif 

La compilación condicional permite indicar que un determinado bloque de código debe compilarse solo en el caso de que la compilación tenga lugar en un asesor para el lenguaje MQL5. Al compilarse en MQL4 y otras versiones del lenguaje, este bloque de código simplemente se descartará, usándose en su lugar el bloque de código que sigue al operador #else (si se ha establecido).

De esta forma, si alguna funcionalidad se implementa de manera distinta en MQL4 y en MQL5, la implementaremos de ambas formas, y la compilación condicional nos permitirá elegir la opción que necesitemos para un lenguaje concreto.

En el resto de los casos, usaremos una sintaxis que funcione igualmente en MQL4 y en MQL5.

Sobre los sistemas comerciales de cuadrícula

Antes de comenzar el desarrollo de nuestro asesor, vamos a hablar un poco de lo que suponen las estrategias comerciales de cuadrícula. Para que, en lo sucesivo, todos lo entendamos de la misma manera.

Un gradador es un experto cuyo principal principio de trabajo consiste en colocar simultáneamente varias órdenes límite por encima del precio actual, y la misma cantidad por debajo.

Las órdenes límite no se colocan a un precio determinado, sino con un salto concreto. Es decir, a una distancia determinada del precio actual, por encima, se coloca la primer orden límite. Después, a la misma distancia de la orden colocada, se coloca la segunda orden límite, etcétera. etc. Tanto el número de órdenes de este tipo, como el salto al que se colocan, pueden variar.

Por encima del precio actual se colocan las órdenes en una dirección, y por debajo del precio actual, en la contraria. Se considera que:

Podemos trabajar tanto con stop loss y take profit, como sin ellos.

Si no colocamos stop loss y take profit, todas las posiciones abiertas, tanto las rentables, como las que conllevan pérdidas, existirán hasta que el beneficio de todas las posiciones abiertas alcance un nivel determinado. Después de ello, todas las posiciones abiertas, así como las órdenes límite no alcanzadas por el precio, se cerrarán. Y se colocará una nueva cuadrícula.

Podemos ver un ejemplo de cuadrícula abierta en esta captura de pantalla:

Ejemplo de cuadrícula colocada

De esta forma, en teoría, los sistemas comerciales de cuadrícla permiten obtener beneficio en cualquier mercado, sin esperar puntos de entrada únicos, ni tampoco usar indicador alguno.

Si se usan stop loss y take profit, el beneficio se logrará gracias a que las pérdidas de algunas posiciones serán cubiertas por el beneficio total del resto en el caso de que el precio se mueva en una dirección.

Si no se usan stop loss y take profit, el beneficio se obtendrá gracias a la apertura de un mayor número de órdenes en la dirección correcta. Incluso si inicialmente el precio engancha una posición en una dirección, y después vira, las nuevas posiciones en la dirección correcta cubrirán las pérdidas de las anteriormente abiertas, ya que serán más en total.

Principio de funcionamiento de nuestro gradador

Más arriba hemos explicado el principio de funcionamiento del gradador más sencillo. Aparte de él, podemos pensar nuestras propias variantes de cuadrícula, modificando la dirección de apertura de las órdenes, añadiendo la posibilidad de abrir varias órdenes a un mismo precio, añadiendo indicadores, etc.

En este artículo, primero vamos a intentar implementar la variante más sencilla de cuadrícula sin stop loss, ya que la idea sobre la que funciona es muy atractiva.

Y es que, realmente, la idea de que el precio tarde o temprano traerá beneficios al moverse en una dirección, incluso si inicialmente se han abierto posiciones en la dirección incorrecta, se antoja muy tentadora. Supongamos que el precio, en primer lugar, entre en corrección y toque 2 órdenes. Después de ello, el precio primero se mueve en la dirección opuesta, es decir, en la dirección de la tendencia principal. En este caso, tarde o temprano, se abrirán más de 2 órdenes en la dirección correcta, y al moverse en la misma dirección, pasado un tiempo, nuestras pérdidas iniciales se convertirán en beneficio. ¿Por qué no debería funcionar este sistema comercial?

Y es que parece que el único caso en el que este sistema puede provocar pérdidas es cuando el precio toca primero una orden, luego retrocede y toca la opuesta, y después cambia de nuevo su dirección y toca otra orden más. Y cambia una vez más su dirección, modificando así su rumbo constantemente, tocando órdenes cada vez más alejadas. Pero, ¿es posible que el precio muestre ese comportamiento en la vida real?

Plantilla del asesor

Comenzaremos a desarrollar el asesor por la plantilla que vamos a utilizar. Así, veremos de inmediato qué funciones estándar de MQL se usarán.

#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://www.mql5.com/ru/users/needtome"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
  }

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
   {
   }

Se diferencia de la plantilla estándar generada al crear el asesor con la ayuda del wizard MQL5, solamente en la línea #property strict. La añadimos para que nuestro asesor también funcione en MQL4.

Necesitamos la función OnChartEvent() para tener la posibilidad de reaccionar a la pulsación de botones. A continuación en el artículo, vamos a implementar el botón Cerrar todo, con su ayuda, podremos cerrar todas las posiciones y órdenes de un instrumento si hemos alcanzado el capital deseado o simplemente queremos finalizar el funcionamiento del asesor.

Función de apertura de posiciones

Seguramente, la funcionalidad más importante de cualquier asesor sea la posibilidad de colocar una orden. Y en ese punto nos esperan los primeros problemas. En MQL5 y MQL4 las órdenes se colocan de formas totalmente distintas. Y para unificar de alguna manera esta funcionalidad, tendremos que desarrollar una función propia para la colocación de órdenes.

Cada orden tiene su tipo: orden de compra, orden de venta, orden límite de compra o de venta. La variable en la que se establece este tipo al colocar una orden, también se diferencia en MQL5 y MQL4.

En MQL4, el tipo de orden se establece con una variable del tipo int. Mientras que en MQL5 se usa la enumeración especial ENUM_ORDER_TYPE. Y, lo que es más, dicha enumeración no existe en MQL4.x Y, lo que es más, dicha enumeración no existe en MQL4. Por eso, para combinar ambos métodos, vamos a crear una enumeración propia, que usaremos precisamente para establecer el tipo de orden. Gracias a ello, la función que vamos a crear posteriormente no dependerá de la versión de MQL:

enum TypeOfPos{
   MY_BUY,
   MY_SELL,
   MY_BUYSTOP,
   MY_BUYLIMIT,
   MY_SELLSTOP,
   MY_SELLLIMIT,
}; 

Ahora, podemos crear una función propia para colocar órdenes. La llamaremos pdxSendOrder(). En ella transmitiremos todo lo necesario para colocar una orden: el tipo de orden, el precio de apertura, el stop loss (0, si no lo definimos), el take profit (0, si no lo definimos), el volumen, el ticket de la posición abierta (si debemos modificar una posición abierta en MQL5), el comentario y el símbolo (si debemos abrir una orden según un símbolo distinto al abierto en el gráfico actual):

// order sending function
bool pdxSendOrder(TypeOfPos mytype, double price, double sl, double tp, double volume, ulong position=0, string comment="", string sym=""){
   // check passed values
   if( !StringLen(sym) ){
      sym=_Symbol;
   }
   int curDigits=(int) SymbolInfoInteger(sym, SYMBOL_DIGITS);
   if(sl>0){
      sl=NormalizeDouble(sl,curDigits);
   }
   if(tp>0){
      tp=NormalizeDouble(tp,curDigits);
   }
   if(price>0){
      price=NormalizeDouble(price,curDigits);
   }
   
   #ifdef __MQL5__ 
      ENUM_TRADE_REQUEST_ACTIONS action=TRADE_ACTION_DEAL;
      ENUM_ORDER_TYPE type=ORDER_TYPE_BUY;
      switch(mytype){
         case MY_BUY:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_BUY;
            break;
         case MY_BUYSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_STOP;
            break;
         case MY_BUYLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_LIMIT;
            break;
         case MY_SELL:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_SELL;
            break;
         case MY_SELLSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_STOP;
            break;
         case MY_SELLLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_LIMIT;
            break;
      }
      
      MqlTradeRequest mrequest;
      MqlTradeResult mresult;
      ZeroMemory(mrequest);
      
      mrequest.action = action;
      mrequest.sl = sl;
      mrequest.tp = tp;
      mrequest.symbol = sym;
      if(position>0){
         mrequest.position = position;
      }
      if(StringLen(comment)){
         mrequest.comment=comment;
      }
      if(action!=TRADE_ACTION_SLTP){
         if(price>0){
            mrequest.price = price;
         }
         if(volume>0){
            mrequest.volume = volume;
         }
         mrequest.type = type;
         mrequest.magic = EA_Magic;
         switch(useORDER_FILLING_RETURN){
            case FOK:
               mrequest.type_filling = ORDER_FILLING_FOK;
               break;
            case RETURN:
               mrequest.type_filling = ORDER_FILLING_RETURN;
               break;
            case IOC:
               mrequest.type_filling = ORDER_FILLING_IOC;
               break;
         }
         mrequest.deviation=100;
      }
      if(OrderSend(mrequest,mresult)){
         if(mresult.retcode==10009 || mresult.retcode==10008){
            return true;
         }else{
            msgErr(GetLastError(), mresult.retcode);
         }
      }
   #else 
      int type=OP_BUY;
      switch(mytype){
         case MY_BUY:
            type=OP_BUY;
            break;
         case MY_BUYSTOP:
            type=OP_BUYSTOP;
            break;
         case MY_BUYLIMIT:
            type=OP_BUYLIMIT;
            break;
         case MY_SELL:
            type=OP_SELL;
            break;
         case MY_SELLSTOP:
            type=OP_SELLSTOP;
            break;
         case MY_SELLLIMIT:
            type=OP_SELLLIMIT;
            break;
      }
      if(OrderSend(sym, type, volume, price, 100, sl, tp, comment, EA_Magic, 0)<0){
         msgErr(GetLastError());
      }else{
         return true;
      }
   
   #endif 
   return false;
}

Primero, comprobamos los valores transmitidos a la función y normalizamos los precios.

Parámetros de entrada. Después, con la ayuda de la compilación condicional, definimos la versión actual de MQL y colocamos una orden según sus reglas. Para MQL5, además, se usa el parámetro adicional de entrada useORDER_FILLING_RETURN. Con su ayuda, podemos ajustar el modo de ejecución de la orden, de acuerdo con los modos soportados por nuestro bróker. Puesto que el parámetro de entrada useORDER_FILLING_RETURN es necesario solo para la versión MQL5 del asesor, vamos a usar de nuevo la compilación condicional para añadirlo:

#ifdef __MQL5__ 
   enum TypeOfFilling //Filling Mode
     {
      FOK,//ORDER_FILLING_FOK
      RETURN,// ORDER_FILLING_RETURN
      IOC,//ORDER_FILLING_IOC
     }; 
   input TypeOfFilling  useORDER_FILLING_RETURN=FOK; //Filling Mode
#endif 

Asimismo, al colocar una orden, se usa el parámetro de entrada EA_Magic, que contiene el número mágico de nuestro asesor.

Si en los ajustes del asesor no se establece este parámetro, el asesor considerará como propia cualquier posición del símbolo en el que se encuentre iniciado. Y él mismo decidirá cuándo cerrarlas y qué hacer con ellas en general.

Si el número mágico ha sido establecido, el asesor tendrá en cuenta en su trabajo solo las posiciones con este número mágico.

Representación de errores. Si la orden se ha colocado con éxito, se retornará el valor true. En caso contrario, el código de error que aparecerá, se transmite a la función msgErr() para su posterior análisis y representación en un mensaje de error comprensible para el usuario. Esta función muestra un mensaje localizado con una descripción detallada del error surgido. No tiene sentido mostrar el código completo, por eso, mostraremos solo una parte del mismo:

void msgErr(int err, int retcode=0){
   string curErr="";
   switch(err){
      case 1:
         curErr=langs.err1;
         break;
//      case N:
//         curErr=langs.errN;
//         break;
      default:
         curErr=langs.err0+": "+(string) err;
   }
   if(retcode>0){
      curErr+=" ";
      switch(retcode){
         case 10004:
            curErr+=langs.retcode10004;
            break;
//         case N:
//            curErr+=langs.retcodeN;
//            break;
      }
   }
   
   Alert(curErr);
}

Hablaremos con más detalle sobre la localización en el siguiente apartado de este artículo.

Localización del asesor

Antes de continuar con el desarrollo del asesor, vamos a preocuparnos un poco de los futuros usuarios que desconocen el idioma ruso. O, por lo menos, de aquellos que saben inglés. Vamos a añadir la posibilidad de elegir en qué idioma mostrará el asesor todos sus mensajes. Tendremos dos idiomas: ruso e inglés.

Vamos a crear una enumeración con las posibles opciones de idioma y añadir un parámetro de entrada para seleccionar el idioma necesario:

enum TypeOfLang{
   MY_ENG, // English
   MY_RUS, // Русский
}; 

input TypeOfLang  LANG=MY_RUS; // Language

A continuación, crearemos una estructura que se usará para guardar todas las líneas de prueba utilizadas en el asesor. Después, declararemos una variable del tipo que hemos creado:

struct translate{
   string err1;
   string err2;
//   ... other strings
};
translate langs;

Ya tenemos una variable que contiene las líneas. Pero esta todavía no contiene las propias líneas. Vamos a crear una función que la rellenará con líneas en el idioma seleccionado en el parámetro de entrada Idioma. Llamaremos a la función init_lang(). Aquí mostramos parte de su código:

void init_lang(){
   switch(LANG){
      case MY_ENG:
         langs.err1="No error, but unknown result. (1)";
         langs.err2="General error (2)";
         langs.err3="Incorrect parameters (3)";
//         ... other strings
         break;
      case MY_RUS:
         langs.err0="Во время выполнения запроса произошла ошибка";
         langs.err1="Нет ошибки, но результат неизвестен (1)";
         langs.err2="Общая ошибка (2)";
         langs.err3="Неправильные параметры (3)";
//         ... other strings
         break;
   }
}

Lo único que nos queda es llamar la función init_lang(), para que las líneas se rellenen con los valores que necesitamos. El lugar ideal para llamarla es la función estándar OnInit(), ya que se llama justo después de iniciar el asesor. Y esto es justo lo que necesitamos.

Parámetros de entrada principales

Ha llegado el momento de añadir a nuestro asesor los parámetros de entrada principales. Aparte de EA_Magic y LANG, que ya hemos visto, añadiremos:

input double      Lot=0.01;     //Lot size
input uint        maxLimits=7;  //Number of limit orders in the grid in one direction
input int         Step=10;      //Grid step in points
input double      takeProfit=1; //Close deals when reaching the specified profit, $

Es decir, abriremos órdenes maxLimits en una dirección y las mismas en dirección opuesta. La primera orden se encontrará a Step puntos del precio actual. La segunda, a Step puntos de la primera orden, etc.

Fijaremos el beneficio en cuanto alcance takeProfit dólares. En este caso, además, se cerrarán todas las posiciones abiertas, y también se cancelarán todas las órdenes colocadas. Después de ello, el asesor colocará de nuevo su cuadrícula.

Ni siquiera vamos a analizar la posibilidad de perder, ya que el take profit será la única condición para el cierre de posiciones.

Rellenando la función OnInit

Como ya hemos dicho anteriormente OnInit() se llama una vez, al iniciar nuestro asesor. Ya le hemos añadido la llamada de nuestra función init_lang(). Vamos a rellenarla hasta el final, para no volver más a ella.

En el marco de nuestro asesor, lo único para lo que verdaderamente necesitamos la función OnInit(), es para corregir el parámetro de entrada Step, si el precio tiene 3 o 5 dígitos decimales. Es decir, si nuestro bróker para este instrumento usa un dígito decimal adicional:

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }

De esta forma, en el propio asesor, usaremos el parámetro ST corregido en lugar del parámetro de entrada Step. Vamos a declararlo antes de llamar cualquier función, indicando el tipo double.

Ya que posteriormente, para formar la cuadrícula necesitaremos la distancia, no en puntos, sino en el precio del instrumento, vamos a realizar la conversión directamente:

   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);

Asimismo, en esta función podemos comprobar si está permitido el comercio para nuestro asesor. Ya que, si el comercio está permitido, será mejor comunicar de inmediato este hecho al usuario, para que pueda corregir la situación.

Podemos realizar esta comprobación con la ayuda de un pequeño código:

   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string) EA_Magic+")");
      ExpertRemove();
   }   

Si el comercio está prohibido, informamos sobre ello en el idioma elegido por el usuario, después de lo cual, finalizamos el funcionamiento del asesor.

Como resultado, la función OnInit() adoptará el siguiente aspecto final:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   init_lang();
   
   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string) EA_Magic+")");
      ExpertRemove();
   }

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }
   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   return(INIT_SUCCEEDED);
  }

Añadiendo el botón Cerrar todo

La comodidad a la hora de trabajar con el asesor es tan importante como el hecho de que este siga la estrategia comercial al pie de la letra.

En nuestro caso, la comodidad se expresa en la posibilidad de comprender en un solo vistazo cuántas posiciones en Long y Short hay ya abiertas, así como el beneficio total de todas las posiciones abiertas en este momento.

Y lo que es igual de importante: deberemos tener la posibilidad de cerrar todas las órdenes abiertas y colocar posiciones, si el beneficio obtenido no nos conviene o algo va mal.

Por eso, vamos a añadir un botón en el que se representará toda la información necesaria, y al pulsar el cual se cerrarán todas las posiciones y órdenes.

Prefijo de objetos gráficos. Cada objeto gráfico en MetaTrader debe tener un nombre. En este caso, además, el nombre de los objetos creados por un mismo asesor no deberá coincidir con los nombres de los objetos creados en el gráfico manualmente o por otros asesores. Por eso, en primer lugar, vamos a definir el prefijo que añadiremos a los nombres de todos los objetos gráficos creados:

string prefix_graph="grider_";

Calculando las posiciones y el beneficio. Ahora, podemos crear una función que calculará el número de posiciones abiertas en Long y Short, así como el beneficio total de las mismas. Después de ello, mostramos el botón con la información obtenida o actualizamos el texto en el mismo, si el botón ya existe. Llamamos a la función getmeinfo_btn():

void getmeinfo_btn(string symname){
   double posPlus=0;
   double posMinus=0;
   double profit=0;
   double positionExist=false;

   // count the number of open Long and Short positions,
   // and total profit on them
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=symname) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         positionExist=true;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
         
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY){
            posPlus+=PositionGetDouble(POSITION_VOLUME);
         }else{
            posMinus+=PositionGetDouble(POSITION_VOLUME);
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=symname) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            positionExist=true;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
            
            if(OrderType()==OP_BUY){
               posPlus+=OrderLots();
            }else{
               posMinus+=OrderLots();
            }
         }
      }
   #endif 
   
   // if there are open positions,
   // add the close button
   if(positionExist){
      createObject(prefix_graph+"delall", 233, langs.closeAll+" ("+DoubleToString(profit, 2)+") L: "+(string) posPlus+" S: "+(string) posMinus);
   }else{
      // otherwise, delete the button for closing positions
      if(ObjectFind(0, prefix_graph+"delall")>0){
         ObjectDelete(0, prefix_graph+"delall");
      }
   }
   
   // update the current chart to display
   // the implemented changes
   ChartRedraw(0);
}

Aquí hemos usado por segunda vez la compilación condicional, puesto que la funcionalidad del trabajo con posiciones abiertas se diferencia en MQL5 y MQL4. Por este motivo, vamos a usar más de una vez la compilación condicional en el artículo.

Mostrando el botón. Asismismo, preste atención a que, para mostrar el botón en el gráfico, usamos la función propia createObject(). Esta función comprueba si existe ya en el gráfico un botón con el nombre transmitido como primer argumento de la función.

Si el botón ya ha sido creado, solo tenemos que actualizar el texto en este botón de acuerdo con el texto transmitido en el tercer argumento de la función.

Si no hay botón, lo crearemos en la esquina superior derecha del gráfico. En este caso, además, el argumento de la función establece la anchura del botón creado:

void createObject(string name, int weight, string title){
   // if there is no 'name' button on the chart, create it
   if(ObjectFind(0, name)<0){
      // define the shift relative to the chart right angle where the button is to be displayed
      long offset= ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
      long offsetY=0;
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset>offsetY){
            offsetY=tmpOffset;
         }
      }
      
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset!=offsetY ){
            continue;
         }
         
         tmpOffset=ObjectGetInteger(0, objName, OBJPROP_XDISTANCE);
         if( tmpOffset>0 && tmpOffset<offset){
            offset=tmpOffset;
         }
      }
      offset-=(weight+1);
      if(offset<0){
         offset=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
         offsetY+=25;
         offset-=(weight+1);
      }
  
     ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
     ObjectSetInteger(0,name,OBJPROP_XDISTANCE,offset); 
     ObjectSetInteger(0,name,OBJPROP_YDISTANCE,offsetY); 
     ObjectSetString(0,name,OBJPROP_TEXT, title); 
     ObjectSetInteger(0,name,OBJPROP_XSIZE,weight); 
     ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 8);
     ObjectSetInteger(0,name,OBJPROP_COLOR, clrBlack);
     ObjectSetInteger(0,name,OBJPROP_YSIZE,25); 
     ObjectSetInteger(0,name,OBJPROP_BGCOLOR, clrLightGray);
     ChartRedraw(0);
  }else{
     ObjectSetString(0,name,OBJPROP_TEXT, title);
  }
}

Respuesta a la pulsación del botón. Ahora, si llamamos a la función getmeinfo_btn(), en el gráfico aparacerá el botón Cerrar todo… (si tenemos posiciones abiertas). Sin embargo, al pulsar este botón, no sucederá nada.

Para añadir la respuesta a la pulsación de nuestro botón, debemos interceptar esta pulsación en la función estándar OnChartEvent(). Puesto que esto es lo único para lo que necesitamos la función OnChartEvent(), podemos mostrar su código final:

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
{
   string text="";
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         // if the name of the clicked button is prefix_graph+"delall", then
         if (sparam==prefix_graph+"delall"){
            closeAllPos();
         }
         break;
   }
}

Ahora, al pulsar el botón de cierre de posiciones, se llamará a la función closeAllPos(). Esta función todavía no ha sido creada. Pero solucionaremos este problema en el siguiente apartado del artículo.

Acciones adicionales. Ya tenemos la función getmeinfo_btn(), que calcula los datos que necesitamos y muestra el botón de cierre de posiciones. Lo que es más, hemos implementado incluso la acción que tendrá lugar al pulsar este botón. Sin embargo, la propia función getmeinfo_btn() no se llama aún en ninguna parte del asesor. Por eso, no se mostrará por el momento en el gráfico.

Usaremos la función getmeinfo_btn() cuando nos ocupemos del código de la función estándar OnTick().

Por el momento, podemos recordar sobre la función OnDeInit(). Dado que nuestro asesor crea un objeto gráfico, debemos ocuparnos de que al cerrar el asesor, todos los objetos gráficos creados por él sean eliminados del gráfico. Para ello, vamos a necesitar la función OnDeInit(). Esta se llama de forma automática precisamente al cerrar el asesor.

Como resultado, el cuerpo de la función OnDeInit() tendrá el aspecto que sigue:

void OnDeinit(const int reason)
  {
      ObjectsDeleteAll(0, prefix_graph);
  }

Con la ayuda de esta línea, al cerrar el asesor, eliminamos del gráfico todos los objetos gráficos cuyos nombres contengan nuestro prefijo. Por ahora, solo tenemos un objeto de este tipo.

Vamos a implementar la función de cierre de todas las posiciones

Ya que hemos comenzado a usar la función closeAllPos(), vamos a implementar su código.

La función closeAllPos() cierra todas las posiciones abiertas en este momento, asimismo, elimina todas las órdenes colocadas.

Pero no todo resulta tan simple. Esta función no solo elimina todas las posiciones abiertas en este momento. Si tenemos una posición abierta en Long y la misma posición abierta en Short, trataremos de cerrar una de estas posiciones con la opuesta. Si el bróker con el que usted trabaja permite esta operación con el instrumento actual, de esta forma recuperaremos el spread que hemos pagado por abrir las dos posiciones. Gracias a esto, la rentabilidad de nuestro asesor aumentará. Así, al cerrar todas las posiciones por take profit, en realidad obtendremos un beneficio un poco mayor que el indicado en el parámetro de entrada takeProfit.

De esta forma, con la primera línea de la función closeAllPos(), se dará la llamada de otra función más: closeByPos().

La función closeByPos() precisamente intentará cerrar las posiciones con la ayuda de las posiciones abiertas opuestas. Después de que todas las opuestas sean cerradas, la función closeAllPos() cerrará las posiciones restantes de la forma habitual. Tras lo cual, cerrará las órdenes pendientes.

Sucede que, para cerrar las posiciones en MQL5, el autor utiliza el objeto CTrade. Por eso, antes de implementar nuestras dos funciones, vamos a incluir esta clase. Después crearemos de inmediato un objeto de esta clase:

#ifdef __MQL5__ 
   #include <Trade\Trade.mqh>
   CTrade Trade;
#endif 

Ahora, podemos proceder a escribir una función que cierre todas las posiciones con opuestas:

void closeByPos(){
   bool repeatOpen=false;
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){
            long closefirst=PositionGetInteger(POSITION_TICKET);
            double closeLots=PositionGetDouble(POSITION_VOLUME);
            
            for(int ti2=cntMyPos-1; ti2>=0; ti2--){
               if(PositionGetSymbol(ti2)!=_Symbol) continue;
               if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
               if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) continue;
               if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) continue;
               
               MqlTradeRequest request;
               MqlTradeResult  result;
               ZeroMemory(request);
               ZeroMemory(result);
               request.action=TRADE_ACTION_CLOSE_BY;
               request.position=closefirst;
               request.position_by=PositionGetInteger(POSITION_TICKET);
               if(EA_Magic>0) request.magic=EA_Magic;
               if(OrderSend(request,result)){
                  repeatOpen=true;
                  break;
               }
            }
            if(repeatOpen){
               break;
            }
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               int closefirst=OrderTicket();
               double closeLots=OrderLots();
               
               for(int ti2=cntMyPos-1; ti2>=0; ti2--){
                  if(OrderSelect(ti2,SELECT_BY_POS,MODE_TRADES)==false) continue; 
                  if( OrderSymbol()!=_Symbol ) continue;
                  if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
                  if( OrderType()!=OP_SELL ) continue;
                  if( OrderLots()<closeLots ) continue;
                  
                  if( OrderCloseBy(closefirst, OrderTicket()) ){
                     repeatOpen=true;
                     break;
                  }
               }
               if(repeatOpen){
                  break;
               }
            }
                        
         }
      }
   #endif 
   // if we closed a position by an opposite one,
   // launch the closeByPos function again
   if(repeatOpen){
      closeByPos();
   }
}

Esta función se llama a sí misma, si se ha logrado cerrar una posición con otra opuesta. Esto resulta imprescindible, ya que las posiciones pueden distinguirse, y no siempre el cierre de dos posiciones va a provocar realmente el cierre de estas. Nos referimos a que, si los volúmenes son distintos, una de las posiciones, en lugar de cerrarse, verá su volumen reducido, y en el siguiente inicio de la función, será posible cerrarla con una nueva posición opuesta.

Después de que hayan sido cerradas todas las posiciones opuestas, la función closeAllPos() ejecuta el cierre de las demás:

void closeAllPos(){
   closeByPos();
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;

         Trade.PositionClose(PositionGetInteger(POSITION_TICKET));
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         
         Trade.OrderDelete(orderTicket);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.bid,100)){
               }
            }else if(OrderType()==OP_SELL){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.ask,100)){
               }
            }else{
               if(!OrderDelete(OrderTicket())){
               }
            }
                        
         }
      }
   #endif 
   // delete the position closing button
   if(ObjectFind(0, prefix_graph+"delall")>0){
      ObjectDelete(0, prefix_graph+"delall");
   }

}

Implementando la función OnTick

Ya hemos implementando prácticamente toda la funcionalidad del asesor. Solo queda más importante: colocar la cuadrícula de órdenes. Vamos a ocuparnos finalmente de ello.

La función OnTick() se llama con la llegada de cada tick del instrumento. Precisamente en ella vamos a comprobar si existe nuestra cuadrícula de órdenes, y si no, la crearemos.

Comprobando el inicio de la barra. Sin embargo, resulta excesivo comprobar cada tick. Sería suficiente comprobar si hay una cuadrícula, por ejemplo, una vez cada 5 minutos. Para ello, vamos a añadir a la función OnTick() un código que compruebe el inicio de la barra. Y si no se trata del primer tick del inicio de la barra, finalizaremos el funcionamiento de la función sin hacer nada:

   if( !pdxIsNewBar() ){
      return;
   }

La función pdxIsNewBar() tiene el siguiente aspecto:

bool pdxIsNewBar(){
   static datetime Old_Time;
   datetime New_Time[1];

   if(CopyTime(_Symbol,_Period,0,1,New_Time)>0){
      if(Old_Time!=New_Time[0]){
         Old_Time=New_Time[0];
         return true;
      }
   }
   return false;
}

Por consiguiente, para que el asesor compruebe nuestras condiciones una vez cada 5 minutos, deberemos iniciarlo en el marco temporal M5.

Comprobando el take profit. Antes de comprobar si hay alguna cuadrícula, debemos comprobar si se ha alcanzado el take profit de todas las posiciones abiertas en este momento en la cuadrícula que ya hemos abierto. Si es así, llamaremos a la función closeAllPos(), que ya conocemos.

   if(checkTakeProfit()){
      closeAllPos();
   }

Para comprobar la presencia de take profit, vamos a llamar a la función checkTakeProfit(). Esta calcula el beneficio de todas las posiciones abiertas en este momento y lo compara con el valor del parámetro de entrada takeProfit:

bool checkTakeProfit(){
   if( takeProfit<=0 ) return false;
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   if(profit>takeProfit){
      return true;
   }
   return false;
}

Mostrando el botón Cerrar todo.... Pero esto no es todo. No debemos olvidarnos del botón Cerrar todo…, que ya hemos implementado, pero que no hemos mostrado. Es el momento ideal para añadir la llamada de su función:

getmeinfo_btn(_Symbol);

Este será su aspecto aproximado:

Botón Cerrar todo...

Colocando la cuadrícula. Y, al fin, vamos a pasar a la parte más importante de nuestro asesor. Parece muy sencilla, ya que todo el código se encuentra de nuevo oculto tras las funciones:

   // if a symbol features open positions or placed orders, then
   if( existLimits() ){
   }else{
   // otherwise, place the grid
      initLimits();
   }

La función existLimits() retorna true si hay posiciones abiertas o se ha colocado alguna orden de este instrumento:

bool existLimits(){
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         return true;
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         return true;
      }
   #else 
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            return true;
         }
      }
   #endif 
   
   return false;
}

Si esta función ha retornado true, no hacemos nada. De lo contrario, colocamos una nueva cuadrícula de órdenes con la función initLimits():

void initLimits(){
   // price for setting grid orders
   double curPrice;
   // current symbol price
   MqlTick lastme;
   SymbolInfoTick(_Symbol, lastme);
   // if no current price is obtained, cancel placing the grid
   if( lastme.bid==0 ){
      return;
   }

   // minimum distance from the price available for placing stop losses and,
   // most probably, pending orders
   double minStop=SymbolInfoDouble(_Symbol, SYMBOL_POINT)*SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   
   // place Long orders
   curPrice=lastme.bid;
   for(uint i=0; i<maxLimits; i++){
      curPrice+=ST;

      if( curPrice-lastme.ask < minStop ) continue;
      if(!pdxSendOrder(MY_BUYSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
   // place Short orders
   curPrice=lastme.ask;
   for(uint i=0; i<maxLimits; i++){
      curPrice-=ST;
      if( lastme.bid-curPrice < minStop ) continue;
      if(!pdxSendOrder(MY_SELLSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
}

Poniendo a prueba el funcionamiento del asesor

Felicidades, nuestro asesor está listo. Ha llegado el momento de hacer pruebas y sacar conclusiones respecto al rendimiento de la estrategia comercial que hamos diseñado.

Puesto que nuestro asesor funciona tanto en MetaTrader 4, como en MetaTrader 5, podemos elegir en qué versión del terminal vamos a realizar la simulación. Aunque, en este caso, la elección parece obvia. El simulador de estrategias de MetaTrader 5 es más claro visualmente y, según se dice, tiene mayor calidad.

Para comenzar, vamos a realizar la simulación sin ninguna optimización. Nuestro asesor no debería depender especialmente del valor de los parámetros de entrada, si usamos valores razonables. Vamos a elegir:

Vamos a dejar los parámetros de entrada por defecto (lote 0.01, salto 10 puntos, 7 órdenes en la cuadrícula, take profit 1 dólar).

El resultado se muestra en la imagen:

Gráfico de balance en la primera simulación del asesor

Como podemos ver por el gráfico, durante un mes y una semana, todo ha marchado bien. Hemos podido ganar 100 dólares con una reducción de 30 dólares. Y después ha sucedido lo que nos parecía imposible. Vamos a echar un vistazo al visualizador, y veremos cómo se movía el precio en septiembre:

Resultado de la visualización

Todo comenzó el 13 de septiembre, poco después de las 16:15. Primero, el precio tocó una orden en Long; después, 2 órdenes en Short; más tarde, 2 órdenes en Long; y a continuación, las 5 órdenes en Short restantes. En total, tenemos 3 órdenes en Long y 7 órdenes en Short abiertas.

A continuación, aunque no podemos verlo por la imagen, el precio no continuó su movimiento, y regresó al punto superior el 20 de septiembre, tocando las 4 órdenes en Long restantes.

En total, tenemos las 7 órdenes en Short y las 7 órdenes en Long abiertas. Por lo tanto, ya nunca alcanzaremos el take profit.

Si miramos el movimiento posterior del precio, este irá hacia arriba unos 80 puntos más. Si en la cadena no tuviéramos 7 órdenes, sino, digamos, 13, podríamos salir de esta situación con beneficios.

Pero, por si fuera poco, más tarde, el precio descendería 200 puntos, así que con 30 órdenes en la cadena, teóricamente, podríamos obtener un take profit. Bien es cierto que tardaríamos un mes en ello. Y que la reducción sería considerable.

Poniendo a prueba el número de órdenes en la cuadrícula. Vamos a comprobar nuestras suposiciones. 13 órdenes en la cuadrícula no han cambiado nada. Pero 20 órdenes si que nos han permitido salirnos con la nuestra:

Simulación de EURUSD, 20 órdenes en la cuadrícula

Pero nuestra reducción estaba en torno a los 300 dólares, mientras que el beneficio era de poco más de 100. Así que nuestra estrategia puede sobrevivir y puede funcionar, pero necesita cambios cardinales.

Por este motivo, la optimización no tiene ahora especial sentido. Aun así, vamos a intentar realizarla.

Optimizando. La optimización se realizará con los siguientes parámetros:

Los mejores resultados los ha dado el salto de 13 puntos, con un número de órdenes en la cuadrícula igual a 16:

Simulación de EURUSD, 16 órdenes en la cuadrícula, salto de 13 puntos

Se trata del resultado de la simulación en el modo Cada tick basado en datos reales. A pesar de que el resultado es positivo, el beneficio en cinco meses es de 119 dólares con una reducción de 221 dólares, por lo que no estamos ante el mejor resultado posible. Así que nuestra estrategia comercial verdaderamente requiere mejoras.

Opciones de mejora de la estrategia comercial

Parece que un solo take profit para todas las posiciones resulta insuficiente. A veces, se dan situaciones en las que el precio toca todas o la mayoría de las órdenes en ambas direcciones. En tal caso, podemos esperar a obtener beneficio, sino una eternidad, sí bastantes meses.

Vamos a pensar qué podemos hacer para resolver este problema.

Control manual. Claro que lo más sencillo sería controlar el asesor manualmente de vez en cuando. Y si surge una situación problemática, tratar de solucionarla colocando órdenes adicionales o simplemente cerrando todas las órdenes.

Colocando una cuadrícula adicional. Podemos intentar colocar otra cuadrícula, si, por ejemplo, el 70% de las órdenes en una dirección y el 70% de las órdenes en la otra han sido tocadas. Y tener la esperanza de que las órdenes de la cuadrícula adicional nos permitan aumentar el número de posiciones abiertas en una dirección, logrando con ello alcanzar más rápido el take profit.

Aparte del número de posiciones abiertas, podemos comprobar la fecha de apertura de la última posición. Y si, desde el momento de apertura de la última posición ha pasado más de una semana, colocar una nueva cuadrícula.

Con ambas opciones, corremos el riesgo de complicar aun más la situación, aumentando una reducción ya de por sí considerable.

Cerrando la cuadrícula completa y abriendo una nueva. Aparte de colocar una cuadrícula adicional, tenemos la opción de cerrar todas las posiciones y órdenes colocadas de la cuadrícula actual, aceptando con ello nuestra derrota en esta batalla, pero no en la guerra.

Hay más opciones cuando podemos hacer esto:

Como ejemplo, vamos a intentar implementar el último punto de esta lista. Para ser más concretos, el parámetro de entrada en el que definiremos el tamaño de las pérdidas en dólares, con el que deberemos cerrar las posiciones de la cuadrícula actual y abrir una nueva. Definiremos las pérdidas con un número inferior a 0:

input double      takeLoss=0; //Close in case of a loss, $

Ahora, deberemos reescribir la función checkTakeProfit(), para que retorne, no true o false, sino el beneficio actual de todas las posiciones abiertas:

double checkTakeProfit(){
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   return profit;
}

Hemos destacado los cambios en amarillo.

Bien, ahora podemos mejorar la función OnTick(), para que en ella se compruebe no solo el take profit, sino también el stop loss de todas las posiciones:

   if(takeProfit>0 && checkTakeProfit()>takeProfit){
      closeAllPos();
   }else if(takeLoss<0 && checkTakeProfit()<takeLoss){
      closeAllPos();
   }

Simulación adicional

Veamos si hemos conseguido alguna mejora.

Solo vamos a optimizar el stop loss en dólares. En el rango de los -5 a los -100 dólares. El resto de parámetros los dejaremos igual que en la última simulación (salto de 13 puntos, número de órdenes en la cuadrícula 16).

Como resultado, obtenemos mayor beneficio con un stop loss de -56 dólares. El beneficio en 5 meses es de 156 dólares con una reducción máxima de 83 dólares:

Simulación de EURUSD, stop loss de -56 dólares

Mirando al gráfico, podemos notar que el stop loss en 5 meses se ha activado una sola vez. Y, la proporción de beneficio con respecto a la reducción es mejor, por supuesto.

Sin embargo, antes de sacar conclusiones definitivas, vamos a comprobar si nuestro asesor puede reportar algún beneficio a largo plazo con los parámetros seleccionados. Aunque sea durante los últimos 5 años:

Simulación de EURUSD con stop loss, 5 años

La imagen es demoledora. Es posible que una optimización adicional pueda mejorarla. Pero, en cualquier caso, el uso de esta estrategia de cuadrícula requiere mejoras radicales. Desde el punto de vista del comercio automático a largo plazo, resulta imposible confiar en que las posiciones abiertas adicionales nos puedan ayudar a salir hasta el beneficio.

Añadiendo stop loss y take profit para las órdenes

Por desgracia, las otras opciones de mejora del asesor que hemos enumerado anteriormente, no dan un mejor resultado. Pero, ¿qué podemos decir de los stop loss para transacciones individuales? Si no hemos conseguido hacer sin stop loss un asesor para el comercio automático a largo plazo, ¿quizá la adición de stop loss logre cambiarlo todo?

La optimización en la historia a 5 años ha mostrado resultados mejores que los de arriba.

El nivel de stop loss de 140 puntos y el take profit de 50 han mostrado los mejores resultados. en este caso, además, si no se ha abierto ninguna posición en la cuadrícula actual en los últimos 30 días, esta se cierra, abriéndose acto seguido una nueva.

Bien, aquñi tenemos el resultado final:

Usando stop loss y take profit para las órdenes

Obtenemos un beneficio de 351 dólares con una reducción de 226 dólares. Esto, por supuesto, está mucho mejor que los resultados sin stop loss. No obstante, no podemos dejar de notar que todos los resultados obtenidos al cerrar la cuadrícula actual generan pérdidas menos de 30 días después de realizar la última transacción. Los números superiores a 30, en su mayoría, también provocan pérdidas. Así que este resultado probablemente sea una casualidad, y no una norma.

Conclusión

El principal objeto de este artículo ha consistido en tratar de escribir un asesor comercial que funcione tanto en MetaTrader 4, como en MetaTrader 5. Y lo hemos conseguido.

Asimismo, hemos podido comprobar una vez más que la simulación de un asesor en varios meses de historia no es suficiente, si no tenemos la intención de ajustar sus parámetros de funcionamiento cada semana.

Por desgracia, no podemos decir que las ideas basadas en gradadores simples tenga razón de ser. Claro que puede que hayamos pasado algo por alto. Si el lector sabe cómo construir un gradador basado en principios simples que traiga beneficios, no dude en detallarlo en los comentarios.

Sin embargo, tampoco conviene pensar que las estrategias comerciales de cuadrículas no pueden dar beneficios. Por ejemplo, eche un vistazo a estas señales:

Se trata del mismo gradador que el descrito en el presente artículo, pero más complejo. Y verdaderamente puede reportar hasta un 100% de beneficio al depósito en un mes. Pero ya hablaremos de ello con mayor detalle en el próximo artículo sobre este tema.