
Control de la pendiente de la curva de balance durante el funcionamiento de un Expert Advisor
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:
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:
- escalonada -el volumen cambia en pasos discretos entre el volumen máximo y mínimo y viceversa;
- lineal -el volumen cambia de forma lineal en función del ángulo de la pendiente de la curva de balance dentro del intervalo regulado;
- escalonada con histéresis -la transición entre el volumen máximo y mínimo y viceversa se lleva a cabo a distintos valores del ángulo de la pendiente.
Utilizamos el diagrama para ilustrarlo:
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:
- la efectividad de la gestión de la pendiente de la línea de balance depende directamente de la relación entre el volumen de operaciones en modo de funcionamiento normal y el volumen en modo de rechazo de volumen. Cuan mayor es esta relación, mayor es la efectividad de gestión. Es por eso que el volumen de operaciones inicial debe ser significativamente mayor que el mínimo posible.
- el intervalo medio de las fluctuaciones de subidas y bajadas del balance del Expert Advisor debe ser considerablemente mayor que el tiempo de reacción del sistema de control. De lo contrario, el sistema no conseguirá regular la pendiente de la curva de balance. Cuan mayor es la relación entre el intervalo medio y el tiempo de reacción, mayor es la efectividad del sistema. Estos requisitos afectan a casi todos los sistemas de regulación automática.
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:
- _magic - "número mágico" de las operaciones que hay que leer desde el historial. Si se especifica el valor cero, entonces se van a leer todas las operaciones con cualquier "número mágico".
- _type - tipo de transacciones que hay que leer. Puede tener los siguientes valores - DEAL_TYPE_BUY (para la lectura de operaciones largas solo), DEAL_TYPE_SELL (para la lectura de operaciones largas solo) y -1 (para la lectura tanto de operaciones largas como cortas).
- _limit - límites de profundidad del análisis del historial de operaciones. Si es igual a cero, se analiza todo el historial disponible.
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:
- _max_trades - número máximo de operaciones que hay que leer del historial y escribir en las matrices de salida. Dado que necesitamos por lo menos dos puntos para calcular el ángulo de la pendiente, este valor no puede ser inferior a dos. Si este valor es igual a cero, se analizará todo el historial de operaciones. En la práctica, se especifica el número de puntos necesarios para el cálculo de la pendiente de la curva de balance aquí.
//---------------------------------------------------------------------+ // 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:
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:
- STEP_WITH_HYSTERESISH - característica de regulación escalonada con histéresis;
- STEP_WITHOUT_HYSTERESIS - característica de regulación escalonada sin histéresis;
- LINEAR - característica de regulación lineal;
- NON_LINEAR - característica de regulación no lineal (no está implementada en esta versión);
void SetControlParams( double _min_slope, double _max_slope, double _centr_slope );
Los parámetros de entrada son los siguientes:
- _min_slope - ángulo de la pendiente de la curva de balance correspondiente al trading con el volumen mínimo;
- _max_slope - ángulo de la pendiente de la curva de balance correspondiente al trading con el volumen máximo;
- _centr_slope - ángulo de la pendiente de la curva de balance correspondiente a la característica de regulación escalonada sin histéresis;
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:
- Operar con el volumen mínimo, hasta que se acumule la cantidad mínima especificada de operaciones. Es lógico, ya que no se sabe en qué período (rentable o no) se encuentra el Expert Advisor en este momento.
- Si la función de regulación es la escalonada sin histéresis, entonces para establecer el ángulo de cambio entre los modos de funcionamiento del trading mediante el método TBalanceSlopeControl::SetControlParams, solo se debe utilizar el parámetro _centr_slope. Los parámetros _min_slope y _max_slope no se tienen en cuenta. Esto se hace para realizar la optimización correcta mediante este parámetro en el probador de estrategias de MetaTrader 5.
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.
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.
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:- Mediante el trading virtual cuando el Expert Advisor entra en un período de trabajo desfavorable. Con lo cual, el volumen normal de trabajo no tiene ninguna importancia. Esto permitirá disminuir la reducción (drawdown).
- Mediante algoritmos más complejos para determinar el estado de trabajo actual del Expert Advisor (rentable o no). Por ejemplo, podemos intentar aplicar una red neuronal a este análisis. Por supuesto, en este caso se requieren investigaciones adicionales.
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.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/145





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso