English Русский Deutsch 日本語 Português
preview
Desarrollamos de un asesor multidivisa (Parte 1): Funcionamiento conjunto de varias estrategias comerciales

Desarrollamos de un asesor multidivisa (Parte 1): Funcionamiento conjunto de varias estrategias comerciales

MetaTrader 5Trading | 17 julio 2024, 13:01
152 0
Yuriy Bykov
Yuriy Bykov

A lo largo de mi trabajo, he tenido que lidiar con diversas estrategias comerciales. Por regla general, los asesores para el trading automatizado implementan solo una idea comercial. Las dificultades para garantizar el trabajo conjunto estable de muchos asesores en un terminal obligaban a elegir solo un pequeño número de los mejores. Aun así, es una pena desechar estrategias perfectamente viables por esta causa. ¿Cómo conseguir varios asesores trabajen juntos?


Planteamiento del problema

En primer lugar, debemos definir lo que queremos y lo que tenemos.

Tenemos (bueno, o casi):

  • algunas estrategias comerciales diferentes que funcionan en diferentes símbolos y marcos temporales en forma de código de asesor listo para usar o un simple conjunto de reglas formulado para realizar transacciones comerciales
  • un depósito inicial
  • una reducción máxima admisible

Queremos:

  • el funcionamiento conjunto de todas las estrategias seleccionadas en una cuenta en varios símbolos y marcos temporales
  • la distribución del depósito inicial entre todas por igual o según coeficientes establecidos
  • el cálculo automático de los volúmenes de las posiciones abiertas para respetar la tasa máxima de reducción permitida
  • el procesamiento correcto del reinicio del terminal
  • la posibilidad de ejecutar el inicio en MT5 y MT4

Usaremos el enfoque orientado a objetos, MQL5, y el simulador estándar en MetaTrader 5.

El conjunto de tareas es bastante amplio, así que lo resolveremos paso a paso.

En el primer paso, tomaremos una idea comercial sencilla, y crearemos un asesor sencillo basado en ella. Luego lo optimizaremos y seleccionaremos los dos mejores conjuntos de parámetros. Después crearemos un asesor que contenga en su interior dos ejemplares del asesor simple original y observaremos los resultados.


De la idea a la estrategia

Como idea comercial de prueba, tomaremos una como la que sigue.  

Supongamos que cuando un símbolo se comercia mucho, el precio puede cambiar con más fuerza por unidad de tiempo que cuando el símbolo se comercia poco. Entonces, si vemos que el comercio se ha intensificado y el precio ha cambiado en alguna dirección, es posible que siga cambiando en la misma dirección en un futuro próximo. Vamos a intentar sacar provecho de ello.

Una estrategia comercial es un conjunto de reglas para abrir y cerrar posiciones basadas en una idea comercial y no contiene ningún parámetro desconocido. Este conjunto de reglas debe permitir determinar, para un momento dado del funcionamiento de la estrategia, si debemos abrir alguna posición y, en caso afirmativo, cuáles.

Intentemos convertir una idea en una estrategia. En primer lugar, necesitaremos detectar de algún modo el aumento en la intensidad del comercio. Sin esto, no podremos determinar cuándo abrir posiciones. Para ello utilizaremos el volumen de ticks, es decir, el número de precios nuevos recibido por el terminal durante la vela actual. Un mayor volumen de ticks se considerará señal de una negociación más intensa. No obstante, la intensidad puede variar considerablemente según el símbolo. Por ello, no podremos establecer ningún nivel único para el volumen de ticks por encima del cual consideraremos que ha comenzado una negociación intensiva. Entonces podemos utilizar el volumen medio de varias velas para determinar dicho nivel. Tras reflexionar un poco, podemos ofrecer esta descripción:

Colocaremos una orden pendiente en el momento en que el volumen de ticks de la vela supere el volumen medio en la dirección de la vela actual. Cada orden tendrá un plazo de caducidad, transcurrido el cual será eliminada. Si una orden pendiente se ha convertido en una posición, solo se cerrará cuando se alcancen los niveles StopLoss y TakeProfit indicados. Si el volumen de ticks es incluso superior a la media, podremos colocar órdenes adicionales además de la orden pendiente ya abierta.

Esto ya es una descripción más detallada, pero aún incompleta. Así que volveremos a leerla y subrayamos todos los lugares en los que algo no esté claro. En estos lugares se requerirán explicaciones más detalladas. 

Estas son las preguntas que me han surgido:

  • "Colocamos una orden pendiente..." - ¿Qué órdenes pendientes vamos a colocar?
  • "... volumen medio,..." - ¿Cómo calculamos el volumen medio de una vela?
  • "...supera el volumen medio, ..." ¿Cómo se define la superación del volumen medio?
  • "... Si el volumen de ticks es aún mayor que la media, ... "- ¿Cómo se define dicho exceso aún mayor?
  • "... pueden colocarse órdenes adicionales" ¿Cuántas órdenes se pueden colocar?

¿Qué órdenes pendientes realizaremos? Basándonos en la idea, esperamos que el precio continúe moviéndose en la misma dirección que adoptó desde el principio de la vela. Si, por ejemplo, el precio en el momento actual es más alto que al principio del periodo de la vela, deberíamos abrir una orden de compra pendiente. Si abrimos BUY_LIMIT, para que funcione, primero el precio deberá volver (bajar) un poco, y luego para que la posición abierta obtenga beneficios, el precio deberá volver a subir. Si abrimos BUY_STOP, el precio deberá seguir moviéndose (subir) un poco más para abrir la posición, y luego subir aún más para obtener beneficios.

No está claro cuál de estas opciones sería mejor. Así que, para simplificar, elegiremos que siempre vamos a abrir órdenes stop (BUY_STOP y SELL_STOP). En el futuro, esto podría convertirse en un parámetro de estrategia cuyo valor determinará qué órdenes se abrirán.

¿Cómo calcular el volumen medio de una vela? Para calcular el volumen medio, tendremos que seleccionar las velas cuyos volúmenes se incluirán en el cálculo de la media. Tomaremos un cierto número de velas cerradas consecutivas. A continuación, si establecemos el número de velas, podremos calcular el volumen medio de ticks.

¿Cómo se determinará si hemos superado el volumen medio? Si tomamos una condición de la forma

V > V_avr ,

dónde
V es el volumen de ticks de la vela actual,
V_avr es el volumen medio por tick,
entonces el cumplimiento de esta condición se alcanzará en aproximadamente la mitad de las velas. Basándonos en esta idea, deberíamos colocar órdenes solo cuando el volumen se encuentre no solo por encima de la media, sino notablemente por encima de la media. De lo contrario, aún no podrá considerarse una señal de un comercio más intenso en esta vela en comparación con las velas anteriores. Podemos usar, por ejemplo, una fórmula como ésta:

V > V_avr + D * V_avr,

donde D será el coeficiente numérico. Si D = 1, la apertura se producirá cuando el volumen actual sea 2 veces el volumen medio, y si, por ejemplo, D = 2, la apertura se producirá cuando el volumen actual supere el volumen medio en 3 veces.

Sin embargo, esta condición solo podrá aplicarse para abrir una orden, ya que si se utiliza la misma condición para abrir la segunda orden y las siguientes, estas se abrirán inmediatamente después de la primera, que podrá sustituirse simplemente abriendo una orden de mayor volumen.

¿Cómo definir esta superación aún mayor? Para ello, añadiremos un parámetro más a la fórmula de la condición: el número de órdenes abiertas N:

V > V_avr + D * V_avr + N * D * V_avr.

Entonces, para que la segunda orden se abra después de la primera (es decir, N = 1), deberá cumplirse la condición:

V > V_avr + 2 * D * V_avr.

Para abrir la primera orden (N = 0), la fórmula adoptará la forma que ya conocemos:

V > V_avr + D * V_avr.

Una última corrección a la fórmula de apertura. Crearemos dos parámetros independientes D y D_add en lugar del mismo parámetro D para la primera orden y las siguientes:

V > V_avr + D * V_avr + N * D_add * V_avr,

V > V_avr * (1 + D + N * D_add)

Parece que esto nos dará más libertad a la hora de seleccionar los parámetros óptimos para la estrategia.

Si nuestra condición usa el valor N como número total de órdenes y posiciones, supondremos que cada orden pendiente se convertirá en una posición independiente, en lugar de aumentar el volumen de una posición ya abierta. Por ello, de momento, tendremos que limitar el alcance de dicha estrategia a trabajar en cuentas con registro de posición independiente ("cobertura").  

Cuando todo esté claro, enumeraremos las magnitudes que pueden tomar distintos valores, no un único valor. Estos serán nuestros parámetros de entrada para la estrategia. Tendremos en cuenta que para abrir órdenes necesitamos conocer el volumen, la distancia desde el precio actual, la hora de expiración y los niveles StopLoss y TakeProfit. Luego tendremos esta descripción:

El asesor se iniciará en un determinado símbolo y periodo (timeframe) en la cuenta Hedge

Luego configuraremos los parámetros de entrada:

  • Número de velas para promediar el volumen (K)
  • Desviación relativa respecto a la media para la apertura de la primera orden (D)
  • Desviación relativa respecto a la media en la apertura de la segunda orden y siguientes (D_add)
  • Distancia desde el precio hasta la orden pendiente
  • Stop Loss (en pips)
  • Take Profit (en pips)
  • Hora de expiración de las órdenes pendientes (en minutos)
  • Número máximo de órdenes abiertas simultáneamente (N_max)
  • Volumen de una orden

Encontramos el número de órdenes y posiciones abiertas (N).
Si es menor que N_max, entonces:
        calcularemos el volumen medio por tick de las últimas K velas cerradas, obtendremos el valor V_avr.
        Si se cumple la condición V > V_avr * (1 + D + N * D_add), entonces:
                determinaremos la dirección del cambio de precio en la vela actual: si el precio ha subido, colocaremos una orden pendiente BUY_STOP, y en caso contrario, SELL_STOP
                colocaremos una orden pendiente a la distancia, la hora de expiración y los niveles StopLoss y TakeProfit especificados en los parámetros.


Implementación de la estrategia comercial

Vamos a escribir el código. Para empezar, enumeraremos todos los parámetros, dividiéndolos en grupos para mayor claridad y proporcionando a cada parámetro un comentario. Estos comentarios (si los hay) se mostrarán en la ventana de diálogo de configuración de parámetros al iniciar el asesor y en la pestaña de parámetros en el simulador de estrategias en lugar de los nombres de las variables que hayamos elegido para ellos.

Estableceremos valores por defecto solo por ahora; buscaremos los mejores durante la optimización.

input group "===  Opening signal parameters"
input int         signalPeriod_        = 48;    // Number of candles for volume averaging 
input double      signalDeviation_     = 1.0;   // Relative deviation from the average to open the first order 
input double      signaAddlDeviation_  = 1.0;   // Relative deviation from the average for opening the second and subsequent orders

input group "===  Pending order parameters"
input int         openDistance_        = 200;   // Distance from price to pending order
input double      stopLevel_           = 2000;  // Stop Loss (in points)
input double      takeLevel_           = 75;    // Take Profit (in points)
input int         ordersExpiration_    = 6000;  // Pending order expiration time (in minutes)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders
input double      fixedLot_            = 0.01;  // Single order volume

input group "===  EA parameters"
input ulong       magicN_              = 27181; // Magic

Como el asesor realizará operaciones comerciales, vamos a crear un objeto global de la clase CTrade. Usando la llamada a los métodos de este objeto, colocaremos órdenes pendientes.

CTrade            trade;            // Object for performing trading operations 

En cualquier caso, recordemos que las variables (u objetos) globales son variables (u objetos) declarados en el código del asesor, no dentro de alguna función. Por consiguiente, estarán disponibles en todas nuestras funciones del asesor. No deberán confundirse con las variables globales del terminal.

Para calcular los parámetros de las órdenes de apertura, necesitaremos obtener los precios actuales y otras propiedades del símbolo sobre el que se ejecutará el asesor. Para ello, crearemos un objeto global de la clase CSymbolInfo.

CSymbolInfo       symbolInfo;       // Object for obtaining data on the symbol properties

También tendremos que contar el número de órdenes y posiciones abiertas. Para ello, crearemos objetos globales de las clases COrderInfo y CPositionInfo, a través de los cuales obtendremos información sobre las órdenes y posiciones abiertas. La cantidad en sí se almacenará en dos variables globales countOrders y countPositions.

COrderInfo        orderInfo;        // Object for receiving information about placed orders
CPositionInfo     positionInfo;     // Object for receiving information about open positions

int               countOrders;      // Number of placed pending orders
int               countPositions;   // Number of open positions

Para calcular el volumen medio de varias velas, podremos utilizar, por ejemplo, el indicador técnico iVolumes. Para obtener sus valores, necesitaremos una variable que almacene el manejador de este indicador (un número entero que almacene el número de serie de este indicador de entre todos los indicadores que se utilizarán en el asesor). Para hallar el volumen medio, primero deberemos copiar los valores del búfer del indicador en un array preparado previamente. Este array también lo haremos global.

int               iVolumesHandle;   // Tick volume indicator handle
double            volumes[];        // Receiver array of indicator values (volumes themselves) 

Ahora podemos pasar a la función de inicialización del asesor OnInit() y a la función de procesamiento de ticks OnTick().

Durante la inicialización podemos hacer lo siguiente:

  • Descargaremos el indicador para obtener los volúmenes de ticks y memorizaremos su manejador
  • Estableceremos el tamaño del array-receptor según el número de velas para calcular el volumen medio y estableceremos su direccionamiento como en las series temporales 
  • Estableceremos el número mágico para colocar órdenes a través del objeto comercial

Este será el aspecto que tendrá nuestra función de inicialización:

int OnInit() {
   // Load the indicator to get tick volumes
   iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK);
   
   // Set the size of the tick volume receiving array and the required addressing
   ArrayResize(volumes, signalPeriod_);
   ArraySetAsSeries(volumes, true);

   // Set Magic Number for placing orders via 'trade'
   trade.SetExpertMagicNumber(magicN_);
   
   return(INIT_SUCCEEDED);
}

En la función de procesamiento de ticks, según la descripción de la estrategia, deberíamos empezar por encontrar el número de órdenes y posiciones abiertas. Implementaremos esto como una función separada UpdateCounts(). En ella iteraremos todas las posiciones y órdenes abiertas y contaremos solo aquellos Magic Numbers que coincidan con el Magic Number de nuestro asesor.

void UpdateCounts() {
// Reset position and order counters
   countPositions = 0;
   countOrders = 0;

// Loop through all positions
   for(int i = 0; i < PositionsTotal(); i++) {
      // If the position with index i is selected successfully and its Magic is ours, then we count it 
      if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) {
         countPositions++;
      }
   }

// Loop through all orders
   for(int i = 0; i < OrdersTotal(); i++) {
      // If the order with index i is selected successfully and its Magic is the one we need, then we consider it 
      if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) {
         countOrders++;
      }
   }
}

A continuación, deberemos comprobar que el número de posiciones y órdenes abiertas no supere el número establecido en la configuración. En este caso será necesario comprobar si se cumple la condición para abrir una nueva orden. Implementamos esta comprobación como una función aparte SignalForOpen() . Retornará uno de los tres valores posibles:

  • +1 - señal para abrir una orden BUY_STOP
  •  0 - sin señal de apertura
  • -1 - señal para abrir una orden SELL_STOP

También escribiremos dos funciones aparte para colocar órdenes pendientes: OpenBuyOrder() y OpenSellOrder(). 

Ahora podremos escribir una implementación completa de la función OnTick().

void OnTick() {
// Count open positions and orders
   UpdateCounts();

// If their number is less than allowed
   if(countOrders + countPositions < maxCountOfOrders_) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1) {          // If there is a buy signal, then 
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

Después de eso, añadiremos la implementación de las funciones restantes y el código del asesor estará listo. Lo guardaremos en el archivo SimpleVolumes.mq5 en la carpeta actual.

#include <Trade\OrderInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\Trade.mqh>

input group "===  Opening signal parameters"
input int         signalPeriod_        = 48;    // Number of candles for volume averaging 
input double      signalDeviation_     = 1.0;   // Relative deviation from the average to open the first order 
input double      signaAddlDeviation_  = 1.0;   // Relative deviation from the average for opening the second and subsequent orders

input group "===  Pending order parameters"
input int         openDistance_        = 200;   // Distance from price to pending order
input double      stopLevel_           = 2000;  // Stop Loss (in points)
input double      takeLevel_           = 75;    // Take Profit (in points)
input int         ordersExpiration_    = 6000;  // Pending order expiration time (in minutes)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders
input double      fixedLot_            = 0.01;  // Single order volume

input group "===  EA parameters"
input ulong       magicN_              = 27181; // Magic


CTrade            trade;            // Object for performing trading operations 

COrderInfo        orderInfo;        // Object for receiving information about placed orders
CPositionInfo     positionInfo;     // Object for receiving information about open positions

int               countOrders;      // Number of placed pending orders
int               countPositions;   // Number of open positions

CSymbolInfo       symbolInfo;       // Object for obtaining data on the symbol properties

int               iVolumesHandle;   // Tick volume indicator handle
double            volumes[];        // Receiver array of indicator values (volumes themselves) 

//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+
int OnInit() {
// Load the indicator to get tick volumes
   iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK);

// Set the size of the tick volume receiving array and the required addressing
   ArrayResize(volumes, signalPeriod_);
   ArraySetAsSeries(volumes, true);

// Set Magic Number for placing orders via 'trade'
   trade.SetExpertMagicNumber(magicN_);

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void OnTick() {
// Count open positions and orders
   UpdateCounts();

// If their number is less than allowed
   if(countOrders + countPositions < maxCountOfOrders_) {
      // Get an open signal
      int signal = SignalForOpen();

      if(signal == 1) {          // If there is a buy signal, then 
         OpenBuyOrder();         // open the BUY_STOP order
      } else if(signal == -1) {  // If there is a sell signal, then
         OpenSellOrder();        // open the SELL_STOP order
      }
   }
}

//+------------------------------------------------------------------+
//| Calculate the number of open orders and positions                |
//+------------------------------------------------------------------+
void UpdateCounts() {
// Reset position and order counters
   countPositions = 0;
   countOrders = 0;

// Loop through all positions
   for(int i = 0; i < PositionsTotal(); i++) {
      // If the position with index i is selected successfully and its Magic is ours, then we count it 
      if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) {
         countPositions++;
      }
   }

// Loop through all orders
   for(int i = 0; i < OrdersTotal(); i++) {
      // If the order with index i is selected successfully and its Magic is the one we need, then we consider it 
      if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) {
         countOrders++;
      }
   }
}

//+------------------------------------------------------------------+
//| Open the BUY_STOP order                                          |
//+------------------------------------------------------------------+
void OpenBuyOrder() {
// Update symbol current price data
   symbolInfo.Name(Symbol());
   symbolInfo.RefreshRates();

// Retrieve the necessary symbol and price data
   double point = symbolInfo.Point();
   int digits = symbolInfo.Digits();
   double bid = symbolInfo.Bid();
   double ask = symbolInfo.Ask();
   int spread = symbolInfo.Spread();

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(openDistance_, spread);

// Opening price
   double price = ask + distance * point; 
   
// StopLoss and TakeProfit levels
   double sl = NormalizeDouble(price - stopLevel_ * point, digits);
   double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits);
   
// Expiration time
   datetime expiration = TimeCurrent() + ordersExpiration_ * 60; 
   
// Order volume
   double lot = fixedLot_; 
   
// Set a pending order
   bool res = trade.BuyStop(lot,
                            NormalizeDouble(price, digits),
                            Symbol(),
                            NormalizeDouble(sl, digits),
                            NormalizeDouble(tp, digits),
                            ORDER_TIME_SPECIFIED,
                            expiration);

   if(!res) {
      Print("Error opening order");
   }
}

//+------------------------------------------------------------------+
//| Open the SELL_STOP order                                         |
//+------------------------------------------------------------------+
void OpenSellOrder() {
// Update symbol current price data
   symbolInfo.Name(Symbol());
   symbolInfo.RefreshRates();

// Retrieve the necessary symbol and price data
   double point = symbolInfo.Point();
   int digits = symbolInfo.Digits();
   double bid = symbolInfo.Bid();
   double ask = symbolInfo.Ask();
   int spread = symbolInfo.Spread();

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(openDistance_, spread);

// Opening price
   double price = bid - distance * point;
   
// StopLoss and TakeProfit levels
   double sl = NormalizeDouble(price + stopLevel_ * point, digits);
   double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits);

// Expiration time
   datetime expiration = TimeCurrent() + ordersExpiration_ * 60;

// Order volume
   double lot = fixedLot_;

// Set a pending order
   bool res = trade.SellStop(lot,
                             NormalizeDouble(price, digits),
                             Symbol(),
                             NormalizeDouble(sl, digits),
                             NormalizeDouble(tp, digits),
                             ORDER_TIME_SPECIFIED,
                             expiration);

   if(!res) {
      Print("Error opening order");
   }
}

//+------------------------------------------------------------------+
//| Signal for opening pending orders                                |
//+------------------------------------------------------------------+
int SignalForOpen() {
// By default, there is no signal
   int signal = 0;

// Copy volume values from the indicator buffer to the receiving array
   int res = CopyBuffer(iVolumesHandle, 0, 0, signalPeriod_, volumes);

// If the required amount of numbers have been copied
   if(res == signalPeriod_) {
      // Calculate their average value
      double avrVolume = ArrayAverage(volumes);

      // If the current volume exceeds the specified level, then
      if(volumes[0] > avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) {
         // if the opening price of the candle is less than the current (closing) price, then 
         if(iOpen(Symbol(), PERIOD_CURRENT, 0) < iClose(Symbol(), PERIOD_CURRENT, 0)) {
            signal = 1; // buy signal
         } else {
            signal = -1; // otherwise, sell signal
         }
      }
   }

   return signal;
}

//+------------------------------------------------------------------+
//| Number array average value                                       |
//+------------------------------------------------------------------+
double ArrayAverage(const double &array[]) {
   double s = 0;
   int total = ArraySize(array);
   for(int i = 0; i < total; i++) {
      s += array[i];
   }

   return s / MathMax(1, total);
}
//+------------------------------------------------------------------+

Vamos a ejecutar la optimización de los parámetros del asesor para EURGBP en el periodo H1 con las cotizaciones de MetaQuotes para el periodo desde 2018-01-01 hasta 2023-01-01 con un depósito inicial de 100 000 dólares con un lote mínimo de 0,01. Deberemos considerar que el mismo asesor puede mostrar resultados ligeramente distintos cuando se prueba con cotizaciones de diferentes brókeres. Y a veces estos resultados pueden variar enormemente.

A partir de los resultados obtenidos en las pruebas, seleccionaremos dos buenos conjuntos de parámetros con estos resultados:

Figura 1. Resultados de las pruebas con los parámetros [130, 0,9, 1,4, 231, 3750, 50, 600, 3, 0,01]. 


Figura 2. Resultados de las pruebas con los parámetros [159, 1,7, 0,8, 248, 3600, 495, 39000, 3, 0,01]. 

Las pruebas se han realizado con un gran depósito inicial por un motivo: si el asesor abre posiciones de volumen fijo, la ejecución puede terminar prematuramente si la reducción es mayor que los fondos disponibles. En este caso, no sabremos si, con los mismos parámetros, podríamos haber reducido razonablemente el volumen de las posiciones abiertas (o, lo que es lo mismo, aumentado el depósito inicial) para evitar perder el depósito.

Aquí vemos un ejemplo. Supongamos que tenemos un depósito inicial de $1 000. Al ejecutarlo en el simulador hemos obtenido estas cifras:

  • Depósito final $11 000 (1000% de beneficios, el asesor ha ganado +$10 000 que se suman a los $1 000 iniciales)
  • Reducción máxima absoluta $2 000.

Obviamente, hemos tenido suerte de que esta reducción se produjera después de que el asesor aumentase el depósito a más de $2 000. Por eso la pasada en el simulador finalizó y pudimos ver estos resultados. Si dicha reducción se hubiera producido antes (por ejemplo, hubiéramos elegido un inicio diferente del periodo de prueba), lo único que habríamos conseguido es que el asesor perdiera todo el depósito.

Si realizamos las pasadas manualmente, podremos cambiar el volumen en los parámetros o aumentar el depósito inicial y volver a empezar la pasada. Pero si las pasadas se realizan durante el proceso de optimización, no existirá esa posibilidad. En este caso, un conjunto de parámetros potencialmente bueno puede ser rechazado debido a unos ajustes de gestión de capital incorrectamente elegidos. Para reducir la probabilidad de que se produzcan estos resultados, podemos ejecutar la optimización con un depósito inicial muy grande y un volumen mínimo.

Volviendo al ejemplo, si el depósito inicial fuera de $100 000, si se repitiera la reducción de $2 000, no se perdería la totalidad del depósito y el simulador obtendría estos resultados. Y podríamos calcular que si la reducción máxima permitida para nosotros es del 10%, entonces el depósito inicial debería ser de al menos $20 000 dólares. La rentabilidad en este caso será solo del 50% (el asesor ha sumado $10 000 a los $20 000 iniciales)

Vamos a realizar cálculos similares para nuestras dos combinaciones seleccionadas de parámetros para un tamaño del depósito inicial de $10 000 y una reducción permitida del 10% del depósito inicial.

Parámetros Lote  Reducción Beneficios  Reducción
Permitida
Lote
Permitido
Reducción
Permitido
   L key  D  P  Da La = L * (Da / D) Pa =   P * (Da / D)
[130, 0.9, 1.4, 231,
3750, 50, 600, 3, 0.01]

0.01 28.70 (0.04%)  260.41 1000 (10%) 0.34 9073 (91%)
[159, 1.7, 0.8, 248,
3600, 495, 39000, 3, 0.01
]
0.01 92.72 (0.09%)  666.23 1000 (10%)
0.10 7185 (72%)

Como podemos ver, ambas opciones de parámetros de entrada pueden producir rendimientos más o menos similares (~80%). La primera opción gana menos en términos absolutos, pero con menos reducciones. Por lo tanto, podemos aumentar el volumen de las posiciones abiertas para ella más que para la segunda opción, que gana más, pero permite una reducción mayor.

Por consiguiente, hemos encontrado varias combinaciones de parámetros de entrada que resultan prometedoras: vamos a empezar a combinarlas en un asesor.


Clase básica de estrategia

Crearemos una clase CStrategy en la que recogeremos todas las propiedades y métodos inherentes a todas las estrategias. Por ejemplo, cualquier estrategia tendrá algún símbolo y marco temporal independientemente de su relación con los indicadores. Además, cada estrategia tendrá su propio Magic Number para abrir las posiciones e indicar el tamaño de las mismas. Por simplicidad, por ahora no analizaremos el funcionamiento de la estrategia con un tamaño de posición variable, definitivamente añadiremos esto, pero más tarde.

De los métodos necesarios, solo podemos destacar el constructor que inicializa los parámetros de la estrategia, el método de inicialización y el manejador de eventos OnTick. Obtendremos el código siguiente:

class CStrategy : public CObject {
protected:
   ulong             m_magic;          // Magic
   string            m_symbol;         // Symbol (trading instrument)
   ENUM_TIMEFRAMES   m_timeframe;      // Chart period (timeframe)
   double            m_fixedLot;       // Size of opened positions (fixed)

public:
   // Constructor
   CStrategy(ulong p_magic,
             string p_symbol,
             ENUM_TIMEFRAMES p_timeframe,
             double p_fixedLot);

   virtual int       Init() = 0; // Strategy initialization - handling OnInit events
   virtual void      Tick() = 0; // Main method - handling OnTick events
};

Los métodos Init() y Tick() se declaran puramente virtuales (después del encabezado tenemos = 0). Esto significa que no escribiremos la implementación de estos métodos en la clase CStrategy. Partiendo de esta clase, crearemos las clases descendientes, en las que necesariamente deberán encontrarse los métodos Init() y Tick(), y que contienen la implementación de reglas comerciales específicas.

La descripción de la clase está lista, vamos a añadir tras ella la implementación del constructor necesario. Como se trata de una función de método que se llama automáticamente al crearse el objeto de estrategia, precisamente en ella deberemos asegurarnos de que se inicializan los parámetros de la estrategia. El constructor tomará cuatro parámetros y asignará sus valores a las variables de miembro correspondientes de la clase a través de la lista de inicialización.

CStrategy::CStrategy(
   ulong p_magic,
   string p_symbol,
   ENUM_TIMEFRAMES p_timeframe,
   double p_fixedLot) :
// Initialization list
   m_magic(p_magic),
   m_symbol(p_symbol),
   m_timeframe(p_timeframe),
   m_fixedLot(p_fixedLot)
{}

Guardaremos este código en el archivo Strategy.mqh de la carpeta actual.


Clase de estrategia comercial

Vamos a transferir la lógica del asesor simple original a la nueva clase descendiente CSimpleVolumesStrategy. Para ello, haremos que todas las variables de los parámetros de entrada y las variables globales sean miembros de la clase. Solo eliminaremos las variables fixedLot_ y magicN_, en cuyo lugar utilizaremos los miembros de la clase básica m_fixedLot y m_magic, heredados de la clase básica CStrategy.

#include "Strategy.mqh"

class CSimpleVolumeStrategy : public CStrategy {
   //---  Open signal parameters
   int               signalPeriod_;       // Number of candles for volume averaging
   double            signalDeviation_;    // Relative deviation from the average to open the first order
   double            signaAddlDeviation_; // Relative deviation from the average for opening the second and subsequent orders

   //---  Pending order parameters
   int               openDistance_;       // Distance from price to pending order
   double            stopLevel_;          // Stop Loss (in points)
   double            takeLevel_;          // Take Profit (in points)
   int               ordersExpiration_;   // Pending order expiration time (in minutes)

   //---  Money management parameters
   int               maxCountOfOrders_;   // Maximum number of simultaneously open orders

   CTrade            trade;               // Object for performing trading operations

   COrderInfo        orderInfo;           // Object for receiving information about placed orders
   CPositionInfo     positionInfo;        // Object for receiving information about open positions

   int               countOrders;         // Number of placed pending orders
   int               countPositions;      // Number of open positions

   CSymbolInfo       symbolInfo;          // Object for obtaining data on the symbol properties

   int               iVolumesHandle;      // Tick volume indicator handle
   double            volumes[];           // Receiver array of indicator values (volumes themselves)  
};

Las funciones OnInit() y OnTick() se convertirán en los métodos públicos Init() y Tick(), y todas las demás funciones se convertirán en nuevos métodos privados de la clase CSimpleVolumesStrategy. Los métodos públicos podrán ser llamados para estrategias desde código externo, por ejemplo desde los métodos de un objeto de asesor. Los métodos privados solo podrán llamarse desde métodos de una clase determinada. Vamos a añadir el encabezado de los métodos a la descripción de la clase.

class CSimpleVolumeStrategy : public CStrategy {
private:
   //---  ... previous code
   double            volumes[];           // Receiver array of indicator values (volumes themselves)

   //--- Methods
   void              UpdateCounts();      // Calculate the number of open orders and positions
   int               SignalForOpen();     // Signal for opening pending orders
   void              OpenBuyOrder();      // Open the BUY_STOP order
   void              OpenSellOrder();     // Open the SELL_STOP order
   double            ArrayAverage(
      const double &array[]);             // Average value of the number array

public:
   //--- Public methods
   virtual int       Init();              // Strategy initialization method
   virtual void      Tick();              // OnTick event handler
};

En los lugares donde se encuentre la implementación de estas funciones, añadiremos el prefijo "CSimpleVolumesStrategy::" a sus nombres para que el compilador sepa que ya no son solo funciones, sino funciones de método de nuestra clase. 

class CSimpleVolumeStrategy : public CStrategy {
   // Class description listing properties and methods...
};

int CSimpleVolumeStrategy::Init() {
// Function code ...
}

void CSimpleVolumeStrategy::Tick() {
// Function code ...
}

void CSimpleVolumeStrategy::UpdateCounts() {
// Function code ...
}

int CSimpleVolumeStrategy::SignalForOpen() {
// Function code ...
}

void CSimpleVolumeStrategy::OpenBuyOrder() {
// Function code ...
}

void CSimpleVolumeStrategy::OpenSellOrder() {
// Function code ...
}

double CSimpleVolumeStrategy::ArrayAverage(const double &array[]) {
// Function code ...
}

En el asesor simple original, los valores de los parámetros de entrada se asignaban durante la declaración, y al iniciar el asesor compilado, se les asignaban los valores del diálogo de parámetros de entrada, no los especificados en el código. No podemos hacer eso en la descripción de la clase, así que aquí es donde el constructor entrará en escena.

Crearemos un constructor con la lista de parámetros necesaria. El constructor también debe ser público, de lo contrario no podremos crear objetos de estrategia desde código externo.

class CSimpleVolumeStrategy : public CStrategy {
private:
   //---  ... previous code   

public:
   //--- Public methods
   CSimpleVolumeStrategy(
      ulong            p_magic,
      string           p_symbol,
      ENUM_TIMEFRAMES  p_timeframe,
      double           p_fixedLot,
      int              p_signalPeriod,
      double           p_signalDeviation,
      double           p_signaAddlDeviation,
      int              p_openDistance,
      double           p_stopLevel,
      double           p_takeLevel,
      int              p_ordersExpiration,
      int              p_maxCountOfOrders
   );                                     // Constructor

   virtual int       Init();              // Strategy initialization method
   virtual void      Tick();              // OnTick event handler
};

La descripción de la clase ya está lista. Todos sus métodos ya tienen una implementación, salvo el constructor. Vamos a añadirlo. En el caso más sencillo, el constructor de esta clase solo asignará los valores de los parámetros obtenidos a los miembros correspondientes de la clase. Y los cuatro primeros parámetros lo harán llamando al constructor de la clase básica.

CSimpleVolumeStrategy::CSimpleVolumeStrategy(
   ulong            p_magic,
   string           p_symbol,
   ENUM_TIMEFRAMES  p_timeframe,
   double           p_fixedLot,
   int              p_signalPeriod,
   double           p_signalDeviation,
   double           p_signaAddlDeviation,
   int              p_openDistance,
   double           p_stopLevel,
   double           p_takeLevel,
   int              p_ordersExpiration,
   int              p_maxCountOfOrders) : 
   // Initialization list
   CStrategy(p_magic, p_symbol, p_timeframe, p_fixedLot), // Call the base class constructor
   signalPeriod_(p_signalPeriod),
   signalDeviation_(p_signalDeviation),
   signaAddlDeviation_(p_signaAddlDeviation),
   openDistance_(p_openDistance),
   stopLevel_(p_stopLevel),
   takeLevel_(p_takeLevel),
   ordersExpiration_(p_ordersExpiration),
   maxCountOfOrders_(p_maxCountOfOrders)
{}

Ya no queda mucho por hacer. Ahora cambiaremos el nombre de fixedLot_ y magicN_, por m_fixedLot y m_magic en todos los lugares donde aparecen. Después sustituiremos el uso de la función de obtención del símbolo actual Symbol() por la variable de clase básica m_symbol y la constante PERIOD_CURRENT por m_timeframe. Guardaremos este código en el archivo SimpleVolumesStrategy.mqh en la carpeta actual.


Clase de experto

Vamos ahora a crear una clase básica CAdvisor, cuya tarea principal será almacenar la lista de objetos de estrategias comerciales específicas e iniciar sus manejadores de eventos. El nombre CExpert resultaría más apropiado para esta clase, pero ya se utiliza en la biblioteca estándar, así que vamos a utilizar un análogo.

#include "Strategy.mqh"

class CAdvisor : public CObject {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
   int               m_strategiesCount;// Number of strategies

public:
   virtual int       Init();           // EA initialization method
   virtual void      Tick();           // OnTick event handler
   virtual void      Deinit();         // Deinitialization method

   void              AddStrategy(CStrategy &strategy);   // Strategy adding method
};

Los métodos Init() y Tick() iteran todas las estrategias del array m_strategies[] y llaman a los métodos de procesamiento de los eventos correspondientes para ellas.

void CAdvisor::Tick(void) {
   // Call OnTick handling for all strategies
   for(int i = 0; i < m_strategiesCount; i++) {
      m_strategies[i].Tick();
   }
}

En el método de adición de estrategias ocurrirá exactamente esto.

void CAdvisor::AddStrategy(CStrategy &strategy) {
   // Increase the strategy number counter by 1
   m_strategiesCount = ArraySize(m_strategies) + 1;
   
   // Increase the size of the strategies array
   ArrayResize(m_strategies, m_strategiesCount);
   // Write a pointer to the strategy object to the last element
   m_strategies[m_strategiesCount - 1] = GetPointer(strategy);
}

Guardaremos este código en el archivo Advisor.mqh de la carpeta actual. Basándonos en esta clase, podremos crear descendientes que implementen cualquier forma específica para gestionar el funcionamiento de múltiples estrategias. Pero por ahora nos limitaremos a esta clase básica y no interferiremos en modo alguno con las estrategias individuales.


Asesor comercial con varias estrategias

Para escribir un asesor comercial, solo necesitaremos crear un objeto global de asesor (de la clase CAdvisor).

En el manejador OnInit() del evento de inicialización, al adjuntar al gráfico, crearemos los objetos de estrategia con los parámetros seleccionados y los añadiremos al objeto de asesor. Después de eso, llamaremos al método Init() del objeto asesor para inicializar todas las estrategias en él.

Los manejadores de eventos OnTick() y OnDeinit() simplemente llamarán a los métodos correspondientes del objeto de asesor.

#include "Advisor.mqh"
#include "SimpleVolumesStartegy.mqh"

input double depoPart_  = 0.8;      // Part of the deposit for one strategy
input ulong  magic_     = 27182;    // Magic

CAdvisor     expert;                // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   expert.AddStrategy(...);
   expert.AddStrategy(...);

   int res = expert.Init();   // Initialization of all EA strategies

   return(res);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   expert.Tick();
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   expert.Deinit();
}
//+------------------------------------------------------------------+

Veamos ahora más de cerca la creación de objetos de estrategia. Como cada ejemplar de estrategia abre y contabiliza sus propias órdenes y posiciones, deberán tener un Magic diferente. El valor Magic es el primer parámetro del constructor de la estrategia. Por lo tanto, para garantizar diferentes Magic, añadiremos diferentes números al Magic original dado en el parámetro magic_.

   expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 1, ...));
   expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 2, ...));

Los parámetros segundo y tercero del constructor serán el símbolo y el periodo. Como estamos realizando la optimización con EURGBP y el periodo H1, especificaremos estos valores concretos.

   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 1, "EURGBP", PERIOD_H1, ...));
   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 2, "EURGBP", PERIOD_H1, ...));

El siguiente parámetro es muy importante: el tamaño de las posiciones abiertas. Más arriba hemos calculado el tamaño correspondiente para las dos estrategias (0,34 y 0,10). Pero está dimensionado para funcionar con reducciones de hasta el 10% de 10 000 dólares cuando las estrategias funcionan por separado. Si las dos estrategias funcionan al mismo tiempo, la reducción de la primera puede sumarse a la de la segunda. En el peor de los casos, para mantenernos dentro del 10% establecido, tendríamos que reducir a la mitad el tamaño de las posiciones abiertas. Sin embargo, puede ocurrir que las reducciones de ambas estrategias no coincidan o incluso se compensen en cierta medida. En este caso, podríamos reducir el tamaño de las posiciones no tanto y seguir sin superar el 10%. Así que haremos que el multiplicador decreciente sea un parámetro del asesor (depoPart_), para el que luego encontraremos el valor óptimo.

El resto de parámetros del constructor de la estrategia serán los conjuntos de valores que hemos elegido tras optimizar el asesor simple. El resultado final será:

   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 1, "EURGBP", PERIOD_H1,
                         NormalizeDouble(0.34 * depoPart_, 2),
                         130, 0.9, 1.4, 231, 3750, 50, 600, 3)
                     );
   expert.AddStrategy(new CSimpleVolumeStrategy(
                         magic_ + 2, "EURGBP", PERIOD_H1,
                         NormalizeDouble(0.10 * depoPart_, 2),
                         159, 1.7, 0.8, 248, 3600, 495, 39000, 3)
                     );

Guardaremos el código obtenido en el archivo SimpleVolumesExpert.mq5 en la carpeta actual.


Resultados de las pruebas

Antes de probar el asesor combinado, recordemos que la estrategia con el primer conjunto de parámetros se suponía que debía dar un beneficio de alrededor del 91%, mientras que con el segundo conjunto de parámetros debía dar un 72% (para un depósito inicial de $10 000 y una reducción máxima del 10% ($ 1000) en el lote óptimo).

Ahora elegiremos el valor óptimo del parámetro depoPart_ según el criterio de sostenimiento de la reducción dado y obtendremos los siguientes resultados.

Figura 3. Resultado del funcionamiento del asesor conjunto

El balance al final del periodo de prueba ha sido de aproximadamente $22 400, lo que supone un rendimiento del 124%. Eso es más de lo que obtuvimos al ejecutar ejemplares individuales de esta estrategia. Hemos logrado mejorar los resultados comerciales trabajando únicamente con la estrategia comercial existente sin introducir ningún cambio en ella.


Conclusión

Solo hemos dado un pequeño paso hacia la consecución de nuestro objetivo. Dicho paso nos ha dado una mayor confianza en que este enfoque puede mejorar la calidad del comercio. Todavía no hemos abordado de forma alguna muchos aspectos importantes en el asesor escrito.

Por ejemplo, hemos considerado una estrategia muy simple que no controla de ninguna forma el cierre de posiciones, funciona sin necesidad de determinar el inicio exacto de la barra y no utiliza cálculos voluminosos. Para restablecer el estado después de reiniciar el terminal, no será necesario hacer ningún esfuerzo adicional, bastará con calcular las posiciones y órdenes abiertas, cosa que puede hacer el asesor. Pero no todas las estrategias serán tan sencillas. Además, el asesor no puede trabajar en cuentas de compensación y puede mantener posiciones opuestas abiertas al mismo tiempo. No hemos analizado la posibilidad de trabajar con distintos símbolos. Y etcétera, etcétera....

Estos aspectos deberán tenerse en cuenta antes de empezar a comerciar. Pero hablaremos más sobre ellos en los siguientes artículos.


    Traducción del ruso hecha por MetaQuotes Ltd.
    Artículo original: https://www.mql5.com/ru/articles/14026

    Archivos adjuntos |
    SimpleVolumes.mq5 (21.22 KB)
    Strategy.mqh (3.78 KB)
    Advisor.mqh (6.29 KB)
    Algoritmos de optimización de la población: Algoritmo genético binario (Binary Genetic Algorithm, BGA). Parte II Algoritmos de optimización de la población: Algoritmo genético binario (Binary Genetic Algorithm, BGA). Parte II
    En este artículo, analizaremos el algoritmo genético binario (BGA), que modela los procesos naturales que ocurren en el material genético de los seres vivos en la naturaleza.
    Desarrollo de un sistema de repetición (Parte 45): Proyecto Chart Trade (IV) Desarrollo de un sistema de repetición (Parte 45): Proyecto Chart Trade (IV)
    Lo principal en este artículo es precisamente la presentación y explicación de la clase C_ChartFloatingRAD. Tenemos el indicador Chart Trade, que funciona de una manera bastante interesante. No obstante, si te das cuenta, aún tenemos un número bastante reducido de objetos en el gráfico. Y aun así, tenemos exactamente el comportamiento esperado. Se pueden editar los valores presentes en el indicador. La pregunta es: ¿Cómo es esto posible? En este artículo comenzarás a entenderlo.
    Desarrollo de un sistema de repetición (Parte 46): Proyecto Chart Trade (V) Desarrollo de un sistema de repetición (Parte 46): Proyecto Chart Trade (V)
    ¿Cansado de perder tiempo buscando ese archivo que es necesario para que tu aplicación funcione? ¿Qué tal si incluimos todo en el ejecutable? Así nunca perderás tiempo buscando las cosas. Sé que muchos utilizan exactamente esa forma de distribuir y guardar las cosas. Pero existe una manera mucho más adecuada. Al menos en lo que respecta a la distribución de ejecutables y almacenamiento de los mismos. La forma que explicaré aquí, puede ser de gran ayuda. Ya que puedes usar el propio MetaTrader 5 como un gran ayudante, así como el MQL5. No es algo tan complejo ni difícil de entender.
    Desarrollo de un sistema de repetición (Parte 44): Proyecto Chart Trade (III) Desarrollo de un sistema de repetición (Parte 44): Proyecto Chart Trade (III)
    En el artículo anterior, expliqué cómo puedes manipular los datos de la plantilla para usarlos en un OBJ_CHART. Allí solo introduje el tema sin entrar en muchos detalles, ya que en esa versión el trabajo se hizo de una manera muy simplificada. Sin embargo, se hizo de esa forma precisamente para facilitar la explicación del contenido. Pues, a pesar de parecer simple hacer ciertas cosas, algunas no son tan evidentes, y sin comprender la parte más simple y básica, no entenderás realmente lo que estoy haciendo.