Obtener estadísticas financieras de prueba: TesterStatistics

Solemos evaluar la calidad de un Asesor Experto basándonos en un informe de trading, que es similar a un informe de simulación cuando se trata de un probador. Contiene un gran número de variables que caracterizan el estilo de trading, la estabilidad y, por supuesto, la rentabilidad. Todas estas métricas, con algunas excepciones, están disponibles para el programa MQL a través de una función especial TesterStatistics. Así, el desarrollador del Asesor Experto tiene la capacidad de analizar variables individuales en el código y construir sus propios criterios combinados de calidad de optimización a partir de ellas.

double TesterStatistics(ENUM_STATISTICS statistic)

La función TesterStatistics devuelve el valor de la variable estadística especificada, calculada en base a los resultados de una ejecución separada del Asesor Experto en el probador. Se puede llamar a una función en el manejador OnDeinit o OnTester, que aún está por discutir.

Todas las variables estadísticas disponibles se resumen en la enumeración ENUM_STATISTICS. Algunas de ellas son características cualitativas, es decir, números reales (normalmente beneficios totales, reducciones, ratios, etc.), y las otras son cuantitativas, es decir, números enteros (por ejemplo, el número de transacciones). Sin embargo, ambos grupos están controlados por la misma función con el resultado double.

En la siguiente tabla se muestran los indicadores reales (coeficientes e importes monetarios). Todos los importes monetarios se expresan en la divisa del depósito.

Identificador

Descripción

STAT_INITIAL_DEPOSIT

Depósito inicial

STAT_WITHDRAWAL

Importe de los fondos retirados de la cuenta

STAT_PROFIT

Beneficio o pérdida neta al final de la simulación, la suma de STAT_GROSS_PROFIT y STAT_GROSS_LOSS

STAT_GROSS_PROFIT

Beneficio total, la suma de todas las operaciones rentables (mayor o igual a cero)

STAT_GROSS_LOSS

Pérdida total, la suma de todas las operaciones perdedoras (menor o igual a cero)

STAT_MAX_PROFITTRADE

Beneficio máximo: el mayor valor de entre todas las operaciones rentables (mayor o igual a cero)

STAT_MAX_LOSSTRADE

Pérdida máxima: el valor más pequeño de entre todas las operaciones perdedoras (menor o igual a cero)

STAT_CONPROFITMAX

Beneficio máximo total en una serie de operaciones rentables (mayor o igual a cero)

STAT_MAX_CONWINS

Beneficio total en la serie más larga de operaciones rentables

STAT_CONLOSSMAX

Pérdida máxima total en una serie de operaciones perdedoras (menor o igual a cero)

STAT_MAX_CONLOSSES

Pérdida total en la serie más larga de operaciones perdedoras

STAT_BALANCEEMIN

Valor del saldo mínimo

STAT_BALANCE_DD

Disposición máxima del saldo en dinero

STAT_BALANCEDD_PERCENT

Disposición de saldo en porcentaje, que se registró en el momento de la máxima disposición de saldo en dinero (STAT_BALANCE_DD)

STAT_BALANCE_DDREL_PERCENT

Disposición máxima del saldo en porcentaje

STAT_BALANCE_DD_RELATIVE

Disposición de saldo en equivalente monetario, que se registró en el momento de la máxima disposición de saldo en porcentaje (STAT_BALANCE_DDREL_PERCENT)

STAT_EQUITYMIN

Valor mínimo de los fondos propios

STAT_EQUITY_DD

Disposición máxima en dinero

STAT_EQUITYDD_PERCENT

Disposición en porcentaje, que se registró en el momento de la disposición máxima de fondos en el dinero (STAT_EQUITY_DD)

STAT_EQUITY_DDREL_PERCENT

Disposición máxima en porcentaje

STAT_EQUITY_DD_RELATIVE

Disposición en dinero que se registró en el momento de la disposición máxima en porcentaje (STAT_EQUITY_DDREL_PERCENT)

STAT_EXPECTED_PAYOFF

Expectativa matemática de ganancias (media aritmética del beneficio total y el número de transacciones)

STAT_PROFIT_FACTOR

Rentabilidad, que es el cociente STAT_GROSS_PROFIT/STAT_GROSS_LOSS (si STAT_GROSS_LOSS = 0; la rentabilidad toma el valor DBL_MAX).

STAT_RECOVERY_FACTOR

Factor de recuperación: el ratio de STAT_PROFIT/STAT_BALANCE_DD

STAT_SHARPE_RATIO

Ratio de Sharpe

STAT_MIN_MARGINLEVEL

Nivel de margen mínimo alcanzado

STAT_CUSTOM_ONTESTER

El valor del criterio de optimización personalizado devuelto por la función OnTester.

En la siguiente tabla muestra los indicadores enteros (importes).

Identificador

Descripción

STAT_DEALS

Número total de transacciones completadas

STAT_TRADES

Número de operaciones (transacciones de salida del mercado)

STAT_PROFIT_TRADES

Operaciones rentables

STAT_LOSS_TRADES

Operaciones perdedoras

STAT_SHORT_TRADES

Operaciones cortas

STAT_LONG_TRADES

Operaciones largas

STAT_PROFIT_SHORTTRADES

Operaciones cortas rentables

STAT_PROFIT_LONGTRADES

Operaciones largas rentables

STAT_PROFITTRADES_AVGCON

Duración media de una serie de operaciones rentables

STAT_LOSSTRADES_AVGCON

Duración media de una serie de operaciones perdedoras

STAT_CONPROFITMAX_TRADES

Número de operaciones que formaron STAT_CONPROFITMAX (beneficio máximo en la secuencia de operaciones rentables)

STAT_MAX_CONPROFIT_TRADES

Número de operaciones en la serie más larga de operaciones rentables STAT_MAX_CONWINS

STAT_CONLOSSMAX_TRADES

Número de operaciones que formaron STAT_CONLOSSMAX (pérdida máxima en la secuencia de operaciones perdedoras)

STAT_MAX_CONLOSS_TRADES

Número de operaciones en la serie más larga de operaciones perdedoras STAT_MAX_CONLOSSES

Intentemos utilizar las métricas presentadas para crear nuestro propio criterio complejo de calidad de Asesor Experto. Para ello, necesitamos algún tipo de ejemplo «experimental» de un programa MQL. Tomemos el Asesor Experto MultiMartingale.mq5 como punto de partida, pero lo simplificaremos: eliminaremos la multidivisa, el tratamiento de errores integrados y la programación. Además, elegiremos para ella una estrategia de trading de señales con un único cálculo en la barra, es decir, a los precios de apertura. Esto acelerará la optimización y ampliará el campo de experimentación.

La estrategia se basará en las condiciones de sobrecompra y sobreventa determinadas por el indicador OsMA. El indicador de Bandas de Bollinger superpuesto a OsMA le ayudará a encontrar dinámicamente los límites del exceso de volatilidad, lo que se traduce en señales de trading.

Cuando OsMA regrese dentro del corredor, cruzando el borde inferior de abajo hacia arriba, abriremos una operación de compra. Cuando OsMA cruce el límite superior de la misma forma de arriba a abajo, venderemos. Para salir de las posiciones, utilizamos la media móvil, también aplicada a OsMA. Si OsMA muestra un movimiento inverso (hacia abajo para una posición larga o hacia arriba para una posición corta) y toca MA, la posición se cerrará. Esta estrategia se ilustra en la siguiente captura de pantalla.

Estrategia de trading basada en los indicadores OsMA, BBands y MA

Estrategia de trading basada en los indicadores OsMA, BBands y MA

La línea vertical azul corresponde a la barra en la que se abre la compra, ya que en las dos barras anteriores, la banda inferior de Bollinger fue atravesada por el histograma OsMA de abajo arriba (este lugar está marcado con una flecha azul hueca en la subventana). La línea vertical roja es la ubicación de la señal inversa, por lo que se cerró la compra y se abrió la venta. En la subventana, en este lugar (o mejor dicho, en las dos barras anteriores, donde se encuentra la flecha roja hueca), el histograma OsMA cruza la banda de Bollinger superior de arriba a abajo. Por último, la línea verde indica el cierre de la venta, debido a que el histograma comenzó a subir por encima de la MA roja.

Llamemos al Asesor Experto BandOsMA.mq5. Los ajustes generales incluirán un número mágico, un lote fijo y una distancia de Stop Loss en puntos. Para el Stop Loss, utilizaremos TrailingStop del ejemplo anterior. Aquí no se utiliza el Take Profit.

input group "C O M M O N   S E T T I N G S"
sinput ulong Magic = 1234567890;
input double Lots = 0.01;
input int StopLoss = 1000;

Hay tres grupos de ajustes destinados a los indicadores.

input group "O S M A   S E T T I N G S"
input int FastOsMA = 12;
input int SlowOsMA = 26;
input int SignalOsMA = 9;
input ENUM_APPLIED_PRICE PriceOsMA = PRICE_TYPICAL;
   
input group "B B A N D S   S E T T I N G S"
input int BandsMA = 26;
input int BandsShift = 0;
input double BandsDeviation = 2.0;
   
input group "M A   S E T T I N G S"
input int PeriodMA = 10;
input int ShiftMA = 0;
input ENUM_MA_METHOD MethodMA = MODE_SMA;

En el Asesor Experto de MultiMartingale.mq5 no teníamos señales de trading, mientras que la dirección de apertura la fijaba el usuario. Aquí tenemos señales de trading, y tiene sentido organizarlas como una clase separada. En primer lugar, describamos la interfaz abstracta TradingSignal.

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

Es tan sencillo como nuestra otra interfaz TradingStrategy. Y esto es bueno. Cuanto más sencillas sean las interfaces y los objetos, más probable es que hagan una sola cosa, lo cual es un buen estilo de programación porque minimiza los errores y hace más comprensibles los grandes proyectos de software. Debido a la abstracción en cualquier programa que utilice TradingSignal, será posible sustituir una señal por otra. También podemos sustituir la estrategia. Nuestras estrategias se encargan ahora de preparar y enviar las órdenes, y las señales las inician basándose en el análisis del mercado.

En nuestro caso, vamos a empaquetar la implementación específica de TradingSignal en la clase BandOsMaSignal. Por supuesto, necesitamos variables para almacenar los descriptores de los 3 indicadores. Las instancias del indicador se crean y eliminan en el constructor y el destructor, respectivamente. Todos los parámetros se pasarán desde las variables de entrada. Tenga en cuenta que iBands y iMA se basan en el manejador hOsMA.

class BandOsMaSignalpublic TradingSignal
{
   int hOsMAhBandshMA;
   int direction;
public:
   BandOsMaSignal(const int fastconst int slowconst int signal,
      const ENUM_APPLIED_PRICE price,
      const int bandsconst int shiftconst double deviation,
      const int periodconst int xENUM_MA_METHOD method)
   {
      hOsMA = iOsMA(_Symbol_Periodfastslowsignalprice);
      hBands = iBands(_Symbol_PeriodbandsshiftdeviationhOsMA);
      hMA = iMA(_Symbol_PeriodperiodxmethodhOsMA);
      direction = 0;
   }
   
   ~BandOsMaSignal()
   {
      IndicatorRelease(hMA);
      IndicatorRelease(hBands);
      IndicatorRelease(hOsMA);
   }
   ...

La dirección de la señal de trading actual se coloca en la variable direction: 0 - sin señales (situación indefinida), +1 - compra, -1 - venta. Rellenaremos esta variable en el método signal. Su código repite la descripción verbal anterior de las señales en MQL5.

   virtual int signal(voidoverride
   {
      double osma[2], upper[2], lower[2], ma[2];
      // get two values of each indicator on bars 1 and 2
      if(CopyBuffer(hOsMA012osma) != 2return 0;
      if(CopyBuffer(hBandsUPPER_BAND12upper) != 2return 0;
      if(CopyBuffer(hBandsLOWER_BAND12lower) != 2return 0;
      if(CopyBuffer(hMA012ma) != 2return 0;
      
      // if there was a signal already, check if it has ended
      if(direction != 0)
      {
         if(direction > 0)
         {
            if(osma[0] >= ma[0] && osma[1] < ma[1])
            {
               direction = 0;
            }
         }
         else
         {
            if(osma[0] <= ma[0] && osma[1] > ma[1])
            {
               direction = 0;
            }
         }
      }
      
      // in any case, check if there is a new signal
      if(osma[0] <= lower[0] && osma[1] > lower[1])
      {
         direction = +1;
      }
      else if(osma[0] >= upper[0] && osma[1] < upper[1])
      {
         direction = -1;
      }
      
      return direction;
   }
};

Como puede ver, los valores del indicador se leen para las barras 1 y 2, ya que trabajaremos en la apertura de una barra, y la barra 0 acaba de abrirse cuando llamamos al método signal.

La nueva clase que implemente la interfaz TradingStrategy se llamará SimpleStrategy.

La clase proporciona algunas características nuevas, al tiempo que utiliza algunas partes ya existentes. En concreto, ha conservado los punteros automáticos para PositionState y TrailingStop y tiene un nuevo puntero automático para la señal TradingSignal. Además, como vamos a operar sólo en la apertura de barras, necesitamos la variable lastBar, que almacenará la hora de la última barra procesada.

class SimpleStrategypublic TradingStrategy
{
protected:
   AutoPtr<PositionStateposition;
   AutoPtr<TrailingStoptrailing;
   AutoPtr<TradingSignalcommand;
   
   const int stopLoss;
   const ulong magic;
   const double lots;
   
   datetime lastBar;
   ...

Los parámetros globales se pasan al constructor SimpleStrategy. También pasamos un puntero al objeto TradingSignal: en este caso, será BandOsMaSignal el que tendrá que crear el código de llamada. A continuación, el constructor intenta encontrar entre las posiciones existentes aquellas que tengan el número y el símbolo magic requeridos y, si lo consigue, añade un trailing stop. Esto será útil si el Asesor Experto tiene una interrupción por una razón u otra, y la posición ya ha sido abierta.

public:
   SimpleStrategy(TradingSignal *signalconst ulong mconst int slconst double v):
      command(signal), magic(m), stopLoss(sl), lots(v), lastBar(0)
   {
 // select "our" position among the existing ones (if there is a suitable one)
      PositionFilter positions;
      ulong tickets[];
      positions.let(POSITION_MAGICmagic).let(POSITION_SYMBOL_Symbol).select(tickets);
      const int n = ArraySize(tickets);
      if(n > 1)
      {
         Alert(StringFormat("Too many positions: %d"n));
 // TODO: close extra positions - this is not allowed by the strategy
      }
      else if(n > 0)
      {
         position = new PositionState(tickets[0]);
         if(stopLoss)
         {
           trailing = new TrailingStop(tickets[0], stopLossstopLoss / 50);
         }
      }
   }

La implementación del método trade es similar al ejemplo de la martingala. No obstante, hemos eliminado las multiplicaciones de lotes y hemos añadido la llamada al método signal.

   virtual bool trade() override
   {
      // we work only once when a new bar appears
      if(lastBar == iTime(_Symbol_Period0)) return false;
      
      int s = command[].signal(); // getting a signal
      
      ulong ticket = 0;
      
      if(position[] != NULL)
      {
         if(position[].refresh()) // position exists
         {
            // the signal has changed to the opposite or disappeared
            if((position[].get(POSITION_TYPE) == POSITION_TYPE_BUY && s != +1)
            || (position[].get(POSITION_TYPE) == POSITION_TYPE_SELL && s != -1))
            {
               PrintFormat("Signal lost: %d for position %d %lld",
                  sposition[].get(POSITION_TYPE), position[].get(POSITION_TICKET));
               if(close(position[].get(POSITION_TICKET)))
               {
                  position = NULL;
               }
               else
               {
                 // update internal flag 'ready'
                 // according to whether or not there was a closure
                  position[].refresh();
               }
            }
            else
            {
               position[].update();
               if(trailing[]) trailing[].trail();
            }
         }
         else // position is closed
         {
            position = NULL;
         }
      }
      
      if(position[] == NULL && s != 0)
      {
         ticket = (s == +1) ? openBuy() : openSell();
      }
      
      if(ticket > 0// new position just opened
      {
         position = new PositionState(ticket);
         if(stopLoss)
         {
            trailing = new TrailingStop(ticketstopLossstopLoss / 50);
         }
      }
      // store the current bar
      lastBar = iTime(_Symbol_Period0);
      
      return true;
   }

Los métodos auxiliares openBuy, openSell y otros han sufrido cambios mínimos, por lo que no los enumeraremos (se adjunta el código fuente completo).

Dado que siempre tenemos una sola estrategia en este Asesor Experto, en contraste con la martingala multidivisa en la que cada símbolo requería su propia configuración, vamos a excluir el grupo de estrategias y gestionar el objeto estrategia directamente.

AutoPtr<TradingStrategystrategy;
   
int OnInit()
{
   if(FastOsMA >= SlowOsMAreturn INIT_PARAMETERS_INCORRECT;
   strategy = new SimpleStrategy(
      new BandOsMaSignal(FastOsMASlowOsMASignalOsMAPriceOsMA,
         BandsMABandsShiftBandsDeviation,
         PeriodMAShiftMAMethodMA),
         MagicStopLossLots);
   return INIT_SUCCEEDED;
}
   
void OnTick()
{
   if(strategy[] != NULL)
   {
      strategy[].trade();
   }
}

Ahora tenemos un Asesor Experto listo que podemos utilizar como herramienta para estudiar el probador. En primer lugar, vamos a crear una estructura auxiliar TesterRecord para consultar y almacenar todos los datos estadísticos.

struct TesterRecord
{
   string feature;
   double value;
   
   static void fill(TesterRecord &stats[])
   {
      ResetLastError();
      for(int i = 0; ; ++i)
      {
         const double v = TesterStatistics((ENUM_STATISTICS)i);
         if(_LastErrorreturn;
         TesterRecord t = {EnumToString((ENUM_STATISTICS)i), v};
         PUSH(statst);
      }
   }
};

En este caso, el campo de cadena feature sólo es necesario para la salida de registro informativo. Para guardar todos los indicadores (por ejemplo, para poder generar más tarde su propio formulario de informe), basta con un simple array del tipo double de longitud adecuada.

Utilizando la estructura del manejador OnDeinit, nos aseguramos de que la API de MQL5 devuelva los mismos valores que el informe del probador.

void OnDeinit(const int)
{
   TesterRecord stats[];
   TesterRecord::fill(stats);
   ArrayPrint(stats2);
}

Por ejemplo, al ejecutar el Asesor Experto en EURUSD, H1 con un depósito de 10000 y sin ninguna optimización (con la configuración por defecto), obtendremos aproximadamente los siguientes valores para 2021 (fragmento):

                        [feature]  [value]
[ 0] "STAT_INITIAL_DEPOSIT"       10000.00
[ 1] "STAT_WITHDRAWAL"                0.00
[ 2] "STAT_PROFIT"                    6.01
[ 3] "STAT_GROSS_PROFIT"            303.63
[ 4] "STAT_GROSS_LOSS"             -297.62
[ 5] "STAT_MAX_PROFITTRADE"          15.15
[ 6] "STAT_MAX_LOSSTRADE"           -10.00
...
[27] "STAT_DEALS"                   476.00
[28] "STAT_TRADES"                  238.00
...
[37] "STAT_CONLOSSMAX_TRADES"         8.00
[38] "STAT_MAX_CONLOSS_TRADES"        8.00
[39] "STAT_PROFITTRADES_AVGCON"       2.00
[40] "STAT_LOSSTRADES_AVGCON"         2.00

Conociendo todos estos valores, podemos inventar nuestra propia fórmula para la métrica combinada de la calidad del Asesor Experto y, al mismo tiempo, la función de optimización objetivo. Pero, en cualquier caso, el valor de este indicador deberá comunicarse al probador. Y eso es lo que hace la función OnTester.