Control de la pendiente de la curva de balance durante el funcionamiento de un Expert Advisor

Dmitriy Skub | 11 marzo, 2014


Introducción

En este artículo se describe una de las metodologías que permite mejorar el rendimiento de los Expert Advisors mediante la creación de una retroalimentación. En este caso, se basa la retroalimentación en la medición de la pendiente de la curva de balance. El control de la pendiente se lleva a cabo automáticamente mediante la regulación del volumen de operaciones. Un Expert Advisor puede operar en los siguientes modos: con un volumen de corte, con la cantidad de lotes de trabajo (según el ajuste inicial) y con un volumen intermedio. Se activa el modo de funcionamiento automáticamente.

Se utilizan distintas características de regulación en la cadena de retroalimentación: escalonada, escalonada con histéresis y lineal. Esto permite ajustar el sistema de control de la pendiente de la curva de balance a ciertas características.

La idea principal es automatizar el proceso de toma de decisiones para un trader mientras hace el seguimiento de su propio sistema de trading. Es razonable reducir los riesgos durante los períodos de trabajo desfavorables. Al volver al modo de trabajo normal, se pueden restablecer los riesgos a su nivel inicial.

Por supuesto, este sistema no es la panacea, y no va a convertir un Expert Advisor con pérdidas en uno rentable. De algún modo, este es un complemento a la gestión de fondos (MM -Money Management del inglés) del Expert Advisor que permite evitar pérdidas cuantiosas en una cuenta.

El artículo incluye una librería que permite incorporar esta función al código de cualquier Expert Advisor.


Principios de funcionamiento

Vamos a echar un vistazo a los principios de funcionamiento del sistema que controla la pendiente de la curva de balance. Supongamos que tenemos un Expert Advisor de trading. Su curva hipotética es la siguiente:

Principios de funcionamiento del sistema que controla la pendiente de la curva de balance

Figura 1. Principios de funcionamiento del sistema que controla la pendiente de la curva de balance

En la figura anterior se muestra la curva inicial del balance del Expert Advisor que utiliza un volumen de operaciones constante. Las operaciones cerradas se muestran con puntos rojos. Vamos a conectar estos puntos con una línea curva, que representa el cambio de balance del Expert Advisor durante el trading (línea negra gruesa).

Ahora vamos a hacer un seguimiento continuo del ángulo de la pendiente de esta línea con el eje del tiempo (se muestra con líneas azules finas). O para ser más precisos, antes de la apertura de cada operación mediante una señal, calcularemos el ángulo de la pendiente mediante las dos operaciones cerradas anteriores (o mediante dos operaciones, para simplificar). Si el ángulo de la pendiente se vuelve inferior al valor indicado, entonces nuestro sistema de control empieza a funcionar; disminuye el volumen en función del valor calculado del ángulo y la función de regulación indicada.

De este modo, si la operación entra en un intervalo de pérdidas, disminuye el volumen de Vmax. a Vmin. dentro del intervalo de trading Т3...Т5. Después del punto Т5, se realiza el trading con el volumen mínimo indicado -en el modo de rechazo del volumen de la operación. Una vez se recupera la rentabilidad del Expert Advisor y el ángulo de la pendiente de la curva de balance se sitúa por encima del valor indicado, empieza a aumentar el volumen. Esto ocurre en el intervalo Т8...Т10. Después del punto T10, se restaura el volumen de las operaciones de trading al estado inicial Vmax.

La curva de balance que se forma como resultado de dicha regulación se muestra en la parte inferior de la figura 1. Se puede observar que la reducción inicial de B1 a B2 ha disminuido y es de B1 a B2*. También se puede observar que el beneficio a disminuido ligeramente en el intervalo de restauración del volumen T8...T10 -esta es la otra cara de la moneda.

La parte de la curva de balance en la cual se ha realizado el trading con el volumen mínimo indicado está resaltada con el color verde. El color amarillo representa las partes de la transición entre el volumen máximo y mínimo y viceversa. En este caso pueden haber distintas opciones de transición:

Utilizamos el diagrama para ilustrarlo:

Tipos de características de regulación

Figura 2. Tipos de características de regulación

Las características de regulación afectan la velocidad de control del sistema -el retardo en la activación/desactivación, el proceso de transición entre el volumen máximo y mínimo y viceversa. Se recomienda elegir las características sobre una base experimental al alcanzar los mejores resultados en las pruebas.

Por consiguiente, mejoramos el sistema de trading con una retroalimentación basada en el ángulo de la pendiente de la curva de balance. Cabe señalar que esta regulación de volumen solo es adecuada en aquellos sistemas en los cuales el volumen no es una parte del sistema de trading en sí. Por ejemplo, al utilizar el principio de la martingala, no se puede utilizar el sistema directamente sin introducir cambios en el Expert Advisor inicial.

Además, tenemos que llamar su atención sobre los siguientes puntos muy importantes:


Implementación en MQL5 mediante la Programación orientada a objetos

Vamos a escribir una librería que lleva a cabo el enfoque que acabamos de describir. Para hacerlo, usaremos la nueva característica de MQL5 -el enfoque orientado a objetos. Este enfoque permite desarrollar y expandir fácilmente nuestra librería sin tener que escribir la mayor parte del código desde cero.


La clase TradeSymbol

Puesto que las pruebas multidivisas están implementadas en la nueva plataforma de MetaTrader 5, necesitamos una clase que empaquete en sí misma todas las operaciones con cualquier símbolo de trabajo. Permite utilizar esta librería en Expert Advisors multidivisa. Esta clase no afecta directamente el sistema de control, es complementaria. Por lo tanto, se utilizará esta clase para las operaciones con el símbolo de trabajo.

//---------------------------------------------------------------------
//  Operations with work symbol:
//---------------------------------------------------------------------
class TradeSymbol
{
private:
  string  trade_symbol;                          // work symbol

private:
  double  min_trade_volume;                      // minimum allowed volume for trade operations
  double  max_trade_volume;                      // maximum allowed volume for trade operations
  double  min_trade_volume_step;                 // minimum change of volume
  double  max_total_volume;                      // maximum change of volume
  double  symbol_point;                          // size of one point
  double  symbol_tick_size;                      // minimum change of price
  int     symbol_digits;                         // number of digits after decimal point

protected:

public:
  void    RefreshSymbolInfo( );                  // refresh market information about the work symbol
  void    SetTradeSymbol( string _symbol );      // set/change work symbol
  string  GetTradeSymbol( );                     // get work symbol
  double  GetMaxTotalLots( );                    // get maximum cumulative volume
  double  GetPoints( double _delta );            // get change of price in points

public:
  double  NormalizeLots( double _requied_lot );  // get normalized trade volume
  double  NormalizePrice( double _org_price );   // get normalized price with consideration of step of change of quote

public:
  void    TradeSymbol( );                       // constructor
  void    ~TradeSymbol( );                      // destructor
};

La estructura de la clase es muy sencilla. Su objetivo es obtener, almacenar y procesar la información actual del mercado mediante un símbolo determinado. Los métodos principales son: TradeSymbol::RefreshSymbolInfo, TradeSymbol::NormalizeLots, TradeSymbol::NormalizePrice. Vamos a analizarlos uno por uno.


El método TradeSymbol::RefreshSymbolInfo tiene por objetivo actualizar la información del mercado mediante el símbolo de trabajo.

//---------------------------------------------------------------------+
//  Refresh market information by work symbol:                         |
//---------------------------------------------------------------------+
void
TradeSymbol::RefreshSymbolInfo( )
{
//  If a work symbol is not set, don't do anything:
  if( GetTradeSymbol( ) == NULL )
  {
    return;
  }

//  Calculate parameters necessary for normalization of volume:
  min_trade_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_MIN );
  max_trade_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_MAX );
  min_trade_volume_step = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_STEP );

  max_total_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_LIMIT );

  symbol_point = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_POINT );
  symbol_tick_size = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_TRADE_TICK_SIZE );
  symbol_digits = ( int )SymbolInfoInteger( GetTradeSymbol( ), SYMBOL_DIGITS );
}

Preste atención a un punto importante que se utiliza en varios métodos. Puesto que la implementación actual de MQL5 no permite el uso de constructores con los parámetros, se debe llamar al siguiente método para la configuración inicial de los símbolos de trabajo.

void    SetTradeSymbol( string _symbol );      // set/change work symbol


El método TradeSymbol::NormalizeLots se utiliza para obtener un volumen correcto y normalizado. Sabemos que el tamaño de una posición no puede ser inferior al valor mínimo permitido por un broker. El paso mínimo del cambio de una posición lo determina también el broker, y puede ser distinto. Este método devuelve el valor más próximo del volumen desde arriba.

También comprueba si el volumen de la supuesta posición excede el valor máximo permitido por el broker.

//---------------------------------------------------------------------+
//  Get normalized trade volume:                                       |
//---------------------------------------------------------------------+
//  - input necessary volume;                                          |
//  - output is normalized volume;                                     |
//---------------------------------------------------------------------+
double
TradeSymbol::NormalizeLots( double _requied_lots )
{
  double   lots, koeff;
  int      nmbr;

//  If a work symbol is not set, don't do anything:
  if( GetTradeSymbol( ) == NULL )
  {
    return( 0.0 );
  }

  if( this.min_trade_volume_step > 0.0 )
  {
    koeff = 1.0 / min_trade_volume_step;
    nmbr = ( int )MathLog10( koeff );
  }
  else
  {
    koeff = 1.0 / min_trade_volume;
    nmbr = 2;
  }
  lots = MathFloor( _requied_lots * koeff ) / koeff;

//  Lower limit of volume:
  if( lots < min_trade_volume )
  {
    lots = min_trade_volume;
  }

//  Upper limit of volume:
  if( lots > max_trade_volume )
  {
    lots = max_trade_volume;
  }

  lots = NormalizeDouble( lots, nmbr );
  return( lots );
}


El método TradeSymbol::NormalizePrice se utiliza para obtener un precio correcto y normalizado. Puesto que hay que determinar el número de decimales significativas (precisión del precio) para un símbolo determinado, tenemos que truncar el precio. Además, algunos símbolos (por ejemplo, los futuros) tienen un paso mínimo de cambio de precio superior a un punto. Es por eso que necesitamos que los valores de los precios sean múltiplos de mínimos discretos.

//---------------------------------------------------------------------+
//  Normalization of price with consideration of step of price change: |
//---------------------------------------------------------------------+
double
TradeSymbol::NormalizePrice( double _org_price )
{
//  Minimal step of quote change in points:
  double  min_price_step = NormalizeDouble( symbol_tick_size / symbol_point, 0 );

  double  norm_price = NormalizeDouble( NormalizeDouble(( NormalizeDouble( _org_price / symbol_point, 0 )) / min_price_step, 0 ) * min_price_step * symbol_point, symbol_digits );
  return( norm_price );
}

Se introduce el precio necesario sin normalizar en la función. Y esta devuelve el precio normalizado, que es el más cercano al necesario.

Se describe claramente el propósito de los otros métodos en los comentarios; no se requiere ninguna descripción adicional.


La clase TBalanceHistory

Esta clase ha sido diseñada para operar en el historial del balance de una cuenta, su nombre lo dice todo. También es una clase base para varias clases que se describen más adelante. El propósito principal de esta clase es acceder al historial de las operaciones de un Expert Advisor. Además, le permite filtrar el historial mediante el símbolo de trabajo, el "número mágico", la fecha de inicio del seguimiento del Expert Advisor o mediante los tres elementos a la vez.

//---------------------------------------------------------------------+
//  Operations with balance history:                                   |
//---------------------------------------------------------------------+
class TBalanceHistory
{
private:
  long      current_magic;            // value of "magic number" when accessing the history of deals ( 0 - any number )
  long      current_type;             // type of deals ( -1 - all )
  int       current_limit_history;    // limit of depth of history ( 0 - all history )
  datetime   monitoring_begin_date;   // date of start of monitoring history of deals
  int       real_trades;              // number of actual trades already performed

protected:
  TradeSymbol  trade_symbol;          // operations with work symbol

protected:
//  "Raw" arrays:
  double    org_datetime_array[ ];    // date/time of trade
  double    org_result_array[ ];      // result of trade

//  Arrays with data grouped by time:
  double    group_datetime_array[ ];  // date/time of trade
  double    group_result_array[ ];    // result of trade

  double    last_result_array[ ];     // array for storing results of last trades ( points on the Y axis )
  double    last_datetime_array[ ];   // array for storing time of last trades ( points on the X axis )

private:
  void      SortMasterSlaveArray( double& _m[ ], double& _s[ ] ); // synchronous ascending sorting of two arrays

public:
  void      SetTradeSymbol( string _symbol );                     // set/change work symbol
  string    GetTradeSymbol( );                                    // get work symbol
  void      RefreshSymbolInfo( );                                 // refresh market information by work symbol
  void      SetMonitoringBeginDate( datetime _dt );               // set date of start of monitoring
  datetime  GetMonitoringBeginDate( );                            // get date of start of monitoring
  void      SetFiltrParams( long _magic, long _type = -1, int _limit = 0 );// set parameters of filtration of deals

public:
// Get results of last trades:
  int       GetTradeResultsArray( int _max_trades );

public:
  void      TBalanceHistory( );       // constructor
  void      ~TBalanceHistory( );      // destructor
};

Se configura el filtrado durante la lectura de los resultados de las últimas operaciones y el historial mediante el método TBalanceHistory::SetFiltrParams. Tiene los siguientes parámetros de entrada:

Por defecto, se establecen estos valores al crearse el objeto de la clase TBalanceHistory : _magic = 0, _type = -1, _limit = 0.


El método principal de esta clase es TBalanceHistory::GetTradeResultsArray. Está diseñado para rellenar las matrices de la clase last_result_array y last_datetime_array con los resultados de las últimas operaciones. El método tiene los siguientes parámetros de entrada:

//---------------------------------------------------------------------+
//  Reads the results of last (by time) trades to arrays:              |
//---------------------------------------------------------------------+
//  - returns the number of actually read trades                       |
// but not more than specified;                                        |
//---------------------------------------------------------------------+
int
TBalanceHistory::GetTradeResultsArray( int _max_trades )
{
  int       index, limit, count;
  long      deal_type, deal_magic, deal_entry;
  datetime   deal_close_time, current_time;
  ulong     deal_ticket;                        // ticket of deal
  double    trade_result;
  string    symbol, deal_symbol;

  real_trades = 0;

//  Number of trades should be no less than two:
  if( _max_trades < 2 )
  {
    return( 0 );
  }


//  If a work symbol is not specified, don't do anything:
  symbol = trade_symbol.GetTradeSymbol( );
  if( symbol == NULL )
  {
    return( 0 );
  }

//  Request the history of deals and orders from the specified time to the current moment:
  if( HistorySelect( monitoring_begin_date, TimeCurrent( )) != true )
  {
    return( 0 );
  }

//  Calculate number of trades:
  count = HistoryDealsTotal( );

//  If there are less trades in the history than it is necessary, then exit:
  if( count < _max_trades )
  {
    return( 0 );
  }

//  If there are more trades in the history than it is necessary, then limit them:
  if( current_limit_history > 0 && count > current_limit_history )
  {
    limit = count - current_limit_history;
  }
  else
  {
    limit = 0;
  }

//  If needed, adjust dimension of "raw" arrays by the specified number of trades:
  if(( ArraySize( org_datetime_array )) != ( count - limit ))
  {
    ArrayResize( org_datetime_array, count - limit );
    ArrayResize( org_result_array, count - limit );
  }

//  Fill the "raw" array with trades from history base:
  real_trades = 0;
  for( index = count - 1; index >= limit; index-- )
  {
    deal_ticket = HistoryDealGetTicket( index );

//  If those are not closed deals, don't go further:
    deal_entry = HistoryDealGetInteger( deal_ticket, DEAL_ENTRY );
    if( deal_entry != DEAL_ENTRY_OUT )
    {
      continue;
    }

//  Check "magic number" of deal if necessary:
    deal_magic = HistoryDealGetInteger( deal_ticket, DEAL_MAGIC );
    if( current_magic != 0 && deal_magic != current_magic )
    {
      continue;
    }

//  Check symbol of deal:
    deal_symbol = HistoryDealGetString( deal_ticket, DEAL_SYMBOL );
    if( symbol != deal_symbol )
    {
      continue;
    }
                
//  Check type of deal if necessary:
    deal_type = HistoryDealGetInteger( deal_ticket, DEAL_TYPE );
    if( current_type != -1 && deal_type != current_type )
    {
      continue;
    }
    else if( current_type == -1 && ( deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL ))
    {
      continue;
    }
                
//  Check time of closing of deal:
    deal_close_time = ( datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME );
    if( deal_close_time < monitoring_begin_date )
    {
      continue;
    }

//  So, we can read another trade:
    org_datetime_array[ real_trades ] = deal_close_time / 60;
    org_result_array[ real_trades ] = HistoryDealGetDouble( deal_ticket, DEAL_PROFIT ) / HistoryDealGetDouble( deal_ticket, DEAL_VOLUME );
    real_trades++;
  }

//  if there are less trades than necessary, return:
  if( real_trades < _max_trades )
  {
    return( 0 );
  }

  count = real_trades;

//  Sort the "raw" array by date/time of closing the order:
  SortMasterSlaveArray( org_datetime_array, org_result_array );

// If necessary, adjust dimension of group arrays for the specified number of points:
  if(( ArraySize( group_datetime_array )) != count )
  {
    ArrayResize( group_datetime_array, count );
    ArrayResize( group_result_array, count );
  }
  ArrayInitialize( group_datetime_array, 0.0 );
  ArrayInitialize( group_result_array, 0.0 );

//  Fill the output array with grouped data ( group by the identity of date/time of position closing ):
  for( index = 0; index < count; index++ )
  {
//  Get another trade:
    deal_close_time = ( datetime )org_datetime_array[ index ];
    trade_result = org_result_array[ index ];

//  Now check if the same time already exists in the output array:
    current_time = ( datetime )group_datetime_array[ real_trades ];
    if( current_time > 0 && MathAbs( current_time - deal_close_time ) > 0.0 )
    {
      real_trades++;                      // move the pointer to the next element
      group_result_array[ real_trades ] = trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
    else
    {
      group_result_array[ real_trades ] += trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
  }
  real_trades++;                          // now this is the number of unique elements

//  If there are less trades than necessary, exit:
  if( real_trades < _max_trades )
  {
    return( 0 );
  }

  if( ArraySize( last_result_array ) != _max_trades )
  {
    ArrayResize( last_result_array, _max_trades );
    ArrayResize( last_datetime_array, _max_trades );
  }

//  Write the accumulated data to the output arrays with reversed indexation:
  for( index = 0; index < _max_trades; index++ )
  {
    last_result_array[ _max_trades - 1 - index ] = group_result_array[ index ];
    last_datetime_array[ _max_trades - 1 - index ] = group_datetime_array[ index ];
  }

//  In the output array replace the results of single trades with the accumulating total:
  for( index = 1; index < _max_trades; index++ )
  {
    last_result_array[ index ] += last_result_array[ index - 1 ];
  }

  return( _max_trades );
}

Se realizan comprobaciones obligatorias al inicio -si se especifican los símbolos de trabajo, y los parámetros de entrada son correctos.

A continuación, leemos el historial de transacciones y órdenes desde la fecha especificada hasta el momento actual. Se lleva a cabo en la siguiente parte del código:

//  Request the history of deals and orders from the specified time to the current moment:
  if( HistorySelect( monitoring_begin_date, TimeCurrent( )) != true )
  {
    return( 0 );
  }

//  Calculate number of trades:
  count = HistoryDealsTotal( );

//  If there are less trades in the history than it is necessary, then exit:
  if( count < _max_trades )
  {
    return( 0 );
  }

Además, se comprueba el número total de transacciones en el historial. Si es inferior al número especificado, no hay necesidad de realizar más acciones. En cuanto estén las matrices "brutas" (raw) preparadas, se ejecuta el bucle para rellenarlas con la información del historial de las operaciones. Se hace del siguiente modo:

//  Fill the "raw" array from the base of history of trades:
  real_trades = 0;
  for( index = count - 1; index >= limit; index-- )
  {
    deal_ticket = HistoryDealGetTicket( index );

//  If the trades are not closed, don't go further:
    deal_entry = HistoryDealGetInteger( deal_ticket, DEAL_ENTRY );
    if( deal_entry != DEAL_ENTRY_OUT )
    {
      continue;
    }

//  Check "magic number" of deal if necessary:
    deal_magic = HistoryDealGetInteger( deal_ticket, DEAL_MAGIC );
    if( _magic != 0 && deal_magic != _magic )
    {
      continue;
    }

//  Check symbols of deal:
    deal_symbol = HistoryDealGetString( deal_ticket, DEAL_SYMBOL );
    if( symbol != deal_symbol )
    {
      continue;
    }
                
//  Check type of deal if necessary:
    deal_type = HistoryDealGetInteger( deal_ticket, DEAL_TYPE );
    if( _type != -1 && deal_type != _type )
    {
      continue;
    }
    else if( _type == -1 && ( deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL ))
    {
      continue;
    }
                
//  Check time of closing of deal:
    deal_close_time = ( datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME );
    if( deal_close_time < monitoring_begin_date )
    {
      continue;
    }

//  So, we can rad another trade:
    org_datetime_array[ real_trades ] = deal_close_time / 60;
    org_result_array[ real_trades ] = HistoryDealGetDouble( deal_ticket, DEAL_PROFIT ) / HistoryDealGetDouble( deal_ticket, DEAL_VOLUME );
    real_trades++;
  }

//  If there are less trades than necessary, exit:
  if( real_trades < _max_trades )
  {
    return( 0 );
  }

Al principio, se lee el ticket de la transacción desde el historial mediante la función HistoryDealGetTicket; se realizan más lecturas de los detalles de la transacción mediante el ticket obtenido. Ya que nos interesan solamente las operaciones cerradas (vamos a analizar el balance), se comprueba el tipo de transacción al principio. Se hace llamando a la función HistoryDealGetInteger con el parámetro DEAL_ENTRY. Si la función devuelve DEAL_ENTRY_OUT, entonces es el cierre de una posición.

Después de este "número mágico" de la transacción, se comprueba el tipo de transacción (si se ha especificado el parámetro de entrada del método) y el símbolo de la transacción. Si todos los parámetros de la transacción cumplen los requisitos, entonces se comprueba el último parámetro -el tiempo de cierre de la transacción. Se hace del siguiente modo:

//  Check the time of closing of deal:
    deal_close_time = ( datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME );
    if( deal_close_time < monitoring_begin_date )
    {
      continue;
    }

Se compara la fecha/hora de la transacción con la fecha/hora dada al principio del seguimiento del historial. Si la fecha/hora de la transacción es mayor, entonces vamos a leer nuestra operación en la matriz, leer el resultado de la operación en puntos y el tiempo de la operación en minutos (en este caso, el tiempo de cierre). A continuación, se incrementa el contador de lectura de transacciones real_trades; y el bucle continua.

Una vez se rellenan las matrices "brutas" con las informaciones necesarias, debemos ordenar la matriz en la cual está almacenado el tiempo de cierre de las transacciones. Al mismo tiempo, tenemos que mantener la concordancia entre el tiempo de cierre en la matriz org_datetime_array y los resultados de las transacciones en la matriz org_result_array. Esto se hace mediante el método escrito a propósito:

TBalanceHistory::SortMasterSlaveArray( double& _master[ ], double& _slave[ ] ). El primer parámetro es _master, la matriz en orden ascendente. El segundo parámetro es _slave - the array, sus elementos deben ser movidos de forma sincronizada con los elementos de la primera matriz. La ordenación se hace mediante el método "bubble".

Después de todas las operaciones descritas antes, disponemos de dos matrices con el tiempo y los resultados de las transacciones ordenados por el tiempo. Puesto que un solo punto de la curva de balance (punto en el eje Y) puede corresponder a cada momento (punto en el eje X), tenemos que agrupar los elementos de la matriz con el mismo tiempo de cierre (si lo hay). Se lleva a cabo esta operación mediante el siguiente código:

//  Fill the output array with grouped data ( group by identity of date/time of closing of position ):
  real_trades = 0;
  for( index = 0; index < count; index++ )
  {
//  Get another trade:
    deal_close_time = ( datetime )org_datetime_array[ index ];
    trade_result = org_result_array[ index ];

//  Now check, if the same time already exists in the output array:
    current_time = ( datetime )group_datetime_array[ real_trades ];
    if( current_time > 0 && MathAbs( current_time - deal_close_time ) > 0.0 )
    {
      real_trades++;                      // move the pointer to the next element
      group_result_array[ real_trades ] = trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
    else
    {
      group_result_array[ real_trades ] += trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
  }
  real_trades++;                          // now this is the number of unique elements

En la práctica, se suman aquí todas las operaciones con el mismo tiempo de cierre. Se escriben los resultados en las matrices TBalanceHistory::group_datetime_array (tiempo de cierre) y TBalanceHistory::group_result_array (resultados de las operaciones). Después de esto, obtenemos dos matrices ordenadas con los mismos elementos. En este caso, identificamos el tiempo dentro de un minuto. Se puede ilustrar esta transformación gráficamente:

Agrupación de las transacciones con el mismo tiempo

Figura 3. Agrupación de las transacciones con el mismo tiempo

Todas las transacciones dentro del minuto (parte izquierda de la figura) se agrupan en una solo, redondeando el tiempo y sumando los resultados (parte derecha de la figura). Esto permite el suavizado de las "oscilaciones" del tiempo de cierre de las transacciones y mejorar la estabilidad de la regulación.

Después de esto, tiene que transformar dos veces las matrices obtenidas. Invertir el orden de los elementos de modo que la primera transacción corresponda el elemento cero; y sustituir los resultados de las operaciones individuales con el total acumulado; es decir, con el balance. Se hace en la siguiente parte del código:

//  Write the accumulated data into output arrays with reversed indexation:
  for( index = 0; index < _max_trades; index++ )
  {
    last_result_array[ _max_trades - 1 - index ] = group_result_array[ index ];
    last_datetime_array[ _max_trades - 1 - index ] = group_datetime_array[ index ];
  }

//  Replace the results of single trades with the cumulative total in the output array:
  for( index = 1; index < _max_trades; index++ )
  {
    last_result_array[ index ] += last_result_array[ index - 1 ];
  }


La clase TBalanceSlope

Esta clase está destinada a realizar operaciones con la curva de balance de una cuenta. Se generó a partir de la clase TBalanceHistory, y hereda todos sus métodos y datos protegidos y privados. Vamos a examinar su estructura:

//---------------------------------------------------------------------+
//  Operations with the balance curve:                                 |
//---------------------------------------------------------------------+
class TBalanceSlope : public TBalanceHistory
{
private:
  double    current_slope;               // current angle of slope of the balance curve
  int       slope_count_points;          // number of points ( trades ) for calculation of slope angle
        
private:
  double    LR_koeff_A, LR_koeff_B;      // rates for the equation of the straight-line regression
  double    LR_points_array[ ];          // array of point of the straight-line regression

private:
  void      CalcLR( double& X[ ], double& Y[ ] );  // calculate the equation of the straight-line regression

public:
  void      SetSlopePoints( int _number );        // set the number of points for calculation of angle of slope
  double    CalcSlope( );                         // calculate the slope angle

public:
  void      TBalanceSlope( );                     // constructor
  void      ~TBalanceSlope( );                    // destructor
};


Vamos a determinar el ángulo de la pendiente de la curva de balance mediante el ángulo de la pendiente de la línea de regresión linear dibujada para una cantidad determinada de puntos (operaciones) en la curva de balance. Por lo tanto, y en primer lugar, tenemos que calcular la ecuación de la línea recta de regresión de la siguiente forma: A*x + B. Se hace esta tarea mediante el siguiente método:

//---------------------------------------------------------------------+
//  Calculate the equation of the straight-line regression:            |
//---------------------------------------------------------------------+
//  input parameters:                                                  |
//    X[ ] - arras of values of number series on the X axis;           |
//    Y[ ] - arras of values of number series on the Y axis;           |
//---------------------------------------------------------------------+
void
TBalanceSlope::CalcLR( double& X[ ], double& Y[ ] )
{
  double    mo_X = 0, mo_Y = 0, var_0 = 0, var_1 = 0;
  int       i;
  int       size = ArraySize( X );
  double    nmb = ( double )size;

//  If the number of points is less than two, the curve cannot be calculated:
  if( size < 2 )
  {
    return;
  }

  for( i = 0; i < size; i++ )
  {
    mo_X += X[ i ];
    mo_Y += Y[ i ];
  }
  mo_X /= nmb;
  mo_Y /= nmb;

  for( i = 0; i < size; i++ )
  {
    var_0 += ( X[ i ] - mo_X ) * ( Y[ i ] - mo_Y );
    var_1 += ( X[ i ] - mo_X ) * ( X[ i ] - mo_X );
  }

//  Value of the A coefficient:
  if( var_1 != 0.0 )
  {
    LR_koeff_A = var_0 / var_1;
  }
  else
  {
    LR_koeff_A = 0.0;
  }

//  Value of the B coefficient:
  LR_koeff_B = mo_Y - LR_koeff_A * mo_X;

//  Fill the array of points that lie on the regression line:
  ArrayResize( LR_points_array, size );
  for( i = 0; i < size; i++ )
  {
    LR_points_array[ i ] = LR_koeff_A * X[ i ] + LR_koeff_B;
  }
}

Aquí, hemos utilizado el método de los mínimos cuadrados para calcular el error mínimo de la posición de la línea de regresión con respecto a los datos iniciales. También se rellena la matriz que almacena las coordenadas Y, que se encuentran en la línea calculada. Esta matriz no se utiliza por el momento y servirá para otros desarrollos.


El método principal que se utiliza en esta clase es TBalanceSlope::CalcSlope. Devuelve el ángulo de la pendiente de la curva de balance, que se calcula mediante la cantidad especificada de la última operación. Esta es su implementación:

//---------------------------------------------------------------------+
//  Calculate slope angle:                                             |
//---------------------------------------------------------------------+
double
TBalanceSlope::CalcSlope( )
{
//  Get result of trading from the history of trades:
  int      nmb = GetTradeResultsArray( slope_count_points );
  if( nmb < slope_count_points )
  {
    return( 0.0 );
  }

//  Calculate the regression line by the results of last trades:
  CalcLR( last_datetime_array, last_result_array );
  current_slope = LR_koeff_A;

  return( current_slope );
}

En primer lugar, se analiza la cantidad especificada de los últimos puntos de la curva de balance. Esto se hace llamando el método de la clase base TBalanceSlope::GetTradeResultsArray. Si la cantidad de puntos leídos no es inferior a la cantidad indicada, se calcula la línea de regresión. Esto se hace mediante el método TBalanceSlope::CalcLR. Se utilizan como argumentos las matrices last_result_array y last_datetime_array, pertenecientes a la clase base y que se han rellenado en la etapa anterior.

Los métodos restantes son sencillos y no requieren ninguna descripción detallada.


La clase TBalanceSlopeControl

Es una clase base, que maneja la pendiente de la curva de balance mediante la modificación del volumen de operaciones. Se generó a partir de la clase TBalanceSlope y hereda todos sus métodos y datos protegidos y privados. El único propósito de esta clase es calcular el volumen de operaciones actual en función del ángulo actual de la pendiente de la curva de balance. Vamos a echar un vistazo en ella:

//---------------------------------------------------------------------+
//  Managing slope of the balance curve:                               |
//---------------------------------------------------------------------+
enum LotsState
{
  LOTS_NORMAL = 1,            // mode of trading with normal volume
  LOTS_REJECTED = -1,         // mode of trading with lowered volume
  LOTS_INTERMEDIATE = 0,      // mode of trading with intermediate volume
};
//---------------------------------------------------------------------
class TBalanceSlopeControl : public TBalanceSlope
{
private:
  double    min_slope;          // slope angle that corresponds to the mode of volume rejection
  double    max_slope;          // slope angle that corresponds to the mode of normal volume
  double    centr_slope;        // slope angle that corresponds to the mode of volume switching without hysteresis

private:
  ControlType  control_type;    // type of the regulation function

private:
  double    rejected_lots;      // volume in the rejection mode
  double    normal_lots;        // volume in the normal mode
  double    intermed_lots;      // volume in the intermediate mode

private:
  LotsState current_lots_state; // current mode of volume

public:
  void      SetControlType( ControlType _control );  // set type of the regulation characteristic
  void      SetControlParams( double _min_slope, double _max_slope, double _centr_slope );

public:
  double    CalcTradeLots( double _min_lots, double _max_lots );  // get trade volume

protected:
  double    CalcIntermediateLots( double _min_lots, double _max_lots, double _slope );

public:
  void      TBalanceSlopeControl( );   // constructor
  void      ~TBalanceSlopeControl( );  // destructor
};


Antes de calcular el volumen actual, tenemos que establecer los parámetros iniciales. Esto se hace llamando a los siguientes métodos:

  void      SetControlType( ControlType _control );  // set type of the regulation characteristic

Input parameter_control - este es el tipo de característica de regulación. Puede tener los siguientes valores:


  void      SetControlParams( double _min_slope, double _max_slope, double _centr_slope );

Los parámetros de entrada son los siguientes:


Se calcula el volumen mediante el siguiente método:

//---------------------------------------------------------------------+
//  Get trade volume:                                                  |
//---------------------------------------------------------------------+
double
TBalanceSlopeControl::CalcTradeLots( double _min_lots, double _max_lots )
{
//  Try to calculate slope of the balance curve:
  double    current_slope = CalcSlope( );

//  If the specified amount of trades is not accumulated yet, trade with minimal volume:
  if( GetRealTrades( ) < GetSlopePoints( ))
  {
    current_lots_state = LOTS_REJECTED;
    rejected_lots = trade_symbol.NormalizeLots( _min_lots );
    return( rejected_lots );
  }

//  If the regulation function is stepped without hysteresis:
  if( control_type == STEP_WITHOUT_HYSTERESIS )
  {
    if( current_slope < centr_slope )
    {
      current_lots_state = LOTS_REJECTED;
      rejected_lots = trade_symbol.NormalizeLots( _min_lots );
      return( rejected_lots );
    }
    else
    {
      current_lots_state = LOTS_NORMAL;
      normal_lots = trade_symbol.NormalizeLots( _max_lots );
      return( normal_lots );
    }
  }

//  If the slope of linear regression for the balance curve is less than the allowed one:
  if( current_slope < min_slope )
  {
    current_lots_state = LOTS_REJECTED;
    rejected_lots = trade_symbol.NormalizeLots( _min_lots );
    return( rejected_lots );
  }

//  If the slope of linear regression for the balance curve is greater than specified:
  if( current_slope > max_slope )
  {
    current_lots_state = LOTS_NORMAL;
    normal_lots = trade_symbol.NormalizeLots( _max_lots );
    return( normal_lots );
  }

//  The slope of linear regression for the balance curve is within specified borders (intermediate state):
  current_lots_state = LOTS_INTERMEDIATE;

//  Calculate the value of intermediate volume:
  intermed_lots = CalcIntermediateLots( _min_lots, _max_lots, current_slope );
  intermed_lots = trade_symbol.NormalizeLots( intermed_lots );

  return( intermed_lots );
}

Estos son los puntos fundamentales de la implementación del método TBalanceSlopeControl::CalcTradeLots:

En función del ángulo calculado de la pendiente, se lleva a cabo el trading con el volumen mínimo, máximo o intermedio. Se calcula el volumen intermedio mediante el sencillo método - TBalanceSlopeControl::CalcIntermediateLots. Es un método protegido y se utiliza dentro de la clase. A continuación, se muestra su código:

//---------------------------------------------------------------------+
//  Calculation of intermediate volume:                                |
//---------------------------------------------------------------------+
double
TBalanceSlopeControl::CalcIntermediateLots( double _min_lots, double _max_lots, double _slope )
{
  double    lots;

//  If the regulation function is stepped with hysteresis:
  if( control_type == STEP_WITH_HYSTERESISH )
  {
    if( current_lots_state == LOTS_REJECTED && _slope > min_slope && _slope < max_slope )
    {
      lots = _min_lots;
    }
    else if( current_lots_state == LOTS_NORMAL && _slope > min_slope && _slope < max_slope )
    {
      lots = _max_lots;
    }
  }
//  If the regulation function is linear:
  else if( control_type == LINEAR )
  {
    double  a = ( _max_lots - _min_lots ) / ( max_slope - min_slope );
    double  b = normal_lots - a * .max_slope;
    lots = a * _slope + b;
  }
//  If the regulation function is non-linear ( not implemented yet ):
  else if( control_type == NON_LINEAR )
  {
    lots = _min_lots;
  }
//  If the regulation function is unknown:
  else
  {
    lots = _min_lots;
  }

  return( lots );
}

Los otros métodos de esta clase no requieren ninguna descripción.


Ejemplo de integración del sistema en un Expert Advisor

Vamos a ver el proceso de implementación del sistema de control de la pendiente de la curva de balance en un Expert Advisor paso a paso.


Paso 1 - añadir la instrucción para conectar la librería desarrollada al Expert Advisor:

#include  <BalanceSlopeControl.mqh>


Paso 2 - añadir las variables externas para configurar los parámetros del sistema de control de la pendiente de la línea de balance en un Expert Advisor :

//---------------------------------------------------------------------+
//  Parameters of the system of controlling                            |
//  the slope of the balance curve;                                    |
//---------------------------------------------------------------------+
enum SetLogic 
{
  No = 0,
  Yes = 1,
};
//---------------------------------------------------------------------
input SetLogic     UseAutoBalanceControl = No;
//---------------------------------------------------------------------
input ControlType  BalanceControlType = STEP_WITHOUT_HYSTERESIS;
//---------------------------------------------------------------------
//  Amount of last trades for calculation of LR of the balance curve:
input int          TradesNumberToCalcLR = 3;
//---------------------------------------------------------------------
//  Slope of LR to decrease the volume to minimum:
input double       LRKoeffForRejectLots = -0.030;
//---------------------------------------------------------------------
//  Slope of LR to restore the normal mode of trading:
input double       LRKoeffForRestoreLots = 0.050;
//---------------------------------------------------------------------
//  Slope of LR to work in the intermediate mode:
input double       LRKoeffForIntermedLots = -0.020;
//---------------------------------------------------------------------
//  Decrease the initial volume to the specified value when the LR is inclined down
input double       RejectedLots = 0.10;
//---------------------------------------------------------------------
//  Normal work volume in the mode of MM with fixed volume:
input double       NormalLots = 1.0;


Paso 3 - añadir el objeto del tipo TBalanceSlopeControl al Expert Advisor:

TBalanceSlopeControl  BalanceControl;

Se puede añadir esta declaración al principio del Expert Advisor, antes de las definiciones de las funciones.


Paso 4 - añadir el código para la inicialización del sistema de control de la curva de balance a la función OnInit del Expert Advisor:

//  Adjust our system of controlling the slope of the balance curve:
  BalanceControl.SetTradeSymbol( Symbol( ));
  BalanceControl.SetControlType( BalanceControlType );
  BalanceControl.SetControlParams( LRKoeffForRejectLots, LRKoeffForRestoreLots, LRKoeffForIntermedLots );
  BalanceControl.SetSlopePoints( TradesNumberToCalcLR );
  BalanceControl.SetFiltrParams( 0, -1, 0 );
  BalanceControl.SetMonitoringBeginDate( 0 );


Paso 5 - añadir la llamada del método para actualizar la información actual del mercado a la función OnTick del Expert Advisor:

//  Refresh market information:
  BalanceControl.RefreshSymbolInfo( );

Se puede añadir la llamada de este método al principio de la función OnTick o después de comprobar la llegada de una nueva barra (para los Expert Advisors con esta comprobación).


Paso 6 - añadir el código para calcular el volumen actual antes del código en el cual se abren las posiciones:

  if( UseAutoBalanceControl == Yes )
  {
    current_lots = BalanceControl.CalcTradeLots( RejectedLots, NormalLots );
  }
  else
  {
    current_lots = NormalLots;
  }

Si se utiliza un sistema de gestión de fondos en el Expert Advisor, entonces en lugar de NormalLots hay que escribir el método TBalanceSlopeControl::CalcTradeLots -se calcula el volumen actual mediante el sistema de gestión de fondos del Expert Advisor.

El Expert Advisor de prueba BSCS-TestExpert.mq5 con el sistema incorporado descrito antes está adjunto a este artículo. Su principio de funcionamiento se basa en la intersección de los niveles del indicador CCI. Se ha desarrollado este Expert Advisor para las pruebas y no es adecuado para trabajar con cuentas reales. Lo vamos a probar con la periodicidad H4 (2008.07.01 - 2010.09.01) del par EURUSD.

Vamos a analizar los resultados de funcionamiento de este EA. A continuación, se muestra el gráfico del cambio de balance con el sistema de control de pendiente desactivado. Para verlo, asigne el valor No al parámetro externo UseAutoBalanceControl.

Gráfico inicial del cambio de balance

Figura 4. Gráfico inicial del cambio de balance


Ahora, asigne al parámetro externo UseAutoBalanceControl el valor Yes y prueba el Expert Advisor. Obtendrá el gráfico con el sistema de control de pendiente del balance activado.

Gráfico del cambio de balance con el sistema de control activado

Figura 5. Gráfico del cambio de balance con el sistema de control activado

Se puede observar que la mayoría de los intervalos en el gráfico superior están recortados (fig. 4), y tienen una forma plana en el gráfico inferior (fig. 5). Este es el resultado del funcionamiento de nuestro sistema. Puede comparar los parámetros principales del funcionamiento del Expert Advisor:

 Parámetro
 UseAutoBalanceControl = No  UseAutoBalanceControl = Yes
 Beneficio neto:  18 378.00 17 261.73
 Factor de beneficio:  1.47 1.81
 Factor de recuperación:  2.66  3.74
 Beneficio previsto:  117.1 110.65
 Reducción absoluta del balance: 1 310.50 131.05
 Reducción absoluta del patrimonio:  1 390.50 514.85
 Reducción máxima del balance:  5 569.50 (5.04%) 3 762.15 (3.35%)
 Reducción máxima del patrimonio: 6 899.50 (6.19%)
4 609.60 (4.08%)


Los mejores parámetros de la comparación están resaltados con el color verde. La ganancia y el beneficio previsto han disminuido ligeramente; esta es la otra cara de la regulación, que se debe a los retardos en el cambio entre los estados del volumen de operaciones. Pero por lo general, hay una mejora en el ritmo de trabajo del Expert Advisor. Especialmente, mejoras de la reducción (drawdown) y el factor de beneficio (profit factor).


Conclusión

Veo varias maneras de mejorar este sistema:

Por lo tanto, hemos tratado los principios y los resultados del trabajo del sistema que permite mejorar las características cualitativas de un Expert Advisor. En algunos casos, juntar este funcionamiento con un sistema de gestión de fondos, puede aumentar la rentabilidad sin aumentar los riesgos.

Te lo recuerdo otra vez: no existe ningún sistema adicional que pueda convertir un Expert Advisor con pérdidas a uno rentable.