English Русский 中文 Deutsch 日本語 Português
Sistema comercial 'Turtle Soup' y su modificación 'Turtle Soup Plus One'

Sistema comercial 'Turtle Soup' y su modificación 'Turtle Soup Plus One'

MetaTrader 5Ejemplos | 12 diciembre 2016, 07:36
1 943 0
Alexander Puzanov
Alexander Puzanov

  1. Introducción
  2. Sistema comercial 'Turtle Soup' y su modificación 'Turtle Soup Plus One'
  3. Definición de los parámetros del canal
  4. Función de la generación de señales
  5. Asesor Experto básico para comprobar la Estrategia Comercial
  6. Prueba de la estrategia a base de datos históricos
  7. Conclusión

Introducción

Los autores del libro «Street Smarts: High Probability Short-Term Trading Strategies», Linda Raschke y Laurence Connors, son dos traders de éxito con un total de 34 años de experiencia en el mundo de trading. Su rica experiencia incluye la negociación en las bolsas de valores, trabajo en los bancos y fondos de cobertura (hedge fund), en las empresas de corretaje y asesoramiento. Según su opinión, para un trading establemente rentable es suficiente disponer sólo de una Estrategia Comercial. No obstante, su libro contiene casi dos docenas de versiones de Estrategias Comerciales divididas en cuatro grupos. Cada grupo se refiere a una determinada fase de los ciclos de mercado y explora uno de los patrones estables del comportamiento del precio.

Las estrategias descritas en este libro recibieron bastante amplia acogida, pero es importante comprender que sus autores las ideaban basándose en el comportamiento del mercado de hace 15-20 años. Por esa razón, el presente artículo tiene dos objetivos: es decir, vamos a empezar con la implementación en el lenguaje MQL5 de la primera de las Estrategias Comerciales descritas por Linda Raschke y Laurence Connors, y luego intentaremos evaluar su eficacia usando el Probador de Estrategias MT5. Además, vamos a utilizar los datos históricos de los últimos años que están disponibles a través del servidor demo de MetaQuotes.

Al escribir el código, voy a orientarme a los usuarios de MQL5 que ya tienen conocimientos básicos del lenguaje, es decir, a los principiantes ligeramente avanzados. Por tanto, aquí no habrá explicaciones sobre el trabajo de las funciones estándar, argumentaciones de la elección de los tipos de variables y todo aquello con lo que es necesario practicar antes de empezar a programar los Asesores Expertos. Por otro lado, tampoco voy a orientarme a los desarrolladores experimentados de los robots, ya que por regla general ya poseen las librerías de sus propias soluciones y no van a rechazarlas durante la implementación de una nueva Estrategia Comercial.

Para la mayoría de los programadores interesados en este artículo, dominar la programación orientada a objetos (POO) es un desafío constante. Por eso, intentaré hacer que el proceso de la creación de este Asesor Experto (EA) sea útil para conseguir el propósito mencionado. Para que la transición del enfoque procesal al enfoque orientado a objetos sea más fácil, no vamos a usar lo más complicado en POO, las clases. En vez de eso, vamos a usar su equivalente más simple, las estructuras. Las estructuras combinan los datos relacionados lógicamente de diferentes tipos y las funciones para trabajar con ellos, por eso poseen casi todas las características propias de las clases, inclusive la herencia. Pero su uso no requiere el conocimiento de las reglas para formatear el código de las clases, basta con complementar la programación procesal a la que ya está acostumbrado con el mínimo de detalles.


Sistema comercial 'Turtle Soup' y su modificación 'Turtle Soup Plus One'


La estrategia comercial llamada «Sopa de tortuga» (Turtle Soup) abre un conjunto de las estrategias de la serie con el nombre lacónico "Tests". Para que sea más claro a basé de qué característica fue compuesta esta serie, habría que nombrarla «Prueba con el precio de los límites del intervalo o niveles de soporte/resistencia». Turtle Soup se basa en la suposición de que el precio no podrá romper el intervalo de 20 días sin rebotar de los límites de este intervalo. Nuestra tarea consiste en intentar sacar beneficio del retroceso temporal desde el límite o de la ruptura falsa. La posición de trading siempre será dirigida hacia adentro del intervalo, y eso nos da la razón para incluir la Estrategia Comercial en la categoría de las «estrategias de rebote».

Por cierto, la semejanza del nombre "Turtle Soup" con la famosa estrategia "Turtles" no es casual, ya que las dos monitorean el comportamiento del precio en los límites del intervalo de 20 días. Según los autores del libro, durante un tiempo trataban de usar un par de estrategias de ruptura, incluyendo "Turtles", no obstante este tipo de trading no era eficaz debido a la abundancia de rupturas falsas y retrocesos profundos. Sin embargo, gracias a eso, los patrones revelados ayudaron a crear un conjunto de reglas para sacar beneficio del movimiento del precio en la dirección contraria a la ruptura.

El conjunto completo de las reglas de la Estrategia Comercial "Turtle Soup" para entrar en la operación de compra puede ser formulado de la siguiente manera:

  1. Asegúrese de que han pasado no menos de 3 días de trading desde el mínimo anterior de 20 días.
  2. Espere a que el precio del instrumento caiga por de bajo del mínimo de 20 días.
  3. Coloque una orden pendiente de compra a 5-10 puntos por encima del mínimo recientemente roto.
  4. Inmediatamente después de la activación de la orden pendiente, coloque su StopLoss a 1 punto por debajo del mínimo de este día.
  5. Utilice el Trailiing Stop cuando la posición se haga rentable.
  6. Si la posición se ha cerrado por el Stop en el primer o el segundo día, se permite volver a entrar en el nivel inicial. 

Las reglas para la entrada en la operación de venta son semejantes, y como se puede entender, hay que aplicarlas para el límite superior del intervalo, o sea para el máximo de precios de 20 días.

En la librería de los códigos fuente hay un indicador que, según determinadas configuraciones, muestra los límites del canal en cada barra del historial. Se puede utilizarlo para la visualización durante el trading manual.

 

En la descripción de la Estrategia Comercial no hay respuesta directa a la pregunta cuánto tiempo se debe mantener la orden pendiente, por eso vamos a seguir la lógica simple. A saber, durante la prueba de los límites del intervalo, el precio creará un extremo nuevo, lo que al día siguiente hará imposible cumplir la primera de las condiciones descritas anteriormente. Y como este día no hay señal, tenemos que cancelar la orden pendiente del día anterior.

La modificación de esta Estrategia, llamada «Turtle Soup Plus One», tiene sólo dos diferencias:

  1. En vez de colocar una orden pendiente inmediatamente después de la ruptura del intervalo de 20 días, es necesario esperar la confirmación de la señal: el cierre de la barra de este día fuera de los límites del intervalo. Nos valdrá perfectamente si el día se cierre justo en el límite del canal horizontal examinado.
  2. Para determinar el nivel del StopLoss inicial, se utiliza el extremo correspondiente de dos días (máximo o mínimo).

  

Definición de los parámetros del canal


Para verificar la conformidad de las condiciones, tenemos que saber el precio máximo y mínimo del intervalo. Y para su cálculo, a su vez, hay que determinar los límites de tiempo. Estas cuatro variables determinan el canal para cada momento dado de tiempo, por eso es lógico combinarlas en una estructura común. Añadimos dos variables más involucradas en la Estrategia Comercial: número de días (barras) transcurridos desde que el precio alcance el máximo y el mínimo del intervalo:

struct CHANNEL {
  double    d_High;           // precio del límite superior del intervalo
  double    d_Low;            // precio del límite inferior del intervalo
  datetime  t_From;           // fecha/hora de la primera barra (más antigua) del canal
  datetime  t_To;             // fecha/hora de la última barra del canal
  int       i_Highest_Offset; // número de barras a la derecha del máximo del precio
  int       i_Lowest_Offset;  // número de barras a la derecha del mínimo del precio
};

Todas estas variables serán actualizadas debidamente por la función f_Set. Para eso hay que especificarle a partir de qué barra tiene que construirse el canal virtual (i_Newest_Bar_Shift) y con qué profundidad vamos a ver el historial (i_Bars_Limit):

void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) {
  double da_Price_Array[]; // array adicional para los precios High/Low de todas las barras del canal
  
  // definición del límite superior del intervalo:
  
  int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  int i_Bar = ArrayMaximum(da_Price_Array);
  d_High = da_Price_Array[i_Bar]; // el límite superior del intervalo está definido
  i_Highest_Offset = i_Price_Bars - i_Bar; // crecimiento del máximo (en barras)
  
  // definición del límite inferior del intervalo:
  
  i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  i_Bar = ArrayMinimum(da_Price_Array);
  d_Low = da_Price_Array[i_Bar]; // el límite inferior del intervalo está definido
  i_Lowest_Offset = i_Price_Bars - i_Bar; // crecimiento del mínimo (en barras)
  
  datetime ta_Time_Array[];
  i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array);
  t_From = ta_Time_Array[0];
  t_To = ta_Time_Array[i_Price_Bars - 1];
}

Esta función contiene sólo 13 cadenas de caracteres, pero si Usted ha leído atentamente la guía sobre las funciones MQL para la extracción de datos desde las series temporales (CopyHigh, CopyLow, CopyTime, etc.), entonces sabe que no todo es tan fácil con ellas. En algunas ocasiones, las funciones devuelven el número de valores diferente del que Usted ha establecido, porque los datos solicitados pueden no estar listos todavía durante el primer acceso a la serie temporal necesaria. Sin embargo, al procesar correctamente los resultados, el copiado de datos desde la serie temporal funciona tal como Usted ha planteado.

Por eso nosotros vamos a cumplir por lo menos los criterios mínimos de la programación de calidad e incluiremos los manejadores de errores más simples en el código. Para que sea más fácil comprenderlos, vamos a imprimir la información sobre los errores en el registro. El registro también es muy útil para la depuración, porque le permite tener la información detallada sobre la razón por la cual el robot ha tomado una determinada decisión. Por eso, vamos a introducir una nueva variable del tipo de enumeración que va a determinar cuántos detalles debe contener nuestro registro:

enum ENUM_LOG_LEVEL { // Lista de los niveles del registro
  LOG_LEVEL_NONE,     // registro desactivado
  LOG_LEVEL_ERR,      // sólo información sobre los errores
  LOG_LEVEL_INFO,     // errores + comentarios del robot
  LOG_LEVEL_DEBUG     // todos sin excepciones
};

El nivel deseado va a ser seleccionado por el usuario, los operadores de visualización de la información en el log les colocaremos en muchas funciones. Por consiguiente, la lista y la variable personalizada Log_Level no deben ubicarse en el bloque de señal, sino en el inicio del programa principal.

Pues, volveremos a la función f_Set -obtendrá el siguiente aspecto con todas las verificaciones (las líneas adicionadas están resaltadas):

void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) {
  double da_Price_Array[]; // array adicional para los precios High/Low de todas las barras del canal
  
  // definición del límite superior del intervalo:
  
  int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  
  if(i_Price_Bars == WRONG_VALUE) {
    // procesamiento del error de la función CopyHigh
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: error #%u", __FUNCSIG__, _LastError);
    return;
  }
  
  if(i_Price_Bars < i_Bars_Limit) {
    // la función CopyHigh ha extraído los datos parcialmente
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: copiado %u barras de %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
    return;
  }
  
  int i_Bar = ArrayMaximum(da_Price_Array);
  if(i_Bar == WRONG_VALUE) {
    // procesamiento del error de la función ArrayMaximum
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMaximum: error #%u", __FUNCSIG__, _LastError);
    return;
  }
  
  d_High = da_Price_Array[i_Bar]; // el límite superior del intervalo está definido
  i_Highest_Offset = i_Price_Bars - i_Bar; // crecimiento del máximo (en barras)
  
  // definición del límite inferior del intervalo:
  
  i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
  
  if(i_Price_Bars == WRONG_VALUE) {
    // procesamiento del error de la función CopyLow
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: error #%u", __FUNCSIG__, _LastError);
    return;
  }
  
  if(i_Price_Bars < i_Bars_Limit) {
    // la función CopyLow ha extraído los datos parcialmente
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: copiado %u barras de %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
    return;
  }
  
  i_Bar = ArrayMinimum(da_Price_Array);
  if(i_Bar == WRONG_VALUE) {
    // procesamiento del error de la función ArrayMinimum
    if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMinimum: error #%u", __FUNCSIG__, _LastError);
    return;
  }
  d_Low = da_Price_Array[i_Bar]; // el límite inferior del intervalo está definido
  i_Lowest_Offset = i_Price_Bars - i_Bar; // crecimiento del mínimo (en barras)
  
  datetime ta_Time_Array[];
  i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array);
  if(i_Price_Bars < 1) t_From = t_To = 0;
  else {
    t_From = ta_Time_Array[0];
    t_To = ta_Time_Array[i_Price_Bars - 1];
  }
}

Al detectar el error, actuamos de manera más simple: interrumpimos la ejecución a la expectativa de que, en el siguiente tick, el terminal cargue la cantidad suficiente de datos históricos para el funcionamiento correcto de la función del copiado. Y para que las demás funciones personalizadas no utilicen el canal hasta la finalización completa del procedimiento, añadimos a la estructura la bandera correspondiente b_Ready (true = datos preparados, false = proceso no finalizado). Al mismo tiempo, aññadimos la bandera del cambio de parámetros del canal (b_Updated). Para un desempeño óptimo, es útil de saber si los parámetros involucrados en la Estrategia Comercial no han sido alterados. Para eso habrá que introducir una variable más: se trata de la signatura del canal (s_Signature), siendo una especie del molde de parámetros. La función f_Set también se colocará en la signatura, y la estructura CHANNEL tomará el aspecto final: 

// información sobre el canal y las funciones para su compilación y actualización reunidas en la estructura
struct CHANNEL {
  // variables
  double    d_High;          // precio del límite superior del intervalo
  double    d_Low;            // precio del límite inferior del intervalo
  datetime  t_From;           // fecha/hora de la primera barra (más antigua) del canal
  datetime  t_To;             // fecha/hora de la última barra del canal
  int       i_Highest_Offset; // número de barras a la derecha del máximo del precio
  int       i_Lowest_Offset;  // número de barras a la derecha del mínimo del precio
  bool      b_Ready;          // ¿se ha terminado el procedimiento de actualización de parámetros?
  bool      b_Updated;        // ¿han sido alterados los parámetros del canal?
  string    s_Signature;      // la signatura del último conjunto conocido de datos
  
  // funciones:
  
  CHANNEL() {
    d_High = d_Low = 0;
    t_From = t_To = 0;
    b_Ready = b_Updated = false;
    s_Signature = "-";
    i_Highest_Offset = i_Lowest_Offset = WRONG_VALUE;
  }
  
  void f_Set(int i_Bars_Limit, int i_Newest_Bar_Shift = 1) {
    b_Ready = false; // PitStop: colocamos la bandera de mantenimiento técnico
    
    double da_Price_Array[]; // array adicional para los precios High/Low de todas las barras del canal
    
    // definición del límite superior del intervalo:
    
    int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
    if(i_Price_Bars == WRONG_VALUE) {
      // procesamiento del error de la función CopyHigh
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: error #%u", __FUNCSIG__, _LastError);
      return;
    }
    
    if(i_Price_Bars < i_Bars_Limit) {
      // la función CopyHigh ha extraído los datos parcialmente
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: copiado %u barras de %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
      return;
    }
    
    int i_Bar = ArrayMaximum(da_Price_Array);
    if(i_Bar == WRONG_VALUE) {
      // procesamiento del error de la función ArrayMaximum
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMaximum: error #%u", __FUNCSIG__, _LastError);
      return;
    }
    
    d_High = da_Price_Array[i_Bar]; // el límite superior del intervalo está definido
    i_Highest_Offset = i_Price_Bars - i_Bar; // crecimiento (en barras) del máximo
    
    // definición del límite inferior del intervalo:
    
    i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array);
    
    if(i_Price_Bars == WRONG_VALUE) {
      // procesamiento del error de la función CopyLow
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: error #%u", __FUNCSIG__, _LastError);
      return;
    }
    
    if(i_Price_Bars < i_Bars_Limit) {
      // la función CopyLow ha extraído los datos parcialmente
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: copiado %u barras de %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
      return;
    }
    
    i_Bar = ArrayMinimum(da_Price_Array);
    if(i_Bar == WRONG_VALUE) {
      // procesamiento del error de la función ArrayMinimum
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMinimum: error #%u", __FUNCSIG__, _LastError);
      return;
    }
    d_Low = da_Price_Array[i_Bar]; // el límite inferior del intervalo está definido
    i_Lowest_Offset = i_Price_Bars - i_Bar; // crecimiento (en barras) del mínimo
    
    datetime ta_Time_Array[];
    i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array);
    if(i_Price_Bars < 1) t_From = t_To = 0;
    else {
      t_From = ta_Time_Array[0];
      t_To = ta_Time_Array[i_Price_Bars - 1];
    }
    
    string s_New_Signature = StringFormat("%.5f%.5f%u%u", d_Low, d_High, t_From, t_To);
    if(s_Signature != s_New_Signature) {
      // los datos del canal han cambiado
      b_Updated = true;
      if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: canal actualizado: %s .. %s / %s .. %s, мин: %u máx: %u ", __FUNCTION__, DoubleToString(d_Low, _Digits), DoubleToString(d_High, _Digits), TimeToString(t_From, TIME_DATE|TIME_MINUTES), TimeToString(t_To, TIME_DATE|TIME_MINUTES), i_Lowest_Offset, i_Highest_Offset);
      s_Signature = s_New_Signature;
    }
    
    b_Ready = true; // la actualización de datos se ha terminado con éxito
  }
};
En seguida declaramos globalmente un objeto-canal de este tipo (para que esté disponible desde diferentes funciones definidas por el usuario):

CHANNEL go_Channel;

Función de la generación de señales


La señal de compra de este sistema se determina según dos condiciones obligatorias:

1. Desde el mínimo anterior de 20 días han pasado no menos de 3 días de trading.

2a. El precio del instrumento ha caído por de bajo del mínimo de 20 días (Turtle Soup)

2b. La barra del día se ha cerrado no por encima del mínimo de 20 días (Turtle Soup Plus One)



 

Todas las demás reglas de la Estrategia arriba mencionadas están relacionadas con los parámetros de la orden comercial y con el seguimiento de la posición, no vamos a incluirlas en el bloque de señal.

En el módulo, vamos a programar la detección de las señales según las reglas de ambas modificaciones de la Estrategia (Turtle Soup y Turtle Soup Plus One), mientras que en los ajustes del Asesor Experto, vamos a añadir la posibilidad de la selección de la versión necesaria de las reglas. La variable personalizada correspondiente la vamos a llamar Strategy_Type. Por ahora, la lista de estrategias va a tener solamente dos opciones, de modo que sería más fácil limitarse con la selección true/false (la variable tipo bool). Pero nos dejaremos la posibilidad (al finalizar este ciclo de artículos) de reunir todas las estrategias del libro convertidas en el código en el mismo Asesor Experto, por eso usaremos la lista numerada, aunque sea cortita.

enum ENUM_STRATEGY {     // Lista de estrategias
  TS_TURTLE_SOUP,        // Turtle Soup
  TS_TURTLE_SOUP_PLUS_1  // Turtle Soup Plus One
};
input ENUM_STRATEGY  Strategy_Type = TS_TURTLE_SOUP;  // Estrategia Comercial:

A la función de la detección de la señal del programa principal hay que pasarle el tipo de la estrategia, es decir hay que hacerle saber si es necesario esperar el cierre de la barra (día)- la variable b_Wait_For_Bar_Close tipo bool. La segunda variable necesaria es la duración de la pausa después del extremo anterior i_Extremum_Bars. La función debe devolver el estatus de la señal que dice si hay condiciones para la compra/venta o hay que esperar. La lista numerada correspondiente también será colocada en el archivo principal de Asesor Experto:

enum ENUM_ENTRY_SIGNAL {  // Lista de señales para la entrada
  ENTRY_BUY,              // señal de compra
  ENTRY_SELL,             // señal de venta
  ENTRY_NONE,             // no hay señal
  ENTRY_UNKNOWN           // estatus no determinado
};

Otra estructura que será usada por el módulo de señal y por las funciones del programa principal es el objeto global go_Tick que contiene la información sobre el tick más reciente. Se trata de la estructura estándar tipo MqlTick que será declarada en el archivo principal, más tarde programaremos su actualización en el cuerpo del programa principal (en la función OnTick).

MqlTick go_Tick; // información sobre el último tick conocido

Ahora por fin podemos pasar a la función más importante del módulo

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(
  bool b_Wait_For_Bar_Close = false,
  int i_Extremum_Bars = 3
) {}

Vamos a empezar con la comprobación de las condiciones para la señal de venta: si han pasado días suficientes (barras) desde el máximo anterior (la primera condición), y si ha roto el precio el límite superior del intervalo (la segunda condición):

if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1-a condición
  if(go_Channel.d_High < d_Actual_Price) // 2-a condición
    return(ENTRY_SELL); // ambas condiciones de la entrada para la venta han sido cumplidas

La comprobación de las condiciones para la señal de compra se realiza de la misma manera:

if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-a condición
  if(go_Channel.d_Low > d_Actual_Price) { // 2-a condición
    return(ENTRY_BUY); // ambas condiciones de la entrada para la compra han sido cumplidas

Aquí se utiliza la variable d_Actual_Price que contiene el precio actual para esa versión de la Estrategia Comercial. Para Turtle Soup es el último precio conocido bid, para Turtle Soup Plus One es el precio del cierre del día (barra) anterior:

double d_Actual_Price = go_Tick.bid; // precio por defecto - para la versión Turtle Soup
if(b_Wait_For_Bar_Close) { // для версии Turtle Soup Plus One
  double da_Price_Array[1]; // array adicional
  CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array));
  d_Actual_Price = da_Price_Array[0];
}

La función que incluye el mínimo exigido es así:

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) {
  double d_Actual_Price = go_Tick.bid; // precio por defecto - para la versión Turtle Soup
  if(b_Wait_For_Bar_Close) { // para la versión Turtle Soup Plus One
    double da_Price_Array[1];
    CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array));
    d_Actual_Price = da_Price_Array[0];
  }
  
  // límite superior:
  if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1-a condición
    if(go_Channel.d_High < d_Actual_Price) { // 2-a condición
      // el precio ha roto el límite superior
      return(ENTRY_SELL);
    }
  
  // límite inferior:
  if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-a condición
    if(go_Channel.d_Low > d_Actual_Price) { // 2-a condición
      // el precio ha roto el límite inferior
      return(ENTRY_BUY);
    }
  
  return(ENTRY_NONE);
}

Ahora vamos a recordar que el objeto-canal puede no estar preparado para la lectura de datos desde él (la bandera go_Channel.b_Ready = false). Pues, hay que añadir la comprobación de esta bandera. En esta función, también usamos una de las funciones estándar para copiar los datos desde la serie temporal (CopyClose), por eso nos ocuparemos del procesamiento de un posible error. Tampoco hay que olvidar del registro de datos importantes en log para facilitar la depuración:

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) {
  if(!go_Channel.b_Ready) {
    // los datos no están listos para el uso
    if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: parámetros del canal no están preparados", __FUNCTION__);
    return(ENTRY_UNKNOWN);
  }
  
  double d_Actual_Price = go_Tick.bid; // precio por defecto - para la versión Turtle Soup
  if(b_Wait_For_Bar_Close) { // para la versión Turtle Soup Plus One
    double da_Price_Array[1];
    if(WRONG_VALUE == CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)) {
      // procesamiento del error de la función CopyClose
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyClose: error #%u", __FUNCSIG__, _LastError);
      return(ENTRY_NONE);
    }
    d_Actual_Price = da_Price_Array[0];
  }
  
  // límite superior:
  if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1-a condición
    if(go_Channel.d_High < d_Actual_Price) { // 2-a condición
      // el precio ha roto el límite superior
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: el precio (%s) ha roto el límite superior (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_High, _Digits));
      return(ENTRY_SELL);
    }
  
  // límite inferior:
  if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-a condición
    if(go_Channel.d_Low > d_Actual_Price) { // 2-a condición
      // el precio ha roto el límite inferior
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: el precio (%s) ha roto el límite inferior (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_Low, _Digits));
      return(ENTRY_BUY);
    }
  
  // si el programa ha llegado hasta está línea, entonces el precio se encuentra dentro del intervalo, la 2-a condición no está complida
  
  return(ENTRY_NONE);
}

Esta función será llamada en cada tick, y son centenares de miles de veces al día. Sin embargo, si la primera condición (no menos de tres días desde el último extremo) no se ha cumplido, todo este trabajo es inútil después de la primera comprobación. Las normas de buen estilo de programación exigen reducir el consumo de recursos al mínimo, por eso vamos a enseñar a la función hibernarse hasta la siguiente barra (día), es decir hasta la actualización de los parámetros del canal:

ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal(bool b_Wait_For_Bar_Close = false, int i_Extremum_Bars = 3) {
  static datetime st_Pause_End = 0; // hora de la siguiente comprobación
  if(st_Pause_End > go_Tick.time) return(ENTRY_NONE);
  st_Pause_End = 0;
  
  if(go_Channel.b_In_Process) {
    // los datos no están listos para el uso
    if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: parámetros del canal no están preparados", __FUNCTION__);
    return(ENTRY_UNKNOWN);
  }
  if(go_Channel.i_Lowest_Offset < i_Extremum_Bars && go_Channel.i_Highest_Offset < i_Extremum_Bars) {
    // 1-a condición no ha sido cumplida
    if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: 1-a condición no ha sido cumplida", __FUNCTION__);
    
    // ponemos a pausa hasta la actualización del canal
    st_Pause_End = go_Tick.time + PeriodSeconds() - go_Tick.time % PeriodSeconds();
    
    return(ENTRY_NONE);
  }
  
  double d_Actual_Price = go_Tick.bid; // precio por defecto - para la versión Turtle Soup
  if(b_Wait_For_Bar_Close) { // para la versión Turtle Soup Plus One
    double da_Price_Array[1];
    if(WRONG_VALUE == CopyClose(_Symbol, PERIOD_CURRENT, 1, 1, da_Price_Array)) {
      // procesamiento del error de la función CopyClose
      if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyClose: error #%u", __FUNCSIG__, _LastError);
      return(ENTRY_NONE);
    }
    d_Actual_Price = da_Price_Array[0];
  }
  
  // límite superior:
  if(go_Channel.i_Highest_Offset > i_Extremum_Bars) // 1-a condición
    if(go_Channel.d_High < d_Actual_Price) { // 2-a condición
      // el precio ha roto el límite superior
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: el precio (%s) ha roto el límite superior (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_High, _Digits));
      return(ENTRY_SELL);
    }
  
  // límite inferior:
  if(go_Channel.i_Lowest_Offset > i_Extremum_Bars) // 1-a condición
    if(go_Channel.d_Low > d_Actual_Price) { // 2-a condición
      // el precio ha roto el límite inferior
      if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: el precio (%s) ha roto el límite inferior (%s)", __FUNCTION__, DoubleToString(d_Actual_Price, _Digits), DoubleToString(go_Channel.d_Low, _Digits));
      return(ENTRY_BUY);
    }
  
  // si el programa ha llegado hasta está línea, entonces el precio se encuentra dentro del intervalo, la 2-a condición no está cumplida
  
  if(b_Wait_For_Bar_Close) // para la versión Turtle Soup Plus One
    // ponemos a pausa hasta el cierre de la barra actual
    st_Pause_End = go_Tick.time + PeriodSeconds() - go_Tick.time % PeriodSeconds();
  
  return(ENTRY_NONE);
}

Este es el código definitivo de la función. El archivo del módulo de la señal lo llamaremos Signal_Turtle_Soup.mqh, colocaremos el código referente al canal y a las señales dentro de él, y al principio del archivo añadiremos los campos de edición de los ajustes personalizados de la estrategia:

enum ENUM_STRATEGY {     // Versión de la estrategia
  TS_TURTLE_SOUP,        // Turtle Soup
  TS_TURTLE_SOUP_PLIS_1  // Turtle Soup Plus One
};

// ajustes de usuario
input ENUM_STRATEGY  Turtle_Soup_Type = TS_TURTLE_SOUP;  // Turtle Soup: Versión de la estrategia
input uint           Turtle_Soup_Period_Length = 20;     // Turtle Soup: Profundidad de la busca de extremos (en barras)
input uint           Turtle_Soup_Extremum_Offset = 3;    // Turtle Soup: Pausa después del último extremo (en barras)
input double         Turtle_Soup_Entry_Offset = 10;      // Turtle Soup: Entrada: Margen desde el nivel del extremo (en puntos)
input double         Turtle_Soup_Exit_Offset = 1;        // Turtle Soup: Entrada: Margen desde el extremo opuesto (en puntos)

Hay que guardar este archivo en la carpeta de datos del terminal: para las librerías de señal es MQL5\Include\Expert\Signal.

 

Asesor Experto básico para probar la Estrategia Comercial


Colocaremos los campos de los ajustes personalizados más cerca del inicio del código, y antes de ellos, las listas del tipo enumerado enum que se utilizan en los ajustes. Los campos de los ajustes estarán divididos en dos grupos: "Ajustes de la estrategia" y "Apertura y gestión de posiciones". Los ajustes del primer grupo serán incluidos desde el archivo de la librería de señal durante la compilación. Por ahora hemos creado un archivo de este tipo, pero en los siguientes artículos serán formalizadas y programadas otras estrategias del libro y aparecerá la posibilidad de reemplazar (o añadir) los módulos de señal, junto con los ajustes necesarios para ellos.

Aquí mismo, en el inicio del código, incluimos el el archivo de la librería estándar MQL5 para realizar las operaciones comerciales:

enum ENUM_LOG_LEVEL {  // Lista de los niveles del registro
  LOG_LEVEL_NONE,      // registro desactivado
  LOG_LEVEL_ERR,       // sólo información sobre errores
  LOG_LEVEL_INFO,      // errores + comentarios del robot
  LOG_LEVEL_DEBUG      // todo sin excepciones
};
enum ENUM_ENTRY_SIGNAL {  // Lista de señales para la entrada
  ENTRY_BUY,              // señal de compra
  ENTRY_SELL,             // señal de venta
  ENTRY_NONE,             // no hay señal
  ENTRY_UNKNOWN           // estatus no determinado
};

#include <Trade\Trade.mqh> // clase para ejecutar operaciones de trading



input string  _ = "** Ajustes de la estrategia:";  // .

#include <Expert\Signal\Signal_Turtle_Soup.mqh> // módulo de señal

                                                        
input string  __ = "** Apertura y gestión de posiciones:"; // .
input double  Trade_Volume = 0.1;                  // Volumen de la operación
input uint    Trail_Trigger = 100;                 // Trailing: Distancia de la activación del trailing (en puntos)
input uint    Trail_Step = 5;                      // Trailing: Paso de desplazamiento de SL (en puntos)
input uint    Trail_Distance = 50;                 // Trailing: Distancia máxima desde el precio hasta SL (en puntos)
input ENUM_LOG_LEVEL  Log_Level = LOG_LEVEL_INFO;  // Modo de registro:

Los autores no mencionan ningún esquema especial del control de riesgos o gestión del capital para esta estrategia, por eso vamos a usar el tamaño fijo del lote para todas las operaciones.

Los ajustes del trailing se insertan en puntos. Con la aparición de las cotizaciones de cinco dígitos, también ha aparecido una cierta confusión con estas unidades de medida, por eso merece la pena aclarar una cosa: un punto corresponde al cambio mínimo del precio del símbolo. Eso quiere decir que en caso de las cotizaciones de cinco dígitos tras la coma, un punto es igual a 0,00001, y para las cotizaciones de cuatro dígitos es igual a 0,0001. No hay que confundir los puntos con los pips, ya que estos últimos ignoran la precisión real de las cotizaciones pasándoles siempre en las de cuatro dígitos. Es decir, si el cambio mínimo del precio del símbolo (punto) es igual a 0,00001, entonces un pip es igual a 10 puntos, y si el punto es de 0,0001, los precios del pip y del punto coinciden.

La función del trailing stop utiliza estos ajustes en cada tick. Y el recálculo de los puntos introducidos por el usuario en los precios reales del instrumento aunque no consume los recursos significativos del procesador, pero se realizan centenares de miles de veces al día. Lo correcto sería recalcular una vez los valores introducidos por el usuario al inicializar el EA, y guardarlos en las variables globales para el uso posterior. Lo mismo se puede hacer también con las variables que van a usarse durante la normalización del tamaño del lote. Es que las limitaciones del servidor en cuanto el tamaño mínimo y máximo, así como el paso del cambio permanecen constantes en todos los momentos del trabajo del robot, por eso no hay necesidad de leerlos cada vez de nuevo. La declaración de las variables globales y la función de inicialización será la siguiente:

int
  gi_Try_To_Trade = 4, // número de intentos para enviar la orden comercial
  gi_Connect_Wait = 2000 // pausa entre los intentos (en milisegundos)
;
double
  gd_Stop_Level, // StopLevel de los ajuste del servidor convertido en el precio del símbolo
  gd_Lot_Step, gd_Lot_Min, gd_Lot_Max, // limitación del tamaño del lote de los ajustes del servidor
  gd_Entry_Offset, // entrada: margen desde el extremo en precios del símbolo
  gd_Exit_Offset, // salida: margen desde el extremo en precios del símbolo
  gd_Trail_Trigger, gd_Trail_Step, gd_Trail_Distance // parámetros del trailing convertidos en el precio del símbolo
;
MqlTick go_Tick; // información sobre el último tick conocido



int OnInit() {
  // Conversión de ajustes de los puntos en los precios del símbolo:
  double d_One_Point_Rate = pow(10, _Digits);
  gd_Entry_Offset = Turtle_Soup_Entry_Offset / d_One_Point_Rate;
  gd_Exit_Offset = Turtle_Soup_Exit_Offset / d_One_Point_Rate;
  gd_Trail_Trigger = Trail_Trigger / d_One_Point_Rate;
  gd_Trail_Step = Trail_Step / d_One_Point_Rate;
  gd_Trail_Distance = Trail_Distance / d_One_Point_Rate;
  gd_Stop_Level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) / d_One_Point_Rate;
  // Inicialiación de las limitaciones del lote:
  gd_Lot_Min = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
  gd_Lot_Max = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
  gd_Lot_Step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
  
  return(INIT_SUCCEEDED);
}

Cabe señalar que la librería estándar MQL5 incluye el módulo del trailing del tipo que necesitamos, (TrailingFixedPips.mqh), y podríamos incluirlo en el código tal como lo hemos hecho con la clase para la optimización de las operaciones comerciales. Pero no corresponde de todo a las particularidades de este EA en cuestión, por eso nosotros mismos escribiremos el trailing y lo insertaremos en el cuerpo del robot en forma de una separada función de usuario:

bool fb_Trailing_Stop(    // Función del desplazamiento de SL de la posición del símbolo actual
  double d_Trail_Trigger,  // distancia de la activación del trailing (en precios del símbolo)
  double d_Trail_Step,    // paso de desplazamiento de SL (en precios del símbolo)
  double d_Trail_Distance  // distancia mínima desde el precio hasta SL (en precios del símbolo)
) {
  if(!PositionSelect(_Symbol)) return(false); // la posición no existe, no hay nada a que aplicar el trailing
  
  // valor base para calcular el nuevo nivel SL - valor actual del precio:
  double d_New_SL = PositionGetDouble(POSITION_PRICE_CURRENT);
  
  if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { // para una posición larga
    if(d_New_SL - PositionGetDouble(POSITION_PRICE_OPEN) < d_Trail_Trigger)
      return(false); // el precio no ha pasado la distancia suficiente para activar el trailing
      
    if(d_New_SL - PositionGetDouble(POSITION_SL) < d_Trail_Distance + d_Trail_Step)
      return(false); // el cambio del precio es menor que el paso del desplazamiento de SL
    
    d_New_SL -= d_Trail_Distance; // nuevo nivel de SL
  } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { // para la posición corta
    if(PositionGetDouble(POSITION_PRICE_OPEN) - d_New_SL < d_Trail_Trigger)
      return(false); // el precio no ha pasado la distancia suficiente para activar el trailing
    
    if(PositionGetDouble(POSITION_SL) > 0.0) if(PositionGetDouble(POSITION_SL) - d_New_SL < d_Trail_Distance + d_Trail_Step)
      return(false); // el precio no ha pasado la distancia suficiente para activar el trailing
    
    d_New_SL += d_Trail_Distance; // nuevo nivel de SL
  } else return(false);
  
  // ¿Si permiten los ajustes del servidor establecer SL de cálculo a esta distancia del precio actual?
  if(!fb_Is_Acceptable_Distance(d_New_SL, PositionGetDouble(POSITION_PRICE_CURRENT))) return(false);
  
  CTrade Trade;
  Trade.LogLevel(LOG_LEVEL_ERRORS);
  // mover SL
  Trade.PositionModify(_Symbol, d_New_SL, PositionGetDouble(POSITION_TP));
  
  return(true);
}



bool fb_Is_Acceptable_Distance(double d_Level_To_Check, double d_Current_Price) {
  return(
    fabs(d_Current_Price - d_Level_To_Check)
    >
    fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid)
  );
}

Aquí la comprobación de la admisibilidad de colocación de SL en el nivel calculado se ha pasado en una función separada fb_Is_Acceptable_Distance, para poder usarla durante la validación del nivel de colocación de una orden pendiente y durante la colocación de StopLoss para una posición abierta.

Ahora pasamos al área principal de trabajo en el código del EA, ella es llamada por la función-manejador del evento de la llegada del nuevo tick OnTick. De acuerdo con las reglas de la estrategia, si hay una posición abierta, no hace falta buscar señales nuevas, por eso comenzaremos con la comprobación correspondiente. Si la posición existe, el robot tendrá dos opciones: calcular y establecer el StopLoss inicial para posición nueva, o bien activar la función del trailing que determinará si es necesario mover el StopLoss, y realizará la operación correspondiente. Con la llamada a la función de trailing todo está claro. Pero para el cálculo del nivel StopLoss vamos a usar el margen desde el extremo gd_Exit_Offset que ha sido introducido por el usuario y convertido de los puntos en el precio del símbolo. El extremo del precio se determina usando la función MQL5 CopyHigh o CopyLow. Habrá que comprobar la validez de los niveles calculados de esta manera usando la función fb_Is_Acceptable_Distance y el valor actual del precio en la estructura go_Tick. Vamos a separar estos cálculos y comprobaciones para las órdenes BuyStop y SellStop:

if(PositionSelect(_Symbol)) { // hay una posición abierta
  if(PositionGetDouble(POSITION_SL) == 0.) { // posición nueva
    double
      d_SL = WRONG_VALUE, // уровень SL
      da_Price_Array[] // array adicional
    ;
    
    // calcular el nivel StopLoss:
    if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { // para una posición larga
      if(WRONG_VALUE == CopyLow(_Symbol, PERIOD_CURRENT, 0, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) {
        // procesamiento del error de la función CopyLow
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: error #%u", __FUNCTION__, _LastError);
        return;
      }
      d_SL = da_Price_Array[ArrayMinimum(da_Price_Array)] - gd_Exit_Offset;
      
      // ¿es suficiente la distancia desde el precio actual?
      if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.bid)) {
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat("El nivel de cálculo SL %s ha sido reemplazado por el mínimo admisible %s", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.bid + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits));
        d_SL = go_Tick.bid - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid);
      }
      
    } else { // para una posición corta
      if(WRONG_VALUE == CopyHigh(_Symbol, PERIOD_CURRENT, 0, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) {
        // procesamiento del error de la función CopyHigh
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: error #%u", __FUNCTION__, _LastError);
        return;
      }
      d_SL = da_Price_Array[ArrayMaximum(da_Price_Array)] + gd_Exit_Offset;
      
      // ¿es suficiente la distancia desde el precio actual?
      if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.ask)) {
        if(Log_Level > LOG_LEVEL_NONE) PrintFormat(" El nivel de cálculo SL %s ha sido reemplazado por el mínimo admisible %s", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.ask - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits));
        d_SL = go_Tick.ask + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid);
      }
    }
    
    CTrade Trade;
    Trade.LogLevel(LOG_LEVEL_ERRORS);
    // establecer SL
    Trade.PositionModify(_Symbol, d_SL, PositionGetDouble(POSITION_TP));
    return;
  }
  
  // trailing
  fb_Trailing_Stop(gd_Trail_Trigger, gd_Trail_Step, gd_Trail_Distance);
  return;
}

A parte de los nuevos parámetros leídos del tick, también hay que actualizar los parámetros del canal, que se utilizan para detectar la señal. Llamar a la función correspondiente f_Set de la estructura go_Channel tiene sentido sólo después del cierre de la próxima barra, el resto de tiempo estos parámetros son inalterables. El robot tiene una tarea más que también está vinculada con el inicio del día nuevo (barra), es la eliminación de la orden pendiente de ayer. Vamos a programas estas dos acciones:

int
  i_Order_Ticket = WRONG_VALUE, // ticket de la orden pendiente
  i_Try = gi_Try_To_Trade, // número de intentos para realizar la operación
  i_Pending_Type = -10 // tipo de la orden pendiente existente
;
static int si_Last_Tick_Bar_Num = 0; // número de la barra del tick anterior (0 = inicio del sistema de cronología MQL)

// procesamiento de eventos vinculados al inicio del día nuevo (barra):
if(si_Last_Tick_Bar_Num < int(floor(go_Tick.time / PeriodSeconds()))) {
  // Hola, día nuevo :)
  si_Last_Tick_Bar_Num = int(floor(go_Tick.time / PeriodSeconds()));
  
  // ¿hay una orden pendiente obsoleta?
  i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket);
  if(i_Pending_Type == ORDER_TYPE_SELL_STOP || i_Pending_Type == ORDER_TYPE_BUY_STOP) {
    // quitamos la orden obsoleta:
    if(Log_Level > LOG_LEVEL_ERR) Print("Eliminación de la orden pendiente de ayer");
    
    CTrade o_Trade;
    o_Trade.LogLevel(LOG_LEVEL_ERRORS);
    while(i_Try-- > 0) { // intentos de eliminar
      if(o_Trade.OrderDelete(i_Order_Ticket)) { // intento con éxito
        i_Try = -10; // bandera de operación con éxito
        break;
      }
      // intento fallido
      Sleep(gi_Connect_Wait); // mantenemos una pausa antes de volver a intentar
    }
    
    if(i_Try == WRONG_VALUE) { // fallo al eliminar la orden pendiente
      if(Log_Level > LOG_LEVEL_NONE) Print("Fallo al eliminar la orden pendiente");
      return; // esperamos el siguiente tick
    }
  }
  
  // actualización de los parámetros del canal:
  go_Channel.f_Set(Turtle_Soup_Period_Length, 1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1));
}

La función fi_Get_Pending_Type utilizada aquí devuelve el tipo de la orden pendiente, y siguiendo la referencia recibida para la variable i_Order_Ticket coloca el número del ticket en ella. Este tipo de la orden será necesario más tarde para la verificación de la dirección actual de la señal en este tick, mientras que el ticket se utiliza en caso de la necesidad de eliminar la orden. Si no hay orden pendiente, ambos valores serán iguales a WRONG_VALUE. El listado de esta función está a continuación:

int fi_Get_Pending_Type( // detector de la presencia de la orden pendiente para el símbolo actual
  int& i_Order_Ticket // referencia al ticket de la orden pendiente seleccionada
) {
  int
    i_Order = OrdersTotal(), // número total de órdenes
    i_Order_Type = WRONG_VALUE // variable para el tipo de la orden
  ;
  i_Order_Ticket = WRONG_VALUE; // el valor del ticket devuelto por defecto
  
  if(i_Order < 1) return(i_Order_Ticket); // no hay órdenes
  
  while(i_Order-- > 0) { // repaso de órdenes existentes
    i_Order_Ticket = int(OrderGetTicket(i_Order)); // lectura del ticket
    if(i_Order_Ticket > 0)
      if(StringCompare(OrderGetString(ORDER_SYMBOL), _Symbol, false) == 0) {
        i_Order_Type = int(OrderGetInteger(ORDER_TYPE));
        // hacen falta sólo órdenes pendientes:
        if(i_Order_Type == ORDER_TYPE_BUY_LIMIT || i_Order_Type == ORDER_TYPE_BUY_STOP || i_Order_Type == ORDER_TYPE_SELL_LIMIT || i_Order_Type == ORDER_TYPE_SELL_STOP)
          break; // orden pendiente ha sido encontrada
      }
    i_Order_Ticket = WRONG_VALUE; // todavía no encontrada
  }
  
  return(i_Order_Type);
}

Ahora tenemos todo listo para determinar el estatus de la señal. Si las condiciones de la Estrategia Comercial no han sido cumplidas (la señal recibirá el estatus ENTRY_NONE o ENTRY_UNKNOWN), se puede finalizar el trabajo del programa principal en este tick:

// obtener el estatus de la señal:
ENUM_ENTRY_SIGNAL e_Signal = fe_Get_Entry_Signal(Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1, Turtle_Soup_Extremum_Offset);
if(e_Signal > 1) return; // no hay señal

Si hay señal, la comparamos con la dirección de la orden pendiente actual, si ésta ya está colocada:

// aclaramos el tipo de la orden pendiente y su ticket, si eso aún no ha sido hecho:
if(i_Pending_Type == -10)
  i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket);

// ¿hace falta la orden pendiente nueva?
if(
  (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_SELL_STOP)
  ||
  (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_BUY_STOP)
) return; // ya existe una orden pendiente en la dirección de la señal

// ¿hace falta eliminar la orden pendiente?
if(
  (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_BUY_STOP)
  ||
  (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_SELL_STOP)
) { // la dirección de la orden pendiente no coincide con la dirección de la señal
  if(Log_Level > LOG_LEVEL_ERR) Print("La dirección de la orden pendiente no corresponde a la dirección de la señal");
    
  i_Try = gi_Try_To_Trade;
  while(i_Try-- > 0) { // intentos de eliminación
    if(o_Trade.OrderDelete(i_Order_Ticket)) { // intento satisfactorio
      i_Try = -10; // bandera de la operación satisfactoria
      break;
    }
    // intento fallido
    Sleep(gi_Connect_Wait); // mantenemos una pausa antes del siguiente intento
  }
  
  if(i_Try == WRONG_VALUE) { // fallo al eliminar la orden pendiente
    if(Log_Level > LOG_LEVEL_NONE) Print("Error de eliminación de la orden pendiente");
    return; // esperamos el siguiente tick
  }
}

Ahora, cuando no hay dudas sobre la necesidad de una orden pendiente nueva, calculamos sus parámetros. De acuerdo con las reglas de la estrategia, hay que colocar la orden con una margen adentro desde los límites del canal. StopLoss debe colocarse en el lado opuesto del límite, junto con el extremo del precio de hoy o de dos días (dependiendo de la versión de la estrategia elegida). Pero debemos calcular la posición de StopLoss sólo después de la activación de la orden pendiente (el código para esta operación está más arriba).



Leeremos los límites actuales del canal desde la estructura go_Channel, mientras que el margen para la entrada introducido por el usuario y recalculado en los precios del símbolo se encuentra en la variable gd_Entry_Offset. Habrá que comprobar la validez del nivel calculado usando la función fb_Is_Acceptable_Distance y el valor actual del precio en la estructura go_Tick. Vamos a separar estos cálculos y comprobaciones para las órdenes BuyStop y SellStop:

double d_Entry_Level = WRONG_VALUE; // nivel de colocación de la orden pendiente
if(e_Signal == ENTRY_BUY) { // para la orden pendiente de compra
  // comprobamos la posibilidad de colocar la orden:
  d_Entry_Level = go_Channel.d_Low + gd_Entry_Offset; // nivel de colocación de la orden
  if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.ask)) {
    // la distancia desde el precio actual no es suficiente 
    if(Log_Level > LOG_LEVEL_ERR)
      PrintFormat("No se puede colocar BuyStop en el nivel %s. Bid: %s Ask: %s StopLevel: %s",
        DoubleToString(d_Entry_Level, _Digits),
        DoubleToString(go_Tick.bid, _Digits),
        DoubleToString(go_Tick.ask, _Digits),
        DoubleToString(gd_Stop_Level, _Digits)
      );
    
    return; // esperamos el cambio del precio actual
  }
} else {
  // comprobamos la posibilidad de colocar la orden:
  d_Entry_Level = go_Channel.d_High - gd_Entry_Offset; // nivel de colocación de la orden
  if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.bid)) {
    // la distancia desde el precio actual no es suficiente 
    if(Log_Level > LOG_LEVEL_ERR)
      PrintFormat("No se puede colocar la orden SellStop en el nivel %s. Bid: %s Ask: %s StopLevel: %s",
        DoubleToString(d_Entry_Level, _Digits),
        DoubleToString(go_Tick.bid, _Digits),
        DoubleToString(go_Tick.ask, _Digits),
        DoubleToString(gd_Stop_Level, _Digits)
      );
    
    return; // esperamos el cambio del precio actual
  }
}

Si el nivel calculado de colocación de la orden pendiente ha superado la comprobación, se puede organizar el envío de la orden necesaria al servidor usando la clase de la librería estándar:

// hagamos que el lote cumpla las exigencias del servidor:
double d_Volume = fd_Normalize_Lot(Trade_Volume);

// colocamos la orden pendiente:
i_Try = gi_Try_To_Trade;

if(e_Signal == ENTRY_BUY) {
  while(i_Try-- > 0) { // intentos de colocar BuyStop
    if(o_Trade.BuyStop(
      d_Volume,
      d_Entry_Level,
      _Symbol
    )) { // intento satisfactorio
      Alert("¡La orden pendiente de compra ha sido colocada!");
      i_Try = -10; // bandera de la operación satisfactoria
      break;
    }
    // fallo
    Sleep(gi_Connect_Wait); // mantenemos una pausa antes del siguiente intento
  }
} else {
  while(i_Try-- > 0) { // intentos de colocar la orden SellStop
    if(o_Trade.SellStop(
      d_Volume,
      d_Entry_Level,
      _Symbol
    )) { // intento satisfactorio
      Alert("¡La orden pendiente de venta ha sido colocada!");
      i_Try = -10; // bandera de la operación satisfactoria
      break;
    }
    // fallo
    Sleep(gi_Connect_Wait); // mantenemos una pausa antes del siguiente intento
  }
}

if(i_Try == WRONG_VALUE) // fallo al colocar la orden pendiente
  if(Log_Level > LOG_LEVEL_NONE) Print(" Error de colocación de la orden pendiente");

Pues bien, aquí terminamos la programación del Asesor Experto, y después de la compilación pasamos al análisis de su trabajo en el Probador de Estrategias.

 

Prueba de la estrategia a base de datos históricos


En su libro, Connors y Raschke ilustran las estrategias usando los gráficos de hace más de 20 años, por eso el objetivo principal de este testeo ha sido la comprobación de su eficacia usando los datos más recientes. Se utilizaban los parámetros originales y el timeframe diario, especificados por los autores. Hace 20 años las cotizaciones de cinco dígitos no se usaban mucho, y esta prueba se realizaba precisamente en la cotizaciones de cinco dígitos del servidor demo de MetaQuotes, por eso los márgenes originales de 1 y 10 puntos fueron transformados en 10 y 100. Los parámetros del trailing no se mencionan en la descripción de la estrategia en absoluto, por eso yo usaba los que parecieron más adecuados para el timeframe diario.

Gráfico de resultados de la prueba de la estrategia Turtle Soup para USDJPY en los últimos cinco años:

Turtle Soup, USDJPY, D1, 5 años


Gráfico de resultados de la prueba de la estrategia Turtle Soup One con los mismos parámetros en el mismo intervalo del historial del mismo símbolo:

Turtle Soup Plus One, USDJPY, D1, 5 años


Gráfico de resultados de la prueba en las cotizaciones del oro para los últimos cinco años: Estrategia Turtle Soup:

Turtle Soup, XAUUSD, D1, 5 años


Turtle Soup Plus One:

Turtle Soup Plus One, XAUUSD, D1, 5 años

 


Gráfico de resultados de la prueba en las cotizaciones del petroleo (crude oil) para los últimos cuatro años: Estrategia Turtle Soup:

Turtle Soup, OIL, D1, 4 años


Turtle Soup Plus One:

Turtle Soup Plus One, OIL, D1, 4 años


Los archivos adjuntos contienen los informes completos de todas las pruebas.

Yo propongo hacer las conclusiones a Ustedes, pero me veo obligado a dar una explicación necesaria. Connors y Raschke alertan contra el seguimiento puramente mecánico de las reglas de cualquiera de las estrategias descritas en su libro. Ellos consideran obligatorio el análisis de cómo precisamente el precio se aproxima a los límites del canal y cómo se comporta después de su testeo. Lamentablemente, no revelan más detalles sobre este asunto. Lo que se refiere a la optimización, desde luego se puede intentar adoptar los parámetros a otros timeframes, eligir los instrumentos y parámetros más convenientes para esta Estrategias Comercial.

Conclusión

Hemos formalizado y programado las reglas del primer par de estrategias comerciales (Turtle Soup y Turtle Soup Plus One) descritas en el libro «Street Smarts: High Probability Short-Term Trading Strategies». El Asesor Experto y la librería de señal contienen todas las reglas descritas por Connors y Raschke, pero ahí no hay algunos detalles importantes sobre el trading de los autores, que han sido apenas mencionados de paso. No es difícil de suponer que como mínimo hay que tomar en cuenta las brechas (gaps) y los límites de las sesiones comerciales. Además de eso, parece lógico intentar limitar la negociación con una entrada al día o con una entrada rentable, mantener la orden pendiente más del inicio del día siguiente. Pues, Usted puede hacerlo si quiere mejorar el Asesor Experto descrito en este artículo.

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

Archivos adjuntos |
MQL5.zip (83.14 KB)
Reports.zip (598.05 KB)
Estrategia de trading '80-20' Estrategia de trading '80-20'
En este artículo se describe la creación de las herramientas (indicador y Asesor Experto) para el análisis de la estrategia comercial '80-20'. Las reglas de esta Estrategia Comercial han sido tomadas del libro titulado «Street Smarts: High Probability Short-Term Trading Strategies» escrito por Linda Raschke y Laurence Connors. Las reglas han sido formalizadas en el lenguaje MQL5, y el indicador y el Asesor Experto diseñados a base de esta estrategia han sido probados en el historial actual del mercado.
Interfaces gráficas X: Campo de edición del texto, slider de imágenes y controles simples (build 5) Interfaces gráficas X: Campo de edición del texto, slider de imágenes y controles simples (build 5)
En este artículo vamos a analizar los controles nuevos, tales como: «Campo de edición del texto», «Slider de imágenes», así como los controles simples adicionales, «Etiqueta de texto» e «Imagen». La librería sigue desarrollándose, y además de la aparición de controles nuevos, se van mejorando los que ya han sido creados anteriormente.
Zigzag universal Zigzag universal
El Zigzag es uno de los indicadores más populares entre los usuario de MetaTrader 5. En este artículo se han analizado las posibilidades de creación de diferentes versiones del Zigzag. Como resultado, obtenemos un indicador universal con amplias posibilidades para la ampliación de la funcionalidad, el cual es muy cómodo utilizar en el desarrollo de los Asesores Expertos y otros indicadores.
Principios de programación en MQL5: Variables globales del terminal  MetaTrader 5 Principios de programación en MQL5: Variables globales del terminal MetaTrader 5
Las variables globales del terminal es un medio imprescindible durante la programación de los Asesores Expertos complejos y seguros. Después de aprender a trabajar con las variables globales, ya no podrá imaginar la creación de los asesores expertos en MQL5 sin usarlas.