Ejemplo de Gran Asesor Experto

Para generalizar y consolidar los conocimientos sobre las capacidades del probador, vamos a considerar paso a paso un amplio ejemplo de un Asesor Experto. En este ejemplo, resumiremos los siguientes aspectos:

  • Utilización de varios símbolos, incluida la sincronización de barras
  • Utilización de un indicador de un Asesor Experto
  • Utilización de eventos
  • Cálculo independiente de las principales estadísticas de trading
  • Cálculo del criterio de optimización personalizado R2 ajustado a lotes variables
  • Envío y tratamiento de frames con datos de aplicación (informes de trading desglosados por símbolos)

Utilizaremos MultiMartingale.mq5 como base técnica del Asesor Experto, pero lo haremos menos arriesgado pasando a operar con señales de sobrecompra/sobreventa multidivisa y aumentando los lotes sólo como añadido opcional. Anteriormente, en BandOsMA.mq5 vimos ya cómo operar basándose en señales de trading de indicadores. Esta vez utilizaremos UseUnityPercentPro.mq5 como indicador de la señal. Sin embargo, primero tenemos que modificarlo. Llamemos a la nueva versión UnityPercentEvent.mq5.

UnityPercentEvent.mq5

Recordemos la esencia del indicador Unity. Calcula la fuerza relativa de las divisas o tickers incluidos en un conjunto de instrumentos dados (se supone que todos los instrumentos tienen una divisa común a través de la cual es posible la conversión). En cada barra se forman lecturas para todas las divisas: algunas serán más caras, otras más baratas, y los dos elementos extremos se encuentran en un estado límite. Más adelante se pueden considerar dos estrategias esencialmente opuestas para ellas:

  • Más desglose (confirmación y continuación de un fuerte movimiento lateral)
  • Retroceso (inversión del movimiento hacia el centro debido a sobrecompra y sobreventa)

Para operar cualquiera de estas señales, debemos hacer un símbolo de trabajo de dos divisas (o tickers en general), si hay algo adecuado para esta combinación en la Observación de Mercado. Por ejemplo, si la línea superior del indicador pertenece a EUR y la inferior a USD, corresponden al par EURUSD, y según la estrategia de desglose deberíamos comprarlo pero según la estrategia de rebote, deberíamos venderlo.

En un caso más general, por ejemplo, cuando se indican CFD o materias primas con una divisa de cotización común en la cesta de instrumentos de trabajo del indicador, no siempre es posible crear un instrumento real. Para estos casos sería necesario complicar más el Asesor Experto introduciendo sintéticos de trading (posiciones compuestas), pero no lo haremos aquí y nos limitaremos al mercado Forex, donde casi todos los tipos cruzados suelen estar disponibles.

Por lo tanto, el Asesor Experto no sólo debe leer todos los búferes de los indicadores, sino también averiguar los nombres de las divisas, que corresponden a los valores máximos y mínimos. Y aquí tenemos un pequeño obstáculo.

MQL5 no permite leer los nombres de los búferes indicadores de terceros y, en general, cualquier propiedad de línea que no sea entera. Existen tres funciones para configurar las propiedades: PlotIndexSetInteger, PlotIndexSetDouble y PlotIndexSetString, pero sólo existe una función para leerlos, PlotIndexGetInteger.

En teoría, cuando los programas MQL compilados en un único complejo de trading son creados por el mismo desarrollador, esto no representa un gran problema. En concreto, podríamos separar una parte del código fuente del indicador en un archivo de encabezado e incluirlo no sólo en el indicador, sino también en el Asesor Experto. A continuación, en el Asesor Experto, sería posible repetir el análisis de los parámetros de entrada del indicador y restaurar la lista de divisas, completamente similar a la creada por el indicador. Duplicar los cálculos no es muy bonito, pero funcionaría. No obstante, también se requiere una solución más universal cuando el indicador tiene un desarrollador diferente, y éste no quiere revelar el algoritmo ni planea cambiarlo en el futuro (entonces las versiones compiladas del indicador y del Asesor Experto serán incompatibles). Este tipo de «acoplamiento» de indicadores ajenos con los propios, o de un Asesor Experto encargado a un servicio independiente, es una práctica muy común. Por lo tanto, el desarrollador del indicador debe facilitar al máximo su integración.

Una de las posibles soluciones es que el indicador envíe mensajes con los números y nombres de los búferes después de la inicialización.

Así es como se hace en el manejador OnInit del indicador UnityPercentEvent.mq5 (el código de abajo se muestra de forma abreviada ya que casi nada ha cambiado).

int OnInit()
{
   // find the common currency for all pairs
   const string common = InitSymbols();
   ...
   // set up the displayed lines in the currency cycle
   int replaceIndex = -1;
   for(int i = 0i <= SymbolCounti++)
   {
      string name;
      // change the order so that the base (common) currency goes under index 0,
      // the rest depends on the order in which the pairs are entered by the user
      if(i == 0)
      {
         name = common;
         if(name != workCurrencies.getKey(i))
         {
            replaceIndex = i;
         }
      }
      else
      {
         if(common == workCurrencies.getKey(i) && replaceIndex > -1)
         {
            name = workCurrencies.getKey(replaceIndex);
         }
         else
         {
            name = workCurrencies.getKey(i);
         }
      }
    
      // set up rendering of buffers
      PlotIndexSetString(iPLOT_LABELname);
      ...
      // send indexes and buffer names to programs where they are needed
      EventChartCustom(0, (ushort)BarLimitiSymbolCount + 1name);
   }
   ...
}

En comparación con la versión original, aquí sólo se ha añadido una línea. Contiene la llamada EventChartCustom. La variable de entrada BarLimit se utiliza como identificador de la copia del indicador (de las que potencialmente puede haber varias). Como el indicador se llamará desde el Asesor Experto y no se mostrará al usuario, basta con indicar un número positivo pequeño, al menos 1, pero tendremos, por ejemplo, 10.

Ahora el indicador está listo y sus señales se pueden utilizar en Asesores Expertos de terceros. Empecemos a desarrollar el Asesor Experto UnityMartingale.mq5. Para simplificar la presentación, la dividiremos en 4 etapas, añadiendo gradualmente nuevos bloques. Tendremos tres versiones preliminares y una versión final.

UnityMartingaleDraft1.mq5

En la primera etapa, para la versión UnityMartingaleDraft1.mq5, utilicemos MultiMartingale.mq5 como base y modifiquémosla.

Cambiaremos el nombre de la antigua variable de entrada StartType, que determinaba la dirección de la primera transacción de la serie, por SignalType. Se utilizará para elegir entre las estrategias consideradas BREAKOUT y PULLBACK.

enum SIGNAL_TYPE
{
   BREAKOUT,
   PULLBACK
};
...
input SIGNAL_TYPE StartType = 0// SignalType

Para configurar el indicador, necesitamos un grupo separado de variables de entrada.

input group "U N I T Y   S E T T I N G S"
input string UnitySymbols = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
input int UnityBarLimit = 10;
input ENUM_APPLIED_PRICE UnityPriceType = PRICE_CLOSE;
input ENUM_MA_METHOD UnityPriceMethod = MODE_EMA;
input int UnityPricePeriod = 1;

Tenga en cuenta que el parámetro UnitySymbols contiene una lista de instrumentos de clúster para construir un indicador, y normalmente difiere de la lista de instrumentos de trabajo con los que queremos operar. Los instrumentos negociados se siguen configurando en el parámetro WorkSymbols.

Por ejemplo, por defecto, pasamos un conjunto de los principales pares de divisas Forex al indicador, y por lo tanto podemos indicar como trading no sólo los pares principales, sino también cualquier cruce. Suele tener sentido limitar este conjunto a los instrumentos con las mejores condiciones de trading (en concreto, diferenciales pequeños o moderados). Además, es deseable evitar distorsiones, es decir, mantener una cantidad igual de cada divisa en todos los pares, neutralizando así estadísticamente los riesgos potenciales de elegir una dirección poco acertada para una de las divisas.

A continuación, envolvemos el control del indicador en la clase UnityController. Además del indicador handle, los campos de clase almacenan los siguientes datos:

  • El número de buffers del indicador, que se recibirán de los mensajes del indicador después de su inicialización.
  • El número de bar del que se están leyendo los datos (normalmente el incompleto actual es 0, o el último completado es 1)
  • El array data con los valores leídos de los búferes de los indicadores en la barra especificada
  • La última hora de lectura lastRead
  • Bandera de funcionamiento por ticks o barras tickwise

Además, la clase utiliza el objeto MultiSymbolMonitor para sincronizar las barras de todos los símbolos implicados.

class UnityController
{
   int handle;
   int buffers;
   const int bar;
   double data[];
   datetime lastRead;
   const bool tickwise;
   MultiSymbolMonitor sync;
   ...

En el constructor, que acepta todos los parámetros del indicador como argumentos, creamos el indicador y configuramos el objeto sync.

public:
   UnityController(const string symbolListconst int offsetconst int limit,
      const ENUM_APPLIED_PRICE typeconst ENUM_MA_METHOD methodconst int period):
      bar(offset), tickwise(!offset)
   {
      handle = iCustom(_Symbol_Period"MQL5Book/p6/UnityPercentEvent",
         symbolListlimittypemethodperiod);
      lastRead = 0;
      
      string symbols[];
      const int n = StringSplit(symbolList, ',', symbols);
      for(int i = 0i < n; ++i)
      {
         sync.attach(symbols[i]);
      }
   }
   
   ~UnityController()
   {
      IndicatorRelease(handle);
   }
   ...

El número de búferes se establece mediante el método attached. Lo llamaremos al recibir un mensaje del indicador.

   void attached(const int b)
   {
      buffers = b;
      ArrayResize(databuffers);
   }

Un método especial isReady devuelve true cuando las últimas barras de todos los símbolos tienen la misma hora. Sólo en el estado de dicha sincronización obtendremos los valores correctos del indicador. Cabe señalar que aquí se supone el mismo calendario de sesiones de trading para todos los instrumentos. Si no es así, habrá que modificar el análisis de los tiempos.

   bool isReady()
   {
      return sync.check(true) == 0;
   }

Definimos la hora actual de diferentes maneras dependiendo del modo de funcionamiento del indicador: cuando se recalcula en cada tick (tickwise es igual a true), utilizamos la hora del servidor, y cuando se recalcula una vez por barra, utilizamos la hora de apertura de la última barra.

   datetime lastTime() const
   {
      return tickwise ? TimeTradeServer() : iTime(_Symbol_Period0);
   }

La presencia de este método nos permitirá excluir la lectura del indicador si la hora actual no ha cambiado y, en consecuencia, los últimos datos leídos almacenados en el búfer data siguen siendo relevantes. Y así es como se organiza la lectura de los búferes indicadores en el método read. Sólo necesitamos un valor de cada búfer para la barra con el índice bar.

   bool read()
   {
      if(!buffersreturn false;
      for(int i = 0i < buffers; ++i)
      {
         double temp[1];
         if(CopyBuffer(handleibar1temp) == 1)
         {
            data[i] = temp[0];
         }
         else
         {
            return false;
         }
      }
      lastRead = lastTime();
      return true;
   }

Al final, sólo guardamos el tiempo de lectura en la variable lastRead. Si está vacía o no es igual a la nueva hora actual, el acceso a los datos del controlador en los siguientes métodos hará que se lean los búferes indicadores utilizando read.

Los principales métodos externos del controlador son getOuterIndices para obtener los índices de los valores máximo y mínimo y el operador '[]' para leer los valores.

   bool isNewTime() const
   {
      return lastRead != lastTime();
   }
   
   bool getOuterIndices(int &minint &max)
   {
      if(isNewTime())
      {
         if(!read()) return false;
      }
      max = ArrayMaximum(data);
      min = ArrayMinimum(data);
      return true;
   }
   
   double operator[](const int buffer)
   {
      if(isNewTime())
      {
         if(!read())
         {
            return EMPTY_VALUE;
         }
      }
      return data[buffer];
   }
};

Anteriormente, el Asesor Experto BandOsMA.mq5 introdujo el concepto de interfaz TradingSignal.

interface TradingSignal
{
   virtual int signal(void);
};

Basándonos en ella, describiremos la implementación de la señal utilizando el indicador UnityPercentEvent. El objeto controlador UnityController se pasa al constructor. También indica los índices de divisas (búferes), de cuyas señales queremos hacer un seguimiento. Podremos crear un conjunto arbitrario de señales diferentes para los símbolos de trabajo seleccionados.

class UnitySignalpublic TradingSignal
{
   UnityController *controller;
   const int currency1;
   const int currency2;
   
public:
   UnitySignal(UnityController *parentconst int c1const int c2):
      controller(parent), currency1(c1), currency2(c2) { }
   
   virtual int signal(voidoverride
   {
      if(!controller.isReady()) return 0// waiting for bars synchronization
      if(!controller.isNewTime()) return 0// waitng for time to change
      
      int minmax;
      if(!controller.getOuterIndices(minmax)) return 0;
      
      // overbought
      if(currency1 == max && currency2 == minreturn +1;
      // oversold
      if(currency2 == max && currency1 == minreturn -1;
      return 0;
   }
};

El método signal devuelve 0 en situación de incertidumbre y +1 o -1 en estados de sobrecompra y sobreventa de dos divisas concretas.

Para formalizar las estrategias de trading utilizamos la interfaz TradingStrategy.

interface TradingStrategy
{
   virtual bool trade(void);
};

En este caso, se crea sobre su base la clase UnityMartingale, que coincide en gran medida con SimpleMartingale de MultiMartingale.mq5. Sólo mostraremos las diferencias.

class UnityMartingalepublic TradingStrategy
{
protected:
   ...
   AutoPtr<TradingSignalcommand;
   
public:
   UnityMartingale(const Settings &stateTradingSignal *signal)
   {
      ...
      command = signal;
   }
   virtual bool trade() override
   {
      ...
      int s = command[].signal(); // get controller signal
      if(s != 0)
      {
         if(settings.startType == PULLBACKs *= -1// reverse logic for bounce
      }
      ulong ticket = 0;
      if(position[] == NULL// clean start - there were (and is) no positions
      {
         if(s == +1)
         {
            ticket = openBuy(settings.lots);
         }
         else if(s == -1)
         {
            ticket = openSell(settings.lots);
         }
      }
      else
      {
         if(position[].refresh()) // position exists
         {
            if((position[].get(POSITION_TYPE) == POSITION_TYPE_BUY && s == -1)
            || (position[].get(POSITION_TYPE) == POSITION_TYPE_SELL && s == +1))
            {
               // signal in the other direction - we need to close
               PrintFormat("Opposite signal: %d for position %d %lld",
                  sposition[].get(POSITION_TYPE), position[].get(POSITION_TICKET));
               if(close(position[].get(POSITION_TICKET)))
               {
                  // position = NULL; - save the position in the cache
               }
               else
               {
                  position[].refresh(); // control possible closing errors
               }
            }
            else
            {
               // the signal is the same or absent - "trailing"
               position[].update();
               if(trailing[]) trailing[].trail();
            }
         }
         else // no position - open a new one
         {
            if(s == 0// no signals
            {
               // here is the full logic of the old Expert Advisor:
               // - reversal for martingale loss
               // - continuation by the initial lot in a profitable direction
               ...
            }
            else // there is a signal
            {
               double lots;
               if(position[].get(POSITION_PROFIT) >= 0.0)
               {
                  lots = settings.lots// initial lot after profit
               }
               else // increase the lot after the loss
               {
                  lots = MathFloor((position[].get(POSITION_VOLUME) * settings.factor) / lotsStep) * lotsStep;
      
                  if(lotsLimit < lots)
                  {
                     lots = settings.lots;
                  }               
               }
               
               ticket = (s == +1) ? openBuy(lots) : openSell(lots);
            }
         }
      }
   }
   ...
}

La parte de trading está lista; queda por considerar la inicialización. En el nivel global se describen un puntero automático al objeto UnityController y el array con los nombres de las divisas. El conjunto de sistemas de trading es completamente similar a los desarrollos anteriores.

AutoPtr<TradingStrategyPoolpool;
AutoPtr<UnityControllercontroller;
   
int currenciesCount;
string currencies[];

En el manejador OnInit, creamos el objeto UnityController y esperamos a que el indicador envíe la distribución de divisas por índices del búfer.

int OnInit()
{
   currenciesCount = 0;
   ArrayResize(currencies0);
   
   if(!StartUp(true)) return INIT_PARAMETERS_INCORRECT;
   
   const bool barwise = UnityPriceType == PRICE_CLOSE && UnityPricePeriod == 1;
   controller = new UnityController(UnitySymbolsbarwise,
      UnityBarLimitUnityPriceTypeUnityPriceMethodUnityPricePeriod);
   // waiting for messages from the indicator on currencies in buffers
   return INIT_SUCCEEDED;
}

Si en los parámetros de entrada del indicador se selecciona el tipo de precio PRICE_CLOSE y un período único, el cálculo en el controlador se realizará una vez por barra. En todos los demás casos, las señales se actualizarán por ticks, pero no más a menudo que una vez por segundo (recuerde la implementación del método lastTime en el controlador).

El método de ayuda StartUp generalmente hace lo mismo que el antiguo manejador OnInit en el Asesor Experto MultiMartingale. Rellena la estructura Settings con opciones de configuración, comprobando que sean correctas y creando un conjunto de sistemas de trading TradingStrategyPool consistente en objetos de la clase UnityMartingale para diferentes símbolos de trading WorkSymbols. Sin embargo, ahora este proceso se divide en dos etapas debido a que tenemos que esperar información sobre la distribución de divisas entre los búferes. Por lo tanto, la función StartUp tiene un parámetro de entrada que denota una llamada de OnInit y posteriormente de OnChartEvent.

Al analizar el código fuente de StartUp, es importante recordar que la inicialización es diferente para los casos en que operamos con un solo instrumento que coincide con el gráfico actual y cuando se especifica una cesta de instrumentos. El primer modo está activo cuando WorkSymbols es una línea vacía. Es conveniente para optimizar un Asesor Experto para un instrumento específico. Una vez encontrados los ajustes para varios instrumentos, podemos combinarlos en WorkSymbols.

bool StartUp(const bool init = false)
{
   if(WorkSymbols == "")
   {
      Settings settings =
      {
         UseTimeHourStartHourEnd,
         LotsFactorLimit,
         StopLossTakeProfit,
         StartTypeMagicSkipTimeOnErrorTrailing_Symbol
      };
      
      if(settings.validate())
      {
         if(init)
         {
            Print("Input settings:");
            settings.print();
         }
      }
      else
      {
         if(initPrint("Wrong settings, please fix");
         return false;
      }
      if(!init)
      {
         ...// creating a trading system based on the indicator
      }
   }
   else
   {
      Print("Parsed settings:");
      Settings settings[];
      if(!Settings::parseAll(WorkSymbolssettings))
      {
         if(initPrint("Settings are incorrect, can't start up");
         return false;
      }
      if(!init)
      {
         ...// creating a trading system based on the indicator
      }
   }
   return true;
}

La función StartUp de OnInit se llama con el parámetro true, lo que significa que sólo se comprueba la corrección de los ajustes. La creación de un objeto del sistema de trading se retrasa hasta que se recibe un mensaje del indicador en OnChartEvent.

void OnChartEvent(const int id,
   const long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_CUSTOM + UnityBarLimit)
   {
      PrintFormat("%lld %f '%s'"lparamdparamsparam);
      if(lparam == 0ArrayResize(currencies0);
      currenciesCount = (int)MathRound(dparam);
      PUSH(currenciessparam);
      if(ArraySize(currencies) == currenciesCount)
      {
         if(pool[] == NULL)
         {
            start up(); // indicator readiness confirmation
         }
         else
         {
            Alert("Repeated initialization!");
         }
      }
   }
}

Aquí recordamos el número de divisas en la variable global currenciesCount y las almacenamos en el array currencies, tras lo cual llamamos a StartUp con el parámetro false (valor por defecto, por tanto omitido). Los mensajes llegan desde la cola en el orden en que existen en los búferes del indicador. Así, obtenemos una coincidencia entre el índice y el nombre de la divisa.

Cuando se vuelve a llamar a StartUp, se ejecuta un código adicional:

bool StartUp(const bool init = false)
{
   if(WorkSymbols == ""// one current symbol
   {
      ...
      if(!init// final initialization after OnInit
      {
         controller[].attached(currenciesCount);
         // split _Symbol into 2 currencies from the currencies array [] 
         int firstsecond;
         if(!SplitSymbolToCurrencyIndices(_Symbolfirstsecond))
         {
            PrintFormat("Can't find currencies (%s %s) for %s",
               (first == -1 ? "base" : ""), (second == -1 ? "profit" : ""), _Symbol);
            return false;
         }
         // create a pool from a single strategy
         pool = new TradingStrategyPool(new UnityMartingale(settings,
            new UnitySignal(controller[], firstsecond)));
      }
   }
   else // symbol basket
   {
      ...
      if(!init// final initialization after OnInit
      {
         controller[].attached(currenciesCount);
      
         const int n = ArraySize(settings);
         pool = new TradingStrategyPool(n);
         for(int i = 0i < ni++)
         {
            ...
            // split settings[i].symbol into 2 currencies from currencies[]
            int firstsecond;
            if(!SplitSymbolToCurrencyIndices(settings[i].symbolfirstsecond))
            {
               PrintFormat("Can't find currencies (%s %s) for %s",
                  (first == -1 ? "base" : ""), (second == -1 ? "profit" : ""),
                  settings[i].symbol);
            }
            else
            {
               // add a strategy to the pool on the next trading symbol
               pool[].push(new UnityMartingale(settings[i],
                  new UnitySignal(controller[], firstsecond)));
            }
         }
      }
   }

La función de ayuda SplitSymbolToCurrencyIndices selecciona la divisa base y la divisa de beneficio del símbolo pasado y encuentra sus índices en el array currencies. De este modo, obtenemos los datos de referencia para generar señales en los objetos de UnitySignal. Cada uno de ellos tendrá su propio par de índices de divisas.

bool SplitSymbolToCurrencyIndices(const string symbolint &firstint &second)
{
   const string s1 = SymbolInfoString(symbolSYMBOL_CURRENCY_BASE);
   const string s2 = SymbolInfoString(symbolSYMBOL_CURRENCY_PROFIT);
   first = second = -1;
   for(int i = 0i < ArraySize(currencies); ++i)
   {
      if(currencies[i] == s1first = i;
      else if(currencies[i] == s2second = i;
   }
   
   return first != -1 && second != -1;
}

En general, el Asesor Experto está listo.

Puede ver que en los últimos ejemplos de Asesores Expertos tenemos clases de estrategias y clases de señales de trading. Deliberadamente las hicimos descendientes de las interfaces genéricas TradingStrategy y TradingSignal para poder posteriormente recopilar colecciones de implementaciones compatibles pero diferentes que puedan combinarse en el desarrollo de futuros Asesores Expertos. Por lo general, estas clases concretas unificadas deben separarse en archivos de encabezado independientes. En nuestros ejemplos no lo hemos hecho para simplificar la modificación paso a paso.
 
No obstante, el enfoque descrito es estándar para la programación orientada a objetos (POO). En particular, como mencionamos en la sección sobre creación de borradores de Asesor Experto, junto con MetaTrader 5 viene un framework de archivos de encabezado con clases estándar de operaciones de trading, indicadores de señal, y gestión de dinero, que se utilizan en el Asistente MQL. En el sitio mql5.com se publican otras soluciones similares en los artículos y en la sección Code Base.
 
Puede utilizar las jerarquías de clases ya creadas como base para sus proyectos, siempre que sean adecuadas en términos de capacidades y facilidad de uso.

Para completar el cuadro, hemos querido introducir en el Asesor Experto nuestro propio criterio de optimización basado en R2. Para evitar la contradicción entre la regresión lineal en la fórmula de cálculo de R2 y los lotes variables que se incluyen en nuestra estrategia, calcularemos el coeficiente no para la línea de balance habitual, sino para sus incrementos acumulados normalizados por tamaños de lote en cada operación.

Para ello, en el manejador OnTester, seleccionamos transacciones con los tipos DEAL_TYPE_BUY y DEAL_TYPE_SELL y con la dirección OUT. Solicitaremos todas las propiedades de transacción que forman el resultado financiero (beneficio/pérdida), es decir, DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE, así como su volumen DEAL_VOLUME.

#defineSTAT_PROPS5// number of requested deal properties

 

doubleOnTester()

{

HistorySelect(0,LONG_MAX);

 

constENUM_DEAL_PROPERTY_DOUBLEprops[STAT_PROPS] =

{

DEAL_PROFIT,DEAL_SWAP,DEAL_COMMISSION,DEAL_FEE,DEAL_VOLUME

};

doubleexpenses[][STAT_PROPS];

ulongtickets[];// needed because of 'select' method prototype, but useful for debugging

 

DealFilterfilter;

filter.let(DEAL_TYPE, (1<<DEAL_TYPE_BUY) | (1<<DEAL_TYPE_SELL),IS::OR_BITWISE)

.let(DEAL_ENTRY, (1<<DEAL_ENTRY_OUT) | (1<<DEAL_ENTRY_INOUT) | (1<<DEAL_ENTRY_OUT_BY),

IS::OR_BITWISE)

.select(props,tickets,expenses);

...

A continuación, en el array balance, acumulamos los beneficios/pérdidas normalizados por los volúmenes de trading y calculamos el criterio R2 para ello.

   const int n = ArraySize(tickets);
   double balance[];
   ArrayResize(balancen + 1);
   balance[0] = TesterStatistics(STAT_INITIAL_DEPOSIT);
   
   for(int i = 0i < n; ++i)
   {
      double result = 0;
      for(int j = 0j < STAT_PROPS - 1; ++j)
      {
         result += expenses[i][j];
      }
      result /= expenses[i][STAT_PROPS - 1]; // normalize by volume
      balance[i + 1] = result + balance[i];
   }
   const double r2 = RSquaredTest(balance);
   return r2 * 100;
}

La primera versión del Asesor Experto está básicamente lista. No hemos incluido la comprobación del modelo de ticks mediante TickModel.mqh. Se supone que el Asesor Experto será probado cuando genere ticks en el modo OHLC M1 o mejor. Cuando se detecta el modelo «sólo precios abiertos», el Asesor Experto enviará un frame especial con un estado de error al terminal y se descargará del probador. Por desgracia, esto sólo detendrá este pase, pero la optimización continuará. Por lo tanto, la copia del Asesor Experto que se ejecuta en el terminal emite una «alerta» para que el usuario interrumpa la optimización manualmente.

void OnTesterPass()
{
   ulong   pass;
   string  name;
   long    id;
   double  value;
   uchar   data[];
   while(FrameNext(passnameidvaluedata))
   {
      if(name == "status" && id == 1)
      {
         Alert("Please stop optimization!");
         Alert("Tick model is incorrect: OHLC M1 or better is required");
         // it would be logical if the next call would stop all optimization,
         // but it is not
         ExpertRemove();
      }
   }
}

Puede optimizar los parámetros de SYMBOL SETTINGS para cualquier símbolo y repetir la optimización para símbolos diferentes. Al mismo tiempo, los grupos COMMON SETTINGS y UNITY SETTINGS deben contener siempre los mismos ajustes, ya que se aplican a todos los símbolos e instancias de los sistemas de trading. Por ejemplo, Trailing debe estar activado o desactivado para todas las optimizaciones. Observe también que las variables de entrada de un solo símbolo (es decir, el grupo SYMBOL SETTINGS) sólo tienen efecto mientras WorkSymbols contenga una cadena vacía. Por lo tanto, en la fase de optimización, debe mantenerla vacía.

Por ejemplo, para diversificar los riesgos, puede optimizar constantemente un Asesor Experto en pares completamente independientes: EURUSD, AUDJPY, GBPCHF, NZDCAD, o en otras combinaciones. El código fuente incluye tres archivos con ejemplos de configuraciones privadas.

#property tester_set "UnityMartingale-eurusd.set"
#property tester_set "UnityMartingale-gbpchf.set"
#property tester_set "UnityMartingale-audjpy.set"

Para operar con tres símbolos a la vez, estos ajustes deben «empaquetarse» en un parámetro común WorkSymbols:

EURUSD+0.01*1.6^5(200,200)[17,21];GBPCHF+0.01*1.2^8(600,800)[7,20];AUDJPY+0.01*1.2^8(600,800)[7,20]

Esta configuración también se incluye en un archivo aparte.

#property tester_set "UnityMartingale-combo.set"

Uno de los problemas con la versión actual del Asesor Experto es que el informe del probador proporcionará estadísticas generales para todos los símbolos (más precisamente, para todas las estrategias de trading, ya que podemos incluir diferentes clases en el conjunto), mientras que sería interesante para nosotros supervisar y evaluar cada componente del sistema por separado.

Para ello es necesario aprender a calcular de forma independiente los principales indicadores financieros de trading, por analogía con la forma en que el probador lo hace por nosotros. Nos ocuparemos de esto en la segunda etapa del desarrollo del Asesor Experto.

UnityMartingaleDraft2.mq5

El cálculo estadístico puede ser necesario con bastante frecuencia, por lo que lo implementaremos en un archivo de encabezado separado TradeReport.mqh, donde organizaremos el código fuente en las clases apropiadas.

Llamemos a la clase principal TradeReport. Muchas variables de trading dependen de las curvas de saldo y margen libre (fondos propios). Por lo tanto, la clase contiene variables para el seguimiento del saldo actual y el beneficio, así como un array constantemente actualizado con el historial de saldos. No almacenaremos el historial del capital, porque puede cambiar en cada tick, y es mejor calcularlo sobre la marcha. Veremos un poco más adelante la razón de tener la curva de equilibrio.

class TradeReport
{
   double balance;     // current balance
   double floating;    // current floating profit
   double data[];      // full balance curve - prices
   datetime moments[]; // and date/time
   ...

La modificación y lectura de los campos de la clase se realiza mediante métodos, incluido el constructor, en el que el saldo se inicializa mediante la propiedad ACCOUNT_BALANCE.

   TradeReport()
   {
      balance = AccountInfoDouble(ACCOUNT_BALANCE);
   }
   
   void resetFloatingPL()
   {
      floating = 0;
   }
   
   void addFloatingPL(const double pl)
   {
      floating += pl;
   }
   
   void addBalance(const double pl)
   {
      balance += pl;
   }
   
   double getCurrent() const
   {
      return balance + floating;
   }
   ...

Estos métodos serán necesarios para calcular iterativamente (sobre la marcha) la reducción del capital. El array de saldos data será necesario para un cálculo puntual de la reducción del saldo (lo haremos al final de la prueba).

En función de las fluctuaciones de la curva (da igual que sea de saldo o capital), se debe calcular la reducción absoluta y relativa utilizando el mismo algoritmo. Por lo tanto, este algoritmo y las variables internas necesarias para él, que almacenan estados intermedios, se implementan en la estructura anidada DrawDown. En el siguiente código se muestran sus principales métodos y propiedades:

   struct DrawDown
   {
      double
      series_start,
      series_min,
      series_dd,
      series_dd_percent,
      series_dd_relative_percent,
      series_dd_relative;
      ...
      void reset();
      void calcDrawdown(const double &data[]);
      void calcDrawdown(const double amount);
      void print() const;
   };

El primer método calcDrawdown calcula las reducciones cuando conocemos todo el array y esto se utilizará para el saldo. El segundo método calcDrawdown calcula la reducción de forma iterativa: cada vez que se le llama, se le indica el siguiente valor de la serie, y éste se utilizará para el capital.

Además de la reducción, como sabemos, existe un gran número de estadísticas estándar para los informes, pero para empezar sólo admitiremos algunas de ellas. Para ello, describimos los campos correspondientes en otra estructura anidada, GenericStats. Se hereda de DrawDown porque seguimos necesitando la reducción en el informe.

   struct GenericStatspublic DrawDown
   {
      long deals;
      long trades;
      long buy_trades;
      long wins;
      long buy_wins;
      long sell_wins;
      
      double profits;
      double losses;
      double net;
      double pf;
      double average_trade;
      double recovery;
      double max_profit;
      double max_loss;
      double sharpe;
      ...

Por los nombres de las variables es fácil adivinar a qué métrica estándar corresponden. Algunas métricas son redundantes y, por tanto, se omiten. Por ejemplo, dado el número total de operaciones (trades) y el número de operaciones de compra entre ellas (buy_trades), podemos encontrar fácilmente el número de operaciones de venta (trades - sell_trades). Lo mismo ocurre con las estadísticas complementarias de ganancias y pérdidas. Las rachas ganadoras y perdedoras no se tienen en cuenta. Quienes lo deseen pueden completar nuestro informe con estos indicadores.

Para la unificación con las estadísticas generales del probador, existe el método fillByTester que rellena todos los campos a través de la función TesterStatistics. Lo utilizaremos más adelante.

      void fillByTester()
      {
         deals = (long)TesterStatistics(STAT_DEALS);
         trades = (long)TesterStatistics(STAT_TRADES);
         buy_trades = (long)TesterStatistics(STAT_LONG_TRADES);
         wins = (long)TesterStatistics(STAT_PROFIT_TRADES);
         buy_wins = (long)TesterStatistics(STAT_PROFIT_LONGTRADES);
         sell_wins = (long)TesterStatistics(STAT_PROFIT_SHORTTRADES);
         
         profits = TesterStatistics(STAT_GROSS_PROFIT);
         losses = TesterStatistics(STAT_GROSS_LOSS);
         net = TesterStatistics(STAT_PROFIT);
         pf = TesterStatistics(STAT_PROFIT_FACTOR);
         average_trade = TesterStatistics(STAT_EXPECTED_PAYOFF);
         recovery = TesterStatistics(STAT_RECOVERY_FACTOR);
         sharpe = TesterStatistics(STAT_SHARPE_RATIO);
         max_profit = TesterStatistics(STAT_MAX_PROFITTRADE);
         max_loss = TesterStatistics(STAT_MAX_LOSSTRADE);
         
         series_start = TesterStatistics(STAT_INITIAL_DEPOSIT);
         series_min = TesterStatistics(STAT_EQUITYMIN);
         series_dd = TesterStatistics(STAT_EQUITY_DD);
         series_dd_percent = TesterStatistics(STAT_EQUITYDD_PERCENT);
         series_dd_relative_percent = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
         series_dd_relative = TesterStatistics(STAT_EQUITY_DD_RELATIVE);
      }
   };

Por supuesto, tenemos que implementar nuestro propio cálculo para aquellos saldos y fondos propios de los sistemas de trading que el probador no puede calcular. Anteriormente se han presentado prototipos de métodos de calcDrawdown. Durante la operación, rellenan el último grupo de campos con el prefijo «serie_dd». Además, la clase TradeReport contiene un método para calcular el ratio de Sharpe. Como entrada, toma una serie de números y un tipo de financiación sin riesgo. El código fuente completo se encuentra en el archivo adjunto.

   static double calcSharpe(const double &data[], const double riskFreeRate = 0);

Como se puede adivinar, al llamar a este método, el array miembro relevante de la clase TradeReport con saldos se pasará en el parámetro data. El proceso de llenar este array y llamar a los métodos anteriores para indicadores específicos ocurre en el método calcStatistics (ver abajo). Se le pasa un objeto filtro de transacciones como entrada (filter), depósito inicial (start) y tiempo (origin). Se supone que el código de llamada configurará el filtro de tal manera que sólo las operaciones del sistema de trading que nos interesa caigan bajo él.

El método devuelve una estructura llena GenericStats, y además, llena dos arrays dentro del objeto TradeReport, data y moments, con valores de saldo y referencias temporales de cambios, respectivamente. Lo necesitaremos en la versión final del Asesor Experto.

   GenericStats calcStatistics(DealFilter &filter,
      const double start = 0const datetime origin = 0,
      const double riskFreeRate = 0)
   {
      GenericStats stats;
      ArrayResize(data0);
      ArrayResize(moments0);
      ulong tickets[];
      if(!filter.select(tickets)) return stats;
      
      balance = start;
      PUSH(databalance);
      PUSH(momentsorigin);
      
      for(int i = 0i < ArraySize(tickets); ++i)
      {
         DealMonitor m(tickets[i]);
         if(m.get(DEAL_TYPE) == DEAL_TYPE_BALANCE//deposit/withdrawal
         {
            balance += m.get(DEAL_PROFIT);
            PUSH(databalance);
            PUSH(moments, (datetime)m.get(DEAL_TIME));
         }
         else if(m.get(DEAL_TYPE) == DEAL_TYPE_BUY 
            || m.get(DEAL_TYPE) == DEAL_TYPE_SELL)
         {
            const double profit = m.get(DEAL_PROFIT) + m.get(DEAL_SWAP)
               + m.get(DEAL_COMMISSION) + m.get(DEAL_FEE);
            balance += profit;
            
            stats.deals++;
            if(m.get(DEAL_ENTRY) == DEAL_ENTRY_OUT 
               || m.get(DEAL_ENTRY) == DEAL_ENTRY_INOUT
               || m.get(DEAL_ENTRY) == DEAL_ENTRY_OUT_BY)
            {
               PUSH(databalance);
               PUSH(moments, (datetime)m.get(DEAL_TIME));
               stats.trades++;        // trades are counted by exit deals
               if(m.get(DEAL_TYPE) == DEAL_TYPE_SELL)
               {
                  stats.buy_trades++; // closing with a deal in the opposite direction
               }
               if(profit >= 0)
               {
                  stats.wins++;
                  if(m.get(DEAL_TYPE) == DEAL_TYPE_BUY)
                  {
                     stats.sell_wins++; // closing with a deal in the opposite direction
                  }
                  else
                  {
                     stats.buy_wins++;
                  }
               }
            }
            else if(!TU::Equal(profit0))
            {
               PUSH(databalance); // entry fee (if any)
               PUSH(moments, (datetime)m.get(DEAL_TIME));
            }
            
            if(profit >= 0)
            {
               stats.profits += profit;
               stats.max_profit = fmax(profitstats.max_profit);
            }
            else
            {
               stats.losses += profit;
               stats.max_loss = fmin(profitstats.max_loss);
            }
         }
      }
      
      if(stats.trades > 0)
      {
         stats.net = stats.profits + stats.losses;
         stats.pf = -stats.losses > DBL_EPSILON ?
            stats.profits / -stats.losses : MathExp(10000.0); // NaN(+inf)
         stats.average_trade = stats.net / stats.trades;
         stats.sharpe = calcSharpe(datariskFreeRate);
         stats.calcDrawdown(data);     // fill in all fields of the DrawDown substructure
         stats.recovery = stats.series_dd > DBL_EPSILON ?
            stats.net / stats.series_dd : MathExp(10000.0);
      }
      return stats;
   }
};

Aquí puede ver cómo llamamos a calcSharpe y calcDrawdown para obtener los indicadores correspondientes en el array data. El resto de indicadores se calculan directamente en el bucle dentro de calcStatistics.

La clase TradeReport está lista, y podemos ampliar la funcionalidad del Asesor Experto a la versión UnityMartingaleDraft2.mq5.

Añadamos nuevos miembros a la clase UnityMartingale.

class UnityMartingalepublic TradingStrategy
{
protected:
   ...
   TradeReport report;
   TradeReport::DrawDown equity;
   const double deposit;
   const datetime epoch;
   ...

Necesitamos el objeto report para llamar a calcStatistics, donde se incluirá la reducción de saldos. El objeto equity es necesario para un cálculo independiente de la reducción de capital. El saldo y la fecha iniciales, así como el inicio del cálculo de la reducción del capital, se establecen en el constructor.

public:
   UnityMartingale(const Settings &stateTradingSignal *signal):
      symbol(state.symbol), deposit(AccountInfoDouble(ACCOUNT_BALANCE)),
      epoch(TimeCurrent())
   {
      ...
      equity.calcDrawdown(deposit);
      ...
   }

La continuación del cálculo de la reducción por capital se realiza sobre la marcha, con cada llamada al método trade.

   virtual bool trade() override
   {
      ...
      if(MQLInfoInteger(MQL_TESTER))
      {
         if(position[])
         {
            report.resetFloatingPL();
            // after reset, sum all floating profits
            // why we call addFloatingPL for each existing position,
            // but this strategy has a maximum of 1 position at a time
            report.addFloatingPL(position[].get(POSITION_PROFIT)
               + position[].get(POSITION_SWAP));
            // after taking into account all the amounts - update the drawdown
            equity.calcDrawdown(report.getCurrent());
         }
      }
      ...
   }

Esto no es todo lo que se necesita para un cálculo correcto. Hay que tener en cuenta la ganancia o pérdida flotante sobre el saldo. La parte de código anterior sólo muestra la llamada a addFloatingPL, pero la clase TradeReport también tiene un método para modificar el saldo: addBalance. No obstante, el saldo sólo cambia cuando se cierra la posición.

Gracias al concepto POO, cerrar una posición en nuestra situación corresponde a borrar el objeto position de la clase PositionState. Entonces, ¿por qué no podemos interceptarla?

La clase PositionState no proporciona ningún medio para ello, pero podemos declarar una clase derivada PositionStateWithEquity con un constructor y un destructor especiales.

Al crear un objeto, no sólo se pasa al constructor el identificador de posición, sino también un puntero al objeto de informe al que habrá que enviar información.

class PositionStateWithEquitypublic PositionState
{
   TradeReport *report;
   
public:
   PositionStateWithEquity(const long tTradeReport *r):
      PositionState(t), report(r) { }
   ...

En el destructor encontramos todas las operaciones por el ID de la posición cerrada, calculamos el resultado financiero total (junto con las comisiones y otras deducciones), y luego llamamos a addBalance para relacionar el objeto report.

   ~PositionStateWithEquity()
   {
      if(HistorySelectByPosition(get(POSITION_IDENTIFIER)))
      {
         double result = 0;
         DealFilter filter;
         int props[] = {DEAL_PROFITDEAL_SWAPDEAL_COMMISSIONDEAL_FEE};
         Tuple4<doubledoubledoubledoubleoverheads[];
         if(filter.select(propsoverheads))
         {
            for(int i = 0i < ArraySize(overheads); ++i)
            {
               result += NormalizeDouble(overheads[i]._12)
                  + NormalizeDouble(overheads[i]._22)
                  + NormalizeDouble(overheads[i]._32)
                  + NormalizeDouble(overheads[i]._42);
            }
         }
         if(CheckPointer(report) != POINTER_INVALIDreport.addBalance(result);
      }
   }
};

Queda por aclarar un punto: cómo crear objetos de clase PositionStateWithEquity para las posiciones en lugar de PositionState. Para ello, basta con cambiar el operador new en un par de lugares en los que se llama en la clase TradingStrategy.

position=MQLInfoInteger(MQL_TESTER) ?

newPositionStateWithEquity(tickets[0], &report) :newPositionState(tickets[0]);

Así, hemos puesto en marcha la recogida de datos. Ahora necesitamos generar directamente un informe, es decir, llamar a calcStatistics. Aquí tenemos que ampliar nuestra interfaz TradingStrategy: le añadimos el método statement.

interface TradingStrategy
{
   virtual bool trade(void);
   virtual bool statement();
};

A continuación, en esta implementación actual, pensada para nuestra estrategia, podremos llevar el trabajo a su conclusión lógica.

class UnityMartingalepublic TradingStrategy
{
   ...
   virtual bool statement() override
   {
      if(MQLInfoInteger(MQL_TESTER))
      {
         Print("Separate trade report for "settings.symbol);
         // equity drawdown should already be calculated on the fly
         Print("Equity DD:");
         equity.print();
         
         // balance drawdown is calculated in the resulting report
         Print("Trade Statistics (with Balance DD):");
         // configure the filter for a specific strategy
         DealFilter filter;
         filter.let(DEAL_SYMBOLsettings.symbol)
            .let(DEAL_MAGICsettings.magicIS::EQUAL_OR_ZERO);
           // zero "magic" number is needed for the last exit deal
           // - it is done by the tester itself
         HistorySelect(0LONG_MAX);
         TradeReport::GenericStats stats =
            report.calcStatistics(filterdepositepoch);
         stats.print();
      }
      return false;
   }
   ...

El nuevo método simplemente imprimirá todos los indicadores calculados en el registro. Reenviando el mismo método a través del conjunto de sistemas de trading TradingStrategyPool, vamos a solicitar informes separados para todos los símbolos al manejador OnTester.

double OnTester()
{
   ...
   if(pool[] != NULL)
   {
      pool[].statement(); // ask all trading systems to display their results
   }
   ...
}

Comprobemos si nuestro informe es correcto. Para ello, vamos a ejecutar el Asesor Experto en el probador, un símbolo cada vez, y a comparar el informe estándar con nuestros cálculos. Por ejemplo, para configurar UnityMartingale-eurusd.set, operando en EURUSD H1 obtendremos tales indicadores para 2021.

Informe del probador para 2021, EURUSD H1

Informe del probador para 2021, EURUSD H1

En el registro, nuestra versión se muestra como dos estructuras: DrawDown con reducción del capital y GenericStats con indicadores de reducción del saldo y otras estadísticas.

Informe comercial separado para EURUSD

Equity DD:

[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »

[0] 10022.48 10017.03 10000.00 9998.20 6.23 0.06 »

» [series_dd_relative_percent] [series_dd_relative]

» 0.06 6.23

 

Trade Statistics (with Balance DD):

[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »

[0] 10022.40 10017.63 10000.00 9998.51 5.73 0.06 »

» [series_dd_relative_percent] [series_dd_relative] »

» 0.06 5.73 »

» [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] »

» 194 97 43 42 19 23 57.97 -39.62 18.35 1.46 »

» [average_trade] [recovery] [max_profit] [max_loss] [sharpe]

» 0.19 3.20 2.00 -2.01 0.15

Es fácil comprobar que estos números coinciden con el informe del probador.

Ahora vamos a empezar a operar en el mismo periodo para tres símbolos a la vez (configurando UnityMartingale-combo.set).

Además de las entradas EURUSD, en el diario aparecerán estructuras para GBPCHF y AUDJPY.

Informe comercial separado para GBPCHF

Equity DD:

[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »

[0] 10029.50 10000.19 10000.00 9963.65 62.90 0.63 »

» [series_dd_relative_percent] [series_dd_relative]

» 0.63 62.90

Trade Statistics (with Balance DD):

[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »

[0] 10023.68 9964.28 10000.00 9964.28 59.40 0.59 »

» [series_dd_relative_percent] [series_dd_relative] »

» 0.59 59.40 »

» [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] »

» 600 300 154 141 63 78 394.53 -389.33 5.20 1.01 »

» [average_trade] [recovery] [max_profit] [max_loss] [sharpe]

» 0.02 0.09 9.10 -6.73 0.01

 

Informe comercial separado para AUDJPY

Equity DD:

[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »

[0] 10047.14 10041.53 10000.00 9961.62 48.20 0.48 »

» [series_dd_relative_percent] [series_dd_relative]

» 0.48 48.20

Trade Statistics (with Balance DD):

[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »

[0] 10045.21 10042.75 10000.00 9963.62 44.21 0.44 »

» [series_dd_relative_percent] [series_dd_relative] »

» 0.44 44.21 »

» [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] »

» 332 166 91 89 54 35 214.79 -170.20 44.59 1.26 »

» [average_trade] [recovery] [max_profit] [max_loss] [sharpe]

» 0.27 1.01 7.58 -5.17 0.09

En este caso, el informe del probador contendrá datos generalizados, por lo que, gracias a nuestras clases, hemos recibido detalles antes inaccesibles.

Sin embargo, consultar un pseudoinforme en un registro no resulta muy cómodo. Además, me gustaría ver una representación gráfica de la línea de balance como mínimo, ya que su aspecto suele decir más sobre la idoneidad del sistema que las meras estadísticas.

Vamos a mejorar el Asesor Experto dándole la capacidad de generar informes visuales en formato HTML: al fin y al cabo, los informes del probador también pueden exportarse a HTML, guardarse y compararse a lo largo del tiempo. Además, en el futuro, estos informes podrán transmitirse en frames al terminal justo durante la optimización, y el usuario podrá empezar a estudiar los informes de pasadas concretas incluso antes de que finalice todo el proceso.

Esta será la penúltima versión del ejemplo UnityMartingaleDraft3.mq5.

UnityMartingaleDraft3.mq5

La visualización del informe de trading incluye una línea de balance y una tabla con indicadores estadísticos. No generaremos un informe completo similar al del probador, sino que nos limitaremos a los valores seleccionados más importantes. Nuestro propósito es implantar un mecanismo de trabajo que luego pueda personalizarse en función de las necesidades personales.

Dispondremos la base del algoritmo en forma de la clase TradeReportWriter (TradeReportWriter.mqh). La clase podrá almacenar un número arbitrario de informes de diferentes sistemas de trading: cada uno en un objeto separado DataHolder, que incluye arrays de valores de saldo y marcas de tiempo (data y when, respectivamente), la estructura stats con estadísticas, así como el título, el color y la anchura de la línea que se desea mostrar.

class TradeReportWriter
{
protected:
   class DataHolder
   {
   public:
      double data[];                   // balance changes
      datetime when[];                 // balance timestamps
      string name;                     // description
      color clr;                       // color
      int width;                       // line width
      TradeReport::GenericStats stats// trading indicators
   };
   ...

Disponemos de un array de punteros automáticos curves asignados a los objetos de la clase DataHolder. Además, necesitaremos límites comunes en cuanto a importes y plazos para que coincidan con las líneas de todos los sistemas de trading del cuadro. Esto lo proporcionarán las variables lower, upper, start y stop.

   AutoPtr<DataHoldercurves[];
   double lowerupper;
   datetime startstop;
   
public:
   TradeReportWriter(): lower(DBL_MAX), upper(-DBL_MAX), start(0), stop(0) { }
   ...

El método addCurve añade una línea de balance.

   virtual bool addCurve(double &data[], datetime &when[], const string name,
      const color clr = clrNONEconst int width = 1)
   {
      if(ArraySize(data) == 0 || ArraySize(when) == 0return false;
      if(ArraySize(data) != ArraySize(when)) return false;
      DataHolder *c = new DataHolder();
      if(!ArraySwap(datac.data) || !ArraySwap(whenc.when))
      {
         delete c;
         return false;
      }
   
      const double max = c.data[ArrayMaximum(c.data)];
      const double min = c.data[ArrayMinimum(c.data)];
      
      lower = fmin(minlower);
      upper = fmax(maxupper);
      if(start == 0start = c.when[0];
      else if(c.when[0] != 0start = fmin(c.when[0], start);
      stop = fmax(c.when[ArraySize(c.when) - 1], stop);
      
      c.name = name;
      c.clr = clr;
      c.width = width;
      ZeroMemory(c.stats); // no statistics by default
      PUSH(curvesc);
      return true;
   }

La segunda versión del método addCurve añade no sólo una línea de balance, sino también un conjunto de variables financieras en la estructura GenericStats.

   virtual bool addCurve(TradeReport::GenericStats &stats,
      double &data[], datetime &when[], const string name,
      const color clr = clrNONEconst int width = 1)
   {
      if(addCurve(datawhennameclrwidth))
      {
         curves[ArraySize(curves) - 1][].stats = stats;
         return true;
      }
      return false;
   }

El método más importante de la clase que visualiza el informe se hace abstracto.

   virtual void render() = 0;

Esto permite implementar muchas formas de visualizar los informes, por ejemplo, tanto registrando en archivos de distintos formatos, como dibujando directamente sobre el gráfico. A continuación nos limitaremos a la formación de archivos HTML, ya que es el método tecnológicamente más avanzado y extendido.

La nueva clase HTMLReportWriter tiene un constructor, cuyos parámetros especifican el nombre del archivo, así como el tamaño de la imagen con curvas de balance. Generaremos la imagen propiamente dicha en el conocido formato de gráficos vectoriales SVG: es ideal en este caso porque es un subconjunto del lenguaje XML, que es el propio HTML.

class HTMLReportWriterpublic TradeReportWriter
{
   int handle;
   int widthheight;
   
public:
   HTMLReportWriter(const string nameconst int w = 600const int h = 400):
      width(w), height(h)
   {
      handle = FileOpen(name,
         FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_REWRITE);
   }
   
   ~HTMLReportWriter()
   {
      if(handle != 0FileClose(handle);
   }
   
   void close()
   {
      if(handle != 0FileClose(handle);
      handle = 0;
   }
   ...

Antes de pasar al principal método público render, es necesario presentar al lector una tecnología que se describirá en detalle en la Parte 7 y última del libro. Estamos hablando de recursos: archivos y arrays de datos arbitrarios conectados a un programa MQL para trabajar con multimedia (sonido e imágenes), incrustar indicadores compilados, o simplemente como repositorio de información de la aplicación. Es esta última opción la que utilizaremos ahora.

La cuestión es que es mejor generar una página HTML no completamente a partir de código MQL, sino basándose en una plantilla (plantilla de página), en la que el código MQL sólo insertará los valores de algunas variables. Se trata de una técnica muy conocida en programación que permite separar el algoritmo y la representación externa del programa (o el resultado de su trabajo). Gracias a ello, podemos experimentar por separado con la plantilla HTML y el código MQL, trabajando con cada uno de los componentes en un entorno familiar. En concreto, MetaEditor todavía no es muy adecuado para editar páginas web y visualizarlas, al igual que un navegador estándar no sabe nada de MQL5 (aunque esto se puede arreglar).

Almacenaremos las plantillas de informes HTML en archivos de texto conectados al código fuente MQL5 como recursos. La conexión se realiza mediante una directiva especial #resource. Por ejemplo, en el archivo TradeReportWriter.mqh aparece la siguiente línea:

#resource "TradeReportPage.htm" as string ReportPageTemplate

Significa que junto al código fuente debe estar el archivo TradeReportPage.htm, que estará disponible en el código MQL como cadena ReportPageTemplate. Por extensión, se puede entender que el archivo es una página web. He aquí el contenido de este archivo con abreviaturas (no tenemos la tarea de formar al lector en desarrollo web, aunque, al parecer, tener conocimientos al respecto puede ser útil también para un operador de trading). Las sangrías se añaden para representar visualmente la jerarquía de anidamiento de las etiquetas HTML; no hay sangrías en el archivo.

<!DOCTYPE html>
<html>
   <head>
      <title>Trade Report</title>
      <style>
         *{font: 9pt "Segoe UI";}
         .center{width:fit-content;margin:0 auto;}
         ...
      </style>
   </head>
   <body>
      <div class="center">
         <h1>Trade Report</h1>
         ~
      </div>
   </body>
   <script>
   ...
   </script>
</html>

Los fundamentos de las plantillas son elegidos por el desarrollador. Existe un gran número de sistemas de plantillas HTML ya preparadas, pero ofrecen muchas funciones redundantes y, por tanto, son demasiado complejas para nuestro ejemplo. Desarrollaremos nuestro propio concepto.

Para empezar, observemos que la mayoría de las páginas web tienen una parte inicial (encabezado), una parte final (pie de página) y entre ambas se sitúa la información útil. El proyecto de informe mencionado no es una excepción en este sentido. Utiliza el carácter de tilde '~' para indicar contenido útil. En lugar de ello, el código MQL tendrá que insertar una imagen de saldo y una tabla con indicadores. Pero la presencia de '~' no es necesaria, ya que la página puede ser un todo único, es decir, la parte central muy útil: después de todo, el código MQL puede, si es necesario, insertar el resultado del procesamiento de una plantilla en otra.

Para terminar la digresión sobre las plantillas HTML, vamos a prestar atención a una cosa más. En teoría, una página web se compone de etiquetas que realizan funciones esencialmente diferentes. Las etiquetas HTML estándar indican al navegador qué debe mostrar. Además de ellos, existen estilos en cascada (CSS), que describen cómo mostrarlo. Por último, la página puede tener un componente dinámico en forma de scripts de JavaScript que controlen interactivamente tanto la primera como la segunda.
 
Normalmente, estos tres componentes se planifican de forma independiente, es decir, por ejemplo, una plantilla HTML, en sentido estricto, debe contener sólo HTML, pero no CSS ni JavaScript. Esto permite «desvincular» el contenido, la apariencia y el comportamiento de la página web, lo que facilita el desarrollo (se recomienda encarecidamente seguir el mismo enfoque en MQL5).
 
Sin embargo, en nuestro ejemplo, hemos incluido todos los componentes en la plantilla. En particular, en la plantilla anterior, vemos la etiqueta <style> con estilos CSS y la etiqueta <script> con algunas funciones JavaScript, que se omiten. Esto se hace para simplificar el ejemplo, con hincapié en las características MQL5 en lugar de desarrollo web.

Teniendo una plantilla de página web en la variable ReportPageTemplate conectada como recurso, podemos escribir el método render.

   virtual void render() override
   {
      string headerAndFooter[2];
      StringSplit(ReportPageTemplate, '~', headerAndFooter);
      FileWriteString(handleheaderAndFooter[0]);
      renderContent();
      FileWriteString(handleheaderAndFooter[1]);
   }
   ...

En realidad, divide la página en mitad superior e inferior mediante el carácter '~', las muestra tal cual y llama a un método de ayuda renderContent entre ellas.

Ya hemos descrito que el informe consistirá en un cuadro general con curvas de balance y tablas con indicadores de sistemas de trading, por lo que la implementación renderContent es natural.

private:
   void renderContent()
   {
      renderSVG();
      renderTables();
   }

La generación de imágenes dentro de renderSVG se basa en otro archivo de plantilla TradeReportSVG.htm, que se vincula a una variable de cadena SVGBoxTemplate:

#resource "TradeReportSVG.htm" as string SVGBoxTemplate

El contenido de esta plantilla es el último que enumeramos aquí. Quienes lo deseen pueden consultar por sí mismos los códigos fuente del resto de plantillas.

<span id="params" style="display:block;width:%WIDTH%px;text-align:center;"></span>
<a id="main" style="display:block;text-align:center;">
   <svg width="%WIDTH%" height="%HEIGHT%" xmlns="http://www.w3.org/2000/svg">
      <style>.legend {font: bold 11px Consolas;}</style>
      <rect x="0" y="0" width="%WIDTH%" height="%HEIGHT%"
         style="fill:none; stroke-width:1; stroke: black;"/>
      ~
   </svg>
</a>

En el código del método renderSVG, veremos el conocido truco de dividir el contenido en dos bloques «antes» y «después» de la tilde, pero aquí hay algo nuevo:

   void renderSVG()
   {
      string headerAndFooter[2];
      if(StringSplit(SVGBoxTemplate, '~', headerAndFooter) != 2return;
      StringReplace(headerAndFooter[0], "%WIDTH%", (string)width);
      StringReplace(headerAndFooter[0], "%HEIGHT%", (string)height);
      FileWriteString(handleheaderAndFooter[0]);
      
      for(int i = 0i < ArraySize(curves); ++i)      
      {
         renderCurve(icurves[i][].datacurves[i][].when,
            curves[i][].namecurves[i][].clrcurves[i][].width);
      }
      
      FileWriteString(handleheaderAndFooter[1]);
   }

En la parte superior de la página, en la cadena headerAndFooter[0], buscamos subcadenas de la forma especial «%WIDTH%» y «%HEIGHT%», y las sustituimos por la anchura y la altura requeridas de la imagen. Este es el principio por el que funciona la sustitución de valores en nuestras plantillas. Por ejemplo, en esta plantilla, estas subcadenas aparecen en la etiqueta rect:

<rect x="0" y="0" width="%WIDTH%" height="%HEIGHT%" style="fill:none; stroke-width:1; stroke: black;"/>

Así, si el informe se pide con un tamaño de 600 por 400, la línea se convertirá en la siguiente:

<rect x="0" y="0" width="600" height="400" style="fill:none; stroke-width:1; stroke: black;"/>

Esto mostrará un borde negro de 1 píxel de grosor de las dimensiones especificadas en el navegador.

La generación de etiquetas para dibujar líneas de balance específicas se gestiona mediante el método renderCurve, al que pasamos todos los arrays necesarios y otros ajustes (nombre, color y grosor). Dejaremos este método y otros muy especializados (renderTables, renderTable) para un estudio independiente.

Volvamos al módulo principal del Asesor Experto UnityMartingaleDraft3.mq5. Ajuste el tamaño de la imagen de los gráficos de balance y conecte TradeReportWriter.mqh.

#define MINIWIDTH  400
#define MINIHEIGHT 200
   
#include <MQL5Book/TradeReportWriter.mqh>

Para «conectar» las estrategias con el generador de informes, tendrá que modificar el método statement en la interfaz TradingStrategy: pase un puntero al objeto TradeReportWriter, que el código de llamada puede crear y configurar.

interface TradingStrategy
{
   virtual bool trade(void);
   virtual bool statement(TradeReportWriter *writer = NULL);
};

Ahora vamos a añadir algunas líneas en la implementación específica de este método en nuestra clase de estrategia UnityMartingale.

class UnityMartingalepublic TradingStrategy
{
   ...
   TradeReport report;
   ...
   virtual bool statement(TradeReportWriter *writer = NULLoverride
   {
      if(MQLInfoInteger(MQL_TESTER))
      {
         ...
         // it's already been done
         DealFilter filter;
         filter.let(DEAL_SYMBOLsettings.symbol)
            .let(DEAL_MAGICsettings.magicIS::EQUAL_OR_ZERO);
         HistorySelect(0LONG_MAX);
         TradeReport::GenericStats stats =
            report.calcStatistics(filterdepositepoch);
         ...
         // adding this
         if(CheckPointer(writer) != POINTER_INVALID)
         {
            double data[];               // balance values
            datetime time[];             // balance points time to synchronize curves
            report.getCurve(datatime); // fill in the arrays and transfer to write to the file
            return writer.addCurve(statsdatatimesettings.symbol);
         }
         return true;
      }
      return false;
   }

Todo se reduce a obtener un array de balance y una estructura con indicadores del objeto report (clase TradeReport) y pasarlo al objeto TradeReportWriter, llamando a addCurve.

Por supuesto, el conjunto de estrategias de trading garantiza la transferencia del mismo objeto TradeReportWriter a todas las estrategias para generar un informe combinado.

class TradingStrategyPoolpublic TradingStrategy
{
   ...
   virtual bool statement(TradeReportWriter *writer = NULLoverride
   {
      bool result = false;
      for(int i = 0i < ArraySize(pool); i++)
      {
         result = pool[i][].statement(writer) || result;
      }
      return result;
   }

Por último, el manejador OnTester ha sufrido la mayor modificación. Las siguientes líneas bastarían para generar un informe HTML de las estrategias de trading.

double OnTester()
{
   ...
   const static string tempfile = "temp.html";
   HTMLReportWriter writer(tempfileMINIWIDTHMINIHEIGHT);
   if(pool[] != NULL)
   {
      pool[].statement(&writer); // ask strategies to report their results
   }
   writer.render(); // write the received data to a file
   writer.close();
}

Sin embargo, para mayor claridad y comodidad del usuario, sería estupendo añadir al informe una curva de balance general, así como un cuadro con indicadores generales. Tiene sentido emitirlos sólo cuando se especifican varios símbolos en la configuración del Asesor Experto porque, de lo contrario, el informe de una estrategia coincide con el general del archivo.

Esto requería un poco más de código.

double OnTester()
{
   ...
   // had it before
   DealFilter filter;
   // set up the filter and fill in the array of deals based on it tickets
   ...
   const int n = ArraySize(tickets);
   
   // add this
   const bool singleSymbol = WorkSymbols == "";
   double curve[];    // total balance curve
   datetime stamps[]; // date and time of total balance points
   
   if(!singleSymbol// the total balance is displayed only if there are several symbols/strategies
   {
      ArrayResize(curven + 1);
      ArrayResize(stampsn + 1);
      curve[0] = TesterStatistics(STAT_INITIAL_DEPOSIT);
      
      // MQL5 does not allow to know the test start time,
      // this could be found out from the first transaction,
      // but it is outside the filter conditions of a specific system,
      // so let's just agree to skip time 0 in calculations
      stamps[0] = 0;
   }
   
   for(int i = 0i < n; ++i// deal cycle
   {
      double result = 0;
      for(int j = 0j < STAT_PROPS - 1; ++j)
      {
         result += expenses[i][j];
      }
      if(!singleSymbol)
      {
         curve[i + 1] = result + curve[i];
         stamps[i + 1] = (datetime)HistoryDealGetInteger(tickets[i], DEAL_TIME);
      }
      ...
   }
   if(!singleSymbol// send the tester's statistics and the overall curve to the report 
   {
      TradeReport::GenericStats stats;
      stats.fillByTester();
      writer.addCurve(statscurvestamps"Overall"clrBlack3);
   }
   ...
}

Veamos qué tenemos. Si ejecutamos el Asesor Experto con la configuración UnityMartingale-combo.set, tendremos el archivo temp.html en la carpeta MQL5/Files de uno de los agentes. Este es el aspecto que tiene en el registro:

Informe HTML para expertos con múltiples estrategias/símbolos de trading

Informe HTML para Asesor Experto con múltiples estrategias/símbolos de trading

Ahora que sabemos cómo generar informes en una pasada de prueba, podemos enviarlos al terminal durante la optimización, seleccionar los mejores sobre la marcha y presentarlos al usuario antes de que finalice todo el proceso. Todos los informes se colocarán en una carpeta separada dentro de MQL5/Files del terminal. La carpeta recibirá un nombre que contendrá el símbolo y el marco temporal de la configuración del probador, así como el nombre del Asesor Experto.

UnityMartingale.mq5

Como sabemos, para enviar un archivo al terminal, basta con llamar a la función FrameAdd. Ya hemos generado el archivo en el marco de la versión anterior.

double OnTester()
{
   ...
   if(MQLInfoInteger(MQL_OPTIMIZATION))
   {
      FrameAdd(tempfile0r2 * 100tempfile);
   }
}

En la instancia receptora del Asesor Experto, realizaremos la preparación necesaria. Describamos la estructura Pass con los principales parámetros de cada pase de optimización.

struct Pass
{
   ulong id;          // pass number
   double value;      // optimization criterion value
   string parameters// optimized parameters as list 'name=value'
   string preset;     // text to generate set-file (with all parameters)
};

En las cadenas parameters, los pares «name=value» se conectan con el símbolo '&'. Esto será útil para la interacción de páginas web de informes en el futuro (el símbolo '&' es el estándar para combinar parámetros en direcciones web). No hemos descrito el formato de los archivos de conjuntos, pero el siguiente código fuente que forma la cadena preset permite estudiar esta cuestión en la práctica.

A medida que lleguen frames, escribiremos mejoras según el criterio de optimización en el array TopPasses. La mejor pasada actual será siempre la última pasada del array y también está disponible en la variable BestPass.

Pass TopPasses[];     // stack of constantly improving passes (last one is best)
Pass BestPass;        // current best pass
string ReportPath;    // dedicated folder for all html files of this optimization

En el manejador OnTesterInit vamos a crear un nombre de carpeta.

void OnTesterInit()
{
   BestPass.value = -DBL_MAX;
   ReportPath = _Symbol + "-" + PeriodToString(_Period) + "-"
      + MQLInfoString(MQL_PROGRAM_NAME) + "/";
}

En el manejador OnTesterPass seleccionaremos secuencialmente sólo aquellos frames en los que el indicador haya mejorado, encontraremos para ellos los valores de los parámetros optimizados y otros, y añadiremos toda esta información al array de estructuras Pass.

void OnTesterPass()
{
   ulong   pass;
   string  name;
   long    id;
   double  value;
   uchar   data[];
   
   // input parameters for the pass corresponding to the current frame
   string  params[];
   uint    count;
   
   while(FrameNext(passnameidvaluedata))
   {
      // collect passes with improved stats
      if(value > BestPass.value && FrameInputs(passparamscount))
      {
         BestPass.preset = "";
         BestPass.parameters = "";
         // get optimized and other parameters for generating a set-file
         for(uint i = 0i < counti++)
         {
            string name2value[];
            int n = StringSplit(params[i], '=', name2value);
            if(n == 2)
            {
               long pvaluepstartpsteppstop;
               bool enabled = false;
               if(ParameterGetRange(name2value[0], enabledpvaluepstartpsteppstop))
               {
                  if(enabled)
                  {
                     if(StringLen(BestPass.parameters)) BestPass.parameters += "&";
                     BestPass.parameters += params[i];
                  }
                  
                  BestPass.preset += params[i] + "||" + (string)pstart + "||"
                    + (string)pstep + "||" + (string)pstop + "||"
                    + (enabled ? "Y" : "N") + "<br>\n";
               }
               else
               {
                  BestPass.preset += params[i] + "<br>\n";
               }
            }
         }
      
         BestPass.value = value;
         BestPass.id = pass;
         PUSH(TopPassesBestPass);
         // write the frame with the report to the HTML file
         const string text = CharArrayToString(data);
         int handle = FileOpen(StringFormat(ReportPath + "%06.3f-%lld.htm"valuepass),
            FILE_WRITE | FILE_TXT | FILE_ANSI);
         FileWriteString(handletext);
         FileClose(handle);
      }
   }
}

Los informes resultantes con las mejoras se guardan en archivos con nombres que incluyen el valor del criterio de optimización y el número de pasada.

Ahora viene lo más interesante: en el manejador OnTesterDeinit, podemos formar un archivo HTML común (overall.htm), que permite ver todos los informes a la vez (o, digamos, los 100 primeros). Utiliza el mismo esquema con plantillas que hemos visto antes.

#resource "OptReportPage.htm" as string OptReportPageTemplate
#resource "OptReportElement.htm" as string OptReportElementTemplate
   
void OnTesterDeinit()
{
   int handle = FileOpen(ReportPath + "overall.htm",
      FILE_WRITE | FILE_TXT | FILE_ANSI0CP_UTF8);
   string headerAndFooter[2];
   StringSplit(OptReportPageTemplate, '~', headerAndFooter);
   StringReplace(headerAndFooter[0], "%MINIWIDTH%", (string)MINIWIDTH);
   StringReplace(headerAndFooter[0], "%MINIHEIGHT%", (string)MINIHEIGHT);
   FileWriteString(handleheaderAndFooter[0]);
   // read no more than 100 best records from TopPasses
   for(int i = ArraySize(TopPasses) - 1k = 0i >= 0 && k < 100; --i, ++k)
   {
      string p = TopPasses[i].parameters;
      StringReplace(p"&"" ");
      const string filename = StringFormat("%06.3f-%lld.htm",
         TopPasses[i].valueTopPasses[i].id);
      string element = OptReportElementTemplate;
      StringReplace(element"%FILENAME%"filename);
      StringReplace(element"%PARAMETERS%"TopPasses[i].parameters);
      StringReplace(element"%PARAMETERS_SPACED%"p);
      StringReplace(element"%PASS%"IntegerToString(TopPasses[i].id));
      StringReplace(element"%PRESET%"TopPasses[i].preset);
      StringReplace(element"%MINIWIDTH%", (string)MINIWIDTH);
      StringReplace(element"%MINIHEIGHT%", (string)MINIHEIGHT);
      FileWriteString(handleelement);
   }
   FileWriteString(handleheaderAndFooter[1]);
   FileClose(handle);
}

En la siguiente imagen se muestra el aspecto de la página web de resumen después de optimizar UnityMartingale.mq5 mediante el parámetro UnityPricePeriod en modo multidivisa.

Página web general con informes de trading de los mejores pases de optimización

Página web general con informes de trading de los mejores pases de optimización

Para cada informe, mostramos sólo la parte superior, donde cae el gráfico del balance. Esta parte es la más conveniente para obtener una estimación con sólo mirarla.

Encima de cada gráfico aparecen listas de parámetros optimizados («name=value&name=value...»). Al pulsar sobre una línea se abre un bloque con el texto del archivo de configuración de todas las opciones de configuración de este pase. Si hace clic dentro de un bloque, su contenido se copiará en el portapapeles. Se puede guardar en un editor de texto y obtener así un archivo de conjunto listo.

Si hace clic en el gráfico, accederá a la página específica del informe, junto con los scorecards (indicados anteriormente).

Al final de la sección abordamos una cuestión más. Antes prometimos demostrar el efecto de la función TesterHideIndicators. El Asesor Experto UnityMartingale.mq5 utiliza actualmente el indicador UnityPercentEvent.mq5. Después de cualquier prueba, el indicador se muestra en el gráfico de apertura. Supongamos que queremos ocultar al usuario el mecanismo de trabajo del Asesor Experto y de dónde toma las señales. A continuación, puede llamar a la función TesterHideIndicators (con el parámetro true) en el manejador OnInit, antes de crear el objeto UnityController, en el que se recibe el descriptor a través de iCustom.

int OnInit()
{
   ...
   TesterHideIndicators(true);
   ...
   controller = new UnityController(UnitySymbolsbarwise,
      UnityBarLimitUnityPriceTypeUnityPriceMethodUnityPricePeriod);
   return INIT_SUCCEEDED;
}

Esta versión del Asesor Experto ya no mostrará el indicador en el gráfico. Sin embargo, no está muy bien escondido. Si miramos en el registro del probador, veremos líneas sobre programas cargados entre un montón de información útil: primero, un mensaje sobre la carga del propio Asesor Experto, y un poco más tarde, sobre la carga del indicador.

...
expert file added: Experts\MQL5Book\p6\UnityMartingale.ex5.
...
program file added: \Indicators\MQL5Book\p6\UnityPercentEvent.ex5. 
...

Así, un usuario meticuloso puede averiguar el nombre del indicador. Esta posibilidad puede eliminarse mediante el mecanismo de recursos, que ya hemos mencionado de pasada en el contexto de los espacios en blanco de las páginas web. Resulta que el indicador compilado también se puede incrustar en un programa MQL (en un Asesor Experto u otro indicador) como un recurso. Y estos programas de recursos ya no se mencionan en el registro del probador. Estudiaremos los recursos en detalle en la 7ª Parte del libro, y ahora mostraremos las líneas asociadas a ellos en la versión final de nuestro Asesor Experto.

En primer lugar, vamos a describir el recurso con la directiva de indicador #resource. De hecho, contiene simplemente la ruta al archivo del indicador compilado (obviamente, ya debe estar compilado de antemano), y aquí es obligatorio utilizar barras invertidas dobles como delimitadores, ya que no se admiten barras diagonales simples en las rutas de recursos.

#resource "\\Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5"

A continuación, en las líneas con la llamada iCustom, sustituimos el operador anterior:

   UnityController(const string symbolListconst int offsetconst int limit,
      const ENUM_APPLIED_PRICE typeconst ENUM_MA_METHOD methodconst int period):
      bar(offset), tickwise(!offset)
   {
      handle = iCustom(_Symbol_Period,
         "MQL5Book/p6/UnityPercentEvent",                      // <---
         symbolListlimittypemethodperiod);
      ...

Exactamente igual, pero con un enlace al recurso (nótese la sintaxis con un par de dos puntos '::' al principio, necesaria para distinguir entre las rutas normales en el sistema de archivos y las rutas dentro de los recursos).

   UnityController(const string symbolListconst int offsetconst int limit,
      const ENUM_APPLIED_PRICE typeconst ENUM_MA_METHOD methodconst int period):
      bar(offset), tickwise(!offset)
   {
      handle = iCustom(_Symbol_Period,
         "::Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5",  // <---
         symbolListlimittypemethodperiod);
      ...

Ahora la versión compilada del Asesor Experto puede entregarse a los usuarios por sí sola, sin un indicador separado, ya que está oculta dentro del Asesor Experto. Esto no afecta en absoluto a su rendimiento, pero teniendo en cuenta el reto TesterHideIndicators, el dispositivo interno queda oculto. Hay que tener en cuenta que si el indicador se actualiza, el Asesor Experto también tendrá que volver a compilarse.