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

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

MetaTrader 5Ejemplos | 21 agosto 2023, 08:24
377 0
Daniel Jose
Daniel Jose

Introducción

Hasta ahora, incluyendo el artículo anterior: Desarrollo de un sistema de repetición — Simulación de mercado (Parte 10): Sólo datos reales para la repetición, todo lo que hicimos involucró datos reales, es decir, estábamos usando ticks realmente negociados. Con esto, los movimientos son precisos y fáciles de crear, ya que no tuvimos que preocuparnos de recopilar información. Lo único que teníamos que hacer era convertir los ticks negociados en barras de 1 minuto y la plataforma MetaTrader 5 se encargó del resto por nosotros.

Sin embargo, ahora nos enfrentaremos a un reto más complejo.


Planificación

Muchos pueden pensar que la planificación es sencilla, sobre todo porque se trata de convertir barras, que siempre deben ser de 1 minuto (la razón se explicará más adelante), en ticks. Sin embargo, la simulación es mucho más compleja de lo que puede parecer a primera vista. El principal reto es que no tenemos una visión clara del comportamiento real de los ticks para crear la barra de 1 minuto. Sólo tenemos la barra y alguna información sobre ella, sin saber cómo se formó. Utilizaremos las barras de 1 minuto precisamente porque nos ofrecen un nivel de complejidad mínimo. Si es capaz de crear un movimiento complejo que se asemeje en gran medida a un movimiento real, habrá conseguido reproducir algo bastante cercano a la realidad.

Este detalle puede no parecer tan crucial, ya que normalmente observamos un movimiento de tipo ZIG-ZAG en el mercado. Independientemente de la complejidad del movimiento, todo se reduce a crear un zig-zag entre los puntos OHCL. Se inicia en el punto de apertura de la barra y se realizan al menos 9 tramos para crear este zig-zag interno, para finalizar siempre en el cierre de la barra y repetir el proceso en la siguiente barra. Esta misma lógica es empleada por el simulador de estrategias de MetaTrader 5. Para obtener más detalles, consulte Ticks reales y generados: comercio algorítmico. Inicialmente, adoptaré esta estrategia; aunque no es perfectamente adecuada, servirá como punto de partida para enfoques más apropiados.

La mención de que la estrategia del simulador no es la más adecuada para el sistema de repetición/simulador se debe a que en el simulador de estrategias comerciales la preocupación por el tiempo no es primordial. Es decir, no es necesario crear y presentar una barra de 1 minuto de forma que realmente dure 1 minuto. De hecho, es incluso más conveniente que no se corresponda con ese tiempo en términos reales. Si este fuera el caso, realizar pruebas de una estrategia sería inviable. Imagina ejecutar una prueba con barras que abarcan varios días o incluso años, si cada barra representara su tiempo real. Sería un reto insostenible. Sin embargo, para un sistema de repetición/simulador, buscamos una dinámica diferente: queremos que la barra de 1 minuto se cree en un intervalo de 1 minuto, acercándose lo más posible a esta hora.


Preparación del terreno

Nuestra atención se centrará exclusivamente en el código del servicio de repetición/simulación. Por ahora no hay que preocuparse de otros aspectos. Así, comenzaremos las modificaciones en el código de la clase C_Replay, intentando optimizar al máximo lo ya desarrollado y probado previamente. El primer procedimiento que aparece en la clase se puede observar a continuación:

inline bool CheckFileIsBar(int &file, const string szFileName)
                        {
                                string  szInfo = "";
                                bool    bRet;
                                
                                for (int c0 = 0; (c0 < 9) && (!FileIsEnding(file)); c0++) szInfo += FileReadString(file);
                                if ((bRet = (szInfo == def_Header_Bar)) == false)
                                {
                                        Print("Arquivo ", szFileName, ".csv não é um arquivo de barras.");
                                        FileClose(file);
                                }
                                
                                return bRet;
                        }

La intención aquí es eliminar de la función de lectura de barras las pruebas que determinan si el fichero indicado es, o no, un fichero de barras previas. Esto es crucial para evitar repetir el código cuando sea necesario utilizar este mismo conjunto para determinar si un fichero es un fichero de barras. En este escenario, estas barras no se utilizarán como barras previas. Se convertirán en ticks simulados, destinados a ser utilizados en el sistema de negociación. Basándonos en esto, introduciremos otra función más:

inline void FileReadBars(int &file, MqlRates &rate[])
                        {
                                rate[0].time = StringToTime(FileReadString(file) + " " + FileReadString(file));
                                rate[0].open = StringToDouble(FileReadString(file));
                                rate[0].high = StringToDouble(FileReadString(file));
                                rate[0].low = StringToDouble(FileReadString(file));
                                rate[0].close = StringToDouble(FileReadString(file));
                                rate[0].tick_volume = StringToInteger(FileReadString(file));
                                rate[0].real_volume = StringToInteger(FileReadString(file));
                                rate[0].spread = (int) StringToInteger(FileReadString(file));
                        }

Esta función leerá los datos, línea por línea, de las barras presentes en el archivo indicado. Creo que no tendrán dificultades para comprender ambos códigos. Continuando con esta fase de preparación, vemos la aparición de otra rutina:

inline bool OpenFileBars(int &file, const string szFileName)
                        {
                                if ((file = FileOpen("Market Replay\\Bars\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        if (!CheckFileIsBar(file, szFileName))
                                                return false;
                                        return true;
                                }
                                Print("Falha ao acessar ", szFileName, ".csv de barras.");
                                
                                return false;
                        }

Ahora hemos centralizado completamente nuestro sistema para obtener un acceso estandarizado a las barras. Tanto cuando las utilicemos como barras previas, como cuando las usemos como barras que serán convertidas de manera simulada en ticks para presentación. Por lo tanto, la rutina anterior de carga de barras previas también tuvo que ser modificada, quedando como se muestra a continuación:

bool LoadPrevBars(const string szFileNameCSV)
        {
                int     file,
                        iAdjust = 0;
                datetime dt = 0;
                MqlRates Rate[1];
                                
                if (OpenFileBars(file, szFileNameCSV))
                {
                        Print("Carregando barras previas para Replay. Aguarde ....");
                        while ((!FileIsEnding(file)) && (!_StopFlag))
                        {
                                FileReadBars(file, Rate);
                                iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust);
                                dt = (dt == 0 ? Rate[0].time : dt);
                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                        }
                        m_dtPrevLoading = Rate[0].time + iAdjust;
                        FileClose(file);
                        
                        return (!_StopFlag);
                }
                m_dtPrevLoading = 0;
                        
                return false;
        }

El modo en que este procedimiento de carga opera no ha cambiado, aunque ahora hay más llamadas. El haber extraído del procedimiento anterior las partes que serán usadas en el nuevo punto que crearemos nos brinda una mayor seguridad, ya que todo el código que utilizaremos ha sido previamente probado. Así, solo tendremos que preocuparnos por las nuevas funcionalidades. Con el escenario ya preparado, necesitamos hacer una nueva inclusión en el archivo de configuración. Esta inclusión tiene como objetivo identificar qué archivos de barras deberán ser simulados en términos de ticks. Para hacerlo, necesitamos agregar una nueva definición:

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"
#define def_STR_BarsToTicks     "[BARS->TICKS]"

Esto ya nos permite realizar una simple prueba, ue es justamente lo que necesitamos para empezar a trabajar en la simulación de los ticks.

                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;
                                
                                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;
                                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
                                                                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("");
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);

                                return (!_StopFlag);
#undef macroERROR
                        }

Observa lo sencillo que es añadir nuevas funcionalidades al código. El mero hecho de añadir esta prueba ya nos ofrece la posibilidad de analizar aún más aspectos. A partir de aquí, podemos considerar el mismo tipo de comportamiento que observamos en otras ocasiones. Cualquier archivo definido en esta fase será tratado como un archivo de barras que debe convertirse en ticks, y esto se realiza a través de esta llamada.

De nuevo, siempre que sea posible, debemos evitar codificar innecesariamente. Es recomendable reutilizar los códigos previamente testados siempre que podamos. Así lo hemos hecho hasta ahora. Sin embargo, pronto abordaremos un tema nuevo, aunque eso será asunto para otro tópico. Pero antes, es crucial comprender un aspecto esencial.


Pequeñas consideraciones antes de la implementación

Antes de empezar realmente con la implementación del sistema de conversión, hay un punto a analizar. ¿Sabes cuántos tipos diferentes de configuración de barras existen? Aunque muchas personas creen que hay muchos tipos, en realidad podemos resumir todas las configuraciones posibles en solo cuatro. Estas se muestran en la figura a continuación:


¿Por qué es esto relevante para nosotros? Es crucial porque determina cómo y cuántas variaciones tendremos que implementar. Si no conoces o no comprendes el hecho de que solo existen estas cuatro variaciones, podrías correr el riesgo de omitir algún caso o, por el contrario, crear más tipos de casos de los necesarios. Una vez más, quiero enfatizar que es imposible replicar un modelo simulado perfecto para recrear las barras. Lo máximo que tú o cualquiera podrá lograr es una estimación, más o menos cercana, de lo que pudo haber sido el movimiento real que resultó en la formación de esa barra específica.

Existen detalles acerca del tipo 2, donde el cuerpo de la barra puede situarse en la parte superior en lugar de la inferior, tal como se muestra en la imagen. Sin embargo, este hecho no afecta al sistema que se implementará. De igual manera, no importa si la barra representa una venta o una compra; la implementación seguirá siendo la misma. El único matiz reside en la dirección inicial que debemos adoptar. Así, reducimos al máximo el número de casos que necesitamos implementar. Pero más allá de los casos presentados en la figura, todavía debemos entender otro aspecto: ¿Cuántos ticks mínimos realmente necesitamos crear? Esta cuestión puede resultar confusa para algunos, aunque, para quien implementa el sistema de repetición/simulación o incluso un simulador.de estrategias, todo tendrá sentido.

Reflexionemos: No es factible usar solo 1 tick en ningún sistema, ya que este representaría solamente una operación de compra o venta, y no un movimiento en sí. Por ende, podemos descartar esta posibilidad. Entonces, podríamos pensar en un mínimo de 2 ticks, que simbolizarían un punto de apertura y otro de cierre. Aunque parece lógico, tampoco tendríamos ningún movimiento real, ya que solo necesitaríamos generar un tick para la apertura de la barra y otro para su cierre. 

NOTA: No estamos intentando generar solo los ticks. Lo que buscamos es realmente generar un movimiento para simular la barra. Profundizaremos en estos temas en los siguientes artículos, pero primero debemos diseñar un sistema básico.

Por tanto, el mínimo de ticks será 3. Así, una barra de 1 minuto podrá reflejar algunas de las configuraciones observadas en la figura anterior. Pero hay que tener en cuenta que el hecho de contar con al menos 3 ticks no implica que el precio se haya movido exactamente 1 tick hacia arriba y otro hacia abajo, o 3 ticks hacia arriba o hacia abajo. Es posible que el movimiento haya sido diferente a este 1 tick debido a la falta de liquidez en el momento en que se generó la barra.

Importante: Es posible que puedas confundir algunos de los términos usados aquí. Pero aclararemos esto para evitar malentendidos: Cuando menciono el término TICK, me refiero efectivamente al evento de negociación, es decir, un evento de compra o venta del activo al precio indicado. En cuanto al término TICK, me refiero a la menor variación respecto al precio negociado. Para entender esta diferencia, considera lo siguiente: 1 tick en el mercado de acciones tiene un valor de 0,01 punto, mientras que 1 tick en el dólar futuro equivale a 0,5 puntos y, en el índice futuro, vale 5 puntos.

Aunque esto pueda complicar en ciertos aspectos, dado que la simulación del movimiento ya no refleja la realidad exacta sino un movimiento idealizado, es esencial mencionar este hecho para que tengas presente que, muchas veces, el sistema de simulación de ticks usando barras de 1 minuto no reproduce con precisión lo que realmente sucedió o sucede en el mercado. Por ello, siempre es preferible usar el marco temporal más breve posible. Y si este marco es de 1 minuto, deberías utilizarlo siempre.

Quizás aún no hayas comprendido el verdadero problema de utilizar barras como método para crear ticks. Pero considera lo siguiente: si en el mercado real, una barra de 1 minuto abre a un precio específico y, debido a la falta de liquidez, el precio salta 3 ticks y, después de un rato, desciende 1 tick, cerrando en esa última posición, el escenario final que tendremos es el siguiente:

Puede que la imagen de arriba te resulte confusa y no la hayas entendido bien. Representa la siguiente información: En la esquina izquierda, observamos el movimiento real en términos de ticks. Las pequeñas barras horizontales simbolizan cada tick. Los círculos indican los precios a los que se detuvo realmente el activo entre un tick y otro. La línea verde muestra el salto que dio el precio. Obsérvese que hubo momentos en los que determinados ticks no tuvieron operaciones. Pero al analizar el valor OHCL, no vemos claramente los saltos de los ticks. Entonces, al simular el movimiento utilizando sólo la barra de velas, tenemos lo que se muestra en la siguiente imagen.

La línea azul representa el movimiento simulado.En este, vamos a pasar por todos los ticks, independientemente de lo que realmente ocurrió durante la negociación real. Por lo tanto, ten siempre en cuenta: simular no es lo mismo que utilizar datos reales. Por muy sofisticado que sea el sistema de simulación, nunca representará fielmente la realidad.


Sobre la implementación del sistema básico de conversión

Lo primero que hay que hacer, como ya se ha comentado, es determinar el tamaño de un tick de precio. Para ello, tendremos que incluir algunos elementos adicionales en el fichero de configuración. Estos elementos deben ser reconocidos por la clase C_Replay. Por lo tanto, tendremos que añadir algunas definiciones y código adicional a esta clase. Comenzaremos con las siguientes líneas.

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"
#define def_STR_BarsToTicks     "[BARS->TICKS]"
#define def_STR_ConfigSymbol    "[CONFIG]"
#define def_STR_PointsPerTicks  "POINTSPERTICK"
#define def_Header_Bar          "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>"
#define def_Header_Ticks        "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>"
#define def_BarsDiary           540

Esta línea define una cadena que identificará los datos de configuración que manejaremos. Proporcionará la primera de las configuraciones que podremos definir a partir de ahora. De nuevo, necesitaremos incorporar más líneas de código al sistema para que estas configuraciones sean interpretadas y aplicadas. Sin embargo, las incorporaciones son relativamente sencillas. A continuación, veremos lo que hay que hacer:

                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;
                                
                                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;
                                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("");
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                
                                return (!_StopFlag);
#undef macroERROR
                        }

Aquí indicamos que toda la información a partir de la línea siguiente será procesada por el sistema en la etapa de análisis 5. Cuando se activa una entrada de información y se captura en la etapa 5, se realiza una llamada a un procedimiento. Para simplificar la descripción de este procedimiento, a continuación se detalla el procedimiento llamado en la etapa 5.

inline bool Configs(const string szInfo)
                        {
                                string szRet[];
                                
                                if (StringSplit(szInfo, '=', szRet) == 2)
                                {
                                        StringTrimRight(szRet[0]);
                                        StringTrimLeft(szRet[1]);
                                        if (szRet[0] == def_STR_PointsPerTicks) m_PointsPerTick = StringToDouble(szRet[1]); else
                                        {
                                                Print("Variável >>", szRet[0], "<< não definida.");
                                                return false;
                                        }
                                        return true;
                                }
                                Print("Definição de configuração >>", szInfo, "<< invalida.");
                                return false;
                        }

Inicialmente, capturamos y aislamos el nombre de la variable interna de repetición del valor que será utilizado. Éste ha sido definido por el usuario en el fichero de configuración de la repetición/simulación. El resultado de esta operación nos dará dos informaciones: la primera es el nombre de la variable a definir y la segunda es su valor. Este valor puede variar de tipo en función de la variable, pero para el usuario será totalmente transparente. No tiene que preocuparse de si el tipo es una cadena, un doble o un entero. La selección del tipo se realiza aquí, en el código.

Antes de utilizar realmente estos datos, es esencial eliminar cualquier elemento que no pertenezca a la información principal. Normalmente, este elemento es algún tipo de formato interno que el usuario puede haber utilizado para facilitar la lectura o escritura de los archivos de configuración. Todo lo que no se entienda o no se aplique se considera un error. Esto nos lleva a devolver un valor falso, resultando en la terminación del servicio. Más tarde, por supuesto, se informa de la razón en la caja de herramientas de la plataforma MetaTrader 5.

Así, nuestro archivo de configuración aparecerá como se muestra en la siguiente imagen. Recuerda que esto es sólo un ejemplo de un archivo de configuración:

[Config]
PointsPerTick = 5

[Bars]
WIN$N_M1_202112060900_202112061824
WIN$N_M1_202112070900_202112071824

[ Ticks -> Bars]

[Ticks]

[ Bars -> Ticks ]
WIN$N_M1_202112080900_202112081824

#Fim do arquivo de configuração...

Antes de continuar, es necesario hacer un último ajuste en el sistema. Este ajuste es crucial. Necesitamos depurar el sistema para asegurarnos de que el mecanismo de barras se está convirtiendo en un modelo de ticks simulados. El cambio requerido se destaca en el siguiente código:

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;
                                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);
        }

Un detalle interesante es que se puede desactivar la visualización para ver cómo se está realizando el movimiento en el gráfico. Se trata de desactivar la función que elimina los segundos valores presentes en los ticks simulados. Al hacerlo, cada tick simulado se representará en el gráfico como una barra.

Aunque pueda parecer un poco inusual, esto puede ayudarte a entender lo que está sucediendo sin necesidad de registrar los datos en un archivo de log. Precisamente por este registro, tenemos que eliminar el valor correspondiente a los segundos. Si no lo hacemos, cada tick simulado generará una barra, y en última instancia eso no es lo que queremos. Queremos que las barras de 1 minuto se muestren como si fueran reales.


Ahora entramos en la fase de implementación.

Antes de sumergirnos en el código, veamos cuál será el movimiento que presentaré en este artículo. En el futuro, te mostraré cómo hacerlo más complejo. Pero antes, es fundamental que funcione correctamente. En la siguiente imagen, puedes visualizar cómo se producirá este movimiento:

Aunque parezca sencillo, estamos convirtiendo una barra en un movimiento simulado de ticks. Como el movimiento nunca es realmente lineal, sino en una especie de zig-zag, he optado por componer el movimiento con tres de estas fluctuaciones. Si lo deseas, puedes aumentar este número. En futuros artículos, mostraré cómo convertir este movimiento básico en algo mucho más elaborado.

Ahora que ya sabemos cómo será el movimiento, podemos pasar al código. El primer procedimiento, esencial para el sistema de conversión, se muestra en el siguiente código:

                bool LoadBarsToTicksReplay(const string szFileNameCSV)
                        {
//#define DEBUG_SERVICE_CONVERT
                                int file, max;
                                MqlRates rate[1];
                                MqlTick tick[];
                                
                                if (OpenFileBars(file, szFileNameCSV))
                                {
                                        Print("Convertendo barras em ticks. Aguarde...");
                                        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                        ArrayResize(m_Ticks.Rate, def_BarsDiary);
                                        ArrayResize(tick, def_MaxSizeArray);
                                        while ((!FileIsEnding(file)) && (!_StopFlag))
                                        {
                                                FileReadBars(file, rate);
                                                max = SimuleBarToTicks(rate[0], tick);
                                                for (int c0 = 0; c0 <= max; c0++)
                                                {
                                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                                }
                                        }
                                        FileClose(file);
                                        ArrayFree(tick);
                                        
#ifdef DEBUG_SERVICE_CONVERT
        file = FileOpen("Infos.txt", FILE_ANSI | FILE_WRITE);
        for (long c0 = 0; c0 < m_Ticks.nTicks; c0++)
                FileWriteString(file, StringFormat("%s.%03d %f --> %f\n", TimeToString(m_Ticks.Info[c0].time, TIME_DATE | TIME_SECONDS), m_Ticks.Info[c0].time_msc, m_Ticks.Info[c0].last, m_Ticks.Info[c0].volume_real));
        FileClose(file);
#endif

                                        return (!_StopFlag);
                                }
                                
                                return false;
                        }

Aunque parezca muy sencillo, y en realidad lo es, esta función es el primer paso encargado de crear la simulación. Al observarla, podrás comprender al menos en parte cómo el sistema lleva a cabo la simulación. Es importante destacar que, a diferencia de un simulador.de estrategias, aquí, cada barra de 1 minuto se simulará de manera que, durante el uso del servicio, tardará aproximadamente un minuto en mostrarse por completo en la pantalla. Esa es la intención. Así que si tu objetivo es probar una estrategia, te recomiendo que utilices la herramienta disponible en la plataforma MetaTrader 5 y uses esta herramienta que estamos mostrando cómo desarrollar solo para aprendizaje y pruebas que desees realizar manualmente.

Existe un aspecto que, en el futuro, dejará de estar presente, pero que en este momento es crucial. Se refiere a una definición, actualmente comentada, que te permite generar un archivo con los datos de los ticks simulados, para así analizar el nivel de complejidad presente en el sistema simulado. Es vital señalar que el archivo creado contendrá solo los datos esenciales para el análisis, sin incluir información adicional o innecesaria.

El resto de la función es bastante intuitivo de entender, ya que el código ya existía previamente. Ahora tenemos una llamada que examinaremos con más detalle más adelante. Tras esta llamada, que simula los ticks, hay un pequeño bucle para almacenar los ticks en la base de datos. Es algo fácil de comprender y no requiere muchas más explicaciones.

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                Pivot(rate.open, rate.low, t0, tick);
                                Pivot(rate.low, rate.high, t0, tick);
                                Pivot(rate.high, 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;
                        }

La función mencionada anteriormente comienza a tornar las cosas un poco más complejas y, por eso, más interesantes. Al mismo tiempo, nos ayuda a montar toda la estructura necesaria de una forma más organizada y manejable. De hecho, para reducir la complejidad de esta función, creé otro procedimiento que se invoca tres veces. Si revisas la documentación "Ticks reales y generados - Trading algorítmico", notarás que en ella, el sistema es llamado no tres, sino cuatro veces. Si lo deseas, puedes añadir más llamadas, pero como mencioné, mostraré cómo aumentar la complejidad de este sistema sin tener que añadir más invocaciones al procedimiento de Pivot.

En cuanto al procedimiento anteriormente mencionado, después de haber ejecutado las tres llamadas a Pivot, tendremos un número de ticks simulados que dependerá de cómo se haya realizado la división. Debido a esto, ahora podemos realizar pequeños ajustes en los datos de las barras de 1 minuto, permitiendo que los datos originales todavía puedan ser utilizados de alguna manera. Lo primero es hacer una división simple del volumen real, de modo que cada tick simulado contenga una fracción del volumen total. Tras eso, hacemos un pequeño ajuste en el tiempo en el que aparecerá cada tick simulado. Una vez definidas las fracciones a usar, podemos entrar en el bucle y asegurarnos de que cada fracción se almacene en su respectivo tick. Por ahora, nos centraremos en que el sistema funcione correctamente. Aunque las funciones descritas pueden mejorarse considerablemente para hacer las cosas más interesantes. Cabe destacar que, a diferencia del tiempo, el volumen debe corregirse para mantenerse idéntico al original. Debido a este detalle, hay un último cálculo en este procedimiento que ajusta para que el volumen final de la barra de 1 minuto sea idéntico al volumen inicial.

Ahora examinaremos la última función de este artículo, la cual creará un pivote basado en los valores y parámetros proporcionados por el código previamente mostrado. Es importante destacar que los valores pueden ser ajustados según su interés, pero es necesario tener precaución al hacerlo para que la función subsiguiente funcione adecuadamente.

//+------------------------------------------------------------------+
#define macroCreateLeg(A, B, C) if (A < B)      {               \
                while (A < B)   {                               \
                        tick[C++].last = A;                     \
                        A += m_PointsPerTick;                   \
                                }                               \
                                                } else {        \
                while (A > B)   {                               \
                        tick[C++].last = A;                     \
                        A -= m_PointsPerTick;                   \
                                }               }
                        
inline void Pivot(const double p1, const double p2, int &t0, MqlTick &tick[], bool b0 = false)
                        {
                                double v0, v1, v2;
                                
                                v0 = (p1 > p2 ? p1 - p2 : p2 - p1);
                                v1 = p1 + (MathFloor((v0 * 0.382) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v2 = p1 + (MathFloor((v0 * 0.618) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v0 = p1;
                                macroCreateLeg(v0, v2, t0);
                                macroCreateLeg(v0, v1, t0);
                                macroCreateLeg(v0, p2, t0);
                                if (b0) tick[t0].last = v0;
                        }
#undef macroCreateLeg
//+------------------------------------------------------------------+

La función mencionada es sencilla en cuanto a su operatividad. A pesar de que sus cálculos pueden parecer extraños para muchos, al observar los valores utilizados para crear el pivote, notarán que siempre intentamos establecer un pivote utilizando la primera y tercera retracción de Fibonacci. Primero, es importante destacar que no importa si el pivote es ascendente o descendente; la función realizará los cálculos adecuadamente. Luego viene un aspecto que puede asustar a aquellos con poco conocimiento en programación: una MACRO. El motivo de usar una macro es que la creación de una parte del pivote se realiza más fácilmente mediante una macro. Sin embargo, nada le impide crear una función para esto. En realidad, si usáramos C++ de manera pura, es probable que esta macro tuviera un código bastante distinto. Pero aquí, utilizando MQL5 de la manera en que se creó, funciona como solución provisional.

Es mucho más eficiente usar esta macro que insertar el código dentro de las áreas donde se declara.


Conclusión

Siempre debemos priorizar un código fácil de leer y comprender, en vez de uno que, al requerir correcciones o modificaciones, nos haga perder horas intentando entender lo que hicimos. Con esto, concluimos este artículo. En el vídeo a continuación, podrán ver el resultado en esta fase actual de desarrollo, utilizando lo que se adjunta al artículo.

Sin embargo, quisiera señalar un problema que surgió al usar los ticks simulados. Se refiere al sistema de desplazamiento o búsqueda de una posición diferente de la que se encuentra el servicio de repetición. Este problema solo ocurrirá si usas ticks simulados. En el próximo artículo, abordaremos y corregiremos este asunto, además de hacer otras mejoras.



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

Archivos adjuntos |
Market_Replay.zip (50.42 KB)
Trading bursátil con cuadrícula usando un asesor con órdenes stop pendientes en la Bolsa de Moscú (MOEX) Trading bursátil con cuadrícula usando un asesor con órdenes stop pendientes en la Bolsa de Moscú (MOEX)
Hoy utilizaremos un enfoque comercial de cuadrícula con órdenes stop pendientes en un asesor experto en el lenguaje de estrategias comerciales MQL5 para MetaTrader 5 en la Bolsa de Moscú (MOEX). Al comerciar en el mercado, una de las estrategias más simples consiste en colocar una cuadrícula de órdenes diseñada para "atrapar" el precio del mercado.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 10): Sólo datos reales para la repetición Desarrollo de un sistema de repetición — Simulación de mercado (Parte 10): Sólo datos reales para la repetición
Aquí veremos cómo se pueden utilizar datos más fiables (ticks negociados) en el sistema de repetición, sin tener que preocuparnos necesariamente de si están ajustados o no.
Envolviendo modelos ONNX en clases Envolviendo modelos ONNX en clases
La programación orientada a objetos permite crear un código más compacto, fácil de leer y modificar. Le presentamos un ejemplo para tres modelos ONNX.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 09): Eventos personalizados Desarrollo de un sistema de repetición — Simulación de mercado (Parte 09): Eventos personalizados
Aquí veremos cómo accionar eventos personalizados y mejorar la cuestión de cómo el indicador informa del estado del servicio de repetición/simulación.