El patrón Bandera

Dmitry Fedoseev | 1 septiembre, 2017


Contenido

Introducción

La peculiridad característica del patrón Bandera, por la cual ha recibido su nombre, es un notorio movimiento vertical del precio ("asta") y después un prolongado movimiento horizontal del precio: el "paño" rectangular (fig. 1).


Fig. 1 Bandera

En los libros y páginas web dedicados al análisis técnico, el patrón Bandera se analiza paralelalmetne con el patrón Banderín. El Banderín, a diferencia de la bandera, tiene el paño triangular (fig. 2), por eso, en algunos manuales de análisis técnico, el patrón Bandera se estudia junto el patrón Triángulo.


Fig. 2. Banderín

Podría parecer que el Banderín y el Triángulo son simplemente nombres distintos del mismo patrón. En algunos manuales de análisis técnico, en particular en el libro de Thomas N. Bulkowsi "Encyclopedie of Chart Patterns", figuran ambos por separado. Asimismo, en este libro se describe el patrón Cuña, semejante al Triángulo, pero representado en horizontal. A la izquierda del mismo se encuentra una parte estrecha, y a la derecha el precio se mueve con amplitud ascendente (fig. 3).


Fig. 3. Cuña

Además del patrón Cuña, es famoso el patrón del Triángulo creciente, así como diferentes formaciones del Triángulo, análogas a la Bandera. Eso significa que deberán existir reglas claras para diferenciar el Banderín del Triángulo, la Cuña del Triángulo creciente y la Bandera de la Formación horizontal. Precisamente esta cuestión vamos a analizar en el artículo en primer lugar. Después, se crearán indicadores para la búsqueda de estos patrones. 

Diferencias entre los patrones Banderín y Triángulo

Vamos a ver las diferencias entre el Banderín y el Triángulo, así como entre todos los patrones enumerados en la Introducción que tengan una forma similar:

  • Formación rectangular — bandera;
  • Triángulo — Banderín;
  • Triángulo creciente — Cuña.

En una categoría entran los patrones formación Triangular, Triángulo y Triángulo creciente; en otra, los patrones Bandera, Banderín y Cuña.

Los patrones de la primera categoría generan su forma con los puntos de viraje del precio (fig. 4), para buscarlos, se puede usar el indicador Zigzag.


Fig. 4. Patrones: a — Formación Horizontal, b — Triángulo, c — Triángulo creciente.  
Patrones mostrados para un supesto movimiento ascendente (para la compra).

Los patrones de la segunda categoría generan su forma rellenando la superficie de la figura con barras (fig. 5). Está claro que no veremos los patrones en el gráfico de una forma tan explícita como se muestra en la imagen, pero la esencia es que una barra se solapa fuertemenete con las adyacentes, y gracias a ello se forma una figura u otra. 


Fig. 5. Patrones: a — Bandera, b — Banderín, c — Cuña.
Patrones mostrados para un supesto movimiento ascendente (para la compra).

Tras aclarar las categorías y sus principales diferencias, vamos a analizar cada uno de los patrones por separado. 

Formación horizontal

Empezaremos analizando los patrones de la primera categoría. Para definirlos, resulta cómodo usar el indicador Zigzag. El primer patrón en esta categoría es la Formación horizontal, a la que en la segunda categoría corresponde la bandera. Sin embargo, en la mayoría de las guías de análisis técnico, precisamente la Formación horizontal es llamada Bandera.

Para generar la Formación horizontal, el precio deberá realizar un movimiento horizontal notorio, después formar como mínimo dos picos a más o menos el mismo nivel de precio, y dos valles también al mismo nivel de precio. A veces sucede que el precio forma tres picos y valles (fig. 6), y en ocasiones, incluso más. Por eso, en el indicador se ha creado un parámetro que determina el número de picos y valles que forman el patrón.


Fig. 6. Formaciones horizontales: a — de dos picos/valles, b — de tres picos/valles.
Patrones mostrados para un supesto movimiento ascendente (para la compra) 

No es obligatorio que el límite inferior y superior del patrón sean horizontales. Es importante que sean paralelos, por eso el indicador tendrá un parámetro más para la selección de la inclinación de la formación: horizontal, con inclinación ascendente, con inclinación descendente (fig. 7).


Fig. 7. a — Formación horizontal, b — formación con inclinación descendente, c — formación con inclinación ascendente. 
Patrones mostrados para un supesto movimiento ascendente (para la compra)

Claro que las formaciones con inclinación ascendente y descendente ya no se pueden denominar horizontales, sin embargo, en general, se encuentran muy próximas a la formación horizontal.

El patrón deja de formarse cuando el precio rompe el nivel formado por los picos (fig. 8).

 
Fig. 8. Final de la formación y momento de la apertura
posiciones buy: a — para la formación horizontal,
b — para la formación con inclinación descendente 

La definición del momento de entrada para la formación ascendente se ejecutará sin tener en cuenta la inclinación de los picos, simplemente se usará el nivel horizontal del último pico (fig. 9).


Fig. 9. Definiendo el momento de entrada (compra) para la formación con inclinación ascendente

Una sencilla variante con nivel horizontal se puede usar también para la formación con inclinación descendente, por eso, en el indicador habrá una variable para la elección del tipo de nivel independientemente del tipo de formación.

El objetivo para la Formación horizontal se define según el tamaño del movimiento vertical hasta el momento de formación del patrón. La misma distancia que haya recorrido el precio hasta el surgimiento de la formación, deberá recorrer tras esta (fig. 10).


Fig. 10. Definiendo el objetivo. La distancia L1, recorrida por el precio hasta la entrada
en la formación, es igual a la distancia L2 tras la salida de la formación.  

Dado que los límites superior e inferior de la formación son paralelos, es posible utilizar una variante más simple para definir el objetivo: basta con medir la distancia recorrida por el precio hasta el primer pico de la formación y trazarlo hacia arriba desde el último valle (fig. 11).


Fig. 11. Método sencillo de definición del objetivo. La distancia L1, recorrida por el precio hasta la formación
del primer pico,
es igual a la distancia L2 desde el último valle hasta el objetivo


Triángulo decreciente

El patrón del Triángulo decreciente no se distingue apenas de la Formación horizontal. La única diferencia consiste en que los segmentos del Zigzag que forma el patrón deberán estrecharse de forma secuencial (fig. 12).


 Fig. 12. Triángulo decreciente. El segmento 3-4 debe ser
menor que el segmento 1-2, y el segmento 5-6 debe ser menor que el segmento 3-4
 

Lo demás es igual que en el patrón Formación horizontal: la ubicación horizontal del triángulo o con inclinación ascendente/descendente, la entrada con la ruptura de la línea de resistencia formada con los dos últimos picos o con el nivel horizontal desde el último pico, el cálculo análogo del objetivo.


Triángulo creciente

Todo lo que respecta al triángulo decreciente, toca también al triángulo creciente, la única diferencia consiste en que ahora los segmentos del Zigzag que constituyen el patrón aumentan (fig. 13).


 Fig. 13. Triángulo creciente. El segmento 3-4 debe ser 
mayor que el segmento 1-2, y el segmento 5-6 debe ser mayor que el segmento 3-4
 

Esta semejanza tan significativa de los tres patrones posibilita la creación de un indicador universal para su búsqueda.  


Indicador universal para la búsqueda de la Formación horizontal y los Triángulos

Para crear un indicador necesitaremos el indicador iUniZigZagSW del artículo "Zigzag universal". Para que funcione, serán necesarios varios archivos adicionales: CSorceData.mqh, CZZDirection.mqh y CZZDraw.mqh. Estos archivos, al igual que el archivo  iUniZigZagSW.mq5, se encuentran en el anexo al artículo "Zigzag universal". Descárguelo, descomprímalo y cópielo desde la carpeta MQL5 a la carpeta de datos del terminal. Después de copiarlo, en la carpeta MQL5/Indicators aparecerá la carpeta ZigZags con varios archivos (incluido el archivo con iUniZigZagSW.mq5), y en la carpeta MQL5/Includes, aparecerá la carpeta ZigZag con los archivos CSorceData.mqh, CZZDirection.mqh y CZZDraw.mqh. Después de copiar los archivos, reinicie el terminal para que los indicadores se compilen, o compílelos todos por separado en el editor MetaEditor. Compruebe necesariamente el funcionamiento del indicador iUniZigZagSW, colocándolo un gráfico del terminal.

En el artículo "Ondas de Wolfe", en uno de los estadios intermedios de creación del indicador se guardó el archivo iWolfeWaves_Step_1.mq5. En él se recurre al indicador iUniZigZagSW.mq5 a través de la función iCustom() y se forma la matriz con todos los picos y valles del zigzag. Descargue el anexo al artículo "Ondas de Wolfe", descomprímalo, copie el archivo iWolfeWaves_Step_1.mq5 en la carpeta MQL5/Indicators, renómbrelo como "iHorizontalFormation" y ábralo en el editor MetaEditor. Todo el trabajo posterior sobre el indicador de búsqueda del patrón Formación horizontal se realizará en este archivo. Es posible que en el archivo sea necesario cambiar la ruta al indicador iUniZigZagSW. Para comprobar esto, compile el indicador e intente colocarlo en el gráfico. Si en este caso se abre una ventana con el mensaje "Error load indicator", encuentre en la función OnInit() la llamada a la función iCustom() y corrija el nombre del indicador a llamar, cambiándolo de "iUniZigZagSW" a "ZigZags\\iUniZigZagSW". Después de corregirlo, compile de nuevo el indicador y asegúrese de que ahora se puede colocar en el gráfico sin mensajes de error. El indicador por el momento no debe dibujar nada.

Todo el proceso de búsqueda de los patrones estudiados aquí se puede dividir claramente en varias tareas prácticamente independientes:

  1. Definición de la magnitud del movimiento del precio que precede a la fomación del patrón.
  2. Definición de la forma del patrón.
  3. Definición de la inclinación del patrón.
  4. Finalización de la formación del patrón: inmediatamente después de la formación del patrón, o bien la espera de la ruptura del nivel.
  5. Cálculo del objetivo.

La resolución de cada tarea (excepto la primera) se presentará con varias opciones. Así lograremos que el indicador sea universal y tendremos la posibilidad de utilizarlo para descrubrir los tres patrones. El paso de una opción a otra se realizará en la ventana de propiedades del indicador con la ayuda de una lista desplegable de enumeraciones. 

Enumeración para la selección de la forma (tipo de patrón):

enum EPatternType{
   PatternTapered,
   PatternRectangular,
   PatternExpanding
};

Variable correspondiente en la ventana de propiedades:

input EPatternType         Pattern        =  PatternRectangular;

Este parámetro permite elegir la forma del patrón: PatternTapered — triángulo decreciente, PatternRectangular — rectángulo, PatternExpanding — triángulo creciente.

Enumeración para la selección de la inclinación del patrón:

enum EInclineType{
   InclineAlong,
   InclineHorizontally,
   InclineAgainst
};

Variable correspondiente en la ventana de propiedades:

input EInclineType         Incline        =  InclineHorizontally; 

Este parámetro permite elegir la inclinación del patrón: InclineAlong — inclinación en la dirección del movimiento supuesto (para la compra, hacia arriba, para la venta, hacia abajo), InclineHorizontally — sin inclinación, InclineAgainst — inclinación en la dirección opuesta al movimiento supuesto del precio.

Enumeración para la selección del método de finalización de la formación del patrón:

enum EEndType{
   Immediately,
   OneLastVertex,
   TwoLastVertices
};

Variable correspondiente en la ventana de propiedades:

input EEndType             CompletionType =  Immediately;

Este parámetro permite elegir las siguientes variantes: Immediately — en cuanto se forma el patrón, OneLastVertex — tras romper el nivel horizontal formado por el último pico del patrón, TwoLastVertices — tras romper el nivel formado por los dos últimos picos del patrón.

Enumeración para la selección de la variente de cálculo del objetivo:

enum ETargetType{
   FromVertexToVertex,
   OneVertex,
   TwoVertices
};

Variable correspondiente en la ventana de propiedades:

input ETargetType          Target         =  OneVertex;

Este parámetro permite elegir entre las siguientes variantes: FromVertexToVertex — de un pico hasta otro pico (fig. 11), OneVertex — conforme a un pico (fig. 10), TwoVertices — conforme a dos picos (se usan los dos valles iniciales del patrón, ver la fig. 14).


Fig. 14. Patrón de tres picos. Variante de definición del objetivo — TwoVertices,
método de finalización del patrón — OneLastVertex.

Con la variante de finalización del patrón Immediately, el parámetro Target no es válido, puesto que solo es posible una variante de definición del objetivo — FromVertexToVertex. Para las otras dos variantes de finalización del patrón (OneLastVertex y TwoLastVertices) es posible la combinación diversa de las tres variantes del parámetro CompletionType. Preste atención a una peculiaridad: en las variantes de definición del objetivo OneVertex y TwoVertices, para definir el valor del objetivo se usan o bien uno o bien los dos primeros picos (punto 2, o los puntos 2 y 4 en la fig. 14), y para definir el nivel de ruptura se usan o bien el último o bien los dos últimos picos (punto 5 o los puntos 3 y 5 en la fig. 14). Si se usara el patrón de dos picos, se usarían entonces el pico 3 o los picos 1 y 3.

Para resolver la tarea 1 será necesario el parámetro que define la magnitud del recorrido del precio que precede al patrón:

input double               K1             =  1.5;

La altura del segmento 1-2 (ver fig. 14) se toma como base del patrón (tamaño de referencia), todas las comprobaciones de los tamaños se ejecutan con respecto a él. El parámetro K1 determina cuántas veces el segmento 0-1 debe ser de altura superior al segmento 1-2.

Para resolver la tarea 2 (definir la forma del patrón) se usa el parámetro K2:

input double               K2             =  0.25;

Cuanto menor sea la magnitud del parámetro, más constante deberá ser la altura del patrón en toda su longitud. Para los patrones triangulares (crecientes y decrecientes), el aumento del parámetro designará la búsqueda de los patrones con una forma triangular más manifiesta.

Para resolver la tarea 3 (definir la forma del patrón) se usa el parámetro K3:

input double               K3             =  0.25;

Cuanto menor sea la magnitud del parámetro, más regularmente deberá ubicarse el patrón. Al buscar patrones inclinados, la magnitud del parámetro K2 permite encontrar los patrones solo con una inclinación manifiesta.

Y al fin, uno de los parámetros principales:

input int                  N              =  2;

El parámetro N determina el número de picos del patrón. 

Como resultado, obtenemos el siguiente conjunto de parámetros externos (excepto los parámetros del Zigzag):

input EPatternType         Pattern        =  PatternRectangular;
input EInclineType         Incline        =  InclineHorizontally;     
input double               K1             =  1.5;
input double               K2             =  0.25;
input double               K3             =  0.25;
input int                  N              =  2;
input EEndType             CompletionType =  Immediately;
input ETargetType          Target         =  OneVertex;

Usando el parámetro N, calcularemos cuántos puntos en total del zigzag serán necesarios para definir el patrón. Primero declaramos la variable global:

int RequiredCount;

En la función OnInit() calculamos su valor:

RequiredCount=N*2+2;

2*N — número de picos que forman el patrón (N superiores y N inferiores). Otro pico define el recorrido anterior del precio y otro pico más, el último punto del nuevo segmento del zigzag (no se usa en los cálculos).

Todo el trabajo posterior se realizará en la función OnTick(). El nuevo código se añadirá al final del todo del indicador principal del ciclo. La comprobación de la formación del patrón se ejecutará con la condición de que haya suficientes puntos del zigzag y solo en los momentos en que cambie su dirección. El seguimiento del precio y el nivel se realizará con cada cambio del zigzag:

if(CurCount>=RequiredCount){
   if(CurDir!=PreDir){      
      // comprobación de las condiciones

   }
   // seguimiento del precio y el nivel

} 

Primero se calcula la magnitud básica: la altura del segmento 1-2 (ver la fig. 14). Esta magnitud se usará al comprobar todas las condiciones de formación del patrón. A continuación, se comprueba la condición 1, la magnitud del recorrido anterior:

int li=CurCount-RequiredCount;                                            // índice del punto inicial del patrón en la matriz PeackTrough
double base=MathAbs(PeackTrough[li+1].Val-PeackTrough[li+2].Val); Я       // magnitud básica
double l1=MathAbs(PeackTrough[li+1].Val-PeackTrough[li].Val);             // altura del segmento 1-2
   if(l1>=base*K1){                                                       // comprabación de la magnitud del recorrido anterior
        // otras comprobaciones

   }

Las comprobaciones posteriores dependerán de si el último segmento del zigzag está dirigido hacia arriba o hacia abajo.

if(CurDir==1){              // el último segmento del zigzag está dirigido hacia arriba
   // comprobando condición para la dirección ascendente  
             
}
else if(CurDir==-1){        // el último segmento del zigzag está dirigido hacia abajo
   // comprobando condición para la dirección descendente

}

Veamos la comprobación de la condición para la dirección ascendente:

if(CheckForm(li,base) && CheckInclineForBuy(li,base)){      // comprobación de la forma y la dirección
   if(CompletionType==Immediately){ 
      // ponemos de inmediato la flecha del indicador
      UpArrowBuffer[i]=low[i];
      // ponemos el punto del objetivo
      UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
   }
   else{
      // establecemos los parámetros del nivel a romper
      SetLevelParameters(1);
      // estebleciendo los parámetros del objetivo
      SetTarget(1,li);
   }
} 

La comprobación de la condición de formación del patrón se realiza con dos funciones: CheckForm() — comprobación de la formación del patrón (tarea 2) y CheckInclineForBuy() — comprobación de la inclinación (tarea 3). Si ya se ha superado la comprobación de la forma y la inclinación, entonces, dependiendo del tipo de finalización del patrón, se ejecuta o bien la colocación de la flecha y el punto del objetivo en el gráfico, o se establecen los parámetros del nivel a romper, después de lo cual el indicador realiza un seguimiento del nivel.

Función CheckForm(). A la función se le transmite el índice del primer punto del patrón en la matriz PeackTrough y la magnitud básica base. Vamos a analizar el código de la función:

bool CheckForm(int li,double base){               
   switch(Pattern){
      case PatternTapered: 
         // decreciente
         return(CheckFormTapered(li,base));
      break;               
      case PatternRectangular: 
         // rectangular
         return(CheckFormRectangular(li,base));
      break;
      case PatternExpanding: 
         // creciente
         return(CheckFormExpanding(li,base));
      break;
   }
   return(true);
}

En la función, dependiendo del valor del parámetro Pattern, se realiza la llamada de las funciones correspondientes: CheckFormTapered() — triángulo decreciente, CheckFormRectangular() — formación rectangular, CheckFormExpanding() — triángulo creciente,

Función CheckFormTapered():

bool CheckFormTapered(int li,double base){
   // ciclo desde 1, el primer segmento no se comprueba,
   // pero todos los segmentos sucesivos se comprueban con respecto a él 
   for(int i=1;i<N;i++){ 
      // cálculo del índice del siguiente punto superior del patrón 
      int j=li+1+i*2;
      // magnitud del siguiente segmento 
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // magnitud del segmento anterior
      double lp=MathAbs(PeackTrough[j-2].Val-PeackTrough[j-1].Val);
      // el segmento anterior debe ser superior, de no ser así,
      // la función retorna false   
      if(!(lp-lv>K2*base)){
         return(false);
      }
   } 
   return(true);
}

En la función se ejecuta la pasada del ciclo por los segmentos del zigzag que constituyen el patrón, cada segmento siguiente debe ser menor que el anterior.

La función CheckFormExpanding() es análoga, la diferencia solo reside en una condición:

if(!(lv-lp>K2*base)){
   return(false);
}

Para ejecutar esta condición, cada siguiente segmento debe ser mayor al anterior.

Función CheckFormRectangular():

bool CheckFormRectangular(int li,double base){   
   // ciclo por todos los picos superiores, excepto el primero      
   for(int i=1;i<N;i++){
      // cálculo del índice del pico siguiente 
      int j=li+1+i*2; 
      // cálculo del tamaño del segmento siguiente
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // el segmento no debe diferenciarse mucho de la magnitud básica 
      if(MathAbs(lv-base)>K2*base){
         return(false); 
      }
   }
   return(true);
}

En esta función se compara cada uno de los segmentos con la magnitud básica. Si la diferencia es notable, la función retorna false.

Si la comporbación de la forma se ha realizado con éxito, se efectúa la comprobación de la inclinación. Función CheckInclineForBuy():

bool CheckInclineForBuy(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // inclinación a favor del movimiento
         return(CheckInclineUp(li,base));
      break;
      case InclineHorizontally:
         // sin inclinación
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // inclinación contra el movimiento
         return(CheckInclineDn(li,base));
      break;
   } 
   return(true);
}  

La función de comprobación de la inclinación para la venta se diferencia solo en dos líneas:

bool CheckInclineForSell(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // inclinación a favor del movimiento
         return(CheckInclineDn(li,base));
      break;
      case InclineHorizontally:
         // sin inclinación
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // inclinación en contra del movimiento
         return(CheckInclineUp(li,base));
      break;
   } 
   return(true);
} 

Para la compra, en el caso de Incline igual a InclineAlong (a favor del movimiento), se llama la función CheckInclineUp(), y para la venta, CheckInclineDn(). En el caso de Incline igual a InclineAgainst (en contra del recorrido del movimiento), será al revés.

Función de comprobación de la inclinación del patrón ascendente CheckInclineUp():

bool CheckInclineUp(int li,double base){   
   // ciclo por todos los picos superiores, excepto el primero      
   for(int v=1;v<N;v++){
      // cálculo del índice del pico superior siguiente
      int vi=li+1+v*2;
      // cálculo de la parte media del siguiente segmento del zigzag
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // cálculo de la parte media del anterior segmento del zigzag
      double mp=(PeackTrough[vi-2].Val+PeackTrough[vi-1].Val)/2;
      // el segmento siguiente debe ser superior al anterior
      if(!(mc>mp+base*K3)){
         return(false);
      }
   }
   return(true);
} 

En la función se ejecuta el paso por todos los segmentos del zigzag, para cada uno de ellos se calcula el valor de su parte media y se compara con la parte media del segmento anterior. Cada segmento debe estar por encima del anterior en una magnitud base*K3.

La función de comprobación de la inclinación del patrón descendente CheckInclineDn() se distingue solo por una condición:

if(!(mc<mp-base*K3)){
   return(false);
}

Para ejecutar esta condición, cada segmento debe ubicarse por debajo del anterior.

Función CheckInclineHorizontally():

bool CheckInclineHorizontally(int li,double base){ 
   // parte media del segmento básico
   double mb=(PeackTrough[li+1].Val+PeackTrough[li+2].Val)/2;        
   for(int v=1;v<N;v++){
      // índice del pico superior siguiente
      int vi=li+1+v*2;
      // parte media del siguiente segmento
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // la parte media del siguiente segmento no deberá desviarse mucho 
      // de la parte media del segmento básico
      if(MathAbs(mc-mb)>base*K3){
         return(false);
      }
   }                  
   return(true);
}

Si se han superado las comprobaciones de la forma y la inclinación, se ejecuta el siguiente fragmento del código:

if(CompletionType==Immediately){                    // entrada inmediata
   UpArrowBuffer[i]=low[i];
   UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
}
else{                                               // esperar la ruptura del nivel
   SetLevelParameters(1);
   SetTarget(1,li);
}

Con la variedad de finalización del patrón Immediately, el indicador dibuja directamente una flecha y pone un punto con el objetivo, en el resto de los casos se ejecuta la colocación del nivel de ruptura con la función SetLevelParameters() y la colocación del objetivo con la función SetTarget().

Función SetLevelParameters():

void SetLevelParameters(int dir){
   CurLevel.dir=dir;   
   switch(CompletionType){
      case OneLastVertex:                            // conforme a un punto
          CurLevel.v=PeackTrough[CurCount-3].Val;
      break;
      case TwoLastVertices:                          // conforme a dos puntos
         CurLevel.x1=PeackTrough[CurCount-5].Bar;
         CurLevel.y1=PeackTrough[CurCount-5].Val;
         CurLevel.x2=PeackTrough[CurCount-3].Bar;
         CurLevel.y2=PeackTrough[CurCount-3].Val;
      break;
   }
} 

En la función SetLevelParameters(), para guardar los parámetros del nivel se usa la estructura SLevelParameters:

struct SLevelParameters{
   int x1;
   double y1;
   int x2;
   double y2;       // de x1 a y2 - parámetros del nivel inclinado
   double v;        // magnitud del nivel horizontal
   int dir;         // dirección
   double target;   // objetivo
   // método para el cálculo del valor del nivel inclinado
   double y3(int x3){
      if(CompletionType==TwoLastVertices){
            return(y1+(x3-x1)*(y2-y1)/(x2-x1));
      }
      else{
         return(v);
      }
   }
   // método para la inicialización o el reseteo de los parámetros
   void Init(){
      x1=0;
      y1=0;
      x2=0;
      y2=0;
      v=0;
      dir=0;   
   }
};

La estructura contiene los campos para los parámetros de la línea: x1, y1, x2, y2; el campo v para el valor del nivel horizontal; d - dirección del patrón; target - objetivo. El objetivo puede indicarse directamente como un nivel-objetivo (en el caso de la variante FromVertexToVertex), y también como la magnitud del nivel de ruptura (para las variantes OneVertex y TwoVertices). El método y3() se usa para calcular el valor del nivel inclinado. El método Init() se usa para inicializar o resetear los resultados.

En el caso de cumplir todas las condiciones de formación del patrón, se llama la función SetLevelParameter(), en esta función, dependiendo del tipo de nivel elegido (horizontal o inclinado) se establecen los parámetros del nivel inclinado (los campos x1, y1, x2, y2) o el valor del nivel horizontal - v. En el método y3() se ejecuta el cálculo del valor del nivel con el uso de los campos x1, y1, x2, y2 o se retorna el valor del campo v.

En el indicador se declaran dos variables del tipo SLevelParameters:

SLevelParameters CurLevel;
SLevelParameters PreLevel;

Esta pareja de variables se usa de forma análoga a las parejas de variables CurCount-PreCount y CurDir-PreDir; antes del cálculo inicial del indicador se ejecuta el reseteo de los valores de las variables (el fragmento de código está ubicado al principio mismo de la función OnTick()):

int start;

if(prev_calculated==0){           // primer cálculo de todas las barras
   start=1;      
   CurCount=0;
   PreCount=0;
   CurDir=0;
   PreDir=0;  
   CurLevel.Init();    
   CurLevel.Init();
   LastTime=0;
}
else{                           // cálculo de las nuevas barras y de la barra en formación 
   start=prev_calculated-1;
}

Al calcular cada barra se ejecuta el desplazamiento de los valores en estas variables (el código se ubica al principio del ciclo de indicador):

if(time[i]>LastTime){
   // primer cálculo de una nueva barra
   LastTime=time[i];
   PreCount=CurCount;
   PreDir=CurDir;
   PreLevel=CurLevel;
}
else{
   // cálculo repetido de una nueva barra
   CurCount=PreCount;
   CurDir=PreDir;
   CurLevel=PreLevel;
}

Con la llamada de la función SetTarget() se establecen los parámetros del objetivo:

void SetTarget(int dir,int li){
   switch(Target){
      case FromVertexToVertex:
         // variante "de pico a pico"
         if(dir==1){
            CurLevel.target=PeackTrough[CurCount-1].Val+(PeackTrough[li+1].Val-PeackTrough[li].Val);
         }
         else if(dir==-1){
            CurLevel.target=PeackTrough[CurCount-1].Val-(PeackTrough[li].Val-PeackTrough[li+1].Val);
         }
      break;
      case OneVertex:
         // conforme a un pico
         CurLevel.target=MathAbs(PeackTrough[li].Val-PeackTrough[li+2].Val);
      break;
      case TwoVertices:
         // conforme a dos picos
         SetTwoVerticesTarget(dir,li);
      break;
   }
}

Para la variante FromVertexToVertex se ejecuta el cálculo del valor de precio. Para la variante OneVertex, al campo target se le asigna la magnitud del recorrido del precio desde el nivel de ruptura hasta el objetivo. El cálculo para la variante SetTwoVerticesTarget se ejecuta en la función SetTwoVerticesTarget():

void SetTwoVerticesTarget(int dir,int li){
   // coordenadas de la línea inicial (larga)
   // patrón - de valle a pico  
   double x11=PeackTrough[li].Bar;
   double y11=PeackTrough[li].Val;
   double x12=PeackTrough[li+1].Bar;
   double y12=PeackTrough[li+1].Val;
   // coordenadas de la línea según dos valles para la compra
   // o según dos picos para la venta
   double x21=PeackTrough[li+2].Bar;
   double y21=PeackTrough[li+2].Val;
   double x22=PeackTrough[li+4].Bar;
   double y22=PeackTrough[li+4].Val;
   // valor en el punto de cruce de las líneas
   double t=TwoLinesCrossY(x11,y11,x12,y12,x21,y21,x22,y22);
   // establecer el valor del objetivo dependiendo de la dirección
   if(dir==1){
      CurLevel.target=t-PeackTrough[li].Val;
   }
   else if(dir==-1){
      CurLevel.target=PeackTrough[li].Val-t;         
   }
}

Para la variante SetTwoVerticesTarget, el campo target recibe los valores del recorrido del precio desde el nivel a romper hasta el objetivo, igual que para la variante OneVertex.

Vamos a analizar cómo se realiza el seguimiento del precio y del nivel (CompletionType no es igual a Immediately):

// se usa el nivel
if(CompletionType!=Immediately){
   // hay cambio en el zigzag
   if(PeackTrough[CurCount-1].Bar==i){
      if(CurLevel.dir==1){                // esperamos la ruptura hacia arriba
         // en la variable cl obtenemos el valor del nivel
         double cl=CurLevel.y3(i); 
         // el zigzag rompe el nivel
         if(PeackTrough[CurCount-1].Val>cl){
            // estableciendo flecha hacia arriba
            UpArrowBuffer[i]=low[i];
            // estableciendo el punto del objetivo
            if(Target==FromVertexToVertex){
               // en el campo target se encuentra el precio
               UpDotBuffer[i]=CurLevel.target;                        
            }
            else{
               // en el campo target se encuentra la distancia desde el nivel
               UpDotBuffer[i]=cl+CurLevel.target;
            }
            // reseteo del campo dir para dejar de seguir el nivel
            CurLevel.dir=0;
         }
      }
      else if(CurLevel.dir==-1){         // esperamos la ruptura hacia abajo
         // en la variable cl obtenemos el valor del nivel
         double cl=CurLevel.y3(i);
         // el zigzag rompe el nivel
         if(PeackTrough[CurCount-1].Val<cl){
            // estableciendo flecha hacia abajo
            DnArrowBuffer[i]=low[i];
            // estableciendo el punto del objetivo
            if(Target==FromVertexToVertex){
               // en el campo target se encuentra el precio
               DnDotBuffer[i]=CurLevel.target;
            }
            else{                     
               // en el campo target se encuentra la distancia desde el nivel
               DnDotBuffer[i]=cl-CurLevel.target;
            }
            // reseteo del campo dir para dejar de seguir el nivel
            CurLevel.dir=0;
         }         
      }         
   }
} 

Esta comprobación se realiza con cada cambio del zigzag. Todos los picos se guardan en la matriz PeackTrough, el cambio en el zigzag se determina según la correspondencia del índice del último punto del zigzag con el índice de la barra actual:

if(PeackTrough[CurCount-1].Bar==i){

Con la ayuda del método y3() se calcula el valor actual del nivel:

double cl=CurLevel.y3(i); 

Se comprueba si el último segmento del zigzag ha roto este nivel:

if(PeackTrough[CurCount-1].Val>cl){

Si el nivel ha sido traspasado, el indicador dibuja una flecha y coloca el punto del objetivo. El campo target puede contener el valor de precio del objetivo, en este caso, el valor se usa directamente; o puede contener la magnitud del recorrido del precio hasta el objetivo, en este caso, el valor del objetivo se calcula teniendo en cuenta el valor actual del nivel: 

if(Target==FromVertexToVertex){
   UpDotBuffer[i]=CurLevel.target;                        
}
else{
   UpDotBuffer[i]=cl+CurLevel.target;
}

Al final, el campo dir se resetea para no realizar más seguimientos del precio hasta la aparición del siguiente patrón:

CurLevel.dir=0;

Con esto ya hemos finalizado la creación del indicador, vamos a mostrar varios fragmentos de su funcionamiento en la fig. 15.


Fig. 15. Varias señales del indicador iHorizontalFormation

De forma adicional, se ha añadido al indicador la función de alerta. Podrá encontrar el indicador completamente preparado en los anexos al artículo, el nombre del archivo es iHorizontalFormation.


Indicador universal para la búsqueda de los patrones Bandera, Banderín y Cuña

Ahora vamos a crear un indicador para la búsqueda de los patrones de la segunda categoría. Estos generan su forma rellenando la superficie de la figura con barras. El patrón comienza con un fuerte movimiento de precio, en este caso, con una barra larga. Para definir las barras largas, usamos el indicador ATR con un periodo amplio. La barra se considerará larga si el tamaño de su sombra supera el valor de ATR multiplicado por el coeficiente. Significa que en el indicador serán necesarios parámetros externos para el periodo ATR y el coeficiente:

input int                  ATRPeriod            =  50;
input double               K1                   =  3;

Declaramos la variable global del indicador para el manejador de ATR:

int h;

En la función OnInit() cargamos el indicador ATR y obtenemos el valor del manejador:

h=iATR(Symbol(),Period(),ATRPeriod);
if(h==INVALID_HANDLE){
   Alert("Error load indicator");
   return(INIT_FAILED);
}

En el ciclo de indicador principal obtenemos el valor de ATR:

double atr[1];
if(CopyBuffer(h,0,rates_total-i-1,1,atr)==-1){
   return(0);
}

Usando el valor de ATR obtenido, comprobamos la magnitud de la barra. Si el tamaño de la sombra supera el valor umbral establecido por el coeficiente, entonces definiremos la dirección del supuesto movimiento del precio. La dirección se determina según el color de la barra (según los valores de los precios open y close). Si el precio close está por encima del precio open, se presupone un movimiento posterior del precio hacia arriba. Si el precio close está por debajo de open, se presupone un movimiento hacia abajo:

if(high[i]-low[i]>atr[0]*K1){    // barra larga
   if(close[i]>open[i]){         // la barra está dirigida hacia arriba
      Cur.Whait=1;
      Cur.Count=0;
      Cur.Bar=i;
   }
   else if(close[i]<open[i]){    // la barra está dirigida hacia abajo
      Cur.Whait=-1;   
      Cur.Count=0;
      Cur.Bar=i;
   }
}

En el caso de cumplirse las condiciones del tamaño de la barra y su dirección, a los campos de la estructura Cur se le asignan los valores correspondientes: en el campo Whait se indica el valor supuesto (1 — hacia arriba, -1 — hacia abajo), se resetea el campo Count. Se le asigna el valor 0, este campo se usará para calcular el número de barras del patrón. En el campo Bar se guarda el índice de la barra inicial (larga) del patrón.

Vamos a ver con más detalle la estructura Cur. En total, la estructura tiene tres campos y un método Init() para resetear rápidamente todos los campos:

struct SCurPre{
   int Whait;
   int Count;
   int Bar;
   void Init(){
      Whait=0;
      Count=0;
      Bar=0;
   }
};

Al principio de la función OnTick() se declaran dos variables estáticas de este tipo y una variable del tipo datetime:

static datetime LastTime=0;   
static SCurPre Cur;          
static SCurPre Pre;

A continuación, se calcula el índice de la primera barra desde la que comienza el cálculo del indicador y se ejecuta la inicialización de las variables Cur y Pre:

int start=0;

if(prev_calculated==0){           // primer cálculo del indicador
   
   start=1;      
   
   Cur.Init();
   Pre.Init();             
     
   LastTime=0;      
}
else{                             // cálculo de las nuevas barras y de la barra en formación
   start=prev_calculated-1;
}

Al inicio del ciclo de indicador principal, se ejecuta el desplazamiento de los valores de las variables Cur y Pre:

if(time[i]>LastTime){       // primer cálculo de la barra
   LastTime=time[i];
   Pre=Cur;              
}
else{                      // cálculo repetido de la barra
   Cur=Pre;
}  

Este método con variables se ha analizado con detalle en el artículo "Ondas de Wolfe" (variables PreCount y СurCount). En este artículo se ha usado al crear el indicador iHorizontalFormation (variables con los prefijos Cur y Pre).

Si la variable Cur.Count no es igual a cero, esto significa que el indicador se encuentra en el modo de especificación de la condición de la definición del patrón. En este caso, además, se ejecuta el cálculo del número de barras que constituyen el patrón: aumenta la variable CurCount. La primera barra tras la barra larga se omite, y comenzando por la tercera barra, se ejecutan las comprobaciones de especificación:

if(Cur.Whait!=0){
   Cur.Count++;            // cálculo del número de barras
   if(Cur.Count>=3){
      // comprobaciones de especificación

   }
}

El principal indicador de las comprobaciones de especificación será la magnitud de superposición de las barras (fig. 16).

Fig. 16. La superposición de dos barras L se define como la diferencia entre
el precio mínimo high y el precio máximo low

La magnitud de la superposición se calcula usando dos barras como la diferencia entre el menor máximo y el mayor mínimo: 

Overlapping=MathMin(high[i],high[i-1])-MathMax(low[i],low[i-1]);

La superposición con la barra inicial no se comprueba, por eso la ejecución de las comprobaciones sobre la superposición comienza desde la tercera barra, y no desde la segunda.

La magnitud de la superposición de las barras deberá superar un cierto valor umbral. Si se establece este valor en puntos, el trabajo del indicador dependerá fuertemente del marco temporal, ya que el valor del parámetro se diferenciará mucho en diferentes marcos temporales. Para no depender del marco temporal, definiremos el valor básico según las barras comprobadas, usando la sombra máxima de las dos barras:

double PreSize=MathMax(high[i-1]-low[i-1],high[i]-low[i]);

Comprobamos la magnitud de superposición de las barras:

if(!(Overlapping>=PreSize*MinOverlapping))

Si las dos barras siguientes no están superpuestas, significa que ha finalizado una serie de barras superpuestas ubicadas de forma ininterrumpida. En este caso, comprobamos el número de barras en la serie:

if(Cur.Count-2>=MinCount){
   // comprobaciones de especificación
}
Cur.Whait=0;

Si el número de barras en la serie supera el valor de la variable MinCount, se ejecutan comprobaciones adicionales de especificación; de lo contrario, se dejará de esperar la formación del patrón con la ayuda del reseteo de la variable CurCount. En el código mostrado más arriba, al comprobar las condiciones, de la variable CurCount se resta 2: se trata de la primera barra larga y la barra final, en la que no se cumplen las condiciones de superposición.

Las variables MinOverlapping y MinCount son variables externas del indicador:

input double               MinOverlapping       =  0.4;
input int                  MinCount             =  5;

Después de cumplirse la condición sobre el número de barras superpuestas, procedemos a la comprobación de las condiciones de especificación: las formas del patrón y la inclinación. Para ello, primero se determinan los patrones de la serie descubierta de barras superpuestas:  

double AverSize,AverBias,AverSizeDif;
PatternParameters(high,low,i-1,Cur.Count-2,AverSize,AverBias,AverSizeDif);

Los parámetros se definen en la función PatternParameters() y se retornan según los enlaces en las variables AverSize, AverBias, AverSizeDif. En la variable AverSize se retorna el tamaño medio de la barra, en la variable AverBias, se retorna el desplazamiento medio del centro de las barras, y en AverSizeDif, la diferencia media de los tamaños de dos barras colindantes. Para comprender exactamente cómo se calculan estos parámetros, analizaremos con detalle la función PatternParameters():

void PatternParameters( const double & high[],
                        const double & low[],
                        int i,
                        int CurCnt,
                        double & AverSize,
                        double & AverBias,
                        double & AverSizeDif
){
            
   // tamaño medio de la barra            
   AverSize=high[i-CurCnt]-low[i-CurCnt];
   // desplazamiento medio de la barra
   AverBias=0;
   // diferencia media de los tamaños de dos barras colindantes
   AverSizeDif=0;
   
   for(int k=i-CurCnt+1;k<i;k++){      // con todas las barras de la serie, excepto la primera
      // tamaño medio
      AverSize+=high[k]-low[k];
      // desplazamiento medio
      double mc=(high[k]+low[k])/2;
      double mp=(high[k-1]+low[k-1])/2;
      AverBias+=(mc-mp);
      // diferencia media de los tamaños
      double sc=(high[k]-low[k]);
      double sp=(high[k-1]-low[k-1]);
      AverSizeDif+=(sc-sp);               
      
   }
   
   // división de las sumas por las cantidades
   AverSize/=CurCnt;
   AverBias/=(CurCnt-1);
   AverSizeDif/=(CurCnt-1); 
} 

A la función se le transmiten dos matrices: high y low, el índice de la barra en la que se termina la serie de barras superpuestas, la longitud de la serie de barras y tres variables para los valores retornados. El cálculo de los índices se ejecuta en el ciclo for, pero, puesto que los índices de AverBias y AverDiff se calculan conforme a dos barras colindantes, la primera barra de la serie se omitirá:

for(int k=i-CurCnt+1;k<i;k++)

Por eso, antes del ciclo, las variables  AverBias y AverDiff se resetean, y a la variable AverSize se le asigna un valor calculado conforme a la barra omitida en el ciclo.

En el ciclo, a la variable AverSize se le añaden los tamaños de las barras:

AverSize+=high[k]-low[k];

Para el indicador de AverBias (desplazamiento) se calculan los puntos medios de las barras y su diferencia; la diferencia obtenida se suma:

double mc=(high[k]+low[k])/2;
double mp=(high[k-1]+low[k-1])/2;
AverBias+=(mc-mp);

Para el indicador de AverSizeDif se calculan los tamaños de las dos barras colindantes y su diferencia; la diferencia obtenida se suma:

double sc=(high[k]-low[k]);
double sp=(high[k-1]-low[k-1]);
AverSizeDif+=(sc-sp);    

Después del ciclo, todas las sumas se dividen por el número de valores sumados:

AverSize/=CurCnt;
AverBias/=(CurCnt-1);
AverSizeDif/=(CurCnt-1); 

Después del cálculo de los parámetros, se comprueba la forma de los patrones. Esta comprobación no depende de la dirección del movimiento supuesto de precio. Para comprobar la forma se usan tres funciones: FormTapered() — forma decreciente (Banderín), FormHorizontal() — forma rectangular (Bandera), FormExpanding() — forma creciente (Cuña):

if(   FormTapered(AverSizeDif,AverSize) ||
      FormHorizontal(AverSizeDif,AverSize) ||
      FormExpanding(AverSizeDif,AverSize)
){ 
   // comprobación de la dirección
}

En los ajustes del indicador iHorizontalFormation era posible elegir solo una forma de las tres, aquí, las tres formas se incluyen de forma independiente. Esto está relacionado con una ejecución menos habitual de las condiciones y, propiamente, con señales comerciales más raras. Para activar/desactivar cada una de las formas se han añadido a los parámetros del indicador tres variables. Además, para cada una de las formas se ha añadido un coeficiente a la ventana de propiedades:

input bool                 FormTapered          =  true;
input double               FormTaperedK         =  0.05;
input bool                 FormRectangular      =  true;
input double               FormRectangularK     =  0.33;
input bool                 FormExpanding        =  true;
input double               FormExpandingK       =  0.05;

 Vamos a analizar las funciones de comprobación de la forma. Función FormTapered():

bool FormTapered(double AverDif, double AverSize){
   return(FormTapered && AverDif<-FormTaperedK*AverSize);
}

Si la diferencia media de los tamaños de las barras es menor al valor umbral negativo, los tamaños de las barras se reducen, lo que se corresponde con una forma acentuada del patrón:

Función FormHorizontal():

bool FormHorizontal(double AverDif, double AverSize){
   return(FormRectangular && MathAbs(AverDif)<FormRectangularK*AverSize);
}

Si el valor absoluto de la diferencia media de los tamaños de las barras es menor al valor umbral, significa que todas las barras tienen aproximadamente el mismo tamaño, lo que corresponde a la forma rectangular:

Función FormExpanding():

bool FormExpanding(double AverDif, double AverSize){
   return(FormExpanding && AverDif>FormExpandingK*AverSize);
}

En esta función, a diferencia del patrón acentuado, la diferencia media de los tamaños de las barras deberá superar el valor umbral positivo, lo que se corresponde con las barras en aumento y la forma creciente.

Si la comprobación de la forma se ha superado, se comprueba la inclinación del patrón. Esta comprobación depende de la dirección supuesta del movimiento del precio. Para la dirección ascendente se usa la función CheckInclineForBuy(), para la dirección descendente, CheckInclineForSell():

if(Cur.Whait==1){
   if(CheckInclineForBuy(AverBias/AverSize)){
      // comprobaciones adicionales para la dirección ascendente

   }
}
else if(Cur.Whait==-1){
   if(CheckInclineForSell(AverBias/AverSize)){   
      // comprobaciones adicionales para la dirección descendente

   }
}

Las variantes de comprobación de la inclinación, al igual que las variantes de comprobación de la forma, se activan por separado. Para ello, en la ventana de propiedades existen las variables correspondientes. Asimismo, para cada variante de inclinación en la ventana de propiedades existe su propio coeficiente:

input bool                 InclineAlong         =  true;
input double               InclineAlongK        =  0.1;
input bool                 InclineHorizontal    =  true;
input double               InclineHorizontalK   =  0.1;
input bool                 InclineAgainst       =  true;
input double               InclineAgainstK      =  0.1;

Función CheckInclineForBuy():

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

A la función se le transmite el valor de desplazamiento de las barras AverBias/AverSize. Si se encuentra por encima del umbral positivo, el patrón estará inclinado hacia arriba, si se encuentra por debajo del negativo, estará inclinado hacia abajo. Si el valor se encuentra dentro de los límites del valor umbral sin que se tenga en cuenta el signo, significa que el patrón se ubica horizontalmente:

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

De forma análoga para la dirección descendente:

bool CheckInclineForSell(double Val){
   return(  (InclineAlong && Val<-InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val>InclineAgainstK)
   );
}  

Solo ahora, a la dirección a favor del movimiento le corresponde la inclinación hacia abajo, y a la dirección en contra del movimiento le corresponde la inclinación hacia arriba.

Ya solo queda ejecutar la última comprobación: la dirección de la barra final. Pueden existir dos variantes de la última comprobación: la barra final está dirigida o bien en la dirección del patrón, o bien contra ella. Para activar las variantes de comprobación final, se han añadido a la ventana de propiedades los parámetros:

input bool                 EnterAlong           =  true;
input bool                 EnterAgainst         =  true;

La comprobación de la dirección ascendente se realiza de la forma que sigue:

if((EnterAlong && close[i]>open[i]) || (EnterAgainst && close[i]<open[i])){
   Label1Buffer[i]=low[i];
   Label3Buffer[i]=close[i]+(high[Cur.Bar]-low[Cur.Bar]);
}

Si se ha elegido la variante EnterAlong y la barra está dirigida hacia arriba o se ha elegido la variante EnterAgainst y la barra está dirigida hacia abajo, el indicador dibujará la flecha y el punto del objetivo. El objetivo se encuentra a una distancia igual a la barra inicial grande.

De forma análoga para la dirección descendente:

if((EnterAlong && close[i]<open[i]) || (EnterAgainst && close[i]>open[i])){
   Label2Buffer[i]=high[i];                  
   Label4Buffer[i]=close[i]-(high[Cur.Bar]-low[Cur.Bar]);
}

Una vez hecho esto, podemos considerar que el indicador está completamente listo. El indicador con la función de alerta se puede encontrar en los anexos al artículo, el nombre del archivo es iFlag. 


Indicador-probador

El método más sencillo para poner a prueba la efectividad del indicador consiste en usar un experto y el simulador de estrategias en el terminal. En el artículo "Ondas de Wolfe" se creó un sencillo experto que tras una pequeña modificación se podía usar también para poner a prueba los indicadores creados en este artículo. La indexación de los búferes de los indicadores iHorizontalFormation e iFlag se corresponde con la indexación del indicador iWolfeWaves, por eso, basta cambiar los parámetros externos del experto y llamar la función iCustom().

Existe otro método muy interesante de simulación de los indicadores, que permite valorar su efectividad sobre la marcha: se trata de un indicador probador. En el indicador adicional se modela el comercio según las flechas del indicador principal, y en el gráfico se representan las líneas de equidad y balance.

La forma más sencilla y obvia de crear un indicador-probador es usando la función iCustom(), sin embargo, este enfoque tiene sus defectos: el indicador principal, que dibuja flechas, se representa en el gráfico de precios, y el indicador-probador, que debe dibujar la línea de equidad o de balance, debe representarse en la subventana. Significa que hay que colocar en el gráfico dos indicadores y establecer para cada uno de ellos un valor idéntico de parámetros. Además, en lo sucesivo, si deseamos cambiar los parámetros, deberemos cambiarlos para ambos indicadores a la vez, y esto no siempre resulta cómodo.

Otra variante es hacer que el indicador-probador dibuje en el gráfico flechas con objetos gráficos.

La tercera variante es usar la función ChartIndicatorAdd(). Esta función permite colocar en el gráfico otro indicador de forma programática. En este caso, al cambiar cada vez los parámetros del indicador principal, deberemos encontrar en el gráfico el indicador-probador adicional, eliminarlo y colocarlo con los nuevos parámetros. Se trata de una variante asumible e incluso cómoda.

Pero también existe una cuarta variante, no menos cómoda que la tercera, pero más sencilla desde el punto de vista de la implementación. Además, es posible crear un indicador-probador universal que se pueda usar tanto con el indicador iHorizontalFormation, como con el indicador iFlag, con solo unas pequeñas modificaciones.

La modificación de iHorizontalFormation e iFlag consiste en crear una variable ID externa: 

input int                  ID             =  1;

A continuación, en la función OnInit, usando el valor de esta variable, se le da al indicador un nombre corto:

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

El nombre corto constituye el nombre de un archivo de indicador al que se le añaden el signo "-" y el valor de la variable ID. Según este nombre, el indicador-probador encontrará el indicador principal (obteniendo su manejador).

El indicador iHorizontalFormation funciona usando como base Zigzag, que puede calcularse sobre los precios high-low, y también con los precios close y según otros indicadores. Al calcular según los precios high-low aparece una flecha que ya no desaparecerá del gráfico. Es decir, al usar un indicador para comerciar, podemos hacer un seguimiento del mismo en la barra en formación. En el resto de los casos, al calcular el Zigzag según los precios close y otros indicadores, habrá que prestar atención a la aparición de la flecha en la barra formada. Eso significa que el indicador-probador notificará de alguna forma en qué barra hay que seguir la aparición de una flecha.

Los indicadores iHorizontalFormation e iFlag dibujan los puntos-objetivo que podemos usar para colocar el take-profit. Sin embargo, para el indicador iHorizontalFormation esto solo es posible en el caso de que el Zigzag se haya calculado según el precio. Esto significa que debemos indicar al indicador-probador si debe usar los puntos-objetivo o los parámetros adicionales de take-profit y stop-loss. La primera idea que surge es usar una variable global para la transmisión de los datos. Sin embargo, el terminal MetaTrader 5 tiene una peculiaridad: al cambiar los parámetros del indicador, se ejecuta la carga de un nuevo ejemplar del indicador con un manejador nuevo, pero el ejemplar anterior no se descarga de la memoria de inmediato. Por eso, si retornamos los parámetros externos del indicador, la nueva carga y el cálculo del indicador no se ejecutarán, y esto significa que tampoco se ejecutará la función OnInit(), ni se reseteará la variable prev_calculated. De esta forma, las variables globales no recibirán un nuevo valor. 

Los parámetros necesarios para el indicador-probador se transmitirán a través del búfer de indicador. Para ello, basta con un elemento del búfer. Utilizaremos el que ya tenemos: el primero y su primer elemento (el del extremo a la izquierda). En total, es necesario transmitir dos elementos. Uno de ellos determina si hay que hacer un seguimiento del indicador principal en la barra formada o se puede usar la que está en formación, y el segundo determina si se puede usar el punto-objetivo para el take-profit de la posición. A la función OnCalculate(), en caso de que prev_calculate=0, se le añade el código:

int ForTester=0;       // variable para el valor
if(!(SrcSelect==Src_HighLow)){
   // trabajo conforme a la barra formada
   ForTester+=10;
}   
if(!(SrcSelect==Src_HighLow || SrcSelect==Src_Close)){
   // se puede usar el punto-objetivo
   ForTester+=1;
}     
UpArrowBuffer[0]=ForTester;  

Transcurrido un elemento de la matriz, deberemos transmitir dos cifras que no superen 10, por eso, una de ellas se multiplica por 10, y a esta se le añade la segunda cifra. Dado que las cifras transmitidas pueden tener solo los valores 0 o 1, se podría utilizar una cifra con la base 2, pero el volumen de los datos transmitidos es poco significativo, por eso aquí no es obligatorio economizar bytes.

Debemos perfeccionar de forma similar el indicador iFlag. En la función OnInit():

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

En la función OnCalculate():

Label1Buffer[0]=11;

El indicador iFlag siempre se debe mirar en la barra formada, y siempre se puede usar un punto-objetivo. Por eso se le asigna el valor 11 sin cálculos.

Al usar una barra formada es obvio que la apertura de posiciones se realizará en la apertura de una nueva barra, pero al entrar en la barra en formación, el precio de entrada no es conocido. Por eso, el indicador iHorizontalFormation es susceptible de una mejora más: se ha añadido otro búfer que dibuja los puntos en las barras con flechas. Con este búfer se indica el nivel de apertura de una posición.

Ahora vamos a ocuparnos en exclusiva del indicador-probador. Creamos un nuevo indicador con el nombre iTester, añadimos los parámetros externos:

input int                  ID             =  1;
input double               StopLoss_K     =  1;
input bool                 FixedSLTP      =  false;
input int                  StopLoss       =  50;
input int                  TakeProfit     =  50;

Donde:

  • ID — es el identificador del indicador principal
  • StopLoss_K — es el coeficiente de cálculo del stop-loss con respecto al take-profit al usar el punto-objetivo
  • FixedSLTP — usar las variables StopLoss y TakeProfit o usar el punto-objetivo y la variable StopLoss_K.

Será muy cómodo si el indicador-probador en la esquina inferior izquierda representa no solo su nombre, sino también el nombre del indicador con cuyas flechas funciona. Pero hasta que el indicador a simular no haya sido colocado en el gráfico, se representará el nombre del propio indicador-probador. Para ello, declararemos la variable global:

string IndName;

En la función OnInit() le asignamos el nombre del indicador-probador:

IndName=MQLInfoString(MQL_PROGRAM_NAME);

La información sobre cada indicador-probador abierto de la posición se guarda en la matriz de estructuras SPos, la variable PosCnt se usa para registrar el número de posiciones abiertas:

struct SPos{
   int dir;
   double price;
   double sl;
   double tp;
   datetime time; 
};
SPos Pos[];
int PosCnt;

La transacción deberá añadirse a la matriz solo una vez, para ello, se usa la variable:

datetime LastPosTime;

Al añadir una posición a la matriz, se ejecuta la comprobación de la hora desde la variable LastPosTime, así como de la hora de la barra en la que se abre la posición. Si la posición se añade, entonces a la variable LastPosTime se le asigna una nueva hora. Si la hora de la barra es igual a la hora de LastPosTime, significa que la posición ya está abierta.

Para cerrar las posiciones, o para ser más concretos, para calcular sus beneficios, necesitaremos dos variables adicionales:

int Closed;
datetime CloseTime;

A la variable Closed se le asigna el beneficio de las posiciones cerradas en una barra, y a la variable CloseTime, la hora de esta barra. Más abajo veremos con más detalle cómo funciona esto.

Todas las variables y la función OnInit() han sido analizadas, pasemos a la función OnCalculate(). Primero se declaran varias variables auxiliares:

string name;
static int last_handle=-1;
static int shift=0;
static int use_target=0;
int handle=-1;     
int start=2;  

Describimos las variables:

  • name se usará para obtener el nombre del indicador con la función ChartIndicatorName();
  • la variable estática last_handle se usará para guardar el manejador del indicador simulado;
  • las estáticas shift y use_target se usarán para los parámetros transmitidos desde el indicador simulado;
  • handle se utilizará para obtener el manejador con la función ChartIndicatorGet();
  • start se utilizará para comenzar a calcular el indicador.

Vamos a analizar el código para buscar el indicador simulado. Primero determinamos el número de indicadores fijados al gráfico de precio:

int it=ChartIndicatorsTotal(0,0);

Pasamos por ellos en un ciclo:

for(int i=0;i<it;i++){        // según todos los indicadores del gráfico
   // obteniendo el nombre del siguiente indicador
   name=ChartIndicatorName(0,0,i);
   // búsqueda de la subcadena "-"
   int p=StringFindRev(name,"-");
   if(p!=-1){
      // subcadena encontrada, comprobando el valor del identificador
      if(StringSubstr(name,p+1,StringLen(name)-p-1)==IntegerToString(ID)){
         // el identificador coincide, obteniendo el manejador
         handle=ChartIndicatorGet(0,0,name);
      }
   }
} 

Vamos a analizar con mayor detalle el segmento de código mostrado más arriba. A la variable name se le asigna el nombre del indicador obtenido por la función ChartIndicatorName(), esta función retorna el nombre del indicador según su índice. Se comprueba que el nombre obtenido se corresponda con el identificador. Para ello, se ejecuta la búsqueda de la última entrada de la subcadena "-". Si el signo "-" ha sido localizado, se extrae la parte de la línea después del mismo. Si se corresponde con el identificador, a la variable handle se le asigna el valor del manejador obtenido con la función ChartIndicatorGet(), esta función retorna el manejador según el nombre del indicador. 

Una vez obtenido el manejador, lo comparamos con el manejador conocido anteriormente desde la variable last_handle (la variable es estática, es decir, guarda su valor después de finalizar el trabajo de la función OnCalculate()):

if(handle!=last_handle){
   if(handle==-1){                    // no hay manejador
      // estableciendo el nombre original
      IndicatorSetString(INDICATOR_SHORTNAME,IndName);
      ChartRedraw(0);
      return(0);
   }
   // comprobando si ha finalizado el cálculo del indicador simulado
   int bc=BarsCalculated(handle);
   if(bc<=0)return(0);                // si no ha finalizado, el funcionamiento de la función se interrumpe
   // copiado de datos con los parámetros de simulación
   double sh[1];
   if(CopyBuffer(handle,0,rates_total-1,1,sh)==-1){
      // en el caso de que no se copie con éxito, el funcionamiento de la función se interrumpirá hasta
      // el siguiente tick
      return(0);
   }
   // extracción de parámetros aparte
   shift=((int)sh[0])/10;              // barra formada o en formación
   use_target=((int)sh[0])%10;         // posibilidad de usar un punto-objetivo
   last_handle=handle;                 // guardar el valor del manejador
   // estableciendo el nombre del indicador
   IndicatorSetString(INDICATOR_SHORTNAME,name);
   ChartRedraw(0);
}
else if(prev_calculated!=0){
   // si no existe un nuevo manejador, se ejecuta el cálculo solo de las barras nuevas 
   // y la barra en formación
   start=prev_calculated-1;
}

Si el valor de la variable handle no es igual al valor de la variable last_handle, esto significa que se han llevado a cabo algunas modificaciones con el indicador simulador. Es posible que acabe de ser fijado al gráfico o que sus parámetros hayan sido modificados. O puede que haya sido retirado por completo del gráfico. Si el indicador ha sido retirado del gráfico, el valor de la variable handle será igual a -1, además, para el indicador-probador se establecerá el nombre por defecto y se finalizará el funcionamiento de la función OnCalculate(). Si la variable hadle tiene un valor válido del manejador, se obtienen los parámetros de simulación, es decir: la barra en formación/formada y el permiso para usar el punto-objetivo. Al ejecutar acto seguido la función OnCalculate(), las variables handle y last_handle son iguales, además, se ejecuta el cálculo habitual de la variable start, la barra inicial desde la que se realiza el cálculo del indicador.

La variable start por defecto tiene el valor 2. Si es necesario recalcular por completo el indicador (esto es imprescindible al cambiar el manejador o con un valor prev_calculated igual 0), en este caso deberemos resetear ciertas variables adicionales:

if(start==2){
   PosCnt=0;
   BalanceBuffer[1]=0;
   EquityBuffer[1]=0;
   LastPosTime=0;
   Closed=0;
   CloseTime=0;      
}

Al resetear, se ponen a cero: el número de posiciones abiertas — PosCnt, los primeros elementos de los búferes de indicador para el balance y la equidad — BalanceBuffer[1] y EquityBuffer[1], la hora de la última posición — LastPosTime, el beneficio de las posiciones cerradas en una barra — Closed y la hora de la barra de cierre — ClosedTime.

Ahora vamos a analizar el ciclo de indicador principal. Primero vamos a mostrar su código completo con comentarios, después se analizará línea por línea:

for(int i=start;i<rates_total;i++){

   // traslado de los valores de equidad y balance conocidos con anterioridad
   BalanceBuffer[i]=BalanceBuffer[i-1];
   EquityBuffer[i]=EquityBuffer[i-1];

   if(CloseTime!=time[i]){ 
      // comienzo del cálculo de una nueva barra
      Closed=0; // reseteo de la variable para el beneficio
      CloseTime=time[i];          
   }

   // obteniendo los datos del indicador simulado
   double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
   int ind=rates_total-i-1+shift;
   if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
      CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
      CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
      CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
   ){
      return(0);
   }
  
   if(shift==0){
      // si la simulación se realiza en la barra en formación, obtenemos el precio 
      // de apertura desde el búfer adicional del indicador simulado       
      if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
         return(0);
      } 
   }
   else{
      // al realizar la simulación en la barra formada, se usa el precio 
      // de apertura de la barra
      enter[0]=open[i];
   }

   // apertura de la barra
   if(buy[0]!=EMPTY_VALUE){
      AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
   }
   // flecha de la venta
   if(sell[0]!=EMPTY_VALUE){
      AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
   }

   // comprobando si las posiciones deben ser cerradas
   CheckClose(i,high,low,close,spread);

   // línea de balance
   BalanceBuffer[i]+=Closed;
   
   // línea de equidad
   EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

}

El valor del balance se obtiene a partir del balance conocido anteriormente y el beneficio de la posiciones cerradas, por eso, el valor del balance conocido con anterioridad se traslada desde el anterior elemento del búfer:

// traslado de los valores de balance y equidad conocidos con anterioridad
BalanceBuffer[i]=BalanceBuffer[i-1];

Al calcular por primera vez cada barra, se resetea la variable para el beneficio de las posiciones cerradas en esta barra:

if(CloseTime!=time[i]){ 
   Closed=0;
   CloseTime=time[i];          
}

Se copian los datos del indicador simulado:

// obteniendo los datos del indicador simulado
double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
int ind=rates_total-i-1+shift;
if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
   CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
   CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
   CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
){
   return(0);
}

En las matrices buy y sell se copian los datos de los búferes con flechas, y en las matrices  buy_target y sell_target se copian los datos sobre los puntos-objetivo. Antes de realizar el copiado, se calcula el índice de la barra ind teniendo en cuenta la variable shift.

Dependiendo del valor de shift, se copiará el búfer adicional o se usará el precio de apertura de la barra:

if(shift==0){
   // si la simulación se da sobre la barra en formación, obtenemos el precio 
   // de apertura desde el búfer adicional del indicador simulado       
   if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
      return(0);
   } 
}
else{
   // al realizar la simulación sobre la barra formada, se usará el precio 
   // de apertura de la barra
   enter[0]=open[i];
}

Si en la barra calculada se detectan flechas, se ejecutará la apertura de la posición mediante la llamada de la función AddPos():

// flecha de compra
if(buy[0]!=EMPTY_VALUE){
   AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
}
// flecha de venta
if(sell[0]!=EMPTY_VALUE){
   AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
}

En la función CheckClose() se comprueba si es necesario cerrar las posiciones. En el caso de ejecutar el cierre, el beneficio obtenido se encontrará en la variable Closed:

// comprobando si es necesario cerrar las posiciones
CheckClose(i,high,low,close,spread);

El beneficio cerrado de la variable Closed se añade al balance:

// línea de balance
BalanceBuffer[i]+=Closed;

La equidad se forma a partir del balance y el beneficio no cerrado, calculado por la función SolveEquity():

EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

Vamos a analizar las funciones AddPos(), CheckClose(), SolveEquity(), más abajo se muestra el código de cada función con comentarios detallados.  

Función AddPos():

void AddPos(int dir, double price,double target,int spread,datetime time,bool use_target){

   if(time<=LastPosTime){
      // la posición con la hora time ya ha sido añadida
      return;
   }
   
   // en la matriz no hay espacio libre
   if(PosCnt>=ArraySize(Pos)){
      // el tamaño de la matriz aumenta en un bloque de 32 elementos
      ArrayResize(Pos,ArraySize(Pos)+32);
   }
   
   // guardando la dirección de la posición
   Pos[PosCnt].dir=dir;
   // hora de apertura de la posición
   Pos[PosCnt].time=time;
   // hora de cierre de la posición
   if(dir==1){
      // para la compra, el precio ask
      Pos[PosCnt].price=price+Point()*spread;  
   }
   else{
      // para la venta, el precio bid
      Pos[PosCnt].price=price;  
   }

   // calculando el stop-loss y el take-profit
   if(use_target && !FixedSLTP){ 
      // se usa un punto-objetivo y se calcula el stop-loss
      if(dir==1){
         Pos[PosCnt].tp=target;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price-StopLoss_K*(Pos[PosCnt].tp-Pos[PosCnt].price),Digits());
      }
      else{
         Pos[PosCnt].tp=target+Point()*spread;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price+StopLoss_K*(Pos[PosCnt].price-Pos[PosCnt].tp),Digits());
      }   
   }
   else{
      // se usan las variables StopLoss y TakeProfit
      if(dir==1){
         Pos[PosCnt].tp=Pos[PosCnt].price+Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price-Point()*StopLoss;
      }
      else{
         Pos[PosCnt].tp=Pos[PosCnt].price-Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price+Point()*StopLoss;
      }     
   }
   
   PosCnt++;
   
}  

Función CheckClose():

void CheckClose(int i,const double & high[],const double & low[],const double & close[],const int & spread[]){
   for(int j=PosCnt-1;j>=0;j--){                                       // conforme a todas las posiciones
      bool closed=false;                                               // el valor false significa que la posición por ahora está abierta  
      if(Pos[j].dir==1){                                               // compra
         if(low[i]<=Pos[j].sl){                                        // el precio está por debajo o es igual al stop-loss
            // beneficio en puntos
            Closed+=(int)((Pos[j].sl-Pos[j].price)/Point());
            closed=true;                                               // la posición con el índice j está cerrada
         }
         else if(high[i]>=Pos[j].tp){                                  // se ha alcanzado el take-profit
            // beneficio en puntos
            Closed+=(int)((Pos[j].tp-Pos[j].price)/Point());    
            closed=true;                                               // la posición con el índice j está cerrada        
         }
      }
      else{ // venta
         if(high[i]+Point()*spread[i]>=Pos[j].sl){                     // se ha alcanzado el stop-loss
            // beneficio en puntos
            Closed+=(int)((Pos[j].price-Pos[j].sl)/Point());
            closed=true;                                               // la posición con el índice j está cerrada
         }
         else if(low[i]+Point()*spread[i]<=Pos[j].tp){                 // el precio está por debajo o es igual al take-profit
            // benefico en puntos
            Closed+=(int)((Pos[j].price-Pos[j].tp)/Point());              
            closed=true;                                               // la posición con el índice j está cerrada
         }         
      }
      // la posición está cerrada, hay que eliminarla de la matriz
      if(closed){ 
         int ccnt=PosCnt-j-1;
         if(ccnt>0){
            ArrayCopy(Pos,Pos,j,j+1,ccnt);
         }
         PosCnt--;
      }
   }
}

En la función CheckClose() se itera por todas las posiciones guardadas en la matriz Pos, y se comparan los valores de sus stop-loss y take-profit con los actuales high o low. Si la posición se cierra, su beneficio en puntos se añade a la variable Closed, después de ello, la posición se elimina de la matriz.

Función SolveEquity():

int SolveEquity(int i,const double & close[],const int & spread[]){
   int rv=0;                                // variable para el resultado
   for(int j=PosCnt-1;j>=0;j--){            // conforme a todas las posiciones
      if(Pos[j].dir==1){                    // compra
                                            // beneficio
         rv+=(int)((close[i]-Pos[j].price)/Point());
      }
      else{                                // venta
         // beneficio
         rv+=(int)((Pos[j].price+Point()*spread[i]-close[i])/Point());         
      }
   }
   return(rv);
}  

En la función SolveEquity() se ejecuta la iteración por todas las posiciones abiertas de la matriz Pos, y teniendo en cuenta el precio actual close, se calcula el beneficio en puntos.

Ya hemos finalizado el análisis del indicador iTester. Podrá encontrar el indicador ya listo en los anexos al artículo, con el nombre iTester.  En la fig. 17 se muestra un gráfico con los indicadores iHorizontalFormation (flechas) e iTester en la subventana. La línea verde representa la equidad, la línea roja, el balance.


Fig. 17. Indicador iTester (en la subventana), funcionando conforme al indicador iHorizontalFormation (flechas en el gráfico)


Conclusión

Los métodos de definición de patrones analizados en el artículo resuelven de forma totalmente aceptable la tarea que se les ha encomendado: en el gráfico se ven con claridad las diferentes formas detectadas por los indicadores: banderas, banderines, triángulos, cuñas. Los métodos analizados no deben ser considerados como únicos y absolutamente correctos. Es posible inventar otros métodos de detección de los mismos patrones. Por ejemplo, se puede usar la regresión lineal: realizar diversos cálculos según los precios high y low, después comparar la inclinación y la convergencia/divergencia de estas líneas. Es posible que surjan aún más ideas durante la solución de las diferentes subtareas que componen la tarea general de la búsqueda de patrones. En cualquier caso, los métodos de análisis de precio estudiados al crear los indicadores en este artículo también pueden resultar útiles para resolver otras tareas de análisis técnico. 


Archivos adjuntos

Todos los indicadores creados en el artículo se encuentran en los anexos:

  • iHorizontalFormation
  • iFlag
  • iTester