Repetición y simulación de mercado: Gran final
Introducción
Hola a todos y bienvenidos a un nuevo artículo de la serie dedicada a la construcción de un sistema de repetición/simulación.
En el artículo anterior, Simulación de mercado: La unión hace la fuerza (III), prácticamente concluimos esta etapa del desarrollo del sistema de repetición/simulador. Sin embargo, todavía falta mostrar cómo realizar un pequeño ajuste y mostrar algunos cambios necesarios para que el sistema de repetición/simulación pueda utilizarse con una metodología de entrenamiento adecuada. En este artículo terminaremos de abordar esos aspectos, ya que en el siguiente quiero comenzar a tratar otro tema. Quizá consideres, estimado lector, que todavía no hemos implementado todos los elementos necesarios para utilizar el sistema de repetición/simulador. Sin embargo, comprobarás que todos ellos ya están desarrollados o prácticamente terminados. Como quiero cerrar esta fase en este artículo, vayamos directamente a lo que nos interesa.
Ajustar el número de dígitos
Durante bastante tiempo, desarrollé el sistema con la intención de darlo por terminado en cuanto completara el sistema de representación gráfica. Sin embargo, algunas de las personas más cercanas propusieron ampliar el sistema de repetición/simulador para permitir también la simulación de operaciones. Esto suponía un desafío, ya que muchos no sabrían cómo implementar una funcionalidad semejante. Al principio comprobé que, efectivamente, se trataba de un reto, porque MetaTrader 5 no ofrecía soporte directo para alcanzar ese objetivo.
Por tanto, fue necesario implementar un indicador de posición. Este indicador se desarrolló y probó por completo mediante el servidor de trading real. Cuando alcanzó un estado adecuado, lo incorporamos al sistema de repetición/simulador. Sin embargo, al hacerlo apareció un fallo. Este fallo se debe a una propiedad que era necesaria para el sistema de representación gráfica, pero que terminó interfiriendo con el indicador de posición. Como consecuencia, los datos mostrados no coincidían con los que observaríamos al estar conectados al servidor real.
Corregir este fallo es muy sencillo. Sin embargo, habrá que hacer algunas modificaciones. Para empezar, fíjate en el fragmento que aparece a continuación.
105. //+------------------------------------------------------------------+ 106. int SetSymbolInfos(void) 107. { 108. int iRet; 109. 110. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5)); 111. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX); 112. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID); 113. 114. return iRet; 115. } 116. //+------------------------------------------------------------------+ 117. public : 118. //+------------------------------------------------------------------+ 119. C_FileTicks() 120. { 121. ArrayResize(m_Ticks.Rate, def_BarsDiary); 122. m_Ticks.nRate = -1; 123. m_Ticks.nTicks = 0; 124. m_Ticks.Rate[0].time = 0; 125. } 126. //+------------------------------------------------------------------+ 127. bool BarsToTicks(const string szFileNameCSV, int MaxTickVolume) 128. { 129. C_FileBars *pFileBars; 130. C_Simulation *pSimulator = NULL; 131. int iMem = m_Ticks.nTicks, 132. iRet = -1; 133. MqlRates rate[1]; 134. MqlTick local[]; 135. bool bInit = false; 136. 137. pFileBars = new C_FileBars(szFileNameCSV); 138. ArrayResize(local, def_MaxSizeArray); 139. Print("Converting bars to ticks. Please wait..."); 140. while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) 141. { 142. if (!bInit) 143. { 144. m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX); 145. pSimulator = new C_Simulation(SetSymbolInfos()); 146. bInit = true; 147. } . . . 165. } 166. //+------------------------------------------------------------------+ 167. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume) 168. { 169. int MemNRates, 170. MemNTicks, 171. nDigits, 172. nShift; 173. datetime dtRet = TimeCurrent(); 174. MqlRates RatesLocal[], 175. rate; 176. MqlTick TicksLocal[]; 177. bool bNew; 178. 179. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 180. nShift = MemNTicks = m_Ticks.nTicks; 181. if (!Open(szFileNameCSV)) return 0; 182. if (!ReadAllsTicks()) return 0; 183. rate.time = 0; 184. nDigits = SetSymbolInfos(); 185. m_Ticks.bTickReal = true; 186. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 187. { 188. if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; 189. if (!BuildBar1Min(c0, rate, bNew)) continue; 190. if (bNew) 191. { 192. if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) 193. { 194. nShift = MemShift; 195. ArrayResize(TicksLocal, def_MaxSizeArray); 196. C_Simulation *pSimulator = new C_Simulation(nDigits); 197. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0) 198. nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 199. delete pSimulator; 200.
Fragmento de C_FileTicks
Este fragmento pertenece a la clase C_FileTicks. En la línea 110 definimos un valor para la propiedad SYMBOL_DIGITS. Sin embargo, al hacerlo, el indicador de posición muestra los valores de una forma extraña. Por otro lado, la propiedad SYMBOL_DIGITS también se utiliza en la línea 184 para indicar al simulador, en la línea 196, cómo debe normalizar los valores que se generarán. Lo mismo ocurre en la línea 145. Muy bien. Modificaremos esta parte como se muestra en el siguiente fragmento.
105. //+------------------------------------------------------------------+ 106. int SetSymbolInfos(void) 107. { 108. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX); 109. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID); 110. 111. return (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5); 112. } 113. //+------------------------------------------------------------------+ 114. public : 115. //+------------------------------------------------------------------+ 116. C_FileTicks() 117. { 118. ArrayResize(m_Ticks.Rate, def_BarsDiary); 119. m_Ticks.nRate = -1; 120. m_Ticks.nTicks = 0; 121. m_Ticks.Rate[0].time = 0; 122. } 123. //+------------------------------------------------------------------+ 124. bool BarsToTicks(const string szFileNameCSV, int MaxTickVolume) 125. { 126. C_FileBars *pFileBars; 127. C_Simulation *pSimulator = NULL; 128. int iMem = m_Ticks.nTicks, 129. iRet = -1; 130. MqlRates rate[1]; 131. MqlTick local[]; 132. bool bInit = false; 133. 134. pFileBars = new C_FileBars(szFileNameCSV); 135. ArrayResize(local, def_MaxSizeArray); 136. Print("Converting bars to ticks. Please wait..."); 137. while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) 138. { 139. if (!bInit) 140. { 141. m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX); 142. pSimulator = new C_Simulation(SetSymbolInfos()); 143. bInit = true; 144. } . . . 162. } 163. //+------------------------------------------------------------------+ 164. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume) 165. { 166. int MemNRates, 167. MemNTicks, 168. nDigits, 169. nShift; 170. datetime dtRet = TimeCurrent(); 171. MqlRates RatesLocal[], 172. rate; 173. MqlTick TicksLocal[]; 174. bool bNew; 175. 176. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 177. nShift = MemNTicks = m_Ticks.nTicks; 178. if (!Open(szFileNameCSV)) return 0; 179. if (!ReadAllsTicks()) return 0; 180. rate.time = 0; 181. nDigits = SetSymbolInfos(); 182. m_Ticks.bTickReal = true; 183. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 184. { 185. if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; 186. if (!BuildBar1Min(c0, rate, bNew)) continue; 187. if (bNew) 188. { 189. if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) 190. { 191. nShift = MemShift; 192. ArrayResize(TicksLocal, def_MaxSizeArray); 193. C_Simulation *pSimulator = new C_Simulation(nDigits); 194. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0) 195. nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 196. delete pSimulator; 197.
Fragmento de C_FileTicks
Observa que el cambio es muy sutil. Sin embargo, también quiero permitir que se configure el número de dígitos. Para ello, modificaremos la clase C_ConfigService de modo que el usuario pueda indicar este valor, como se muestra en el siguiente fragmento.
060. //+------------------------------------------------------------------+ 061. inline bool Configs(const string szInfo) 062. { 063. const string szList[] = { 064. "PATH", 065. "POINTSPERTICK", 066. "VALUEPERPOINTS", 067. "VOLUMEMINIMAL", 068. "LOADMODEL", 069. "ACCOUNT", 070. "MAXTICKSPERBAR", 071. "DIGITS" 072. }; 073. string szRet[]; 074. char cWho; 075. 076. if (StringSplit(szInfo, '=', szRet) == 2) 077. { 078. StringTrimRight(szRet[0]); 079. StringTrimLeft(szRet[1]); 080. for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break; 081. switch (cWho) 082. { 083. case 0: 084. m_GlPrivate.szPath = szRet[1]; 085. return true; 086. case 1: 087. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[1])); 088. return true; 089. case 2: 090. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[1])); 091. return true; 092. case 3: 093. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[1])); 094. return true; 095. case 4: 096. m_GlPrivate.ModelLoading = StringInit(szRet[1]); 097. m_GlPrivate.ModelLoading = ((m_GlPrivate.ModelLoading < 1) && (m_GlPrivate.ModelLoading > 4) ? 1 : m_GlPrivate.ModelLoading); 098. return true; 099. case 5: 100. if (szRet[1] == "HEDGING") m_GlPrivate.AccountHedging = true; 101. else if (szRet[1] == "NETTING") m_GlPrivate.AccountHedging = false; 102. else 103. { 104. Print("Entered account type is not invalid."); 105. return false; 106. } 107. return true; 108. case 6: 109. m_GlPrivate.MaxTickVolume = (int) MathAbs(StringToInteger(szRet[1])); 110. return true; 111. case 7: 112. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, (int) MathAbs(StringToInteger(szRet[1]))); 113. return true; 114. 115. } 116. Print("Variable >>", szRet[0], "<< not defined."); 117. }else 118. Print("Configuration definition >>", szInfo, "<< invalidates."); 119. 120. return false; 121. } 122. //+------------------------------------------------------------------+
Fragmento de C_ConfigService
Observa lo sencillo que resulta. En la línea 71 convertimos a mayúsculas la cadena que deberá utilizar el usuario. Después solo tenemos que añadir el tratamiento correspondiente a esa cadena, lo que hacemos en la línea 111. A continuación, en la línea 112, indicamos dónde se utilizará el valor. Como se ha procesado correctamente, en la línea 113 devolvemos true para garantizar que el servicio de repetición/simulador se inicialice.
El siguiente código muestra cómo puede el usuario definir este valor en el archivo de configuración.
01. [Config] 02. Path = WDO 03. PointsPerTick = 0.5 04. ValuePerPoints = 5.0 05. VolumeMinimal = 1.0 06. Account = NETTING 07. Digits = 3 08. 09. [Bars] 10. WDON22_M1_202206140900_202206141759 11. 12. [ Ticks -> Bars] 13. 14. [ Bars -> Ticks ] 15. WDON22_M1_202206150900_202206151759 16. 17. [Ticks] 18.
Archivo de configuración
Como puedes ver, es muy sencillo. En la línea 7, el usuario indica cuántos dígitos se utilizarán para el símbolo. Esto permite que el indicador de posición muestre los valores tal como aparecerían si estuviéramos conectados al servidor real. Para que comprendas mejor lo que estoy explicando, veamos un caso práctico. El contrato de dólar presenta una variación de 0.5, es decir, medio punto, como suele decirse al operar con este contrato. Si configuras el archivo como se muestra en el código anterior, obtendrás el resultado que aparece a continuación.

Este resultado es incorrecto, porque, al utilizar el indicador de posición en una cuenta DEMO o REAL, el valor mostrado tendrá un dígito decimal, no tres. Para resolver el problema, basta con modificar la línea 7, en la que definimos el número de dígitos, tal como se muestra a continuación.
Digits = 1
De este modo obtendremos el siguiente resultado, que sí es correcto.

Sencillo, ¿verdad? El siguiente problema que quiero explicar puede surgir cuando no tomas las debidas precauciones al trabajar con SQL. Si modificas el código intentando hacer determinadas cosas, podrías terminar dañando la base de datos. Para explicar cómo evitarlo, pasemos a un nuevo apartado.
Evitar dañar la base de datos
Muchas personas evitan utilizar SQL o tienen grandes dificultades para hacerlo porque no comprenden que SQL no es un programa, sino un lenguaje. Si utilizas SQL correctamente, evitarás numerosos problemas y contratiempos. Sin embargo, si se emplea mal, puede convertirse en una pesadilla imposible de resolver. Uno de los factores que definitivamente daña cualquier base de datos es la presencia de registros duplicados o de registros cuyas columnas contienen valores anómalos. Esto destruye definitivamente la base de datos. Muchas personas creen que es necesario recurrir a una programación externa o extremadamente cuidadosa para evitar estos problemas. Sin embargo, la solución es muy sencilla, porque este tipo de situaciones puede evitarse mediante las propias restricciones de SQL. ¿Cómo? Basta con definir las restricciones necesarias. También podemos impedir que determinados registros se modifiquen o eliminen, pero dejaré este tema como ejercicio de investigación. Una pista: utiliza TRIGGERS, ya que constituyen la solución más sencilla.
En el constructor de la clase C_InServer, definimos mediante una instrucción SQL la creación de una tabla. Como nuestro código permite integrar e incluso ejecutar scripts SQL fuera del ejecutable principal, modificaremos el código para poder editar el script SQL desde un archivo. Sin embargo, incorporaremos este script como recurso del ejecutable. Ya he explicado anteriormente cómo hacerlo. Consulta los artículos dedicados a SQL dentro de esta serie. El código que debemos modificar aparece a continuación con su nuevo aspecto.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include "..\Defines.mqh" 05. #include "..\SQL\C_ReplayDataBase.mqh" 06. //+------------------------------------------------------------------+ 07. #resource "Script.sql" as string SQL_01 08. //+------------------------------------------------------------------+ 09. class C_InServer : public C_ReplayDataBase 10. { 11. private : 12. bool m_IsReplay; 13. struct stLocal 14. { 15. ulong numberMagic, 16. ticket; 17. int type; 18. double volume, 19. price, 20. sl, 21. tp; 22. }m_Info; 23. public : 24. //+------------------------------------------------------------------+ 25. C_InServer() 26. :C_ReplayDataBase(), 27. m_IsReplay(_Symbol == def_SymbolReplay) 28. { 29. ZeroMemory(m_Info); 30. ExecResourceSQL(SQL_01); 31. } 32. //+------------------------------------------------------------------+
Fragmento de C_InServer
El script se muestra a continuación.
CREATE TABLE IF NOT EXISTS tb_Replay ( magic NOT NULL DEFAULT 0, -- ID Expert Advisor ticket PRIMARY KEY NOT NULL, -- Position Ticket type NOT NULL, -- Position type volume NOT NULL, -- Position volume price NOT NULL, -- Opening point sl NOT NULL DEFAULT 0, -- Stop loss point tp NOT NULL DEFAULT 0, -- Take profit point history NOT NULL DEFAULT 0 -- If false, the position is open );
Script en SQL
Básicamente, aquí definimos en SQL cómo deberán crearse los registros. Es algo bastante sencillo. Si no sabes dónde colocar el archivo del script, debes guardarlo en la ubicación indicada en la siguiente imagen.

Cuando compiles el código, deberás ver el mensaje resaltado en la imagen que aparece a continuación.

Con esto eliminamos muchos de los problemas que podrían producirse en la base de datos. Es una solución sencilla, pero muy funcional. Llegados a este punto, prácticamente ya no tenemos que modificar nada más para ajustar el sistema. Sin embargo, todavía falta implementar un detalle: hacer que las líneas de Take profit y Stop Loss sean funcionales. Como verás, se trata de algo bastante sencillo. Para separar los temas, pasemos a un nuevo apartado.
Take profit y stop loss
Aquí existe una cuestión que genera dudas entre muchos operadores principiantes. Incluso algunos operadores con cierta experiencia en el mercado interpretan este tema de una forma, como mínimo, equivocada. Las líneas de take profit y stop loss no son órdenes pendientes. Son órdenes, pero de un tipo especial. Algunas plataformas e incluso algunos operadores hacen que este asunto parezca mucho más confuso de lo que realmente es. Lo menciono porque, aquí en Brasil, muchos analistas y operadores profesionales que utilizan otras plataformas tienen grandes dificultades para comprender lo que explicaré a continuación. Es incorrecto afirmar que debe ajustarse el denominado OFFSET para evitar que la línea de stop loss cierre la posición. Esta es la mayor de las falacias difundidas por operadores y analistas de mercado en Brasil. El llamado OFFSET, del que muchos hablan, solo existe en un tipo de orden denominado BUY STOP LIMIT o SELL STOP LIMIT, como puedes observar en la parte resaltada de la siguiente imagen.

Este valor, señalado en la imagen, solo se configura en órdenes pendientes antes de que se conviertan en una posición. No es necesario ajustar ningún offset para evitar que una posición permanezca abierta porque el stop loss o el take profit hayan sido sobrepasados. Por cierto, resulta curioso que, según esta idea, solo pueda sobrepasarse el stop loss y nunca el take profit. ¿Por qué será? En cualquier caso, no estoy aquí para tratar este asunto. Quiero mostrarte cómo hacer que las líneas de take profit y stop loss pasen a ser funcionales. Hasta ahora solo puedes abrir y cerrar una posición, pero debes hacerlo manualmente. Por tanto, dentro del sistema de repetición/simulador, las líneas de take profit y stop loss todavía no funcionan, aunque puedas manipularlas.
Para que estas líneas sean funcionales, solo necesitamos realizar una prueba. Nada más. Si la prueba da un resultado positivo, enviaremos una solicitud al sistema para que cierre la posición. Esta comprobación es bastante sencilla. Sin embargo, debemos programarla con cierto cuidado. Si lo haces de forma descuidada, crearás una falsa impresión que podría causarte algunos problemas. No obstante, precisamente esa programación descuidada de la comprobación también te permitirá hacer otra cosa que explicaré dentro de poco. Antes, veamos cómo debe programarse la comprobación para que el stop loss y el take profit comiencen a funcionar.
Existen varios puntos del sistema en los que podríamos implementar la comprobación necesaria. Algunos son más adecuados que otros, ya que necesitarás transferir entre aplicaciones el precio de cierre y los niveles de take profit y stop loss. El peor lugar para incluirla sería el Asesor Experto. Cada Asesor Experto que desarrolles tendría que incorporar el mismo código para comprobar el take profit y el stop loss cuando se utilice el Asesor Experto en el sistema de repetición/simulador. Definitivamente, es la peor opción. ¿Podríamos realizar la comprobación en el indicador de control? Sí, podríamos, pero existen lugares mejores. ¿Y en el indicador Chart Trade? También sería una opción viable y razonable, aunque todavía disponemos de alternativas más adecuadas.
Entonces, ¿qué tal si la realizamos en el indicador de posición? Sería perfecto, ¿verdad? En efecto, sería un lugar bastante conveniente, porque no necesitaríamos consultar en la base de datos los niveles de take profit y stop loss. El indicador de posición ya dispone de esos niveles. También sería interesante incluir la comprobación en el servicio de repetición. En ese caso, tendríamos que consultar periódicamente la base de datos. Esto permitiría que el sistema simulara determinadas situaciones específicas de una forma bastante interesante. Sin embargo, no quiero que el servicio consulte continuamente la base de datos. Por esta razón, realizaremos la comprobación en el indicador de posición. Aun así, considera la posibilidad de implementarla dentro del servicio de repetición. Comprobarás que también es una solución muy interesante.
Muy bien. Una vez tomada la decisión, veamos qué debemos hacer para que el código del indicador de posición pueda realmente «SIMULAR» lo que haría el servidor de trading real. Puedes observarlo en el siguiente fragmento.
018. //+------------------------------------------------------------------+ 019. struct st00 020. { 021. ulong ticket; 022. string szShortName, 023. szSymbol; 024. double priceOpen, 025. var, 026. sl, tp, 027. tickSize; 028. char digits; 029. bool bIsBuy; 030. }m_Infos; 031. //+------------------------------------------------------------------+ . . . 071. //+------------------------------------------------------------------+ 072. int OnInit() 073. { 074. ZeroMemory(m_Infos); 075. Order = new C_InServer(); 076. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 077. if (!CheckCatch(user00)) 078. { 079. ChartIndicatorDelete(0, 0, def_ShortName); 080. return INIT_FAILED; 081. } 082. 083. return INIT_SUCCEEDED; 084. } 085. //+------------------------------------------------------------------+ 086. int OnCalculate (const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], 087. const double &high[], const double &low[], const double &close[], const long &tick_volume[], 088. const long &volume[], const int &spread[]) 089. { 090. ProfitNow(); 091. 092. if ((close[rates_total - 1] == m_Infos.sl) || (close[rates_total - 1] == m_Infos.tp)) 093. EventChartCustom(0, evMsgClosePositionEA, m_Infos.ticket, 0, m_Infos.szSymbol); 094. 095. return rates_total; 096. } 097. //+------------------------------------------------------------------+ 098. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 099. { . . . 144. volume = (*Order)._PositionGetDouble(POSITION_VOLUME); 145. (*Open).UpdatePrice(0, m_Infos.priceOpen = (*Order)._PositionGetDouble(POSITION_PRICE_OPEN), volume, m_Infos.var); 146. (*Take).UpdatePrice(m_Infos.priceOpen, m_Infos.tp = (*Order)._PositionGetDouble(POSITION_TP), volume, m_Infos.var, (*Order)._PositionGetDouble(POSITION_SL)); 147. (*Stop).UpdatePrice(m_Infos.priceOpen, m_Infos.sl = (*Order)._PositionGetDouble(POSITION_SL), volume, m_Infos.var, (*Order)._PositionGetDouble(POSITION_TP)); 148. ProfitNow();
Fragmento del indicador de posición
Ahora analicemos lo que hace este código. En la línea 26 defino los nuevos valores que utilizaremos en el indicador. En la línea 74 inicializo la estructura de datos con valores cero para garantizar que las variables no contengan valores BASURA. A continuación, en las líneas 146 y 147, obtenemos los valores de take profit y stop loss. Hasta este punto no hemos hecho nada especialmente ambicioso. Ahora presta mucha atención a lo que ocurrirá dentro de la función OnCalculate, situada en la línea 86.
Observa que he modificado esta función con respecto a la versión anterior para asegurarnos de que se compruebe el valor almacenado en el array de precios de cierre. También podríamos haber conservado el modelo anterior, pero presentaba un problema: el usuario o el operador podía modificar el valor del array price, lo que invalidaría por completo el planteamiento utilizado en el indicador.
El detalle al que quiero que prestes atención, estimado lector, se encuentra en la línea 92. Antes de que se me olvide: si la prueba de la línea 92 tiene éxito, en la línea 93 dispararemos un evento. Su finalidad es enviar un mensaje personalizado al Asesor Experto. Este mensaje le indicará que debe enviar una solicitud al servidor para cerrar la posición. La posición afectada será precisamente la posición del indicador.
Volvamos ahora a la línea 92, porque ahí se encuentra la clave de estas comprobaciones. Si comprendes lo que ocurre en esta línea, podrás crear el indicador de órdenes pendientes. Además, si llegas a implementarlo, también conseguirás que funcione correctamente. Para tratar este asunto por separado, pasemos a un nuevo apartado. Aunque el título aborde otro tema, en realidad analizaremos lo que sucede en la línea 92. Por tanto, si todavía no comprendes bien cómo la condición de la línea 92 dispara el evento de la línea 93, presta mucha atención.
Tipos de órdenes pendientes y cómo hacer que funcionen
Aunque el título de este apartado sugiera que trataré directamente las órdenes pendientes, no será exactamente eso lo que haremos. Primero debemos comprender qué ocurre en la línea 92 del último fragmento del apartado anterior. Como puedes observar, allí comparo el precio de cierre con otros dos niveles: el take profit y el stop loss. Pues bien. Cuando cualquiera de estas pruebas da verdadero, se produce un disparo que hace que la posición se cierre durante la repetición/simulación. Pero ¿qué ocurriría si, en lugar de comprobar una igualdad, evaluáramos otra condición? ¿Qué ocurriría? Por ejemplo, supongamos que modificamos este indicador de posición para que, en vez de mostrar una posición abierta, muestre una orden pendiente. ¿Podríamos simular la apertura de una posición?
Si realmente has pensado en esta posibilidad, estimado lector, me gustaría entrevistarte y quizá contratarte, porque acabas de pensar cómo implementar el sistema de órdenes pendientes. Para comprenderlo, aunque no hayas considerado esta hipótesis, analizaremos varias figuras y veremos cómo pueden disparar eventos para simular la apertura de una posición sin modificar ninguna parte del código existente. Todos los mensajes o eventos personalizados que pueden crearse se muestran en la clase C_ChartFloatingRAD, entre las líneas 345 y 356. Solo tendrás que crear la cadena con los valores presentes en el indicador de órdenes pendientes. Después solo tendrás que realizar algunas modificaciones en el indicador de posición para convertirlo en un indicador de órdenes pendientes, y todo funcionará correctamente. La primera figura aparece en la siguiente imagen.

Este es el primer tipo de orden pendiente: una orden BUY LIMIT. Su funcionamiento es el siguiente: cuando el precio de cierre entre en la zona VERDE, es decir, cuando sea menor o igual que el precio de la orden, se ejecutará una compra a mercado. Podemos simular este comportamiento mediante la siguiente comprobación dentro de OnCalculate:
092. if (close[rates_total - 1] <= m_Infos.open) 093. EventChartCustom(...See the article for details...);
Observa que, en el momento exacto en que la prueba tenga éxito, se disparará un evento. Solo tienes que configurarlo para que el Asesor Experto interprete que procede de Chart Trade, aunque en realidad se haya originado en el indicador de órdenes pendientes. Recuerda finalizar el indicador de órdenes pendientes después de disparar el evento para evitar que siga disparando eventos de forma consecutiva.
Del mismo modo que hicimos con BUY LIMIT, tenemos una nueva figura, que aparece a continuación.

En este caso realizaremos una venta cuando el precio de cierre caiga por debajo del nivel indicado. El código de disparo será el mismo que vimos anteriormente. La única diferencia es que, en lugar de ejecutar una compra a mercado, ejecutarás una venta a mercado. Veamos ahora la siguiente figura.

En este caso, el planteamiento es diferente: no venderemos cuando el precio caiga, sino cuando suba por encima de un valor determinado. Este valor estará indicado por el precio de la orden pendiente. Para ello, debemos sustituir la comprobación por la que aparece a continuación.
092. if (close[rates_total - 1] >= m_Infos.open) 093. EventChartCustom(...See the article for details...);
Observa que el cambio en el código es muy sutil. Aun así, bastará con esta modificación para que funcione. El mismo código servirá para la figura que aparece a continuación.

La diferencia es que ahora el evento disparado no corresponderá a una venta a mercado, sino a una compra a mercado. ¿Has visto lo sencillo que resulta simular una orden pendiente? Por esta razón, estoy terminando aquí. No veo ningún motivo para publicar nuevos artículos únicamente para explicar este punto. Si has seguido esta serie desde el principio, podrás realizar fácilmente los ajustes necesarios para que el código pueda ejecutar las simulaciones de órdenes pendientes.
Sin embargo, existe otra cuestión que conviene tratar por separado para no confundirte, estimado lector. Pasemos, por tanto, a un nuevo apartado.
BUY STOP LIMIT y SELL STOP LIMIT
Muy bien. Si has comprendido cómo simular los tipos anteriores, estos serán todavía más sencillos de implementar. Puede parecer complicado, pero en realidad no lo es tanto. Solo necesitas comprender qué debe hacerse y ejecutar lo planificado.
Los tipos BUY STOP LIMIT y SELL STOP LIMIT no son, en realidad, órdenes directas, sino indirectas. Es decir, no se activan a cualquier precio ni se ejecutan inmediatamente cuando el precio alcanza un valor determinado. Estas órdenes están destinadas a evitar grandes picos de volatilidad del mercado. Por esta razón, considero que quizá no tenga mucho sentido que las implementes. No obstante, si deseas hacerlo, primero debes comprender cómo funcionan realmente. Para ello, observa las siguientes imágenes.


Ambas imágenes muestran cómo se componen una orden BUY STOP LIMIT y una orden SELL STOP LIMIT desde el punto de vista del servidor. De hecho, si colocas este tipo de orden en el gráfico de MetaTrader 5, verás que no aparecen tres líneas, sino cuatro. ¿Por qué? La razón es que tres de esas cuatro líneas representan una de las órdenes analizadas en el apartado anterior. La cuarta línea que aparece en el gráfico corresponde al nivel de disparo. Cuando se alcanza este nivel, se dispara uno de los cuatro tipos de órdenes que vimos anteriormente, que podrá ejecutarse de inmediato o permanecer pendiente. Aquí es donde interviene el denominado OFFSET.
Como puedes observar en las figuras, existe una distancia entre ambas órdenes. Esa distancia es el offset. Si la volatilidad es muy elevada, puede ocurrir que se active el nivel de activación, pero que la orden generada —una de las cuatro analizadas en el apartado anterior— no llegue a ejecutarse. En ese caso, verás una orden pendiente en el gráfico, en lugar de la posición abierta que muchos esperarían ver.
Como ya he explicado, este tipo de orden es bastante específico. Para crearla, primero debemos realizar una prueba basada en lo visto en el apartado anterior. Cuando esta primera prueba dé verdadero, dejaremos de ejecutarla y pasaremos a realizar una de las pruebas anteriores. Así de sencillo. Cuando esta segunda prueba dé verdadero, dispararemos el evento que simulará la apertura de la posición.
De este modo, con un esfuerzo mínimo, habremos implementado toda la simulación de órdenes pendientes.
Mostrar el historial
Como último aspecto de esta serie de artículos, veremos cómo mostrar el historial de las operaciones realizadas en el sistema de repetición/simulador. Esto puede hacerse de varias maneras. Todo dependerá de cómo quieras visualizar los datos. Como estarán disponibles en una base de datos SQL, cualquiera podrá analizar lo que haya ocurrido. Sin embargo, quizá prefieras ver el historial de las operaciones directamente en el gráfico para identificar posibles mejoras en la estrategia de negociación. Crear un indicador de este tipo es todavía más sencillo. No obstante, deberás añadir a la tabla de la base de datos la fecha y la hora de cada operación. Como se trata de una modificación muy sencilla, considero que cualquier entusiasta podrá implementarla. Una vez hecho esto, solo tendrás que recorrer la base de datos y buscar los registros cuya columna de historial tenga el valor uno.
Los valores iguales a cero indican que se trata de una posición abierta. Cuando obtengas los datos del registro, solo tendrás que añadir al gráfico algunos objetos de tipo OBJ_ARROW_BUY y OBJ_ARROW_SELL, junto con un objeto OBJ_TREND. Con ello se mostrará directamente en el gráfico el historial de operaciones. También puedes optar por crear estos objetos en el momento en que se cierre la posición. En cualquier caso, la forma de crear estos objetos apenas varía. Todo depende de lo que realmente quieras y necesites desarrollar.
Consideraciones finales
Por fin hemos llegado a un sistema de repetición/simulador que ya puedes utilizar, estimado y paciente lector. Sé que muchos podrían haber imaginado que publicaría más artículos para explicar otros aspectos del sistema. Sin embargo, lo considero innecesario, ya que todo lo que debe hacerse se ha explicado en este artículo. No consideres que dejar algunas partes por implementar sea una omisión injustificada por mi parte, sino un desafío para ti, que estás comenzando a estudiar programación. Yo mismo empecé de esta manera. Por tanto, si realmente deseas convertirte en un programador competente, comienza por los desafíos que he dejado abiertos. Lee atentamente los artículos y observa cómo fui construyendo el sistema de forma gradual, introduciendo cada vez menos modificaciones e intentando siempre reutilizar lo que ya existía o se había implementado. No intentes crear algo completamente desde cero sin probar antes a modificar los elementos existentes como he explicado a lo largo de esta serie.
Un último detalle: si utilizas en el indicador de posición una de las comprobaciones mostradas al explicar cómo simular las órdenes pendientes, podrás construir un sistema que seguirá controlando la posición. Aunque el precio se salte el nivel de stop loss o de take profit, el propio indicador de posición lo detectará y enviará una solicitud al Asesor Experto para que cierre la posición por ti.
Como he mencionado a lo largo de esta serie, cuando leas cualquiera de estos artículos, el código que contienen ya estará completamente obsoleto. En la serie dedicada a la construcción de un Asesor Experto automático no mostré lo que estoy explicando en este artículo ni en esta serie. Por tanto, no esperes que te enseñe todo de una sola vez. Sigue mis publicaciones y continúa estudiando.
Si no sabes cómo compilar el código completo del sistema de repetición/simulador, incluiré entre los archivos adjuntos todas las aplicaciones y los archivos necesarios para que dispongas, al menos, de un ejemplo. De este modo, quizá te interese estudiar y aprender a programar las partes que todavía faltan, pero que se han explicado en este último artículo. Por lo demás, les deseo mucha suerte. Nos veremos en el próximo artículo, en el que comenzaré a tratar otro tema. Quizá en el futuro actualice este sistema de repetición/simulador, pero solo DIOS sabe si realmente lo haré. 😂😁👍 Un fuerte abrazo a todos. Nos veremos en otro artículo.
| Archivo | Descripción |
|---|---|
| Experts\Expert Advisor.mq5 | Muestra la interacción entre Chart Trade y el Asesor Experto. Es necesario Mouse Study para la interacción. |
| Indicators\Chart Trade.mq5 | Crea la ventana en la que se configura la orden que se enviará. Es necesario Mouse Study para la interacción. |
| Indicators\Market Replay.mq5 | Crea los controles necesarios para interactuar con el servicio de repetición/simulador. Es necesario Mouse Study para la interacción. |
| Indicators\Mouse Study.mq5 | Permite la interacción entre los controles gráficos y el usuario. Es necesario tanto para utilizar el sistema de repetición/simulador como para operar en el mercado real. |
| Indicators\Order Indicator.mq5 | Se encarga de mostrar las órdenes de mercado y permite interactuar con ellas y controlarlas. |
| Indicators\Position View.mq5 | Se encarga de mostrar las posiciones de mercado y permite interactuar con ellas y controlarlas. |
| Services\Market Replay.mq5 | Crea y mantiene el servicio de repetición y simulación de mercado. Es el archivo principal de todo el sistema. |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/13582
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Automatización de estrategias de trading en MQL5 (Parte 20): Estrategia multisímbolo con CCI y AO
Del básico al intermedio: Sobrecarga de operadores (V)
Dominando los registros (Parte 9): Implementación del patrón Builder y adición de configuraciones predeterminadas
Motor de decision Multi-IA para MQL5 (Parte 5): backtest del motor en el simulador de estrategias y comparacion contra reglas simples.
- 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