Desarrollo de un sistema de repetición — Simulación de mercado (Parte 25): Preparación para la próxima etapa
Introducción
En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 24): FOREX (V)", demostré cómo es posible integrar, de manera bastante armoniosa, dos universos que a primera vista parecen distintos. De un lado, el mercado con el precio de la graficación en el BID y, de otro, con el precio en el LAST. El verdadero desafío fue crear un método que permitiera simular, o más precisamente, generar un movimiento probable en el precio. contando solo con las barras, que idealmente representan el tiempo gráfico de 1 minuto. Ese fue, sin duda, un desafío intrigante y satisfactorio de superar. La solución presentada, aunque eficaz, no es la única manera de alcanzar ese objetivo específico. Sin embargo, como la solución se mostró eficiente, considero esa etapa concluida. Es decir, hasta que se demuestre incapaz de resolver un modelo específico. En ese caso, volveremos a mejorar la solución propuesta para que pueda cubrir un modelo hasta entonces no resuelto.
Existen ciertos ajustes que todavía necesitamos realizar. Aunque, en verdad, no se trata exactamente de realizar cambios, sino más bien de eliminar algunas funcionalidades que podrían interferir significativamente con elementos que aún pretendemos implementar. Uno de esos aspectos es la capacidad de retroceder en el tiempo.La cuestión de retroceder en el tiempo utilizando el indicador de control es algo que debería eliminarse del sistema. Esta funcionalidad, a pesar de ser interesante, se revela impracticable a largo plazo y, aunque no ha causado problemas hasta ahora, seguramente los traerá cuando implementemos nuevas funcionalidades. Tú puedes haber considerado bastante interesante la idea de retroceder en el tiempo usando el indicador de control. De hecho, coincido en que es un concepto interesante, sin embargo, en la práctica, no se muestra tan funcional. La posibilidad de volver en el tiempo puede, en muchas situaciones, causar grandes dolores de cabeza al intentar resolver los problemas que introduce.
Eliminar este recurso de retorno en el tiempo no es particularmente difícil, solo un poco tedioso, ya que exige la adición de pruebas y verificaciones al indicador de control. Además, estoy considerando eliminar otro elemento del sistema, decisión que tomaré a lo largo de este artículo. Junto a esta alteración en el indicador de control, abordaré algunas otras cuestiones que necesitan refinamiento para asegurar que el servicio funcione de manera eficiente, sin cuellos de botella. Te invito a seguir el desarrollo presentado en este artículo, que promete ser bastante instructivo. Vamos a explorar muchos conceptos interesantes, que sin duda contribuirán a tu aprendizaje en programación y desarrollo de sistemas. Ahora, vamos a comenzar con el primer tema de este artículo.
Restringimos el uso del indicador de control
Iniciaremos imponiendo algunas restricciones al indicador de control para impedir que el usuario vuelva en el tiempo. Cuando me refiero a "volver en el tiempo", quiero decir que, después de un determinado progreso, ya no será posible usar el indicador de control para regresar a una posición anterior. Para revertir acciones, será necesario cerrar el servicio de repetición/simulación y reiniciar el proceso desde el inicio. Entiendo que esta limitación puede parecer desalentadora, pero, créanme, este enfoque evitará muchos problemas futuros que surgirían al intentar utilizar la funcionalidad de retorno en el tiempo.
Implementar esta restricción no es difícil, pero demanda cierto esfuerzo, ya que exige la adición de pruebas específicas en el sistema. Estas pruebas deben aplicarse cuidadosamente para no crear conflictos con otras funcionalidades del indicador, permitiendo que opere eficientemente. Dividiremos esta tarea en etapas, facilitando la implementación de los cambios de manera efectiva.
Primera etapa: Activación y desactivación de los botones de ajuste fino
En esta fase inicial, el procedimiento es bastante directo. Se trata de activar o desactivar el acceso a los botones de ajuste fino, ubicados en los extremos de la barra de control. Estos botones pueden visualizarse en la imagen a continuación:
Figura 01: Ubicación de los Botones de Ajuste Fino
Los botones en cuestión facilitan el ajuste preciso del avance deseado. Con ellos, es posible avanzar o retroceder con gran precisión una cantidad determinada de tiempo, lo cual es bastante útil. Sin embargo, para evitar que el usuario retroceda en el tiempo, es esencial ocultar o mostrar estos botones según sea necesaria su presencia. Para comprender mejor esta etapa, considera lo siguiente: ¿por qué mantener el botón a la izquierda activo si el sistema no ha avanzado ninguna posición? O, ¿por qué necesitar el botón a la derecha cuando el sistema ha alcanzado su límite máximo de avance? O sea, la repetición/simulación estará creando y colocando los últimos ticks de un sistema de barras, ¿necesitamos de hecho el botón derecho? Esto no tiene sentido, ¿verdad? Por lo tanto, el objetivo de esta etapa es informar al usuario de que no es posible avanzar o retroceder más allá de un límite establecido.
Realizar esta tarea es simple y directo, ya que lo principal es probar los límites. Si alcanzamos esos límites, donde el movimiento es imposible, evitaremos la exhibición del botón. Sin embargo, optaré por un enfoque ligeramente diferente, que, en mi opinión, hace el resultado más interesante. Primero, no es necesario desarrollar mucho código, solo hacer pequeños ajustes. El primer paso involucra incluir los bitmaps que representarán los botones cuando estén deshabilitados, como un recurso del indicador de control. Esto se realiza de la siguiente manera:
#define def_ButtonPlay "Images\\Market Replay\\Play.bmp" #define def_ButtonPause "Images\\Market Replay\\Pause.bmp" #define def_ButtonLeft "Images\\Market Replay\\Left.bmp" #define def_ButtonLeftBlock "Images\\Market Replay\\Left_Block.bmp" #define def_ButtonRight "Images\\Market Replay\\Right.bmp" #define def_ButtonRightBlock "Images\\Market Replay\\Right_Block.bmp" #define def_ButtonPin "Images\\Market Replay\\Pin.bmp" #define def_ButtonWait "Images\\Market Replay\\Wait.bmp" #resource "\\" + def_ButtonPlay #resource "\\" + def_ButtonPause #resource "\\" + def_ButtonLeft #resource "\\" + def_ButtonLeftBlock #resource "\\" + def_ButtonRight #resource "\\" + def_ButtonRightBlock #resource "\\" + def_ButtonPin #resource "\\" + def_ButtonWait
Estas líneas añaden, internamente al indicador de control, los bitmaps que simbolizan los botones deshabilitados, ya sea en el límite superior o inferior de la escala. Esto hace la interfaz más atractiva, permitiendo que crees botones con una apariencia más alineada a lo que deseas ofrecer al usuario. Siéntete libre de hacer cambios. Tras este paso, es necesario hacer referencia a estos valores. El código está casi completo, solo necesitamos vincular estos recursos. La referencia se hace en el fragmento siguiente:
void CreteCtrlSlider(void) { u_Interprocess Info; m_Slider.szBarSlider = def_NameObjectsSlider + " Bar"; m_Slider.szBtnLeft = def_NameObjectsSlider + " BtnL"; m_Slider.szBtnRight = def_NameObjectsSlider + " BtnR"; m_Slider.szBtnPin = def_NameObjectsSlider + " BtnP"; m_Slider.posY = 40; CreteBarSlider(82, 436); CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock); CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock); CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0; PositionPinSlider(Info.s_Infos.iPosShift);
Insertar estas referencias permite que el objeto responsable de los botones los maneje de manera que alcance el resultado deseado. Observen que, hasta ahora, no he añadido nada más allá de las referencias a los recursos, y el sistema ya puede realizar la función esperada. Sin embargo, para cambiar los botones al alcanzar los límites de ajuste, es necesario añadir un poco más de código. Pero no se preocupen, pues es una tarea bastante simple y comprensible. El código necesario se presenta a continuación:
inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < 0 ? 0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ChartRedraw(); }
Introduje un nuevo parámetro en la llamada, pero, como utilizaremos el sistema en modo estándar inicialmente, ese parámetro comienza con valor cero. Esto significa que, por ahora, no hay necesidad de cambios. Con esto establecido, podemos proceder a probar los límites en determinadas situaciones: para activar o desactivar el botón a la izquierda del control, usaremos el siguiente cálculo. Ya para desactivar el botón en la esquina derecha del control, aplicaremos otro cálculo. En el caso del botón derecho, el cálculo considerará solo si el control deslizante ha alcanzado o no el límite superior. Sin embargo, el botón izquierdo funcionará de manera diferente, basándose inicialmente solo en el valor cero. Tras compilar el código del indicador de control y ejecutar el servicio de repetición/simulación, observaremos el comportamiento demostrado en la animación a continuación:
Animación 01: Demostración del sistema de habilitar/deshabilitar botón
La solución se mostró extremadamente simple de entender e implementar, representando un excelente punto de partida para lo que realmente necesitamos desarrollar. Ahora, enfrentaremos una tarea un poco más compleja, pero necesaria para asegurar que el usuario entienda lo que está sucediendo. Vamos a abordar este asunto en detalle en el próximo tema.
Informamos al usuario sobre el cambio de límites
Podríamos mantener el proceso bastante simple, solo activando y desactivando el botón de límite inferior conforme el control deslizante alcanzase el punto mínimo indicado por el servicio de repetición/simulación. Sin embargo, esto podría confundir al usuario, que no conseguiría mover el control hasta la parte inferior, es decir, hasta el punto cero. Para aclarar mejor, vea la animación a continuación:
Animación 02: Pero ¿por qué no puedo ir hasta el Cero?
En la animación 02, queda evidente la confusión que puede surgir para el usuario cuando el control deslizante no alcanza el cero, a pesar de que el botón izquierdo señaliza la imposibilidad de movimiento. Esta situación revela que la indicación actual no es suficientemente clara, sugiriendo la necesidad de mejorar la comunicación sobre las limitaciones o barreras existentes, que impiden el avance del control deslizante más allá de cierto punto. Ahora, antes de detallar cómo se implementará esta indicación, debes estar curioso acerca del método utilizado para bloquear el control antes de que alcance el punto cero. ¡Admite que la curiosidad es grande! Afortunadamente, no recurrí a ningún truco complicado de programación; simplemente definí un punto de parada. Pero ¿dónde, exactamente? La ubicación puede verse más abajo:
inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ChartRedraw(); }
Debes estar preguntándote, "¿Qué se ha hecho aquí?" No te preocupes, hay un detalle sutil, pero significativo, a observar: la variable `minimal` está definida como cero. ¿Y si cambiamos ese valor a, digamos, 100 o 80, qué sucedería? Probar el valor en ese punto resultaría en la desactivación del botón en la esquina izquierda. Sin embargo, eso no evitaría que el sistema disminuyera el valor si el usuario hiciera clic en el botón izquierdo o arrastrara el control deslizante hacia la izquierda. Esto es correcto. No obstante, ahora estoy fijando el control deslizante en una posición determinada precisamente por la variable `minimal`. ¿Comprenden? No importa cuánto el usuario intente mover el control deslizante o presionar el botón izquierdo; el punto indicado no caerá por debajo del valor establecido como el mínimo posible.
Interesante, ¿verdad? La definición del valor mínimo posible es tarea del servicio de repetición/simulación, que ajusta ese valor automáticamente a medida que la repetición o la simulación avanza. Sin embargo, el usuario tiene la libertad de cambiar ese punto, siempre que el servicio no haya modificado el valor mínimo que puede utilizarse. Puede parecer complejo, pero es más simple de lo que imaginas, aunque profundizaremos en ello más adelante. Por ahora, vamos a centrarnos en la cuestión planteada por la animación 02, que muestra la falta de una indicación clara al usuario sobre la posición del límite inferior. Existen varias maneras de hacer esto, algunas pueden parecer estéticamente extrañas y otras, bueno, algo bizarras. Sin embargo, podemos optar por una solución intermedia. ¿Qué tal crear una indicación de "pared"? A mi parecer, es una elección acertada, ya que puede ofrecer un aspecto visual interesante. Si eres un artista gráfico, el resultado puede ser aún mejor que el ejemplo que estoy utilizando. Para ello, usaremos el siguiente código:
inline void CreteBarSlider(int x, int size) { ObjectCreate(m_id, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XDISTANCE, x); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - 4); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XSIZE, size); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YSIZE, 9); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_WIDTH, 3); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT); //--- ObjectCreate(m_id, m_Slider.szBarSliderBlock, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, x); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Slider.posY - 9); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, 19); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED); }
Las líneas destacadas en verde indican el código que crea tal indicación de límite inferior. Y sí, estamos utilizando un objeto para eso. Pero, si prefieres, puedes emplear una imagen bitmap para obtener resultados visuales más atractivos. Sin embargo, quiero mantener el código simple, considerando que muchos de los lectores pueden tener conocimiento limitado en programación. Así, un código más accesible facilita el entendimiento de cómo se implementaron las cosas. Añadir una imagen bitmap o incluso un patrón de textura no es complicado, y los resultados pueden ser bastante interesantes, especialmente si utilizas programación a través de DirectX. Y sí, MQL5 permite eso. Pero dejaremos eso para otro momento. Por ahora, vamos a mantener las cosas simples, pero funcionales. Con esto, el resultado será como se muestra en la animación 03, a continuación:
Animación 03: Ahora tenemos una indicación de límite inferior...
La introducción de la barra indicadora de límite inferior facilitó considerablemente para los usuarios entender por qué no pueden retroceder más en la repetición/simulación. Sin embargo, puedes haber notado que el código presentado anteriormente no especifica cómo esta barra indicadora de límite inferior se ajustará en tamaño. El código que define ese tamaño se muestra a continuación:
inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + 2); ChartRedraw(); }
El tamaño de la barra indicadora está determinado por la variable `minimal`, lo que significa que, a medida que el servicio de repetición/simulación actualiza sus datos, la barra se ajustará proporcionalmente. Ahora, el próximo paso es asegurar que esta limitación sea adecuadamente actualizada por el servicio de repetición/simulación. Este tema será abordado en el próximo tema.
Conversamos con el servicio de repetición/simulación
Ahora que la base del sistema está montada, impidiendo al usuario retroceder en el tiempo en el indicador de control, necesitamos que el servicio de repetición/simulación comunique al indicador de control a partir de qué punto el usuario no podrá retroceder más. Esta tarea es relativamente más simple en comparación a lo que ya hemos realizado. Lo esencial es verificar la posición actual del servicio de repetición/simulación en el momento de la pausa. Esta parte es directa. Veamos entonces cómo implementar la funcionalidad necesaria. Inicialmente, es necesario hacer una pequeña alteración en el código, que ahora será así:
class C_Controls { private : //+------------------------------------------------------------------+ string m_szBtnPlay; long m_id; bool m_bWait; struct st_00 { string szBtnLeft, szBtnRight, szBtnPin, szBarSlider, szBarSliderBlock; int posPinSlider, posY, Minimal; }m_Slider; //+------------------------------------------------------------------+ void CreteCtrlSlider(void) { u_Interprocess Info; m_Slider.szBarSlider = def_NameObjectsSlider + " Bar"; m_Slider.szBarSliderBlock = def_NameObjectsSlider + " Bar Block"; m_Slider.szBtnLeft = def_NameObjectsSlider + " BtnL"; m_Slider.szBtnRight = def_NameObjectsSlider + " BtnR"; m_Slider.szBtnPin = def_NameObjectsSlider + " BtnP"; m_Slider.posY = 40; CreteBarSlider(82, 436); CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock); CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock); CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0; m_Slider.Minimal = Info.s_Infos.iPosShift; PositionPinSlider(Info.s_Infos.iPosShift); } //+------------------------------------------------------------------+ inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); m_Slider.posPinSlider = (p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != m_Slider.Minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + 2); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + 2); ChartRedraw(); } //+------------------------------------------------------------------+
Notarás que el código ha sufrido algunas alteraciones simples, pero suficientes para que la barra de limitación sea creada y configurada correctamente, así como los botones de control. Para ello, fue necesario mover la variable de la llamada de la función para dentro de la estructura, inicializándola en un punto específico del código para que después pueda ser accedida en los lugares apropiados. ¿Y por qué opté por esta aproximación? Esto se hizo para evitar alteraciones en otros puntos del código. Cada vez que el servicio de repetición/simulación sea pausado, habrá una llamada a la función `CreateCtrlSlider`. Aunque algunos objetos sean destruidos, esta llamada a la función aún ocurrirá, simplificando toda la lógica de creación.
Ahora que hemos resuelto la cuestión del indicador de control, es hora de concentrarnos en el código del servicio de repetición/simulación para realizar algunas modificaciones. Aunque muchas de estas alteraciones son más estéticas, es fundamental tener un sistema fluyendo sin problemas antes de enfrentarnos a cuestiones más complejas.
Resolvemos los problemas estéticos en el servicio de repetición/simulación
El primer problema que necesitamos abordar no es meramente estético, sino una falla técnica. Ocurre cuando se solicita al servicio de repetición/simulador moverse a una posición futura antes incluso de iniciar la reproducción. En otras palabras, si acabas de abrir el servicio y, en lugar de dar play, decides avanzar el gráfico a algunas posiciones adelante y solo después inicias el play, surgirá un problema en la exhibición correcta del gráfico. Para corregir esto, es necesario forzar al sistema a ejecutar un "falso play", para luego moverse a la posición indicada por el control deslizante. La modificación necesaria en el código es la siguiente:
void AdjustPositionToReplay(const bool bViewBuider) { u_Interprocess Info; MqlRates Rate[def_BarsDiary]; int iPos, nCount; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if ((m_ReplayCount == 0) && (m_Ticks.ModePlot == PRICE_EXCHANGE)) for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++); if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return; 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); CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX); if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); 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) { CreateBarInReplay(true); 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++); nCount = CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount); } for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(false); CustomTicksAdd(def_SymbolReplay, m_Ticks.Info, m_ReplayCount); Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = false; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); }
La llamada de código mencionada es crucial para generar el "falso play" necesario. Sin esta llamada, el error de trazado en el gráfico se manifestará. Además, incluí una línea adicional en el código que añade los ticks faltantes en la ventana de observación del mercado, proporcionando una simulación más realista e interesante. Otras modificaciones fueron hechas en el código, como indicado por las líneas que fueron eliminadas. Estos cambios se derivan del hecho de que una prueba específica previene la entrada en el sistema si estamos en la misma posición de desplazamiento. Esto es una consecuencia directa de nuestra decisión de no permitir que el usuario retroceda en el tiempo, por lo tanto, los códigos asociados a esa funcionalidad pueden ser eliminados sin causar problemas.
Ahora que hemos corregido esa falla, vamos a dedicarnos a un problema estético que persiste desde hace tiempo, pero que ahora tenemos la oportunidad de resolver, haciendo la experiencia del usuario con el servicio de repetición/simulación más agradable. Este problema estético ocurre cuando un archivo es seleccionado para representar las barras anteriores en el gráfico. Al abrir el gráfico por el servicio de repetición/simulación, inicialmente, no se muestran las líneas de precio, lo que, aunque no afecta la funcionalidad del sistema, desde el punto de vista estético, es incómodo observar el gráfico sin ninguna línea de precio. Para corregir o mejor, resolver ese detalle, son necesarias algunas modificaciones. La primera de ellas se presenta en el código a continuación:
bool LoopEventOnTime(const bool bViewBuider) { u_Interprocess Info; int iPos, iTest; if (!m_Infos.bInit) ViewInfos(); if (!m_Infos.bInit) { ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE); m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); m_MountBar.Rate[0].time = 0; m_Infos.bInit = true; ChartRedraw(m_IdReplay); } iTest = 0; while ((iTest == 0) && (!_StopFlag)) { iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1); iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1); iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest); if (iTest == 0) Sleep(100); } if ((iTest < 0) || (_StopFlag)) return false; AdjustPositionToReplay(bViewBuider); iPos = 0; while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag)) { iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0); CreateBarInReplay(true); while ((iPos > 200) && (!_StopFlag)) { if (ChartSymbol(m_IdReplay) == "") return false; GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value); if (!Info.s_Infos.isPlay) return true; Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); Sleep(195); iPos -= 200; } } return (m_ReplayCount == m_Ticks.nTicks); }
Vamos a eliminar las partes del código tachadas y agregar la nueva línea resaltada. Podríamos incluir el código de esa llamada aquí, pero es probable que ese código se transfiera a otra función en el futuro. Por lo tanto, para facilitar esa futura portabilidad, prefiero ensamblar el código necesario en otro lugar.
Para resolver el problema estético de que las líneas de precio no se muestren inmediatamente al abrir el gráfico por el servicio de repetición/simulación, el código necesario es el siguiente:
void ViewInfos(void) { MqlRates Rate[1]; ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE); m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); m_MountBar.Rate[0].time = 0; m_Infos.bInit = true; CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, Rate); if ((m_ReplayCount == 0) && (m_Ticks.ModePlot == PRICE_EXCHANGE)) for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++); if (Rate[0].close > 0) { if (m_Ticks.ModePlot == PRICE_EXCHANGE) m_Infos.tick[0].last = Rate[0].close; else { m_Infos.tick[0].bid = Rate[0].close; m_Infos.tick[0].ask = Rate[0].close + (Rate[0].spread * m_Infos.PointsPerTick); } m_Infos.tick[0].time = Rate[0].time; m_Infos.tick[0].time_msc = Rate[0].time * 1000; }else m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount]; CustomTicksAdd(def_SymbolReplay, m_Infos.tick); ChartRedraw(m_IdReplay); }
Estas líneas de código se extrajeron de la función que tuvo partes tachadas. El enfoque real aquí son las líneas adicionales necesarias. Lo que hacemos es identificar la última barra colocada en el gráfico por el servicio de repetición/simulador, utilizando una función común para trabajar con indicadores. Si logramos capturar la barra, es decir, si el valor de cierre es mayor que cero, montaremos un tick especial, dependiendo del modo de trazado utilizado. Si el valor de cierre es cero, utilizaremos el primer tick válido de la lista de ticks cargados o simulados. La función responsable de encontrar un tick válido es exactamente las dos líneas mencionadas. Esta función será especialmente útil al trabajar en el modo de trazado LAST, ya que en el modo BID, el primer tick ya es válido. De cualquier manera, al final, ese tick especial creado se mostrará en la ventana de observación de mercado, haciendo que las líneas de precio aparezcan en el gráfico tan pronto como el servicio instruya a la plataforma MetaTrader 5 a abrir el gráfico.
La necesidad de otra modificación surge de un problema con el repetición/simulación, que, a pesar de funcionar de manera algo precaria, no indica ninguna barra anterior al conjunto de datos que será presentado. Esto puede resultar en que el trazado de la primera barra sea cortado. Para resolver este problema de forma definitiva, necesitamos especificar una barra como anterior a todo el conjunto que se presentará posteriormente. Esta modificación en el código permitirá que el sistema funcione adecuadamente en gráficos con diferentes intervalos de tiempo, desde 1 minuto hasta diario o incluso semanal, considerando que un gráfico mensual sería exagerado.
inline void FirstBarNULL(void) { MqlRates rate[1]; int c0 = 0; for(; (m_Ticks.ModePlot == PRICE_EXCHANGE) && (m_Ticks.Info[c0].volume_real == 0); c0++); rate[0].close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? m_Ticks.Info[c0].last : m_Ticks.Info[c0].bid); rate[0].open = rate[0].high = rate[0].low = rate[0].close; rate[0].tick_volume = 0; rate[0].real_volume = 0; rate[0].time = macroRemoveSec(m_Ticks.Info[c0].time) - 86400; CustomRatesUpdate(def_SymbolReplay, rate); m_ReplayCount = 0; }
El primer paso es encontrar un tick válido, especialmente si el sistema de trazado utiliza el precio LAST. Hecho esto, construimos una barra anterior usando el primer precio válido de la serie de ticks que se usará en el repetición o en la simulación. El aspecto crucial aquí es la indicación de la posición en el tiempo, que se ajusta restando el valor de 1 día en términos de minutos. Esto asegura que la barra anterior aparezca en el gráfico de manera adecuada, desplazada lo suficiente para que sea completamente visible incluso en un gráfico diario. Este sistema es efectivo tanto para datos de mercados de divisas como para mercados de acciones.
Conclusión
Con los archivos adjuntos proporcionados, podrás probar la implementación actual del servicio de repetición/simulación. El sistema básico está listo, pero como aún no estamos utilizando algunas funcionalidades, serán necesarios más cambios y ajustes para adaptar el sistema a un modo de entrenamiento más efectivo. Por ahora, considera el sistema de repetición/simulación como completo. En los próximos artículos, exploraremos formas de mejorarlo aún más, marcando el inicio de una nueva fase en el desarrollo de este sistema.
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/11203
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso