English Русский 中文 Deutsch 日本語 Português
preview
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 12): Nacimiento del SIMULADOR (II)

Desarrollo de un sistema de repetición — Simulación de mercado (Parte 12): Nacimiento del SIMULADOR (II)

MetaTrader 5Probador | 19 septiembre 2023, 09:35
265 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 11): Nacimiento del SIMULADOR (I)", hicimos que nuestro sistema de repetición/simulación fuera capaz de utilizar barras de 1 minuto para simular posibles movimientos del mercado. Aunque quizás después de leerlo hayas notado que los movimientos no parecen ser tan similares a los del mercado real. Durante ese artículo, te mostré los puntos que necesitarías ajustar para tener un sistema aún más cercano a lo que encontrarías en un mercado real. Sin embargo, por más que lo intentes y experimentes, no podrás, utilizando métodos simples, crear algo que sea similar a los movimientos posibles y probables de un mercado.


Inicio de la implementación

Para hacer lo necesario y hacer que el sistema sea un poco más complejo, vamos a utilizar la generación de números aleatorios. De esta manera, haremos que las cosas sean un poco menos predecibles mientras hacemos que el sistema de repetición/simulación sea más interesante. Siguiendo el consejo que se encuentra en la documentación de MQL5 sobre la generación de números aleatorios, tendremos que seguir algunos pasos, que son bastante simples al principio. No hay motivo para preocuparse, en realidad, es bastante sencillo. Mira lo que vamos a agregar inicialmente al código:

void InitSymbolReplay(void)
        {
                Print("************** Serviço Market Replay **************");
                srand(GetTickCount());
                GlobalVariableDel(def_GlobalVariableReplay);
                SymbolSelect(def_SymbolReplay, false);
                CustomSymbolDelete(def_SymbolReplay);
                CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol);
                CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
                CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
                SymbolSelect(def_SymbolReplay, true);
        }

Lo que estoy haciendo aquí es seguir exactamente el consejo que se encuentra en la documentación. Esto se puede ver al observar la función srand, que es la función que inicializa la generación de números pseudoaleatorios. Como la propia documentación deja claro, si utilizamos un valor fijo en la llamada, como por ejemplo:

srand(5);

Siempre obtendremos la misma secuencia numérica. De esta manera, dejarás de tener una generación aleatoria y obtendrás una secuencia "predecible". Ten en cuenta que he puesto "predecible" entre comillas, ya que la secuencia siempre será la misma. Sin embargo, hasta que todo el bucle de generación se complete, no sabrás con certeza cuál será el próximo valor. En cierto sentido, esto puede ser interesante si deseas crear una simulación en la que la secuencia simulada siempre sea la misma. Por otro lado, este tipo de enfoque facilita mucho, o mejor dicho, hace imposible que realmente tengas un buen aprendizaje al usar el sistema.

Si estás utilizando el simulador para generar estudios aleatorios, no tiene sentido crear una gran cantidad de archivos diferentes. Podemos crear solo un archivo y utilizarlo para introducir toda la aleatoriedad. Por esta razón, no voy a poner un valor fijo en la llamada a srand. Dejaré que el azar se encargue de ello. Pero esto queda a elección de cada uno.


Experimentar con una forma de hacer las cosas un poco más complejas

Lo primero que vamos a hacer es eliminar el hecho de saber que comenzaremos buscando el mínimo. Saber esto hace que sea muy fácil. Solo esperas a que se abra una nueva barra y vendes. Si supera la apertura, compras. Esto no es aprendizaje, es trampa.

Nota: Algunos EAs pueden analizar y darse cuenta de este tipo de cosas. Esto ocurre cuando se realiza en el simulador de estrategias. El hecho de que el EA pueda notar esto invalida cualquier simulación realizada.

Para hacer esto debemos complicar las cosas. Vamos a utilizar un método súper simple pero bastante eficiente. Mira el código a continuación.

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                Pivot(rate.open, p0, t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }

Por favor, no tengas miedo de lo que hace la función anterior. Porque la cosa sigue siendo igual que antes. El único cambio es que ahora no sabrás si la barra comenzará buscando el mínimo o el máximo. El primer punto es que verificaremos si el valor generado aleatoriamente es par o impar. Una vez que sepamos esto, simplemente haremos el intercambio de los valores que crearán nuestro punto de pivote. Pero ten en cuenta que el pivote seguirá creándose de la misma manera. Lo único que no sabremos es si la barra está subiendo porque ya ha alcanzado el mínimo o bajando porque ya ha alcanzado el máximo.

Esto ya es un comienzo. Pero vamos a hacer otra modificación antes de pasar a la siguiente fase. La modificación que haremos es la siguiente: En la versión anterior, tenemos normalmente 9 segmentos entre la apertura y el cierre de la barra. Pero agregando muy poco código, haremos que estas 9 segmentos se conviertan en 11 segmentos. ¿Cómo? Mira cómo debería quedar el código en el fragmento a continuación:

#define def_NPASS 3
inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1, p2;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                p2 = floor((rate.high - rate.low) / def_NPASS);
                                Pivot(rate.open, p0, t0, tick);
                                for (int c0 = 1; c0 < def_NPASS; c0++, p0 = (b1 ? p0 + p2 : p0 - p2)) Pivot(p0, (b1 ? p0 + p2 : p0 - p2), t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }
#undef def_NPASS

Puedes pensar que son iguales. Pero en realidad, hay una gran diferencia aquí. Aunque solo se ha agregado una variable más para indicar un punto intermedio, al encontrar este punto, tenemos la posibilidad de agregar 2 segmentos más. Observa que para agregar estos dos segmentos, continuaremos ejecutando prácticamente el mismo código. Ten en cuenta que la complejidad que creamos en la formación de la barra al crear una simulación aumenta rápidamente, y no al mismo ritmo que aumentamos el código. Un pequeño detalle al que debes prestar atención es que la definición no debe establecerse en cero. En caso de que esto ocurra, habrá un error de división por cero. Entonces, en este caso, lo mínimo que debes usar es el valor 1 en la definición. Pero si defines cualquier valor entre 1 y un máximo cualquiera, podrás agregar más segmentos. Como normalmente no tenemos movimientos lo suficientemente amplios como para crear más segmentos, el valor 3 parece adecuado.

Para entender lo que sucedió aquí, observa las siguientes imágenes.

Antes de agregar nuevos segmentos.


Aunque las cosas funcionaban bien de la manera en que se estaban haciendo, cuando utilizamos la versión que nos permite dividir la amplitud en rangos, tendremos el siguiente escenario:

Figura 02

Después del cambio en el que empezamos a dividir la amplitud de la barra por 3.


Observa que la complejidad ha mejorado un poco. Sin embargo, no noté una gran ventaja en dividir en más de 3 regiones. Por lo tanto, aunque la cosa ya se ha vuelto bastante interesante, el sistema no genera tanta complejidad como debería. Tenemos que adoptar otro enfoque. Esto no hará que el código explote frente a ti en términos de complejidad matemática. La idea es realmente lograr un aumento exponencial de la complejidad sin agregar demasiada complejidad al código.

Para lograr esto de hecho, utilizaré un enfoque completamente diferente. Pero antes, entendamos algo que merece ser explicado. De esta manera, podrás comprender realmente por qué estamos cambiando la forma de abordar el problema.

Si prestaste atención a las modificaciones realizadas en la fase anterior, debiste notar algo curioso en el último código. Durante un momento, tendremos todo el cuerpo de la barra bajo nuestro control, y podemos hacer lo que queramos con él. A diferencia de los demás momentos en los que tenemos un movimiento relativamente direccional, ya sea desde la apertura hasta el máximo o mínimo, ya sea en el momento en que estamos cerrando la barra, saliendo del máximo o mínimo hacia el precio de cierre. Cuando tenemos todo el cuerpo de la barra para trabajar, estamos realizando muy poco trabajo dentro de él.  Por más que lo intentemos, siempre quedamos atrapados en la misma situación. Pero si te fijas, notarás que siempre tenemos 2 valores que se pueden trabajar. Un punto de inicio y un punto final. ¿Y por qué estoy llamando la atención sobre este punto? Piensa un poco: Tenemos 60 mil milisegundos para crear una barra de 1 minuto, si dejamos 5 milisegundos de margen al inicio de la barra, todavía tendremos un tiempo considerable. Si hacemos algunos cálculos simples, notaremos que estamos desperdiciando mucho tiempo que podría utilizarse para hacer que la simulación de la barra sea extremadamente más compleja.

Entonces, podemos pensar en una posible solución: si dejamos 1 segundo libre para que el precio salga del punto de apertura y vaya hacia el máximo o mínimo, e igualmente dejamos 1 segundo para que el precio salga de donde esté y vaya al punto de cierre, tendremos 58 segundos para crear la complejidad que deseemos. Sin embargo, presta atención a lo que se dijo acerca del último segundo: que el precio salga de donde esté y vaya al punto de cierre. Es importante que te des cuenta y entiendas exactamente lo que se dijo. No importa lo que esté sucediendo durante la mayor parte del tiempo, siempre debemos reservar un período de tiempo para que el precio alcance al final su punto de cierre.

De manera notable, solo observarás un movimiento que ocurre en un tiempo superior a poco más de 33 milisegundos o 30 Hz. Entonces, si hacemos que cada tick tenga un tiempo de máximo 30 milisegundos, pensarás que el movimiento será bastante similar al encontrado en un activo. Un detalle importante: esta percepción es muy relativa, ya que algunas personas encuentran difícil operar un activo que se mueve muy rápidamente debido a su alta volatilidad.

Por estas razones, un sistema de repetición/simulación no debe considerarse realmente como un buen entrenamiento. A menos que realmente utilices un archivo que contenga ticks reales negociados. Al simular tales ticks, puede ocurrir que tengas la falsa sensación de que todas las rangos de precios serán visitados. Hasta el momento, este sistema no permite que las barras de 1 minuto sean simuladas de manera que generen gaps, a pesar de que en un mercado real, estos gaps realmente ocurren en momentos muy específicos. Estos son momentos muy peligrosos para abrir o cerrar una operación, ya que la probabilidad de que la orden se ejecute fuera del precio deseado es muy alta, y las posibilidades de que simplemente se omita también son enormes, debido al hecho de que la volatilidad puede ser muy intensa, lo que hace que las cosas funcionen de una manera bastante inesperada.

Creo que podrías estar pensando que voy a utilizar un método para generar siempre una cantidad mínima de ticks. Pero no utilizaré este enfoque por ahora. Pero debes recordar lo siguiente: de ninguna manera es posible recrear los movimientos reales del mercado a través de una simulación. Todo lo que podemos hacer es estimar cuáles serían los posibles movimientos. Pero antes de continuar, necesitamos centrarnos en corregir algunos problemas específicos. Comencemos con un tema un poco más complicado, pero que proporcionará las bases para nuestro simulador. Pero antes de continuar, debemos centrarnos en solucionar algunos problemas específicos.


Si no hay ticks, ¿por qué el servicio está activo?

A pesar de todo y de toda la complicación que tendremos que resolver, antes de avanzar hacia algo más cercano a la realidad, debemos abordar algunos problemas que he estado posponiendo durante algún tiempo y que realmente necesitan solución. El primero de estos problemas es que cuando iniciamos el sistema sin que se haya cargado ninguna barra previa, no podemos acceder al indicador de control. Esta falla ha estado presente en el sistema durante un tiempo, pero como en todos los momentos anteriores siempre había barras previas presentes, pospuse esta corrección que el sistema necesitaba. Pero ahora vamos a solucionarlo. Para resolver este problema, tendremos que hacer algunas adiciones menores en un punto muy específico de nuestro sistema. Esto se hace para facilitar al máximo las cosas, mira a continuación lo que haremos:

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Ocorreu um erro na linha %d", iLine)), "Market Replay", MB_OK); return false; }
                                int     file,
                                        iLine;
                                string  szInfo;
                                char    iStage;
                                bool    bBarPrev;
                                MqlRates rate[1];
                                
                                if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
                                {
                                        MessageBox("Falha na abertura do\narquivo de configuração.", "Market Replay", MB_OK);
                                        return false;
                                }
                                Print("Carregando dados para replay. Aguarde....");
                                ArrayResize(m_Ticks.Rate, def_BarsDiary);
                                m_Ticks.nRate = -1;
                                m_Ticks.Rate[0].time = 0;
                                iStage = 0;
                                iLine = 1;
                                bBarPrev = false;
                                while ((!FileIsEnding(file)) && (!_StopFlag))
                                {
                                        switch (GetDefinition(FileReadString(file), szInfo))
                                        {
                                                case Transcription_DEFINE:
                                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                        if (szInfo == def_STR_ConfigSymbol) iStage = 5; else
                                                                macroERROR(StringFormat("%s não é algo reconhecido dentro do sistema\nna linha %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Comando não reconhecido na linha %d\ndo arquivo de configuração.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                if (m_Ticks.nTicks <= 0)
                                {
                                        MessageBox("Não existem ticks para serem usados.\nServiço sendo encerrado...", "Market Replay", MB_OK);
                                        return false;
                                }
                                if (!bBarPrev)
                                {
                                        rate[0].close = rate[0].open =  rate[0].high = rate[0].low = m_Ticks.Info[0].last;
                                        rate[0].tick_volume = 0;
                                        rate[0].real_volume = 0;
                                        rate[0].time = m_Ticks.Info[0].time - 60;
                                        CustomRatesUpdate(def_SymbolReplay, rate, 1);
                                }
                                
                                return (!_StopFlag);
#undef macroERROR
                        }

Lo primero que haremos es definir dos nuevas variables para uso local. Luego, las inicializamos con un valor falso, indicando que no tenemos ninguna barra previa cargada. Ahora, si en algún momento se carga alguna barra previa, esta variable indicará un valor verdadero. De esta manera, el sistema sabrá que tenemos barras previas cargadas. Con esto, resolvemos parte de nuestro primer problema. Pero aún necesitamos comprobar si se ha cargado algún archivo que genere los ticks que se utilizarán. Si no hay ningún tick presente, no tiene sentido que el servicio esté en ejecución. Por lo tanto, el servicio se detendrá. Ahora, si hay ticks, verificamos si se ha cargado algún tipo de barra previa. En caso de que esto no haya sucedido, inicializaremos una barra "vacía". Sin esta inicialización, no podremos acceder al indicador de control, incluso si el servicio está disponible para su uso.

Sin embargo, al realizar estas correcciones mencionadas anteriormente, todo quedará resuelto. 


Implementación de VOLUMEN DE TICKS

En la lista de cosas por corregir, lo siguiente que vamos a abordar, y que aún no se había implementado, se refiere al sistema que indica el volumen de ticks negociados. A mucha gente le gusta tener en el gráfico el indicador de volumen, y hasta el momento, el único volumen realmente implementado era el volumen real. Es decir, ese volumen en términos del número de contratos ejecutados. Pero de la misma manera, también es importante tener el volumen de ticks, ¿pero sabes cuál es la diferencia entre ellos? Mira la imagen a continuación:

En ella, podemos ver dos valores de volumen. Uno es el volumen de ticks y el otro es el volumen (en este caso, el volumen real). Pero al mirar esta imagen, ¿podrías decirme cuál es la diferencia entre el volumen real y el volumen de ticks? Si no sabes la diferencia, ha llegado el momento de aprender definitivamente la diferencia entre estos dos volúmenes.

El VOLUMEN o VOLUMEN REAL es, de hecho, la cantidad de contratos negociados en ese momento específico. Siempre será un múltiplo de un valor que depende del activo en cuestión. Algunos activos no permiten operar con valores inferiores a 5, por ejemplo, mientras que otros aceptan valores fraccionarios. No intentes entender por qué es posible, simplemente sabe que es posible operar con valores fraccionarios. Entonces, este valor es fácil de entender y tal vez por eso muchos lo utilizan. Ahora, si tomas este valor del VOLUMEN REAL y lo multiplicas por el valor mínimo de cada contrato, obtendrás otro valor, llamado VOLUMEN FINANCIERO. MetaTrader 5 no proporciona directamente este valor, pero como has visto, es fácil de obtener. De esta manera, el servidor de negociación entiende que no necesita informar este VOLUMEN FINANCIERO a las terminales de negociación. Depende de los programadores o usuarios de la plataforma implementar dicho cálculo.

Ahora, el VOLUMEN DE TICKS es un volumen completamente diferente. Se proporciona solo en el contenido de las barras por una razón sencilla: no podrás saber lo que sucedió durante la negociación solo observando el volumen real. Necesitamos otro dato, y ese es el volumen de ticks. ¿Pero por qué el volumen de ticks está presente cuando solicitamos las barras pero no está presente cuando solicitamos los ticks? ¿Y qué volumen es ese que aparece cuando solicitamos los ticks? Si nunca has prestado atención a esto o aún no lo has visto, mira la imagen a continuación:

Los valores informados en el campo VOLUMEN no, repito, NO representan el volumen de ticks. Este valor es, de hecho, el VOLUMEN REAL. Pero entonces, ¿cómo puedo saber el volumen de ticks si no se informa cuando solicito los ticks? Solo aparece cuando solicito barras. El hecho es que, de la misma manera en que el servidor entiende que no necesita informar el VOLUMEN FINANCIERO, también entiende que al proporcionar los ticks negociados, podrás calcular el VOLUMEN DE TICKS. Esto es diferente de lo que ocurriría si se solicitara barras, donde no tenemos acceso a los ticks reales negociados.

¿Aún no lo entiendes? Cuando tienes los datos de los ticks negociados reales, puedes calcular el volumen de ticks. ¿Pero cómo? ¿Hay alguna fórmula secreta? ¿Algo místico o esotérico para hacer esto? Porque siempre que intento, no logro que los valores coincidan. Tranquilo, querido lector. No, no hay ninguna fórmula mágica. Lo que sucede es que quizás no entiendas lo que realmente representa el VOLUMEN DE TICKS. En el artículo anterior y en este, utilizamos un método para simular el movimiento dentro de la barra de 1 minuto. Aunque el movimiento hace que se visiten todos los precios, el volumen de ticks que estamos creando en realidad es mucho menor que el volumen de ticks que se informa en la barra de 1 minuto.

¿Pero cómo así? Tranquilo. Esto se entenderá mejor en el próximo artículo, donde realmente simularemos el mismo volumen de ticks. Pero al mencionar esto, creo que has entendido de qué se trata el volumen de ticks. El volumen de ticks es el número de transacciones que realmente ocurrieron dentro de esa barra. De la manera en que lo estamos haciendo, este volumen es de alrededor de 150 en promedio. Cuando en realidad, muchas veces gira en torno a los 12,890 en promedio.

Pero puedes estar pensando: Entonces, ¿cómo puedo calcular este volumen de ticks? Bueno, es muy fácil hacerlo. Veamos si nuestro sistema puede hacer este cálculo. Porque para entenderlo, realmente debes ver el cálculo en acción.

Actualmente, este cálculo se realiza en dos lugares por razones diferentes. El primer lugar se encuentra justo abajo:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
                        {
                                if (rate.time != macroRemoveSec(tick.time))
                                {
                                        rate.real_volume = 0;
                                        rate.tick_volume = 0;
                                        rate.time = macroRemoveSec(tick.time);
                                        rate.open = rate.low = rate.high = rate.close = tick.last;
                
                                        return true;
                                }
                                rate.close = tick.last;
                                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                                rate.real_volume += (long) tick.volume_real;
                                rate.tick_volume += (tick.last > 0 ? 1 : 0);

                                return false;
                        }

En este punto, realizamos el cálculo del volumen de ticks que estarán presentes en la barra. El segundo punto se ve a continuación:

inline int Event_OnTime(void)
                        {
                                bool    bNew;
                                int     mili, iPos;
                                u_Interprocess Info;
                                static MqlRates Rate[1];
                                static datetime _dt = 0;
                                datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                
                                if (m_ReplayCount >= m_Ticks.nTicks) return -1;
                                if (bNew = (_dt != tmpDT))
                                {
                                        _dt = tmpDT;
                                        Rate[0].real_volume = 0;
                                        Rate[0].tick_volume = 0;
                                }
                                mili = (int) m_Ticks.Info[m_ReplayCount].time_msc;
                                do
                                {
                                        while (mili == m_Ticks.Info[m_ReplayCount].time_msc)
                                        {
                                                Rate[0].close = m_Ticks.Info[m_ReplayCount].last;
                                                Rate[0].open = (bNew ? Rate[0].close : Rate[0].open);
                                                Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high);
                                                Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low);
                                                Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                                Rate[0].tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                                bNew = false;
                                                m_ReplayCount++;
                                        }
                                        mili++;
                                }while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
                                Rate[0].time = _dt;
                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                if (Info.s_Infos.iPosShift != iPos)
                                {
                                        Info.s_Infos.iPosShift = (ushort) iPos;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                }
                                return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
                        }

En este punto, hacemos lo mismo, calculamos el volumen de ticks. ¿Pero es solo eso? Sí. El volumen de ticks se calcula de esta manera, es decir, solo se contarán los ticks que realmente indican la ocurrencia de una operación. Uno por cada operación realizada. Es decir, los ticks cuyas flags BID o ASK estén activadas no participan en el cálculo, solo se calcularán aquellos que tengan la flag SELL o BUY. Pero debido a que estas flags solo estarán activas cuando el valor del precio o el volumen real sea mayor que cero, no realizamos la prueba de las flags, ya que sería innecesaria.

NOTA: Cuando hablemos de FOREX cambiaremos esto. Pero esto se tratará únicamente en artículos específicos de FOREX.

Así que a partir de ahora, el sistema de repetición/simulación contará con el volumen de ticks. Pero hay un detalle: en este momento, al usar una barra para simular los ticks, el volumen siempre será diferente al que se informa en el archivo de barras. En el próximo artículo, corregiremos esto. Pero esto es algo que merece un artículo aparte. Para que pueda explicar con calma lo que tendremos que hacer.


Ajuste del punto de control

El próximo problema a corregir, aunque no es realmente un problema, es hacer que el sistema pueda saber qué representa cada unidad de posición. El problema es que hasta el momento presente, este sistema estaba utilizando una forma muy poco adecuada de realizar el posicionamiento indicado por el usuario. Entonces, cuando se vuelve posible y especialmente deseado por el usuario, el uso de más de un archivo para obtener los datos de los ticks, la situación se vuelve completamente inviable de mantener con el sistema anterior. Por lo tanto, comenzamos a tener problemas con la conversión entre lo que se coloca en el indicador de control y lo que produce la repetición. 

Para resolver esto, será necesario eliminar una línea específica en el sistema de carga.

                bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true)
                        {
                                int     file,
                                        old,
                                        MemNRates,
                                        MemNTicks;
                                string  szInfo = "";
                                MqlTick tick;
                                MqlRates rate,
                                        RatesLocal[];
                                
                                MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
                                MemNTicks = m_Ticks.nTicks;
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                        ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary);
                                        old = m_Ticks.nTicks;
                                        for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                                        if (szInfo != def_Header_Ticks)
                                        {
                                                Print("Arquivo ", szFileNameCSV, ".csv não é um arquivo de tick negociados.");
                                                return false;
                                        }
                                        Print("Carregando ticks de replay. Aguarde...");
                                        while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                                        {
                                                ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                                szInfo = FileReadString(file) + " " + FileReadString(file);
                                                tick.time = StringToTime(StringSubstr(szInfo, 0, 19));
                                                tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                tick.bid = StringToDouble(FileReadString(file));
                                                tick.ask = StringToDouble(FileReadString(file));
                                                tick.last = StringToDouble(FileReadString(file));
                                                tick.volume_real = StringToDouble(FileReadString(file));
                                                tick.flags = (uchar)StringToInteger(FileReadString(file));
                                                if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                                        m_Ticks.Info[old].volume_real += tick.volume_real;
                                                else
                                                {                                                       
                                                        m_Ticks.Info[m_Ticks.nTicks] = tick;
                                                        if (tick.volume_real > 0.0)
                                                        {
                                                                m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                                                rate.spread = (ToReplay ? m_Ticks.nTicks : 0);
                                                                m_Ticks.Rate[m_Ticks.nRate] = rate;
                                                                m_Ticks.nTicks++;
                                                        }
                                                        old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                                                }
                                        }
                                        if ((!FileIsEnding(file)) && (!_StopFlag))
                                        {
                                                Print("Excesso de dados no arquivo de tick.\nNão é possivel continuar...");
                                                FileClose(file);
                                                return false;
                                        }
                                        FileClose(file);
                                }else
                                {
                                        Print("Aquivo de ticks ", szFileNameCSV,".csv não encontrado...");
                                        return false;
                                }
                                if ((!ToReplay) && (!_StopFlag))
                                {
                                        ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                                        ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                                        CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                                        m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time;
                                        m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                                        m_Ticks.nTicks = MemNTicks;
                                        ArrayFree(RatesLocal);
                                }
                                return (!_StopFlag);
                        };

El hecho de realizar esta eliminación ya liberará la variable "spread" para ser ajustada adecuadamente en otro momento. No lo haremos ahora, no en este artículo, ya que todavía no es tan necesario. Pero una vez hecho esto, tendremos que corregir el sistema de conversión. Porque a partir de este momento, el sistema de control de posición siempre indicará un punto no válido. Mejor dicho, diferente de lo que realmente desea el usuario.

Para realizar la conversión correcta, necesitaremos cambiar un procedimiento muy específico. Este se encuentra justo abajo:

                long AdjustPositionReplay(const bool bViewBuider)
                        {
                                u_Interprocess  Info;
                                MqlRates        Rate[def_BarsDiary];
                                int             iPos,
                                                nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return 0;
                                iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
                                Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
                                if (iPos < m_ReplayCount)
                                {
                                        CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0))
                                        {
                                                m_ReplayCount = 0;
                                                Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last;
                                                Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0;
                                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                        }else
                                        {
                                                for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--);
                                                m_ReplayCount++;
                                        }
                                }else if (iPos > m_ReplayCount)
                                {
                                        if (bViewBuider)
                                        {
                                                Info.s_Infos.isWait = true;
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        }else
                                        {
                                                for(; Rate[0].time > m_Ticks.Info[m_ReplayCount].time; m_ReplayCount++);
                                                for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++);
                                                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
                                        }
                                }
                                for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) Event_OnTime();
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);

                                return Event_OnTime();
                        }

La versión de conversión presentada arriba es muy diferente de las versiones encontradas en los artículos anteriores. Esto se debe a que en realidad convierte los valores porcentuales configurados por el usuario en el indicador de control y el sistema de posicionamiento, de modo que no importa cómo ni de qué manera se organicen los ticks. Este procedimiento buscará el punto correcto y comenzará a presentar los datos encontrados en los ticks desde ese punto.

Para hacer esto de manera adecuada, primero realizamos un cálculo para determinar en qué punto se encuentra el lugar deseado en términos porcentuales. Esta posición es muy importante para nosotros. Si el valor es menor, significa que debemos retroceder a algún punto. Luego eliminamos la información hasta llegar cerca de ese punto. Normalmente, siempre se eliminarán algunos datos adicionales, pero es parte del proceso, y volveremos a colocar estos datos después. Puede ser que estemos retrocediendo al inicio de la serie de datos. Pero si no es el caso, haremos que el contador retroceda hasta un punto cercano al valor porcentual. Esta línea específica corrige el problema de retroceder más de lo deseado, y sin ella, la barra anterior a la posición estará equivocada. El sistema de retroceso es más complicado que el sistema de avance. En el caso del avance, simplemente verificamos si el usuario desea ver las barras siendo creadas o no. Si lo desea, se mostrarán; de lo contrario, el sistema avanzará hasta el punto especificado por el valor porcentual. En la mayoría de los casos, necesitaremos hacer un ajuste fino entre el valor porcentual y la posición real. Esto siempre se mostrará, pero como el proceso en estos casos se realiza bastante rápido, si ya hemos aproximado el valor real del contador al valor porcentual, la transición será prácticamente instantánea. Pero si el valor está lejos, habrá una pequeña animación que mostrará cómo se construyen las barras.


Consideraciones finales para este artículo

A pesar de que el sistema parece ser mucho más adecuado para su uso, notarás algunas cosas extrañas al ejecutarlo en el modo de visualización de construcción de las barras. Estas cosas extrañas se pueden ver en el video a continuación. Sin embargo, debido a que requerirían cambios en algunos puntos del código, y no quería que pensaras que estas cosas aparecen de la nada, decidí dejar la "falla". Pero quizás la razón principal es que en el próximo artículo mostraré cómo hacer que el sistema sea más adecuado como simulador. No me gustaría que alguien me preguntara por qué programé el simulador de la forma en que se verá en el próximo artículo.

Dicho esto, mira el video. Ten en cuenta que soy consciente de lo que está sucediendo.



En el anexo encontrarás los archivos utilizados aquí. También tendrás un archivo adicional que muestra tanto las barras de 1 minuto como los ticks negociados en un mismo día. Ejecuta ambas configuraciones y observa los resultados, pero sobre todo, comprende lo que está ocurriendo en el gráfico.

Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/10987

Archivos adjuntos |
Redes neuronales: así de sencillo (Parte 43): Dominando las habilidades sin función de recompensa Redes neuronales: así de sencillo (Parte 43): Dominando las habilidades sin función de recompensa
El problema del aprendizaje por refuerzo reside en la necesidad de definir una función de recompensa, que puede ser compleja o difícil de formalizar. Para resolver esto, se están estudiando enfoques basados en la variedad de acciones y la exploración del entorno que permiten aprender habilidades sin una función de recompensa explícita.
Representaciones en el dominio de la frecuencia de series temporales: El espectro de potencia Representaciones en el dominio de la frecuencia de series temporales: El espectro de potencia
En este artículo, veremos métodos asociados con el análisis de series temporales en el dominio de la frecuencia. También prestaremos atención a los beneficios del estudio de las funciones espectrales de series temporales al construir modelos predictivos. Además, analizaremos algunas perspectivas prometedoras para el análisis de series temporales en el dominio de la frecuencia utilizando la transformada discreta de Fourier (DFT).
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 13): Nacimiento del SIMULADOR (III) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 13): Nacimiento del SIMULADOR (III)
Aquí optimizaremos un poco las cosas para facilitar lo que haremos en el próximo artículo. Y también te explicaré cómo puedes visualizar lo que está generando el simulador en términos de aleatoriedad.
Algoritmo de recompra: simulación del comercio multidivisa Algoritmo de recompra: simulación del comercio multidivisa
En este artículo crearemos un modelo matemático para simular la formación de precios multidivisa y completaremos el estudio del principio de diversificación en la búsqueda de mecanismos para aumentar la eficiencia del trading que inicié en el artículo anterior con cálculos teóricos.