Русский Português
preview
Redes neuronales en el trading: Pipeline inteligente de previsiones (Mezcla dispersa de expertos)

Redes neuronales en el trading: Pipeline inteligente de previsiones (Mezcla dispersa de expertos)

MetaTrader 5Sistemas comerciales |
27 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

Los mercados financieros son sistemas complejos, caóticos y de alta frecuencia, así que las aproximaciones burdas y los valores promedio no funcionan aquí. Cada vela, cada movimiento, es el resultado de muchos factores, desde noticias fundamentales hasta transacciones basadas en el trading impulsivo. Por eso, trabajar con series temporales de mercado requiere un enfoque especial, sensible a los microdetalles, resistente al ruido y capaz de ver la estructura que se esconde tras el caos.

El framework Time-MoE ofrece precisamente este tipo de arquitectura. No se trata simplemente de un transformador adaptado a series temporales: es un sistema holístico en el que cada paso temporal de la historia analizada se considera un token único. Dichos tokens pasan por una secuencia de transformaciones, manteniendo su individualidad y contexto temporal. Este enfoque permite que el modelo trabaje con datos de alta frecuencia e identifique patrones que resultan inaccesibles con la agregación tradicional.

La primera etapa del procesamiento es la capa de incorporación, en la que los datos sufren transformaciones no lineales. Esto ayuda a capturar relaciones complejas entre indicadores, ya sea la relación entre precio y volumen, la dirección de los indicadores o la fuerza del último movimiento. Así, la representación oculta resultante se convierte en la base para un análisis posterior.

Posteriormente, los tokens se envían a una serie de bloques del Transformer. Aquí, el modelo mira hacia atrás, formando una visión del momento actual basada en la experiencia que ha acumulado. Para lograr esto, se usa un mecanismo de atención en el que cada token se compara con los anteriores, mientras que la importancia de ciertos elementos no se determina manualmente, sino por el propio modelo durante el entrenamiento. Esto nos permite considerar tanto los impulsos a corto plazo como las tendencias a largo plazo.

Especial atención en el Time-MoE se presta a la resistencia al ruido. Para lograrlo, la arquitectura incorpora mecanismos de normalización que ayudan a suavizar los valores atípicos aleatorios y a potenciar las señales significativas. Como resultado, la atención no se concentra en anomalías individuales, sino que se distribuye de manera más uniforme y significativa, lo que resulta especialmente importante en condiciones de volatilidad y ruido en el mercado.

La diferencia clave entre el Time-MoE y los transformadores clásicos es el uso de una mezcla dispersa de expertos (Mixture-of-ExpertsMoE). En cada bloque, el enrutador selecciona solo una parte de los expertos disponibles, que son los que realmente participan en el procesamiento del token actual. Esta solución nos permite reducir drásticamente la carga computacional y escalar el modelo sin un crecimiento exponencial de los recursos. Además, el Time-MoE cuenta con un experto general que está siempre activo y garantiza que el modelo sea robusto ante rutas erróneas.

La etapa final es la previsión. Aquí, el modelo genera varias predicciones a la vez para distintos horizontes temporales. Este enfoque permite considerar simultáneamente las señales a corto plazo y las tendencias a largo plazo, proporcionando al tráder o al sistema analítico una amplia gama de escenarios. Durante el entrenamiento, el modelo aprende en todos los horizontes a la vez, lo cual lo hace más flexible y adaptable a las cambiantes condiciones del mercado.

Por lo tanto, el Time-MoE es:

  • un modelo que presta atención a los detalles y que funciona con tokens puntuales;
  • resistente al ruido y a los valores atípicos debido a la normalización de la atención;
  • una arquitectura escalable con uso disperso de expertos;
  • un mecanismo de pronóstico universal para varios horizontes temporales.

A continuación le presentamos la visualización del framework Time-MoE propuesta por el autor.

Hoy continuaremos el trabajo que comenzamos antes y nos centraremos en un elemento clave del framework Time-MoE, la mezcla dispersa de expertos (Sparse Mixture of Experts). Si en la sección anterior construimos los cimientos del modelo paso a paso, creando tokens y representaciones ocultas mediante incorporaciones SwiGLU, ahora es el momento de pasar al aspecto arquitectónico más destacado, que determina en gran medida la eficiencia y la escalabilidad de todo el sistema.

En este artículo, analizaremos con detalle cómo se organiza el trabajo de un grupo de expertos y cómo se distribuyen los cálculos. No nos limitaremos a describir el esquema teórico, sino que pasaremos a la implementación práctica del modelo de elementos dispersos MoE utilizando herramientas MQL5 y centrándonos en los aspectos prácticos.


Desarrollo arquitectónico

Antes de pasar a la implementación propiamente dicha del algoritmo de mezcla dispersa de expertos, le propongo reflexionar por un momento. Al igual que antes, mantendremos nuestro compromiso con la idea de que todos los expertos trabajen en paralelo, un enfoque que encaja perfectamente en el paradigma de la computación masiva. Por ello, transferiremos sin dudarlo la carga principal al contexto OpenCL. Sin embargo, aquí surge una pregunta importante: ¿cómo se puede organizar exactamente la activación selectiva de expertos manteniendo la eficiencia y la capacidad de aprendizaje del modelo?

Primero, debemos recordar el punto clave. En el artículo original, los autores del Time-MoE propusieron un diseño experto que difiere significativamente del clásico bloque FeedForward que nos resulta familiar de la arquitectura del Transformer. En concreto, la primera capa de los expertos se sustituye por una transformación SwiGLU. Este cambio está plenamente justificado: Como ya hemos visto, SwiGLU ofrece al modelo mayor flexibilidad en el procesamiento de características y captura mejor las dependencias no lineales. Afortunadamente, ya tenemos a mano la implementación de esta capa; en el artículo anterior, desarrollamos el componente CNeuronSwiGLUOCL, así que ahora podemos usarlo con seguridad como la primera etapa de nuestra mezcla de expertos, ajustando el número de filtros de salida según el número de submodelos paralelos.

Cada filtro tiene sus propios parámetros entrenables. En esencia, se trata de un modelo experto independiente. Si agrupamos los resultados según el número de expertos, reproduciremos la estructura que necesitamos, es decir, una entrada y muchos expertos independientes trabajando en paralelo. En la segunda etapa del procesamiento, podemos aplicar la convolución multiventana que ya conocemos de trabajos anteriores. Esta capa ayudará a consolidar la información dentro de cada experto y a prepararla para su agregación.

Hasta este punto, todo parece bastante lógico, pero hay una advertencia importante: el algoritmo descrito carece del elemento clave, la esparsidad. Sí, podemos multiplicar las salidas de todos los expertos por la máscara de activación y obtener el resultado correcto. Sin embargo, todos los expertos continúan trabajando, aunque de forma discreta, lo que anula por completo el posible aumento de la productividad. Este esquema es aceptable para modelos pequeños, pero se vuelve ineficaz al aumentar la escala: un incremento en el número de expertos, un aumento en las dimensiones y un aumento en la profundidad de la red incrementan drásticamente la carga.

Y aquí llegamos a la pregunta clave: ¿en qué punto del algoritmo y de qué manera debemos introducir la esparsidad?

A primera vista, podría parecer que la solución óptima sería deshabilitar a los expertos no usados en todos los niveles, ya que esto reduciría drásticamente la cantidad de cálculos. Sin embargo, aquí hay un problema mucho más sutil e importante. La cuestión es que la idea misma del MoE es contar con diferentes expertos capacitados en diferentes subtareas. Al mismo tiempo, cada uno desarrolla su propia especialización. Pero, ¿quién determina qué expertos intervienen en una situación específica? La respuesta es un enrutador que, basándose en los datos de entrada, selecciona un subconjunto de modelos a activar.

El problema surge cuando un enrutador se centra demasiado pronto en un pequeño grupo de expertos y comienza a usarlos en todas partes, independientemente del contexto. Un modelo de este tipo puede mostrar buenos resultados en las primeras etapas, pero perderá su capacidad de adaptación porque nunca intenta rutas alternativas. En tal caso, los expertos no utilizados simplemente no tendrán la oportunidad de demostrar su eficacia en otros escenarios. Se produce un efecto de confort local: el enrutador solo conoce a sus elegidos y, al no probar otros, se encierra en un mismo patrón. Esto conlleva una disminución de la diversidad del modelo y una degradación de su capacidad de generalización.

Por consiguiente, nuestro objetivo consistirá en entrenar al enrutador no solo para seleccionar expertos, sino también para adaptar la estrategia de selección según las características de los datos de origen. Debemos crear las condiciones necesarias para que el modelo aprenda el comportamiento de otros expertos, aunque inicialmente sean menos eficaces. Solo de esta forma podrá aprender a distribuir las tareas de forma más flexible, aumentando la precisión general y la capacidad de adaptación a los cambios del mercado.

En busca de un equilibrio entre la eficiencia computacional y la exhaustividad del entrenamiento, se decidió implementar un uso limitado de expertos únicamente en la segunda capa del bloque MoE, combinando dicha capa con el proceso de agregación de resultados. Este enfoque simplifica la arquitectura, reduce la carga computacional y, al mismo tiempo, conserva la flexibilidad necesaria para el entrenamiento del enrutador.

El punto clave de este diseño es el mecanismo de propagación del gradiente de error. Nosotros rechazamos deliberadamente la idea de entrenar directamente a expertos no utilizados; después de todo, la esencia del MoE consiste precisamente en especializar a diferentes expertos para diferentes tareas. Sin embargo, para evitar que el modelo se quede atascado en el mismo conjunto de expertos, dirigiremos el gradiente hacia el enrutador, indicándole que la elección actual puede ser ineficaz. Esto permite entrenar el propio algoritmo de enrutamiento: la próxima vez, podrá activar un conjunto diferente de expertos si el actual realiza un pronóstico erróneo.

De esta forma, incluso si solo se han activado dos expertos en un caso particular, el enrutador sabrá que podrían haberse seleccionado otros. Esto no es solo un truco técnico, sino una potente herramienta de adaptación que permite al modelo aprender gradualmente la relación entre la naturaleza de los datos de entrada y la configuración óptima de los expertos activos.



Convolución con ventanas enmascaradas

Ya hemos definido los enfoques básicos, así que ahora podemos pasar de la teoría a la práctica. En la primera fase de implementación, desarrollaremos un componente clave: un objeto MoE de segunda capa que se encargará de la activación selectiva de expertos y la agregación de sus resultados. Precisamente este se convertirá en el eslabón central del modelo: los datos de todos los expertos pasarán a través de él, pero en cada pasada específica, solo se activarán unos pocos de ellos.

Asimismo, hemos trasladado la selección de expertos activos a un módulo independiente: el enrutador. Su función consiste en analizar los datos de origen y generar una máscara de activación que determine qué expertos deben participar para cada token. Esta máscara se pasará a la entrada de este objeto junto con el tensor principal de las características analizadas, determinando la configuración de los cálculos activos en el paso actual.

Una suposición importante es que todos los expertos tendrán la misma arquitectura y retornarán resultados de una dimensionalidad fija. Al mismo tiempo, en cualquier momento dado, solo un número limitado de modelos estarán activos, generalmente mucho menos que la dimensionalidad del espacio de salida. Así, los expertos restantes permanecen en modo de suspensión, no participan en los cálculos y no consumen recursos.

Este enfoque ofrece dos ventajas fundamentales a la vez:

  1. La reducción sustancial de la carga computacional resulta fundamental al escalar modelos y aumentar el número de expertos.
  2. La mayor especialización hace posible que cada experto pueda centrarse en el estudio de su subespacio específico de problemas. Como resultado, el modelo genera conocimiento experto real, pero distribuido entre los participantes.

Hemos trasladado la mayor parte de los cálculos al contexto OpenCL, lo que nos permite maximizar la eficiencia de la computación paralela en GPU y otros dispositivos compatibles. Nos centramos en el kernel de pasada directa de la segunda capa de MoE, que implementa la activación dispersa de expertos seleccionados y la agregación de sus resultados.

En los parámetros del kernel, obtenemos los pesos de todos los expertos, los datos iniciales, la máscara de activación y varios parámetros que definen la estructura y las dimensiones de la ventana.

__kernel void FeedForwardMaskMultWinConv(__global const float *matrix_w,
                                         __global const float *matrix_i,
                                         __global const float *masks,
                                         __global float *matrix_o,
                                         const int inputs,
                                         const int window_in,
                                         const int windows_total,
                                         const int activation
                                        )
  {
   const size_t u = get_global_id(0);
   const size_t w = get_global_id(1);
   const size_t v = get_global_id(2);
   const size_t units = get_global_size(0);
   const size_t window_out = get_global_size(1);
   const size_t variables = get_global_size(2);

Cada flujo de cálculo se corresponde con un elemento específico del tensor resultante, según su posición en la secuencia, el elemento del token y la variable. Este enfoque ofrece un alto nivel de paralelismo.

Debemos destacar que partimos de una premisa: el número de expertos activos en cada pasada es significativamente menor que la dimensionalidad del token en el tensor resultante. Este es el punto clave que determina la lógica de distribución de tareas dentro del núcleo. Dividimos el espacio del problema en elementos token y realizamos un ciclo entre los expertos activos dentro del propio kernel. Este enfoque permite un uso eficiente de los recursos informáticos al tiempo que mantiene la escasa activación de expertos.

En el cuerpo del kernel, identificamos el flujo de operaciones en el espacio de tareas y determinamos el desplazamiento en los búferes de datos hacia los elementos que se van a analizar.

   const int shift_in = u * window_in * windows_total;
   const int shift_in_var =  v * units * window_in * windows_total;
   const int shift_out = (u + v * units) * window_out + w;
   const int shift_mask = (u + v * units) * windows_total;
   const int shift_weight = (v * window_out * windows_total + w) * (window_in + 1);
   const int step_weight = window_out * (window_in + 1);

A continuación, organizamos un sistema de ciclos. El proceso externo revisa a todos los expertos y comprueba su estado a través de la máscara de activación. Si un experto no está activo en una ventana determinada, su entrada se omitirá, lo cual elimina cálculos innecesarios y ahorra recursos. Para los expertos en activo, se calcula una suma ponderada basada en los datos de origen con la adición de un sesgo (desplazamiento). Los valores resultantes se acumulan y luego se pasan a través de la función de activación.

float sum = 0;
for(int w_in = 0; w_in < windows_total; w_in++)
  {
   float m = IsNaNOrInf(masks[shift_mask + w_in], 0);
   if(m < FLT_EPSILON)
      continue;
   const int shift_in_loc = shift_in + w_in * window_in;
   const int shift_weight_loc = shift_weight + w_in * step_weight;
   for(int i = 0; i < window_in; i++)
      if((shift_in_loc + i) < (inputs / variables))
         sum += IsNaNOrInf(matrix_i[shift_in_var + shift_in_loc + i], 0) * 
                matrix_w[shift_weight_loc + i] * m;
   sum += matrix_w[shift_weight_loc + window_in] * m;
 }

Tenga en cuenta que, durante el proceso de agregación, no nos limitaremos a sumar las salidas de los expertos activos, sino que multiplicaremos cada una de ellas por el valor de máscara correspondiente. En el caso del enmascaramiento binario, donde la máscara contiene solo ceros y unos, esto no agrega ningún valor. Sin embargo, este enfoque nos deja con una opción importante: la organización de la agregación ponderada. Si la máscara contiene no solo indicadores, sino también coeficientes de ponderación reales, podremos controlar la contribución de cada experto al resultado, incluso hasta el punto de la selección flexible y el enrutamiento probabilístico.

Los valores resultantes se pasarán a través de la función de activación y se almacenarán en el búfer de resultados.

 matrix_o[shift_out] = Activation(sum, activation);
}

Tras implementar la pasada directa, en la que los expertos activos procesan sus subespacios de características y los resultados se ponderan mediante una máscara, pasaremos a la segunda fase importante: la propagación inversa del gradiente de error. En esta etapa, deberemos transmitir cuidadosamente el error no solo a los datos de entrada de cada modelo activo, sino también a la propia máscara de activación, para que pueda corregirse durante el proceso de entrenamiento.

Para solucionar este problema, hemos desarrollado el kernel de OpenCL CalcHiddenGradientMaskMultWinConv, que gestiona todo esto dentro del marco de la computación paralela.

__kernel void CalcHiddenGradientMaskMultWinConv(__global const float *matrix_w,
                                                __global const float *matrix_i,
                                                __global float *matrix_ig,
                                                __global const float *matrix_og,
                                                __global const float *masks,
                                                __global float *masks_g,
                                                const int outputs,
                                                const int window_in,
                                                const int window_out,
                                                const int activation
                                               )
  {
   const size_t u = get_global_id(0);
   const size_t w_in = get_global_id(1);
   const size_t v = get_global_id(2);
   const size_t units = get_global_size(0);
   const size_t windows_total = get_global_size(1);
   const size_t variables = get_global_size(2);

El kernel admite pesos, datos de entrada, gradientes de error a nivel de salida, una máscara y búferes para almacenar los gradientes de los datos de entrada y la máscara. Cada flujo de operaciones en el contexto de OpenCL es responsable de una combinación separada de token, experto y variable. Y en el cuerpo del kernel identificamos inmediatamente el flujo de operaciones a través de todas las dimensiones del espacio de tareas. Después de esto, determinaremos los desplazamientos en los búferes de datos.

const int shift_in = (u + v * units) * window_in * windows_total + w_in * window_in;
const int shift_out = u * window_out;
const int shift_out_var = v * units * window_out;
const int shift_mask = (u + v * units) * windows_total + w_in;
const int shift_weight = (v * window_out * windows_total + w_in * window_out) * (window_in + 1);

En la primera etapa, distribuiremos el gradiente de error al nivel de los datos de origen. Aquí, el trabajo comienza con una verificación de máscara: si el experto correspondiente a un token dado estaba inactivo, entonces simplemente se escribirán valores cero en el búfer de gradiente de los datos de origen y no se desperdiciarán recursos computacionales. Si un experto ha participado en el cálculo, comenzará la propagación del gradiente.

const float m = IsNaNOrInf(masks[shift_mask], 0);
for(int i = 0; i < window_in; i++)
  {
   float sum = 0;
   if(m >= FLT_EPSILON)
     {
      for(int out = 0; out < window_out; out++)
        {
         if((shift_out + out) >= (outputs / variables))
            continue;
         sum += IsNaNOrInf(matrix_og[shift_out_var + shift_out + out] *
                           matrix_w[shift_weight + out * (window_in + 1) + i] *
                           m, 0);
         sum += IsNaNOrInf(matrix_w[shift_weight + out * (window_in + 1) + window_in] *
                           m, 0);
        }
     }
   matrix_ig[shift_in + i] = Deactivation(sum, matrix_i[shift_in + i], activation);
  }

En primer lugar, el kernel itera sobre todos los canales de salida, calculando la contribución de cada elemento de los datos de origen al error final. Los valores obtenidos se corregirán mediante la derivada de la función de activación de la capa de datos de origen y se almacenarán en el elemento correspondiente del búfer de datos global.

Luego se realizará la segunda etapa: la distribución del gradiente de error sobre la máscara. Aquí, agregaremos la contribución de todos los canales al nivel de los resultados de un experto determinado, considerando la influencia del experto actual en el resultado, y generaremos una señal de retroalimentación que indica cuán útil fue la elección de este experto.

 float sum = 0;
 for(int out = 0; out < window_out; out++)
   {
    int shift_weight_loc = out * (window_in + 1) + shift_weight;
    float temp = matrix_w[shift_weight_loc + window_in];
    for(int i = 0; i < window_in; i++)
       temp += IsNaNOrInf(matrix_i[shift_in + i], 0) * matrix_w[shift_weight_loc + i];
    sum += IsNaNOrInf(temp * matrix_og[shift_out_var + shift_out + out], 0);
   }
 masks_g[shift_mask] = IsNaNOrInf(sum, 0);
}

Aunque la máscara sea binaria, este valor ayudará al enrutador a tomar decisiones más inteligentes en el futuro y podrá interpretarse como una puntuación ponderada para la activación ponderada si fuera necesario.

Gracias a este mecanismo, lograremos una arquitectura verdaderamente adaptable, en la que cada decisión de enrutamiento se ajustará mediante gradientes y los expertos inactivos podrán activarse cuando sea necesario. Esto permitirá que un selecto grupo de expertos trabaje de manera eficiente, flexible y verdaderamente inteligente.

El siguiente paso crucial es la actualización de los parámetros del modelo. Aquí es donde el modelo aprende ajustando sus pesos según la señal de error que recibe. Para implementar este proceso, utilizaremos un kernel OpenCL independiente, UpdateWeightsMaskMultWinConvAdam, adaptado a las particularidades de una mezcla dispersa de expertos y que admite la optimización mediante el método Adam.

Al igual que antes, transferiremos la mayor parte de los cálculos al contexto OpenCL, ya que cada peso involucrado en el entrenamiento se puede procesar de forma independiente.

__kernel void UpdateWeightsMaskMultWinConvAdam(__global float *matrix_w,
                                               __global const float *matrix_og,
                                               __global const float *matrix_i,
                                               __global const float *masks,
                                               __global float *matrix_m,
                                               __global float *matrix_v,
                                               const int windows_total,
                                               const int inputs,
                                               const int outputs,
                                               const float l,
                                               const float b1,
                                               const float b2
                                              )
  {
   const size_t id_in = get_global_id(0);
   const size_t id_out = get_global_id(1);
   const size_t id_v = get_global_id(2);
   const size_t window_in = get_global_size(0) / windows_total - 1;
   const size_t window_out = get_global_size(1);
   const size_t variables = get_global_size(2);

El funcionamiento del kernel se organiza según tres coordenadas: id_in se encarga de la dimensionalidad de entrada y la posición en la ventana, id_out se encarga del índice del filtro de salida e id_v se encarga de la variable actual en el paquete. Esta organización tridimensional abarca por completo todos los parámetros de los expertos y permite procesarlos en paralelo.

Es importante señalar que usamos deliberadamente una suposición estructural: todos los expertos trabajan con las mismas ventanas de análisis y los tensores resultantes tienen la misma forma. Y esto significa que todo el sistema se puede reducir a una matriz bidimensional: en un lado está el número de filtros y en el otro, el número total de elementos en las ventanas de análisis, combinados a partir de las aportaciones de todos los expertos. Dicha simplificación permite un direccionamiento eficiente y compacto de los pesos en la memoria, lo cual resulta especialmente importante al actualizar parámetros de forma masiva.

Dentro del kernel, primero identificamos cada flujo de operaciones en todas las dimensiones del espacio de tareas. Luego determinamos el desplazamiento en todos los búferes de datos con respecto a los elementos que se están analizando.

const int w_id = id_in / (window_in + 1);
const int shift_in = id_in - w_id;
const int step_in = window_in * windows_total;
const int units = outputs / window_out;
const int shift_in_var = id_v * inputs;
const int shift_out_var = id_v * outputs;
const int shift_mask_var = id_v * units * windows_total;
const int shift_weight = ((id_v * windows_total + w_id) * window_out + id_out) *
                         (window_in + 1) + id_in % (window_in + 1);
const bool bias = (id_in % (window_in + 1) == window_in);

A continuación, repasamos secuencialmente todos los tokens (unidades) con los que han trabajado los expertos. Para cada uno de estos fragmentos, comprobamos si el experto correspondiente estaba activo con la ayuda de una máscara.

float grad = 0;
for(int u = 0; u < units; u++)
  {
   const int shift_in_loc = shift_in + u * step_in;
   if(shift_in < inputs)
      continue;
   float m = IsNaNOrInf(masks[shift_mask_var + u * windows_total + w_id], 0);
   if(m < FLT_EPSILON)
      continue;
   float inp = (bias ? 1 : IsNaNOrInf(matrix_i[shift_in_var + shift_in_loc], 0));
   grad += IsNaNOrInf(inp * m * matrix_og[shift_out_var + u * window_out + id_out], 0);
  }

Si el experto estaba en modo de suspensión en un paso determinado (la máscara está cerca de cero), entonces no gastaremos recursos en calcular el gradiente de error y pasaremos al siguiente token. Esta es una de las propiedades clave del aprendizaje disperso.

Para los expertos activos, calcularemos el gradiente de error: los datos de origen se multiplicarán por el valor de error en el nivel del tensor de resultado (matrix_og) y la máscara.

float mt = IsNaNOrInf(clamp(b1 * matrix_m[shift_weight] + (1 - b1) * grad, -1.0e5f, 1.0e5f), 0);
float vt = IsNaNOrInf(clamp(b2 * matrix_v[shift_weight] + (1 - b2) * pow(grad, 2), 1.0e-6f, 1.0e6f), 1.0e-6f);
float weight = clamp(matrix_w[shift_weight] + IsNaNOrInf(l * mt / sqrt(vt), 0), -MAX_WEIGHT, MAX_WEIGHT);

A continuación, aplicamos el paso de optimización según el algoritmo Adam: actualizamos los momentos de primer (mt) y segundo (vt) orden, normalizamos el gradiente y ajustamos el valor del peso. Todas las actualizaciones pasan por una función de limitación que restringe los valores de peso a un rango aceptable.

Los valores obtenidos se guardan en búferes globales.

 matrix_w[shift_weight] = weight;
 matrix_m[shift_weight] = mt;
 matrix_v[shift_weight] = vt;
}

De este modo, cada peso se actualiza estrictamente según su contribución al resultado final, y solo si ha intervenido en los cálculos reales. Esta organización garantiza una alta eficiencia computacional y permite formar expertos altamente especializados que no solo se duermen en momentos inoportunos, sino que también aprenden activamente cuando sus conocimientos son realmente necesarios.

Ahora que la lógica computacional del lado OpenCL está completamente implementada, pasaremos al siguiente paso: organizar todo el proceso dentro del programa principal. Aquí es donde se crea y configura el objeto responsable de llamar a los kernels correspondientes y trabajar con las máscaras. Esta función la asume la clase especializada CNeuronMaskMultiWinConv, que hereda de CNeuronConvOCL. Más abajo resumimos la estructura del nuevo objeto:

class CNeuronMaskMultiWinConv    :  public CNeuronConvOCL
  {
protected:
   uint              iWindowsTotal;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *second)override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
                       CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override;

public:
                     CNeuronMaskMultiWinConv(void) {};
                    ~CNeuronMaskMultiWinConv(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window,
                          uint windows_total, uint window_out, uint units_count, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronMaskMultiWinConv;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
  };

Este se convierte en el vínculo entre el modelo y el circuito computacional de bajo nivel. Su tarea consiste en preparar correctamente los datos, pasarlos al contexto OpenCL, ejecutar el kernel necesario y obtener los resultados.

En la estructura del objeto CNeuronMaskMultiWinConv, evitamos deliberadamente declarar nuevos componentes internos. Todo lo que necesitamos ya lo proporciona la clase padre CNeuronConvOCL, y esto es más que suficiente para organizar los cálculos en el lado de OpenCL. Esta solución nos permite mantener la arquitectura limpia y libre de redundancias, además de lograr centralizar la gestión de todos los recursos.

La inicialización de la capa se realiza en el método Init redefinido, donde ajustamos únicamente los parámetros clave sin afectar la lógica general.

bool CNeuronMaskMultiWinConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window,
                                   uint windows_total, uint window_out, uint units_count, uint variables,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   uint win = window * windows_total + MathMax(windows_total, 1) - 1;
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, win, win, window_out, units_count, variables, optimization_type, batch))
      return false;
   iWindowsTotal = windows_total;
   iWindow = window;
//---
   return true;
  }

En concreto, estableceremos el valor iWindowsTotal, que determina el número de expertos que se ejecutan en paralelo. Este valor se utilizará al generar desplazamientos y calcular direcciones dentro de los kernels.

El principal énfasis en el método de inicialización radica en el cálculo correcto de la anchura de la ventana que se está procesando. Dado que cada experto trabaja con su propia ventana de longitud fija, las combinaremos en un único espacio de entrada. Como resultado, se formará el tamaño de la ventana total analizada de los datos de origen. Este cálculo garantiza que, incluso en casos límite, el tamaño de la ventana no se reducirá a cero.

A continuación, delegaremos la ejecución al método homónimo de la clase padre, transmitiéndole los parámetros actualizados, y finalizaremos el método.

Los métodos de pasada directa e inversa de esta capa no implementan lógica de cálculo independiente, sino que solo dan servicio a los kernels OpenCL descritos anteriormente. Cada uno de estos métodos se encarga de preparar los argumentos, transmitir los parámetros y colocar el kernel correspondiente en la cola de ejecución.

El algoritmo operativo en este caso es el típico: transmitimos los punteros a los búferes de datos, incluyendo las máscaras y otros parámetros, tras lo cual llamamos a la función OpenCL con la dimensionalidad del espacio de trabajo requerida. Creo que este procedimiento ya le resulta familiar. Por consiguiente, no será necesario un análisis exhaustivo de cada método en este caso: le sugerimos dejar los métodos indicados para un estudio independiente. El código completo de la clase CNeuronMaskMultiWinConv y todos sus métodos se ofrece en el archivo adjunto.



Mezcla dispersa de expertos

La siguiente etapa importante de nuestro trabajo es la construcción de un módulo de mezcla dispersa de expertos. En nuestra implementación, su arquitectura está representada por la clase CNeuronTimeMoESparseExperts, que hereda de la clase básica CNeuronBaseOCL. Dentro del objeto se concentran todos los componentes clave necesarios para el funcionamiento y la coordinación del trabajo de expertos especializados y generales.

class CNeuronTimeMoESparseExperts   :  public CNeuronBaseOCL
  {
protected:
   CNeuronSwiGLUOCL        cExpertsIn;
   CNeuronSwiGLUOCL        cSharedIn;
   CNeuronMaskMultiWinConv cExpertsOut;
   CNeuronConvOCL          cSharedOut;
   CNeuronTopKGates        cMasks;
   CNeuronConvOCL          cSharedGates;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL)          override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL)   override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL)   override;

public:
                     CNeuronTimeMoESparseExperts(void) {};
                    ~CNeuronTimeMoESparseExperts(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window,
                          uint window_out, uint units_count, uint variables, uint experts,
                          uint topK, ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const {   return defNeuronTimeMoESparseExperts; }
   //--- methods for working with files
   virtual bool      Save(int const file_handle)   override;
   virtual bool      Load(int const file_handle)   override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj)     override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      TrainMode(bool flag) override;
  };

En la estructura de la clase CNeuronTimeMoESparseExperts, podemos observar varios objetos internos, cada uno de los cuales realiza una función estrictamente definida dentro del marco del mecanismo de mezcla de expertos dispersa. Nos familiarizaremos gradualmente con su lógica interna a medida que construyamos los algoritmos que describen cómo funcionan los métodos. En esta etapa, debemos destacar la siguiente decisión arquitectónica: todos los objetos internos se declaran de forma estática, sin asignación dinámica de memoria. Este enfoque no solo simplifica la gestión de recursos, sino que también permite dejar vacíos el constructor y el destructor de la clase. Todos los procesos de inicialización se implementarán en el método Init.

bool CNeuronTimeMoESparseExperts::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window,
                                       uint window_out, uint units_count, uint variables, uint experts,
                                       uint topK, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables,
                                                                 optimization_type, batch))
      return false;
   SetActivationFunction(None);

En los parámetros del método, recibimos un conjunto de constantes clave que nos permiten definir de forma inequívoca la arquitectura del objeto creado. Aquí se proporciona toda la información necesaria: el tamaño de los tokens de entrada y salida, el número total de expertos y el número seleccionado por la máscara Top-K, el tipo de optimización usada, el número de variables y unidades de cálculo.

Algunos de estos parámetros se transmiten posteriormente al método homónimo de la clase padre. Es allí donde ya está organizado el control básico de la corrección de la configuración arquitectónica y la inicialización de las interfaces globales, incluido el contexto OpenCL. De esta forma, construiremos una estructura jerárquicamente transparente, donde cada nivel es responsable de su propio ámbito de responsabilidad y todos los elementos arquitectónicos están conectados en un todo único y coherente.

Una vez completada con éxito la inicialización a nivel de la clase padre, pasaremos a configurar los componentes internos; aquí es donde comienza a formarse la estructura de la propia mezcla de expertos.

El primer objeto de esta cadena es cExpertsIn, que implementa la funcionalidad SwiGLU. Este cumple la función de primer nivel de procesamiento de los datos iniciales: una especie de filtro preliminar que amplifica las señales útiles y genera la información que se envía a cada experto. El número de filtros en esta capa aumenta proporcionalmente al número de expertos, lo cual permite a cada uno de ellos obtener una representación suficientemente expresiva de la tarea.

int index = 0;
if(!cExpertsIn.Init(0, index, OpenCL, window, window, window_out * experts, units_count,
                                                       variables, optimization, iBatch))
   return false;
index++;
if(!cSharedIn.Init(0, index, OpenCL, window, window, window_out, units_count, variables,
                                                                  optimization, iBatch))
   return false;

De manera similar, inicializamos el componente cSharedIn, que representa la primera capa de procesamiento para el experto compartido. A diferencia de los expertos especializados, aquí no es necesario ajustar el tamaño del filtro; en su lugar, estará fijado a un nivel equivalente al de un experto individual. Esto se debe a que el experto general abarca todo el ámbito del problema, y su propósito no es competir con los demás, sino ofrecer un punto de vista universal, una especie de base que se usará para la toma de decisiones.

El uso de SwiGLU en esta función posibilita la selectividad no lineal, lo que mejora la distinción entre los tokens que son potencialmente relevantes para un experto en particular.

A continuación, formaremos la segunda capa de procesamiento, tanto para expertos especializados como para expertos generales. En este caso, la arquitectura sigue estrictamente la lógica previamente establecida: cada bloque desempeña su función específica dentro del mecanismo general de una mezcla dispersa.

Para la mezcla especializada de expertos, usaremos el módulo CNeuronMaskMultiWinConv creado previamente, que implementa la convolución con ventanas enmascaradas. Este componente permite filtrar las características analizadas considerando la identidad individual de cada experto, lo que posibilita una activación precisa y una delimitación estricta de las responsabilidades entre los expertos.

index++;
if(!cExpertsOut.Init(0, index, OpenCL, window_out, experts, window, units_count,
                                               variables, optimization, iBatch))
   return false;
cExpertsOut.SetActivationFunction(None);
index++;
if(!cSharedOut.Init(0, index, OpenCL, window_out, window_out, window, units_count,

                                                 variables, optimization, iBatch))
   return false;
cSharedOut.SetActivationFunction(None);

Para el experto general, a su vez, usaremos la capa convolucional estándar CNeuronConvOCL.

Como objeto para crear una máscara de expertos activos, utilizaremos el módulo CNeuronTopKGates, que ya nos resulta familiar del framework DUET. Este componente desempeña un papel clave en el mecanismo de rarefacción: analiza los tokens de origen y selecciona un número estrictamente limitado de los expertos más relevantes.

En otras palabras, CNeuronTopKGates genera una máscara de activación basada en el principio Top-K, restableciendo rígidamente los canales irrelevantes. Esta solución nos permite reducir sustancialmente la carga computacional manteniendo al mismo tiempo una alta expresividad del modelo. El número de expertos activos (topK) se especifica mediante un parámetro y se puede adaptar fácilmente a las características específicas de una tarea en particular.

index++;
if(!cMasks.Init(0, index, OpenCL, window, units_count, experts, topK, optimization, iBatch))
   return false;

Debemos comprender que el objeto CNeuronTopKGates no devuelve simplemente una máscara binaria. Asimismo, genera una distribución de probabilidad normalizada sobre los canales seleccionados. Esto permite una regulación flexible de la contribución de cada experto al resultado final. En los cálculos participan expertos con distintos grados de confianza, y esta probabilidad se refleja claramente en la ponderación de la máscara final.

Gracias a este enfoque, la arquitectura logra no solo compacidad y eficiencia computacional, sino también resistencia al sobreajuste. Cada experto se asigna estrictamente a la tarea en cuestión, no al azar, lo que resulta especialmente importante al trabajar con series temporales ruidosas o multimodales.

Y la estructura del objeto se completa usando el módulo de puertas de experto general: cSharedGates. Su tarea consiste en determinar el grado de participación del experto general en la formulación de la respuesta final del modelo. Para ello, utilizaremos una capa convolucional clásica ajustada para extraer patrones espaciotemporales. A diferencia de la máscara para expertos individuales, aquí se usa una función de activación sigmoide, que permite valores suaves en el rango de 0 a 1.

   index++;
   if(!cSharedGates.Init(0, index, OpenCL, window, window, window, units_count, variables,
                                                                    optimization, iBatch))
      return false;
   cSharedGates.SetActivationFunction(SIGMOID);
//---
   return true;
  }

Este enfoque no ofrece una decisión binaria de activación/desactivación, sino una escala flexible de la contribución de un experto común, dependiendo del contexto actual. La función sigmoide actúa como un amortiguador flexible: si la confianza en el patrón general es alta, el valor tenderá a uno; si la señal es débil, la máscara correspondiente suprimirá la salida. Todo esto nos permite regular con precisión la interacción entre los componentes localizados y generalizados de la arquitectura.

Una vez que todas las iteraciones se hayan completado con éxito, retornaremos su resultado lógico al programa que ha realizado la llamada y finalizaremos el método.

Ahora pasamos al siguiente paso clave: organizar una pasada directa en el método feedForward. Aquí es donde la lógica del módulo de mezcla de expertos dispersa cobra verdadera importancia.

bool CNeuronTimeMoESparseExperts::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cExpertsIn.FeedForward(NeuronOCL))
      return false;

En primer lugar, realizamos una pasada directa a través de la capa cExpertsIn, que transforma la señal analizada en el espacio de expertos, escalándola según el número de rutas de procesamiento especializadas. Paralelamente, iniciamos el trabajo de la capa cSharedIn, que representa el primer nivel de procesamiento de un experto general más universal.

if(!cSharedIn.FeedForward(NeuronOCL))
   return false;

A continuación, se activan dos ramas de enrutamiento. La capa cSharedGates calcula la máscara de participación del experto compartido: usa una función de activación sigmoide que crea una máscara de significancia basada en gradientes. A su vez, el objeto cMasks es un módulo especializado que selecciona a los expertos más relevantes, formando una máscara Top-K discreta. No solo elimina los canales inactivos, sino que también retorna una distribución normalizada de los pesos a lo largo de las rutas seleccionadas.

if(!cSharedGates.FeedForward(NeuronOCL))
   return false;
if(!cMasks.FeedForward(NeuronOCL))
   return false;

En esta etapa comienza la formación de los resultados. El módulo cExpertsOut se utiliza para iniciar la convolución de un bloque experto enmascarado: cada filtro activo tiene su propio peso y todo el procesamiento se organiza teniendo en cuenta una máscara preparada previamente.

if(!cExpertsOut.FeedForward(cExpertsIn.AsObject(), cMasks.getOutput()))
   return false;

Para el experto general, se usa una convolución regular a través de cSharedOut, después de lo cual su salida se escala adicionalmente mediante la máscara de significancia calculada previamente por cSharedGates. El escalado se realiza mediante la multiplicación elemento a elemento.

if(!cSharedOut.FeedForward(cSharedIn.AsObject()))
   return false;
if(!ElementMult(cSharedOut.getOutput(), cSharedGates.getOutput(), cSharedOut.getPrevOutput()))
   return false;

Tras obtener los resultados de la mezcla de expertos dispersa y del resultado global, ambos ajustados mediante la máscara de significancia, se combinan los dos flujos de información. En esta etapa, los datos se resumen y se escriben en el búfer de datos intermedio.

   const int window = (int)cSharedGates.GetWindow();
   if(!SumAndNormilize(cExpertsOut.getOutput(), cSharedOut.getPrevOutput(), PrevOutput,
                       window, false, 0, 0, 0, 1))
      return false;
   if(!SumAndNormilize(NeuronOCL.getOutput(), PrevOutput, Output,
                       window, true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

No obstante, esta es solo la primera parte de la etapa final de la pasada directa. A continuación, se suman las conexiones residuales a los valores obtenidos: las salidas guardadas del nivel anterior de la arquitectura neuronal. Esta técnica nos permite preservar información importante de la señal original y mejorar la convergencia del modelo usando la estabilización de los gradientes.

Tras combinar todos los componentes, los resultados se normalizan dentro de cada token (a lo largo de la ventana de análisis). Esta operación estandariza los datos, garantizando una interpretación correcta del tensor de salida. El resultado se almacena en el búfer de la interfaz global de resultados.

Como podemos observar, el algoritmo de pasada directa de nuestro módulo tiene una estructura muy ramificada. Los datos iniciales se reciben simultáneamente en cinco flujos de información diferentes, lo cual aumenta significativamente la flexibilidad y la adaptabilidad del modelo. Sin embargo, dicha arquitectura multicanal impone ciertas complejidades en la etapa de propagación inversa.

El método calcInputGradients distribuye cuidadosamente los gradientes de error entre todos los componentes internos según su contribución al resultado final. Es como la fabulosa obra de un director de orquesta, donde cada instrumento suena en perfecta armonía con la orquesta.

bool CNeuronTimeMoESparseExperts::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

En primer lugar, se activan los procedimientos de distribución del gradiente de error para cada flujo de información individual:

  • para la salida de la capa de mezcla de expertos dispersa (cExpertsOut), donde se realiza la desactivación del gradiente teniendo en cuenta la función de activación;
  • para el experto general y las puertas (cSharedOut y cSharedGates), donde se consideran la interacción de los valores de salida y las máscaras, se aplican productos de gradientes teniendo en cuenta las funciones de activación.
if(!DeActivation(cExpertsOut.getOutput(), cExpertsOut.getGradient(), Gradient,
                                                    cExpertsOut.Activation()))
   return false;
if(!ElementMultGrad(cSharedOut.getOutput(), cSharedOut.getGradient(),
                    cSharedGates.getOutput(), cSharedGates.getGradient(),
                    Gradient, cSharedOut.Activation(), cSharedGates.Activation()))
   return false;

A continuación, se producirá una llamada recursiva al cálculo de gradientes ocultos en los objetos internos; esto permitirá desplegar paso a paso la contribución de cada componente, comenzando desde las primeras capas de expertos (cExpertsIn, cSharedIn) y descendiendo hasta el nivel de los datos de origen, lo que garantizará un cálculo profundo y correcto.

   if(!cExpertsIn.calcHiddenGradients(cExpertsOut.AsObject(), cMasks.getOutput(),
                                      cMasks.getGradient(), (ENUM_ACTIVATION)cMasks.Activation()))
      return false;
   if(!cSharedIn.calcHiddenGradients(cSharedOut.AsObject()))
      return false;
//---
   if(!NeuronOCL.calcHiddenGradients(cExpertsIn.AsObject()))
      return false;

Se presta especial atención a la acumulación de gradientes en los búferes intermedios. Aquí, los gradientes se sumarán a través de los tokens de ventana, lo cual garantiza una correcta coordinación de las señales de los diferentes participantes del proceso.

   if(!DeActivation(NeuronOCL.getOutput(), PrevOutput, Gradient, NeuronOCL.Activation()))
      return false;
   const int window = (int)cSharedGates.GetWindow();
   if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), PrevOutput, window, false, 0, 0, 0, 1))
      return false;
   if(!NeuronOCL.calcHiddenGradients(cSharedIn.AsObject()))
      return false;
   if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), PrevOutput, window, false, 0, 0, 0, 1))
      return false;
   if(!NeuronOCL.calcHiddenGradients(cMasks.AsObject()))
      return false;
   if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), PrevOutput, window, false, 0, 0, 0, 1))
      return false;
   if(!NeuronOCL.calcHiddenGradients(cSharedGates.AsObject()))
      return false;
   if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), NeuronOCL.getGradient(), window, false,
                                                                                       0, 0, 0, 1))
      return false;
//---
   return true;
  }

En definitiva, el método calcInputGradients equilibra cuidadosamente el complejo flujo de información y garantiza un entrenamiento eficiente de todo el módulo con una mezcla reducida de expertos, lo cual hace que esta arquitectura no solo sea potente, sino también robusta frente a los errores y el sobreajuste.

El método para actualizar los parámetros del modelo se organiza según el esquema clásico: el control se delega a los componentes internos. Cada uno de ellos implementa su propia estrategia para adaptar los pesos según los gradientes obtenidos. En el cuerpo del método updateInputWeights, solo se producirá una llamada secuencial a los procedimientos correspondientes. Por lo tanto, para evitar repeticiones, le sugerimos que estudie este fragmento por su cuenta. El código fuente completo de la clase CNeuronTimeMoESparseExperts, con la implementación de todos sus métodos, está disponible en el archivo adjunto.



Conclusión

En el presente artículo, hemos examinado con detalle la implementación del módulo de convolución enmascarada y la arquitectura del bloque de mezcla de expertos dispersa, adaptados para tareas de procesamiento de series temporales en el entorno OpenCL. Asimismo, hemos analizado aspectos clave de la configuración de la computación en el dispositivo y la organización de la interacción con el programa principal, prestando especial atención a la correcta distribución del gradiente en condiciones de flujo de información ramificado.

En la siguiente parte, continuaremos desarrollando la arquitectura y nos centraremos en la construcción y el entrenamiento de modelos que utilicen esta estructura como parte de sistemas neuronales más complejos.


Enlaces


Programas usados en el artículo

# Nombre Tipo Descripción
1 Study.mq5 Asesor Asesor de entrenamiento de modelos offline
2 StudyOnline.mq5
Asesor
Asesor de entrenamiento de modelos online
3 Test.mq5 Asesor Asesor para la prueba de modelos
4 Trajectory.mqh Biblioteca de clases Estructura de descripción del estado del sistema y la arquitectura del modelo
5 NeuroNet.mqh Biblioteca de clases Biblioteca de clases para crear una red neuronal
6 NeuroNet.cl Biblioteca Biblioteca de código del programa OpenCL

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

Archivos adjuntos |
MQL5.zip (2856.61 KB)
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Operando con el Calendario Económico MQL5 (Parte 9): Mejorando la interacción con noticias mediante una barra dinámica y un diseño pulido Operando con el Calendario Económico MQL5 (Parte 9): Mejorando la interacción con noticias mediante una barra dinámica y un diseño pulido
En este artículo, mejoramos el Calendario Económico MQL5 con una barra de desplazamiento dinámica para una navegación intuitiva por las noticias. Garantizamos una visualización impecable de los eventos y unas actualizaciones eficientes. Validamos la barra de desplazamiento adaptable y el panel de control pulido mediante pruebas.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Introducción a la exploración de estructuras de mercado fractales con aprendizaje automático Introducción a la exploración de estructuras de mercado fractales con aprendizaje automático
Este artículo intentaremos examinar las series temporales financieras desde la perspectiva de las estructuras fractales autosimilares. Como contamos con demasiadas analogías que confirman la posibilidad de considerar las cotizaciones de mercado como fractales autosimilares, tenemos la oportunidad de formarnos una idea de los horizontes de previsión de dichas estructuras.