Comparar, ordenar y buscar en arrays
La API de MQL5 contiene varias funciones que permiten comparar y ordenar arrays, así como buscar en ellos el máximo, el mínimo o cualquier valor concreto.
int ArrayCompare(const void &array1[], const void &array2[], int start1 = 0, int start2 = 0, int count = WHOLE_ARRAY)
La función devuelve el resultado de comparar dos arrays de tipos integrados o estructuras con campos de tipos integrados, excluidas las cadenas. No se admiten arrays de objetos de clase. Tampoco se pueden comparar arrays de estructuras que contengan arrays dinámicos, objetos de clase o punteros.
Por defecto, la comparación se realiza para arrays completos pero, si es necesario, puede especificar partes de arrays, para lo cual existen los parámetros start1 (posición inicial en el primer array), start2 (posición inicial en el segundo array) y count.
Los arrays pueden ser fijos o dinámicos, así como multidimensionales. Durante la comparación, los arrays multidimensionales se representan como arrays unidimensionales equivalentes (por ejemplo, para arrays bidimensionales, los elementos de la segunda fila siguen a los elementos de la primera, los elementos de la tercera fila siguen a los de la segunda, etc.). Por esta razón, los parámetros start1, start2 y count para arrays multidimensionales se especifican mediante la numeración de elementos, y no con un índice a lo largo de la primera dimensión.
Utilizando varios desplazamientos start1 y start2 puede comparar diferentes partes del mismo array.
Los arrays se comparan elemento por elemento hasta que se encuentra la primera discrepancia o se llega al final de alguno de los arrays La relación entre dos elementos (que se encuentran en las mismas posiciones en ambos arrays) depende del tipo: para los números, se utilizan los operadores '>', '<', '==', y para las cadenas, la función StringCompare. Las estructuras se comparan byte a byte, lo que equivale a ejecutar el siguiente código para cada par de elementos:
uchar bytes1[], bytes2[];
|
Basándose en la relación de los primeros elementos que difieren, se obtiene el resultado de la comparación a granel de los arrays array1 y array2. Si no se encuentran diferencias y la longitud de los arrays es igual, se considera que los arrays son iguales. Si la longitud es diferente, se considera mayor el array más largo.
La función devuelve -1 si array1 es «menor que» array2, +1 si array1 es «mayor que» array2, y 0 si son «iguales».
En caso de error, el resultado es -2.
Veamos algunos ejemplos en el script ArrayCompare.mq5.
Vamos a crear una estructura sencilla para rellenar los arrays que se van a comparar.
struct Dummy
|
Los campos de clase se rellenan con números aleatorios (cada vez que se ejecute el script, recibiremos nuevos valores).
En la función OnStart, describimos un pequeño array de estructuras y comparamos los elementos sucesivos entre sí (como si desplazáramos fragmentos vecinos de un array con la longitud de 1 elemento).
#define LIMIT 10
|
A continuación se muestran los resultados de una de las opciones del array (para facilitar el análisis, la columna con los signos «mayor que» (+1) / «menor que» (-1) se añade directamente a la derecha del contenido del array):
[x] [y] // result
|
Al comparar entre sí las dos mitades del array se obtiene -1:
// compare first and second half
|
A continuación, compararemos arrays de cadenas con datos predefinidos.
string s[] = {"abc", "456", "$"};
|
Por último, vamos a comprobar la proporción de fragmentos del array:
PRT(ArrayCompare(s0, s1, 1, 1, 1)); // second elements (with index 1) are equal
|
bool ArraySort(void &array[])
La función ordena un array numérico (incluyendo posiblemente un array multidimensional) por la primera dimensión. El orden de clasificación es ascendente. Para ordenar un array en orden descendente, aplique la función ArrayReverse al array resultante o procéselo en orden inverso.
La función no admite arrays de cadenas, estructuras o clases.
La función devuelve true si tiene éxito o false en caso de error.
Si se establece la propiedad «timeseries» (series temporales) para un array, los elementos que lo componen se indexan en orden inverso (véanse los detalles en la sección Dirección de indexación de arrays como en las series temporales), y esto tiene un efecto inverso «externo» en el orden de clasificación: cuando procese directamente un array de este tipo, obtendrá valores descendentes. A nivel físico, el array siempre se ordena de forma ascendente, y así es como se almacena.
En el script ArraySort.mq5 se genera un array bidimensional de 10 por 3 y se ordena utilizando ArraySort:
#define LIMIT 10
|
Según el registro, la primera columna está ordenada de forma ascendente (los números concretos variarán debido a la generación aleatoria):
Before sort
|
Los valores de las columnas siguientes se han movido de forma sincronizada con los valores «principales» de la primera columna. En otras palabras: se permutan todas las filas, a pesar de que sólo la primera columna es el criterio de ordenación.
Pero, ¿qué ocurre si se desea ordenar un array bidimensional por una columna distinta de la primera? Puede escribir un algoritmo especial para ello. Una de las opciones se incluye en el archivo ArraySort.mq5 como función de plantilla:
template<typename T>
|
La función dada sólo funciona con arrays dinámicos porque el tamaño de array se duplica para reunir los resultados intermedios en la segunda mitad del array y, por último, la primera mitad (original) se elimina con ArrayRemove. Por eso, el array de prueba original de la función OnStart se distribuyó a través de ArrayResize.
Le animamos a que estudie el principio de clasificación por su cuenta (o a que pase un par de páginas).
Algo similar debería implementarse para arrays con un gran número de dimensiones (por ejemplo, array[][][]).
Recordemos que en la sección anterior planteamos la cuestión de ordenar un array de estructuras por un campo arbitrario. Como sabemos, la función estándar ArraySort no es capaz de hacerlo. Intentemos idear un «rodeo». Tomemos como base la clase del archivo ArraySwapSimple.mq5 de la sección anterior. Vamos a copiarla en ArrayWorker.mq5 y a añadir el código necesario.
En el método Worker::process, proporcionaremos una llamada al método auxiliar de ordenación arrayStructSort, y el campo que se desea ordenar se especificará por número (más adelante describiremos cómo hacerlo):
...
|
Ahora queda claro por qué todos los modos anteriores (valores del parámetro mode) en el método process eran negativos: los valores cero y positivo están reservados para la clasificación y corresponden al número de «columna».
La idea de ordenar un array de estructuras está tomada de ordenar un array bidimensional. Sólo necesitamos mapear de algún modo una única estructura a un array unidimensional (que representa una fila de un array bidimensional). Para ello, en primer lugar hay que decidir de qué tipo debe ser el array.
Dado que la clase worker ya es una plantilla, añadiremos un parámetro más a la plantilla para que el tipo de array se pueda establecer de forma flexible.
template<typename T, typename R>
|
Ahora, volvamos a las asociaciones, que le permiten superponer variables de distintos tipos. Así, obtenemos la siguiente construcción complicada:
union Overlay
|
En esta unión, el tipo de la estructura se combina con un array de tipo R, y su tamaño es calculado automáticamente por el compilador basándose en la relación de los tamaños de los dos tipos, T y R.
Ahora, dentro del método arrayStructSort, podemos duplicar parcialmente el código de ordenación de arrays bidimensionales.
bool arrayStructSort(const int field)
|
En lugar de un array con las estructuras originales, preparamos el array temp[][2] de tipo R, lo ampliamos al número de registros en array, y escribimos lo siguiente en el bucle: la «visualización» del campo field requerido de la estructura en el índice 0 de cada fila, y el índice original de este elemento en el índice 1.
La «visualización» se basa en el hecho de que los campos de las estructuras suelen estar alineados de alguna manera, ya que utilizan tipos estándar. Por lo tanto, con un tipo R adecuadamente elegido, es posible proporcionar una coincidencia total o parcial de los campos en los elementos del array en la «superposición».
Por ejemplo, en la estructura estándar MqlRates, los 6 primeros campos tienen un tamaño de 8 bytes y, por lo tanto, se asignan correctamente al array double o long (son candidatos de tipo de plantilla R).
struct MqlRates
|
Con los dos últimos campos, la situación es más complicada. Si el campo spread todavía se puede alcanzar utilizando el tipo int como R, entonces el campo real_volume resulta estar en un desplazamiento que no es múltiplo de su propio tamaño (debido al tipo de campo int, es decir, 4 bytes, antes de él). Estos son problemas de un método concreto. Se puede mejorar, o inventar otro método.
Pero volvamos al algoritmo de clasificación. Una vez rellenado el array temp, se puede ordenar con la función habitual ArraySort, y luego se pueden utilizar los índices originales para formar un nuevo array con el orden de estructura correcto.
...
|
Antes de salir de la función, volvemos a utilizar ArraySwap para sustituir el contenido de un array intra-objeto array de forma eficiente en recursos por algo nuevo y ordenado, que se recibe en el array local result.
Comprobemos la clase worker en acción: en la función OnStart vamos a definir un array de estructuras MqlRates y a pedir al terminal varios miles de registros.
#define LIMIT 5000
|
La función CopyRates se describirá en una sección aparte. Por ahora, nos basta con saber que rellena el array pasado rates con cotizaciones del símbolo y el marco temporal del gráfico actual en el que se está ejecutando el script. La macro LÍMITE especifica el número de barras solicitadas: debe asegurarse de que este valor no sea superior al configurado en su terminal para el número de barras de cada ventana.
Para procesar los datos recibidos, crearemos un objeto worker con los tipos T=MqlRates y R=double:
Worker<MqlRates, double> worker(rates); |
La clasificación puede iniciarse con una instrucción de la siguiente forma:
worker.process(offsetof(MqlRates, open) / sizeof(double)); |
Aquí utilizamos el operador offsetof para obtener el desplazamiento de byte del campo open dentro de la estructura. Se divide a su vez por el tamaño double y da el número de «columna» correcto para ordenar por el precio open . Puede leer el resultado de la ordenación elemento por elemento u obtener el array completo:
Print(worker[i].open);
|
Tenga en cuenta que obtener un array mediante el método get la desplaza del array interno array al externo (pasado como argumento) con ArraySwap. Por lo tanto, después de eso, las llamadas worker.process() no tienen sentido: no hay más datos en el objeto worker.
Para simplificar el inicio de la ordenación por diferentes campos se ha implementado una función auxiliar sort:
void sort(Worker<MqlRates, double> &worker, const int offset, const string title)
|
Envía un encabezado y los elementos primero y último del array ordenado al registro. Con su ayuda, las pruebas en OnStart para tres campos tienen este aspecto:
voidOnStart() { ... Worker<MqlRates,double>worker(rates); sort(worker,offsetof(MqlRates,open) /sizeof(double),"Sorting by open price..."); sort(worker,offsetof(MqlRates,tick_volume) /sizeof(double),"Sorting by tick volume..."); sort(worker,offsetof(MqlRates,time) /sizeof(double),"Sorting by time..."); } |
Por desgracia, la función estándar print no admite la impresión de estructuras individuales, y no hay ninguna función integrada StructPrint en MQL5. Por lo tanto, tuvimos que escribirla nosotros mismos, basándonos en ArrayPrint: de hecho, basta con poner la estructura en un array de tamaño 1.
template<typename S>
|
Como resultado de la ejecución del script, podemos obtener algo como lo siguiente (dependiendo de la configuración del terminal, es decir, en qué símbolo/marco temporal se ejecuta):
Sorting by open price...
|
Aquí están los datos para EURUSD,M15.
La implementación anterior de la ordenación es potencialmente una de las más rápidas porque utiliza la función ArraySort.
Sin embargo, si las dificultades para alinear los campos de la estructura o el escepticismo ante el propio planteamiento de «mapear» la estructura en un array nos obligan a abandonar este método (y, por tanto, la función ArraySort), sigue a nuestra disposición el acreditado método del «hágalo usted mismo».
Existe un gran número de algoritmos de ordenación que son fáciles de adaptar a MQL5. Una de las opciones de clasificación rápida se presenta en el archivo QuickSortStructT.mqh adjunto al libro. Se trata de una versión QuickSortT.mqh mejorada, que utilizamos en la sección Comparación de cadenas. Tiene el método Compare de la clase de plantilla QuickSortStructT que se hace puramente virtual y debe redefinirse en la clase descendiente para devolver un análogo del operador de comparación '>' para el tipo requerido y sus campos. Para comodidad del usuario, se ha creado una macro en el archivo de encabezado:
#define SORT_STRUCT(T, A, F) \
|
Utilizándolo, para ordenar un array de estructuras por un campo determinado basta con escribir una instrucción. Por ejemplo:
MqlRates rates[];
|
Aquí el array rates de tipo MqlRates está ordenado por el precio high.
int ArrayBsearch(const type &array[], type value)
La función busca un valor dado en un array numérico. Se admiten arrays de todos los tipos numéricos integrados. El array debe estar ordenado de forma ascendente por la primera dimensión, de lo contrario el resultado será incorrecto.
La función devuelve el índice del elemento coincidente (si hay varios, entonces el índice del primero de ellos) o el índice del elemento más cercano en valor (si no hay coincidencia exacta), es decir, puede ser un elemento con un valor mayor o menor que el buscado. Si el valor deseado es menor que el primero (mínimo), se devuelve 0. Si el valor buscado es mayor que el último (máximo), se devuelve su índice.
El índice depende del sentido de la numeración de los elementos del array: directo (del principio al final) o inverso (del final al principio). Este puede identificarse y modificarse mediante las funciones descritas en la sección Dirección de indexación de arrays como en las series temporales.
Si se produce un error, se devuelve -1.
Para arrays multidimensionales, la búsqueda se limita a la primera dimensión.
En el script ArraySearch.mq5 se pueden encontrar ejemplos de uso de la función ArrayBsearch.
void OnStart()
|
Para tres arrays predefinidas (una de ellas vacía), se ejecutan las siguientes sentencias:
PRTS(ArrayBsearch(array, -1)); // 0
|
Además, en la función de ayuda populateSortedArray, el array numbers se rellena con valores aleatorios, y el array se mantiene constantemente en un estado ordenado utilizando ArrayBsearch.
void populateSortedArray(const int limit)
|
Cada nuevo valor va primero en un array de un elemento element, porque así es más fácil insertarlo en el array resultante numbers utilizando la función ArrayInsert.
ArrayBsearch le permite determinar dónde debe insertarse el nuevo valor.
El resultado de la función se muestra en el registro:
void OnStart()
|
int ArrayMaximum(const type &array[], int start = 0, int count = WHOLE_ARRAY)
int ArrayMinimum(const type &array[], int start = 0, int count = WHOLE_ARRAY)
Las funciones ArrayMaximum y ArrayMinimum buscan en un array numérico los elementos con los valores máximo y mínimo, respectivamente. El rango de índices para la búsqueda se establece mediante los parámetros start y count: con los valores predeterminados, se busca en todo el array.
La función devuelve la posición del elemento encontrado.
Si se establece la propiedad «serial» («timeseries») para un array, la indexación de los elementos en ella se realiza en orden inverso, y esto afecta al resultado de esta función (véase el ejemplo). Las funciones integradas para trabajar con la propiedad «serial» se abordan en la sección siguiente. Para más información sobre los arrays «en serie», consulte los capítulos sobre series temporales como indicadores.
En los arrays multidimensionales, la búsqueda se realiza en la primera dimensión.
Si hay varios elementos idénticos en el array con un valor máximo o mínimo, la función devolverá el índice del primero de ellos.
En el archivo ArrayMaxMin.mq5 se ofrece un ejemplo de utilización de funciones.
#define LIMIT 10
|
El script registrará algo parecido al siguiente conjunto de cadenas (debido a la generación aleatoria de datos, cada ejecución será diferente):
22242 5909 21570 5850 18026 24740 10852 2631 24549 14635
|