Intercambio de datos entre indicadores. Es fácil

Alexey Subbotin | 22 enero, 2014

Introducción

Apreciamos a los principiantes porque no están dispuestos a usar la búsqueda de información y hay muchos temas como "Preguntas frecuentes" y "Para principiantes" o "El que hace una pregunta de esta lista, arderá en el infierno". Su verdadera misión es hacer preguntas del tipo "Cómo...", "Es posible...", y a menudo obtienen respuestas del tipo "En absoluto", "Es imposible".

La eterna frase "nunca digas nunca jamás" ha motivado a científicos, ingenieros y programadores a pensar y crear cosas nuevas que ya habían sido olvidadas.


1. Definición del problema

Por ejemplo, esta es una cita de uno de los temas del foro de la Comunidad de MQL4 (traducido del ruso):

 ...hay dos indicadores (llamémosles A y B). El indicador A utiliza datos directamente del gráfico de precio, como siempre, y B utiliza datos del indicador A. Esta es la pregunta: ¿Qué debo hacer para realizar el cálculo de B dinámicamente usando los datos del indicador A (que ya está adjunto), en lugar de usar iCustom ("indicador A",...), es decir, si cambio algunos ajustes en el indicador A, debe reflejarse en los cambios de parámetros en el indicador B?

o en otras palabras:

...Supongamos que hemos adjuntado una media móvil a un gráfico. ¿Cómo obtenemos acceso directo a su buffer de datos?

y esto:

... Si llama a algún indicador usando iCustom, se recarga incluso si se ha cargado antes de la llamada. ¿Hay alguna forma de evitar esto?

La lista de preguntas de este tipo puede ampliarse, aparecen una y otra vez y no solo por parte de principiantes. Por lo general, el problema es que MetaTrader no dispone de una técnica para acceder a los datos del indicador personalizado sin utilizar iCUstom (MQL4) o iCustom + CopyBuffer (MQL5). Pero sería muy tentador para el próximo desarrollo escribir algunas funciones en código MQL que obtenga datos o calcule algo usando datos de un gráfico especificado.

No es aconsejable trabajar mediante las funciones estándar citadas anteriormente: por ejemplo, en MQL4, las copias del indicador se crean para cada llamada cuando se llama a iCustom; en MQL5 el problema se resuelve parcialmente usando controladores y ahora los cálculos se realizan una sola vez para una copia maestra. Pero aún no es una panacea: si el indicador al que nos referimos consume muchos recursos de procesador, la inanición del terminal está virtualmente garantizada durante una docena de segundos en cada reinicialización.

Recuerdo varias formas de acceso al origen que incluían las siguientes:

  • mapeado de archivos en un disco físico o memoria;
  • memoria compartida mediante DLL para intercambio de datos;
  • el uso de variables globales del terminal de cliente para el intercambio de datos y su almacenamiento.

Y algunos otros métodos que son variaciones de los descritos anteriormente, así como algunas formas exóticas como sockets, mailslots, etc. (hay un método radical que se usa a menudo en los asesores expertos: la transferencia de cálculos de los indicadores directamente al código del asesor experto, pero esto se encuentra más allá del alcance de este artículo).

En opinión del autor, hay algo que todos estos métodos tienen en común y que supone una desventaja: los datos primero se copian en alguna ubicación y luego se distribuyen a otras. En primer lugar, requiere algunos recursos de la CPU y, en segundo lugar, crear un nuevo problema con la relevancia de los datos transmitidos (no vamos a tratarlo aquí).


Vamos a intentar definir el problema.

Queremos crear un entorno que proporcione acceso a los datos de los indicadores adjuntos al gráfico y que tenga las siguientes propiedades:

  • ausencia de copiado de datos (y el problema de su relevancia);
  • modificación mínima del código de los métodos disponibles y necesitamos usarlos;
  • Se prefiere el código MQL (por supuesto, tenemos que usar las DLL pero usaremos una docena de strings de código C++).

El autor usó C++ Builder para la creación de DLL y los terminales de cliente de MetaTrader 4 y MetaTrader 5. Los códigos fuente mostrados a continuación se han escrito en MQL5 y los códigos de MQL4 se adjuntan al archivo. Se discutirán las principales diferencias entre ellos.


2. Matrices

En primer lugar, necesitamos algo de teoría ya que vamos a trabajar con buffers de indicador y tenemos que conocer cómo se almacenan sus datos en la memoria. La información no está documentada adecuadamente.

Las matrices dinámicas de MQL son estructuras de tamaño variable y por ello es interesante saber cómo MQL resuelve el problema de la reasignación de datos si el tamaño de la matriz se ha incrementado y no hay memoria libre después de la matriz. Hay dos formas de resolver esto:

  1. reasignar nuevos datos en la parte adicional de la memoria disponible (y guardar las direcciones de todas las partes para esta matriz, por ejemplo, usando una lista referenciada), o
  2. mover toda la matriz al completo a la nueva parte de la memoria, siendo suficiente para asignarla.

El primer método genera algunos problemas adicionales, ya que en este caso tenemos que investigar las estructuras de datos creadas por el compilador de MQL. La siguiente consideración da fe de la segunda variante (que es aún la más lenta): cuando se pasa una matriz dinámica a una función externa (a la DLL) la última lleva el puntero al primer elemento de los datos y se ordenan y ubican otros elementos de la matriz justo después del primer elemento de esta.

Al pasar por referencia, los datos de la matriz pueden cambiarse, y esto significa que en el primer método hay un problema para copiar toda la matriz en un área separada de la memoria para transferirla a una función externa y así añadir el resultado a la matriz fuente, es decir, realizar las mismas funciones utilizadas por el segundo método.

Aunque esta conclusión no es estrictamente lógica al 100%, puede considerarse más bien fiable (está también probado por el funcionamiento correcto de nuestro producto basado en esta idea).

Por tanto, asumiremos que las siguientes afirmaciones son correctas:

  • En cada momento, los datos de las matrices dinámicas se ubican secuencialmente en la memoria, uno detrás del otro, y la matriz puede reubicarse en otras zonas de la memoria, pero solo en su totalidad.
  • La dirección del primer elemento de la matriz se pasa a la librería DLL cuando una matriz dinámica se pasa a una función externa como parámetro por referencia.

Y a continuación asumimos lo siguiente: entendemos como momentos la llamada a la función OnCalculate() (para MQL4 - start ()) del correspondiente indicador. Además, para simplificar, asumimos que nuestros buffers de datos tienen la misma dimensión y tipo de doble [].


3. Condición necesaria

MQL no soporta punteros (excepto los llamados punteros de objeto, que son punteros en sentido estricto), como se ha afirmado y confirmado repetidamente por los representantes de MetaQuotes Software Corp. Vamos a comprobar si esto es cierto.

¿Qué es un puntero? No es un identificador con un asterisco, es la dirección de una celda en la memoria de un ordenador. ¿Y qué es la dirección de un celda? Es el número de secuencia que comienza desde un cierto inicio. Y finalmente ¿qué es un número? Es un entero que se corresponde con una celda de la memoria del ordenador. ¿Y por qué no podemos trabajar con un puntero de la misma forma que con un número? Sí, sí que podemos, ¡ya que los programas MQL funcionan perfectamente con enteros!

Pero ¿cómo convertir un puntero en un entero? La librería de enlaces dinámicos nos será de ayuda, usaremos las posibilidades de encasillado de C++. Debido al hecho de que los punteros de C++ son datos de cuatro bytes, en nuestro caso es aconsejable usar int como este tipo de dato de cuatro bytes.

El bit señal no es importante y no lo tendremos en cuenta (si es igual a 1 significa que el entero es negativo). Lo fundamental es que podemos mantener todos los bits del puntero sin cambios. Por supuesto, podemos usar punteros sin señalar, pero es mejor que los códigos de MQL5 y MQL4 sean similares porque MQL4 no proporciona enteros sin señalar.

Luego,

extern "C" __declspec(dllexport) int __stdcall GetPtr(double *a)
{
        return((int)a);
}

¡Vaya!, ¡ahora tenemos la variable de tipo largo con el valor de la dirección del inicio de la matriz! Ahora necesitamos aprender cómo leer el valor del elemento iésimo de la matriz:

extern "C" __declspec(dllexport) double __stdcall GetValue(int pointer,int i)
{
        return(((double*) pointer)[i]);
}

... y escribir el valor (no es realmente necesario en nuestro caso, aún...)

extern "C" __declspec(dllexport) void __stdcall SetValue(int pointer,int i,double value)
{
        ((double*) pointer)[i]=value;
}

Eso es todo. Ahora podemos trabajar con punteros en MQL.


4. Envolvente

Hemos creados el kernel del sistema y ahora tenemos que prepararlo para el uso de los programas MQL. No obstante, no quiero que los fanáticos de la estética se sientan molestos, habrá algo más.

Hay millones de formas de hacerlo. Elegiremos la siguiente. Recordemos que el terminal de cliente tiene una característica especial para el intercambio de datos entre distintos programas MQL, las variables globales. La forma más natural es usarlas para almacenar punteros en los buffers de indicador a los que accederemos. Vamos a considerar dichas variables como una tabla de descriptores donde cada descriptor tendrá su nombre representado por un string como este:

string_identifier#buffer_number#symbol#period#buffer_length#indexing_direction#random_number,

y su valor será igual a la representación del entero del puntero del buffer correspondiente de la memoria del ordenador.

Algunos detalles sobre los campos del descriptor.

  • string_identifier – puede usarse cualquier string (por ejemplo, el nombre el indicador, la variable short_name). Se usará para buscar el puntero necesario. Queremos decir que algún indicador registrará los descriptores con el mismo identificador y los distinguirá usando el campo:
  • buffer_number – se usará para distinguir los buffers;
  • buffer_length – lo necesitaremos para controlar los límites, ya que de lo contrario podría provocar el colapso del terminal de cliente y de Windows: pantalla azul de la muerte :);
  • symbol, period – símbolo y periodo para especificar la ventana del gráfico;
  • ordering_direction – especifica la dirección de ordenación de los elementos de la matriz: 0 – normal, 1 – inversa (AS_SERIES flag es true);
  • random_number – se usará si hay varias copias de un indicador con diferentes ventanas o con diferentes conjuntos de parámetros adjuntos al terminal de cliente (pueden establecer los mismos valores del primer y segund campo, por eso necesitamos distinguirlos de alguna forma). Puede que no sea la mejor solución, pero funciona.

En primer lugar, necesitamos algunas funciones para registrar el descriptor y borrarlo. Echemos un vistazo al primer string de la función. La llamada de la función UnregisterBuffer() es necesaria para borrar los antiguos descriptores de la lista de variables globales.

En cada nueva barra, el tamaño del buffer se incrementará en 1, por lo que tenemos que llamar a RegisterBuffer(). Y si el tamaño de buffer ha cambiado, se creará el nuevo descriptor en la tabla (la información sobre su tamaño está contenida en el nombre), y el antiguo permanecerá en la tabla. Por eso se usa.

void RegisterBuffer(double &Buffer[], string name, int mode) export
{
   UnregisterBuffer(Buffer);                    //first delete the variable just in case
   
   int direction=0;
   if(ArrayGetAsSeries(Buffer)) direction=1;    //set the right ordering_direction

   name=name+"#"+mode+"#"+Symbol()+"#"+Period()+"#"+ArraySize(Buffer)+"#"+direction;
   int ptr=GetPtr(Buffer);                      // get the buffer pointer

   if(ptr==0) return;
   
   MathSrand(ptr);                              //it's convenient to use the pointer value instead
                                                //of the current time for the random numbers base 
   while(true)
   {
      int rnd=MathRand();
      if(!GlobalVariableCheck(name+"#"+rnd))    //check for unique name - we assume that 
      {                                         //nobody will use more RAND_MAX buffers :)
         name=name+"#"+rnd;                     
         GlobalVariableSet(name,ptr);           //and write it to the global variable
         break;
      }
   }   
}
void UnregisterBuffer(double &Buffer[]) export
{
   int ptr=GetPtr(Buffer);                      //we will register by the real address of the buffer
   if(ptr==0) return;
   
   int gt=GlobalVariablesTotal();               
   int i;
   for(i=gt-1;i>=0;i--)                         //just look through all global variables
   {                                            //and delete our buffer from all places
      string name=GlobalVariableName(i);        
      if(GlobalVariableGet(name)==ptr)
         GlobalVariableDel(name);
   }      
}

Los comentarios detallados son aquí superfluos, describimos el hecho de que la primera función crea un nuevo descriptor del formato anterior en las variables globales. El segundo busca entre todas las variables globales y borra las variables y descriptores con valor igual al puntero.

Vamos a considerar ahora la segunda tarea: obtener los datos del indicador. Antes de implementar un acceso directo a los datos, necesitamos primero encontrar el descriptor correspondiente. Esto puede conseguirse utilizando la siguiente función. Su algoritmo será el siguiente: nos desplazamos por todas las variables globales y comprobamos la presencia de valores de campo especificados en el descriptor.

Si lo hemos encontrado, añadimos su nombre a la matriz pasada como último parámetro. Como resultado, la función devuelve todas las direcciones de memoria que coinciden con las condiciones de búsqueda. El valor devuelto es el número de descriptores encontrados.

int FindBuffers(string name, int mode, string symbol, int period, string &buffers[]) export
{
   int count=0;
   int i;
   bool found;
   string name_i;
   string descriptor[];
   int gt=GlobalVariablesTotal();

   StringTrimLeft(name);                                    //trim string from unnecessary spaces
   StringTrimRight(name);
   
   ArrayResize(buffers,count);                              //reset size to 0

   for(i=gt-1;i>=0;i--)
   {
      found=true;
      name_i=GlobalVariableName(i);
      
      StringExplode(name_i,"#",descriptor);                 //split string to fields
      
      if(StringFind(descriptor[0],name)<0&&name!=NULL) found=false; //check each field for the match
                                                                    //condition
      if(descriptor[1]!=mode&&mode>=0) found=false;
      if(descriptor[2]!=symbol&&symbol!=NULL) found=false;
      if(descriptor[3]!=period&&period>0) found=false;
      
      if(found)
      {
         count++;                                           //conditions met, add it to the list
         ArrayResize(buffers,count);
         buffers[count-1]=name_i;
      }
   }
   
   return(count);
}

Como vemos en el código de la función, algunas condiciones de búsqueda pueden omitirse. Por ejemplo, si pasamos NULL como nombre, la verificación de string_identifier se omitirá. Lo mismo ocurre para los demás campos:  mode<0, symbol:=NULL o period<=0. Esto permite ampliar las opciones de búsqueda en la tabla de descriptores.

Por ejemplo, podemos encontrar todos los indicadores de medias móviles en todas las ventanas de gráfico, o solo en los gráficos EURUSD con periodo M15, etc. Una nota adicional: la comprobación de string_identifier la realiza la función StringFind() en lugar de la comprobación de igualdad estricta. Esto se hace para tener la posibilidad de buscar una parte del descriptor (digamos, cuando varios indicadores establecen un string del tipo "MA(xxx)", la búsqueda puede ser realizada por un substring "MA". Como resultado encontraremos todas las medias móviles registradas).

También hemos usado la función StringExplode(string s, string separator, string &result[]). Divide el string s especificado en substrings usando el separador y escribe los resultados en la matriz obtenida.

void StringExplode(string s, string separator, string &result[])
{
   int i,pos;
   ArrayResize(result,1);
   
   pos=StringFind(s,separator); 
   if(pos<0) {result[0]=s;return;}
   
   for(i=0;;i++)
   {
      pos=StringFind(s,separator); 
      if(pos>=0)
      {
         result[i]=StringSubstr(s,0,pos);
         s=StringSubstr(s,pos+StringLen(separator));
      }
      else break;
      ArrayResize(result,ArraySize(result)+1);
   }
}

Ahora, cuando tenemos la lista de todos los descriptores necesarios, podemos obtener los datos del indicador:

double GetIndicatorValue(string descriptor, int shift) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //check that the descriptor is valid
   {                
      ptr = GlobalVariableGet(descriptor);             //get the pointer value
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //split its name to fields
         size = fields[4];                             //we need the current array size
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //if the ordering_direction is reverse
         if(shift>=0&&shift<size)                      //check for its validity - to prevent crashes
            return(GetValue(MathAbs(ptr),shift));      //ok, return the value
      }   
   } 
   return(EMPTY_VALUE);                                //overwise return empty value 
}

Como puede ver, es una envolvente de la función GetValue() de la DLL. Es necesario comprobar la validez del descriptor para los límites de la matriz y para tener en cuenta la ordenación del buffer del indicador. Si no tiene éxito la función devuelve un EMPTY_VALUE.

La modificación del buffer del indicador es similar:

bool SetIndicatorValue(string descriptor, int shift, double value) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //check for its validity
   {                
      ptr = GlobalVariableGet(descriptor);             //get descriptor value
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //split it to fields
         size = fields[4];                             //we need its size
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //the case of the inverse ordering
         if(shift>=0&&shift<size)                      //check index to prevent the crash of the client terminal
         {
            SetValue(MathAbs(ptr),shift,value);
            return(true);
         }   
      }   
   }
   return(false);
}

Si todos los valores son correctos, llama a la función SetValur() desde la DLL. El valor devuelto corresponde al resultado de la modificación: verdadero si no tiene éxito y falso en caso de error.


5. Comprobar cómo funciona

Vamos ahora a intentar comprobar su funcionamiento. Como objetivo usaremos el rango verdadero promedio (ATR) del paquete estándar y mostraremos lo que tenemos que modificar en el código para poder copiar sus valores en otra ventana de indicador. También probaremos la modificación de sus datos de buffer.

Lo primero que tenemos que hacer es añadir algunas líneas de código a la función OnCalculate():

//+------------------------------------------------------------------+

//| Average True Range                                               |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &Time[],
                const double &Open[],
                const double &High[],
                const double &Low[],
                const double &Close[],
                const long &TickVolume[],
                const long &Volume[],
                const int &Spread[])
  {
   if(prev_calculated!=rates_total)
      RegisterBuffer(ExtATRBuffer,"ATR",0);
…

Como vemos, el registro del nuevo buffer debe realizarse cada vez que tenemos nuevas barras. Esto ocurre a medida que pasa el tiempo (normalmente) o cuando se descargan datos del historial adicionales.

Además de esto, necesitamos borrar los descriptores de la tabla una vez que ha terminado la operación del indicador. Por eso es necesario añadir algo de código al controlador de evento deinit (pero recuerde que debe devolver void y tener un parámetro de entrada const int reason):

void OnDeinit(const int reason)
{
   UnregisterBuffer(ExtATRBuffer);
}

Este es nuestro indicador modificador en la ventana del gráfico del terminal de cliente (Fig. 1) y su descriptor en la lista de variables globales (Fig. 2):

 

Fig. 1. Rango verdadero promedio

Fig. 2. Descriptor creado en la lista de variables globales del terminal de cliente

Fig. 2. Descriptor creado en la lista de variables globales del terminal de cliente

El siguiente paso es obtener acceso a los datos ATR. Vamos a crear un nuevo indicador (vamos a llamarlo test) con el siguiente código:

//+------------------------------------------------------------------+
//|                                                         test.mq5 |
//|                                             Copyright 2009, alsu |
//|                                                 alsufx@gmail.com |
//+------------------------------------------------------------------+
#property copyright "2009, alsu"
#property link      "alsufx@gmail.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#include <exchng.mqh>

//---- plot ATRCopy
#property indicator_label1  "ATRCopy"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Red
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- indicator buffers
double ATRCopyBuffer[];

string atr_buffer;
string buffers[];

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+

int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,ATRCopyBuffer,INDICATOR_DATA);
//---
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& open[],
                const double& high[],
                const double& low[],
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
  {
//---
   int found=FindBuffers("ATR",0,NULL,0,buffers);   //search for descriptors in global variables
   if(found>0) atr_buffer=buffers[0];               //if we found it, save it to the atr_buffer
   else atr_buffer=NULL;
   int i;
   for(i=prev_calculated;i<rates_total;i++)
   {
      if(atr_buffer==NULL) break;
      ATRCopyBuffer[i]=GetIndicatorValue(atr_buffer,i);  //now it easy to get data
      SetIndicatorValue(atr_buffer,i,i);                 //and easy to record them
   }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Como vemos, no es difícil. Obtenemos la lista de descriptores por medio de un substring y leemos los valores del buffer de indicador usando nuestras funciones. Y, finalmente, vamos a escribir algo de basura aquí (en nuestro ejemplo, escribimos los valores que corresponden al índice del elemento de la matriz).

Ahora ejecutamos el indicador test (nuestro objetivo ATR debe estar aún adjunto al gráfico). Como vemos en la Figura 3, los valores de ATR están en la subventana inferior (en nuestro indicador test) y vemos la línea recta en su lugar. De hecho, se ha rellenado con los valores correspondientes a los índices de la matriz.

 

Fig. 3. Resultado de la operación de Test.mq5

Movimiento de muñeca, eso es todo :)


6. Compatibilidad con versiones anteriores

El autor ha intentado crear una librería, escrita en MQL5, que se ajuste al máximo a los estándares de MQL4. Por ello, es necesario realizar algunas modificaciones para su uso en MQL4.

En particular, debemos eliminar la palabra clave export que solo aparece en MQL5 y modificar el código allí donde hemos usado el encasillado indirecto (MQL4 no es tan flexible) Actualmente, el uso de funciones en indicadores es completamente idéntico y las diferencias se encuentran solo en el método de la nueva barra y en el control de la adición al historial.


Conclusión

Unas palabras sobre las ventajas.

Parece que el uso del método descrito en este artículo puede no limitarse solo a su finalidad original. Además de la construcción de indicadores en cascada de gran velocidad, la librería puede aplicarse con éxito a los asesores expertos, incluyendo los casos de pruebas del historial. Es difícil imaginar otro tipo de aplicaciones, pero creo que mis estimados usuarios estarán encantados de trabajar en esta dirección.

Creo que pueden ser necesarios los siguientes aspectos técnicos:

  • mejora de la tabla de descriptores (en particular, la indicación de las ventanas);
  • desarrollo de una tabla adicional para ordenar la creación de descriptores, que puede ser importante para la estabilidad de los sistemas de trading.

También estaré encantado de recibir cualquier otra sugerencia para mejorar la librería.

Todos los archivos necesarios se adjuntan a este artículo. Se han descrito los códigos para MQL5, MQL4 y el código fuente de la librería exchng.dll. Los archivos tienen la misma ubicación que la requerida en las carpetas del terminal de cliente.

Limitación de responsabilidad

Los materiales incluidos en este artículo describen técnicas de programación que pueden originar daños en el software de su ordenador si no se utilizan adecuadamente. El uso de los archivos adjuntos es por su cuenta y riesgo y puede provocar efectos colaterales no especificados.

Reconocimientos

El autor ha utilizado los problemas propuestos en el foro de la comunidad de MQL4 http://forum.mql4.com y las ideas de sus usuarios: igor.senych, satop, bank, StSpirit, TheXpert, jartmailru, ForexTools, marketeer, IlyaA – mis disculpas si la lista está incompleta.