Simulación de mercado: Position View (XIX)
Introducción
Hola a todos y bienvenidos a un nuevo artículo de la serie sobre la construcción de un sistema de repetición/simulación.
En el artículo anterior, Simulación de mercado: Position View (XVIII), mostré cómo resolver algunas situaciones. Por ejemplo, expliqué cómo evitar que el usuario u operador eliminara los objetos gráficos creados por el indicador de posición. Lo hicimos de una forma muy sencilla, sin grandes complicaciones, ya que volver a colocar los objetos en el orden correcto puede resultar bastante costoso y obligarnos a realizar auténticos malabarismos de programación. También añadimos una indicación del volumen, un dato que algunos operadores utilizan para determinar si pueden aplicar alguna estrategia sobre la posición.
Esta información sirve para aumentar de forma consciente una posición que ya genera beneficios o para reducir el riesgo. Si mantienes un volumen considerablemente elevado, un movimiento del mercado contrario a tu posición puede provocar una pérdida muy grande. En muchas ocasiones, esto hace que el riesgo termine cerrando la operación y que, poco después, veas cómo el mercado avanza en la dirección que habías previsto. Además de ser una situación muy frustrante, afecta gravemente al estado psicológico del operador y le hace perder por completo la confianza.
Todo esto se ha implementado de manera bastante sencilla y con muy pocos cambios en el código existente. Confío en que estés siguiendo el desarrollo y comprobando que no resulta muy complicado. Solo debemos analizar con calma cada situación antes de modificar el código. Según el caso, un pequeño cambio en la forma en que se ha implementado una parte del sistema será más que suficiente para resolver un problema concreto.
Ahora, sin embargo, debemos abordar una cuestión algo más compleja cuya resolución he pospuesto durante bastante tiempo. No pienses que el trabajo será extremadamente difícil. Notarás que, aunque el problema presenta cierta complejidad, lo explicaré de una forma sencilla y fácil de comprender. Así que pongámonos manos a la obra.
Liberar la clase C_ElementsTrade
Una de las cuestiones que más me ha incomodado es que la clase C_ElementsTrade contenga código que permite acceder a las posiciones. No lo interpretes como un fallo, porque en realidad no lo es. Sin embargo, hace que algunas de las tareas que deberemos realizar más adelante sean algo más propensas a errores. Todo el trabajo destinado a implementar el indicador de posición se ha desarrollado pensando en utilizarlo dentro del sistema de repetición/simulador. Sin embargo, cuando se ejecute en ese entorno, no tendremos acceso alguno a una posición real. Por tanto, cualquier llamada a la biblioteca de MQL5 destinada a consultar datos de una posición no tendrá ningún efecto en el código y supondrá más un inconveniente que una ventaja, ya que introducirá dependencias en la clase C_ElementsTrade.
Estas dependencias no representan un problema cuando se encuentran en el código principal, es decir, en el código donde se crea el indicador. Sin embargo, mantenerlas dentro de un archivo de cabecera sí constituye un problema que debemos resolver antes de implementar los cambios necesarios para utilizar el indicador de posición junto con el servicio de repetición/simulador.
Por suerte, hasta este punto del desarrollo hemos tenido mucho cuidado con la implementación para reducir al mínimo las dependencias o vínculos con llamadas destinadas a obtener datos de una posición. En esencia, todas ellas se concentran en un único procedimiento: DispatchMessage. Veamos dónde se generan estas dependencias. Para ello, observa el siguiente fragmento, que contiene el código del procedimiento DispatchMessage de la clase C_ElementsTrade.
240. //+------------------------------------------------------------------+ 241. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 242. { 243. string sz0; 244. long _lparam = lparam; 245. double _dparam = dparam; 246. 247. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 248. switch (id) 249. { 250. case (CHARTEVENT_KEYDOWN): 251. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 252. _lparam = (long) m_Info.ticket; 253. _dparam = 0; 254. EventChartCustom(0, evUpdate_Position, _lparam, 0, ""); 255. case CHARTEVENT_CUSTOM + evMsgSetFocus: 256. if ((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)) 257. UpdatePrice(m_Info.open, GetPositionsMouse().Position.Price); 258. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 259. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 260. m_Info.bClick = false; 261. case CHARTEVENT_CHART_CHANGE: 262. ChartChange(); 263. break; 264. case CHARTEVENT_CUSTOM + evMsgSwapViewModePosition: 265. m_Info.ViewMode = (stInfos::e1)((((stInfos::e1)_dparam) + 1) & 0x03); 266. for (int c0 = PositionsTotal() - 1; c0 >= 0; c0--) 267. EventChartCustom(0, evUpdate_Position, PositionGetTicket(c0), 0, NULL); 268. break; 269. case CHARTEVENT_OBJECT_CLICK: 270. sz0 = GetPositionsMouse().szObjNameClick; 271. if (m_Info.bClick) 272. { 273. if (sz0 == def_NameBtnMove) 274. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, m_Info.ev, ""); 275. if (sz0 == def_NameBtnClose) 276. EventChartCustom(0, (ushort) m_Info.ev, m_Info.ticket, PositionGetDouble(m_Info.ev == evMsgCloseTakeProfit ? POSITION_SL : POSITION_TP), PositionGetString(POSITION_SYMBOL)); 277. if (sz0 == def_NameObjLabel) 278. EventChartCustom(0, evMsgSwapViewModePosition, m_Info.ticket, (double)m_Info.ViewMode, NULL); 279. } 280. m_Info.bClick = false; 281. break; 282. case CHARTEVENT_MOUSE_MOVE: 283. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 284. if (m_Info.weight > 1) 285. { 286. UpdateViewPort(_dparam = GetPositionsMouse().Position.Price); 287. if (m_Info.ev != evMsgClosePositionEA) 288. ViewValue(m_Info.bIsBuy ? _dparam - m_Info.open : m_Info.open - _dparam); 289. if (m_Info.bClick) 290. { 291. if ((m_Info.ev == evMsgCloseTakeProfit) || (m_Info.ev == evMsgCloseStopLoss)) 292. EventChartCustom(0, (ushort)(m_Info.ev == evMsgCloseTakeProfit ? evMsgNewTakeProfit : evMsgNewStopLoss), m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 293. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 294. } 295. } 296. break; 297. case CHARTEVENT_OBJECT_DELETE: 298. if (StringFind(sparam, m_Info.szPrefixName) < 0) break; 299. UpdatePrice(m_Info.open, m_Info.price); 300. break; 301. } 302. } 303. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
En este fragmento aparecen básicamente llamadas a PositionsTotal, PositionGetTicket, PositionGetDouble y PositionGetString. Podemos eliminar con mucha facilidad PositionGetString, que es una función que, en nuestro caso, devuelve el nombre del símbolo. Basta con pasar el nombre del símbolo como parámetro al llamar al constructor de la clase y almacenarlo en una variable privada de la clase. Las otras tres llamadas requerirán algo más de trabajo o, mejor dicho, un enfoque diferente. Comencemos, por tanto, resolviendo el problema relacionado con el nombre del símbolo.
Para ello, debemos modificar el código de la clase como se muestra en el siguiente fragmento.
026. //+------------------------------------------------------------------+ 027. class C_ElementsTrade : private C_Mouse 028. { 029. private : 030. //+------------------------------------------------------------------+ 031. struct stInfos 032. { 033. struct st_01 034. { 035. short Width, 036. Height, 037. digits; 038. }Text; 039. ulong ticket; 040. string szPrefixName, 041. szDescr, 042. szSymbol; 043. EnumEvents ev; 044. double price, 045. open, 046. volume, 047. var, 048. tickSize; 049. bool bClick, 050. bIsBuy; 051. char weight; 052. color _color; 053. int sizeText; 054. enum e1 {eValue, eFinance, eTicks, ePercentage} ViewMode; 055. }m_Info; 056. //+------------------------------------------------------------------+ . . . 169. //+------------------------------------------------------------------+ 170. public : 171. //+------------------------------------------------------------------+ 172. C_ElementsTrade(const ulong ticket, string szSymbol, const EnumEvents ev, color _color, char digits, double ticksize, string szDescr = "\n", const bool IsBuy = true) 173. :C_Mouse(0, "") 174. { 175. ZeroMemory(m_Info); 176. m_Info.szPrefixName = StringFormat("%I64u@%03d", m_Info.ticket = ticket, (int)(m_Info.ev = ev)); 177. m_Info._color = _color; 178. m_Info.szDescr = szDescr; 179. m_Info.bIsBuy = IsBuy; 180. m_Info.Text.digits = digits; 181. m_Info.tickSize = ticksize; 182. m_Info.szSymbol = szSymbol; 183. } 184. //+------------------------------------------------------------------+ . . . 242. //+------------------------------------------------------------------+ 243. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 244. { . . . 266. case CHARTEVENT_CUSTOM + evMsgSwapViewModePosition: 267. m_Info.ViewMode = (stInfos::e1)((((stInfos::e1)_dparam) + 1) & 0x03); 268. for (int c0 = PositionsTotal() - 1; c0 >= 0; c0--) 269. EventChartCustom(0, evUpdate_Position, PositionGetTicket(c0), 0, NULL); 270. break; 271. case CHARTEVENT_OBJECT_CLICK: 272. sz0 = GetPositionsMouse().szObjNameClick; 273. if (m_Info.bClick) 274. { 275. if (sz0 == def_NameBtnMove) 276. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, m_Info.ev, ""); 277. if (sz0 == def_NameBtnClose) 278. EventChartCustom(0, (ushort) m_Info.ev, m_Info.ticket, PositionGetDouble(m_Info.ev == evMsgCloseTakeProfit ? POSITION_SL : POSITION_TP), m_Info.szSymbol); 279. if (sz0 == def_NameObjLabel) 280. EventChartCustom(0, evMsgSwapViewModePosition, m_Info.ticket, (double)m_Info.ViewMode, NULL); 281. } 282. m_Info.bClick = false; 283. break; 284. case CHARTEVENT_MOUSE_MOVE: 285. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 286. if (m_Info.weight > 1) 287. { 288. UpdateViewPort(_dparam = GetPositionsMouse().Position.Price); 289. if (m_Info.ev != evMsgClosePositionEA) 290. ViewValue(m_Info.bIsBuy ? _dparam - m_Info.open : m_Info.open - _dparam); 291. if (m_Info.bClick) 292. { 293. if ((m_Info.ev == evMsgCloseTakeProfit) || (m_Info.ev == evMsgCloseStopLoss)) 294. EventChartCustom(0, (ushort)(m_Info.ev == evMsgCloseTakeProfit ? evMsgNewTakeProfit : evMsgNewStopLoss), m_Info.ticket, GetPositionsMouse().Position.Price, m_Info.szSymbol); 295. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 296. } 297. } 298. break;
Fragmento de C_ElementsTrade
En este fragmento he conservado únicamente las partes necesarias para que veas dónde se realizaron los cambios. Comenzamos en la línea 42, donde declaramos una nueva variable destinada a almacenar el nombre del símbolo. Como indiqué antes, el constructor recibirá el nombre del símbolo. Esto puede verse en la línea 172. En la línea 182 almacenamos el valor proporcionado por el autor de llamada. Después, en DispatchMessage, sustituimos las llamadas a PositionGetString por el valor de la variable declarada en la línea 42. Con esto resolvemos el primero de los problemas. Por ahora, no te preocupes por el código principal del indicador. Cuando hayamos eliminado todas las cuestiones pendientes y dependencias de la clase C_ElementsTrade, veremos cómo quedará el nuevo código principal.
Muy bien. Ahora resolveremos otro problema: las llamadas a PositionsTotal y PositionGetTicket, que aparecen en las líneas 268 y 269 del fragmento. Si has seguido esta serie, ya sabrás por qué se incluyeron estas llamadas en el código. Sin embargo, aunque tienen un motivo, ya no tiene sentido mantenerlas. El evento personalizado será recibido por todas las aplicaciones presentes en el gráfico, por lo que podemos obtener el mismo resultado mediante un enfoque diferente dentro del propio código. El fragmento actualizado aparece a continuación.
266. case CHARTEVENT_CUSTOM + evMsgSwapViewModePosition: 267. m_Info.ViewMode = (stInfos::e1)((((stInfos::e1)_dparam) + 1) & 0x03); 268. EventChartCustom(0, evUpdate_Position, m_Info.ticket, 0, NULL); 269. break;
Fragmento de C_ElementsTrade
Resulta interesante detenerse a pensar en lo que acabamos de hacer y en por qué no lo hicimos antes. Muchas personas creen que, al programar o implementar algo completamente nuevo, somos capaces de crear desde el principio un código perfecto. Sin embargo, esto no es del todo cierto. Cuando volvemos a revisar el código, advertimos que determinadas partes ya no tienen demasiado sentido o pueden mejorarse. Eso es precisamente lo que estamos haciendo en el fragmento anterior.
Cuando disparamos el evento personalizado para cambiar el modo de visualización del resultado de una posición, todas las aplicaciones presentes en el gráfico detectarán el evento. Sin embargo, esto no resulta evidente durante la programación; solo se aprecia al probar el código. En este punto eliminamos la restricción anterior. Incluso en una cuenta de tipo HEDGING, todos los indicadores de posición recibirán y obedecerán la misma orden: cambiar el modo en que se muestra el resultado. Sin embargo, cambiar únicamente el modo de visualización no actualiza los datos, por lo que debemos disparar un nuevo evento. Este evento solicita al indicador que actualice la información. Como cada indicador conoce el ticket de la posición que representa, la línea 268 sustituye por completo el código del fragmento anterior, en el que necesitábamos llamar a PositionsTotal y PositionGetTicket para abarcar todas las posiciones. Aun así, este código todavía puede mejorarse. Por ahora, lo mantendremos de este modo.
Todavía queda otra cuestión: la llamada a PositionGetDouble. A primera vista, parece mucho más difícil de eliminar, pero ¿realmente lo es? Veamos dónde se utiliza y por qué. De este modo comprenderás cómo la sustituiremos. La línea que contiene esta llamada aparece a continuación.
270. case CHARTEVENT_OBJECT_CLICK: 271. sz0 = GetPositionsMouse().szObjNameClick; 272. if (m_Info.bClick) 273. { 274. if (sz0 == def_NameBtnMove) 275. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, m_Info.ev, ""); 276. if (sz0 == def_NameBtnClose) 277. EventChartCustom(0, (ushort) m_Info.ev, m_Info.ticket, PositionGetDouble(m_Info.ev == evMsgCloseTakeProfit ? POSITION_SL : POSITION_TP), m_Info.szSymbol); 278. if (sz0 == def_NameObjLabel) 279. EventChartCustom(0, evMsgSwapViewModePosition, m_Info.ticket, (double)m_Info.ViewMode, NULL); 280. } 281. m_Info.bClick = false; 282. break;
Fragmento de C_ElementsTrade
Observa la línea 277. Allí utilizamos PositionGetDouble para obtener los valores de take profit o stop loss de la posición. Esto nos permite cerrar o eliminar el nivel límite sobre el que se hizo clic. Es decir, cuando hacemos clic para cerrar el take profit, necesitamos conocer el valor del stop loss. Del mismo modo, cuando cerramos el stop loss, debemos conocer el valor del take profit. Por tanto, existe una referencia cruzada entre ambos valores. Resolver esta cuestión resulta interesante, ya que cada segmento estará vinculado a un valor actual. Es posible que lo que haré a continuación te parezca algo confuso, pero pronto comprenderás el principio de funcionamiento. Para eliminar la llamada a PositionGetDouble, modificaremos la clase C_ElementsTrade como se muestra en el siguiente fragmento.
026. //+------------------------------------------------------------------+ 027. class C_ElementsTrade : private C_Mouse 028. { 029. private : 030. //+------------------------------------------------------------------+ 031. struct stInfos 032. { 033. struct st_01 034. { 035. short Width, 036. Height, 037. digits; 038. }Text; 039. ulong ticket; 040. string szPrefixName, 041. szDescr, 042. szSymbol; 043. EnumEvents ev; 044. double price, 045. open, 046. volume, 047. var, 048. tickSize, 049. tpsl; 050. bool bClick, 051. bIsBuy; 052. char weight; 053. color _color; 054. int sizeText; 055. enum e1 {eValue, eFinance, eTicks, ePercentage} ViewMode; 056. }m_Info; 057. //+------------------------------------------------------------------+ . . . 190. //+------------------------------------------------------------------+ 191. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0, const double special = -1) 192. { 193. m_Info.sizeText = 0; 194. RemoveAllsObjects(); 195. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 196. m_Info.var = (var > 0 ? var : m_Info.var); 197. m_Info.tpsl = (special >= 0 ? special : m_Info.tpsl); . . . 244. //+------------------------------------------------------------------+ 245. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 246. { . . . 272. case CHARTEVENT_OBJECT_CLICK: 273. sz0 = GetPositionsMouse().szObjNameClick; 274. if (m_Info.bClick) 275. { 276. if (sz0 == def_NameBtnMove) 277. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, m_Info.ev, ""); 278. if (sz0 == def_NameBtnClose) 279. EventChartCustom(0, (ushort) m_Info.ev, m_Info.ticket, m_Info.tpsl, m_Info.szSymbol); 280. if (sz0 == def_NameObjLabel) 281. EventChartCustom(0, evMsgSwapViewModePosition, m_Info.ticket, (double)m_Info.ViewMode, NULL); 282. } 283. m_Info.bClick = false; 284. break;
Fragmento de C_ElementsTrade
Observa que en la línea 49 declaramos una nueva variable. Aquí comienza nuestra solución. Después, en la línea 191, modificamos la declaración del procedimiento UpdatePrice para indicar qué valor recibirá la variable declarada en la línea 49. Ahora presta atención. En la línea 197 comprobamos si el valor es mayor o igual que cero. ¿Por qué? Porque, si el take profit o el stop loss no están definidos, el valor recibido será igual a cero. De este modo podemos establecer correctamente el valor correspondiente. Dentro del procedimiento DispatchMessage, el cambio aparece en la línea 279.
En este punto quizá ya estés empezando a dudar de mi cordura, porque estamos haciendo todo lo que se hacía antes, pero sin utilizar ninguna llamada que genere dependencias. Recuerda que las dependencias a las que me refiero son las relacionadas con la existencia de posiciones en el servidor de trading. De este modo, la clase C_ElementsTrade pasa a ser apta para utilizarse junto con el servicio de repetición/simulador.
Sin embargo, todos estos cambios nos obligan a modificar el código principal. A continuación se muestra únicamente la parte necesaria, ya que no fue preciso actualizarlo por completo.
084. //+------------------------------------------------------------------+ 085. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 086. { 087. double volume; 088. 089. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 090. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 091. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 092. switch (id) 093. { 094. case CHARTEVENT_CUSTOM + evUpdate_Position: 095. if (lparam != m_Infos.ticket) break; 096. if (!PositionSelectByTicket(m_Infos.ticket)) 097. { 098. ChartIndicatorDelete(0, 0, m_Infos.szShortName); 099. return; 100. }; 101. if (Open == NULL) Open = new C_ElementsTrade( 102. m_Infos.ticket, 103. m_Infos.szSymbol, 104. evMsgClosePositionEA, 105. clrRoyalBlue, 106. m_Infos.digits, 107. m_Infos.tickSize, 108. StringFormat("%I64u : Position opening price.", m_Infos.ticket), 109. m_Infos.bIsBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) 110. ); 111. if (Take == NULL) Take = new C_ElementsTrade( 112. m_Infos.ticket, 113. m_Infos.szSymbol, 114. evMsgCloseTakeProfit, 115. clrForestGreen, 116. m_Infos.digits, 117. m_Infos.tickSize, 118. StringFormat("%I64u : Take Profit price.", m_Infos.ticket), 119. m_Infos.bIsBuy 120. ); 121. if (Stop == NULL) Stop = new C_ElementsTrade( 122. m_Infos.ticket, 123. m_Infos.szSymbol, 124. evMsgCloseStopLoss, 125. clrFireBrick, 126. m_Infos.digits, 127. m_Infos.tickSize, 128. StringFormat("%I64u : Stop Loss price.", m_Infos.ticket), 129. m_Infos.bIsBuy 130. ); 131. volume = PositionGetDouble(POSITION_VOLUME); 132. (*Open).UpdatePrice(0, m_Infos.priceOpen = PositionGetDouble(POSITION_PRICE_OPEN), volume, m_Infos.var); 133. (*Take).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_TP), volume, m_Infos.var, PositionGetDouble(POSITION_SL)); 134. (*Stop).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_SL), volume, m_Infos.var, PositionGetDouble(POSITION_TP)); 135. ProfitNow(); 136. break; 137. } 138. ChartRedraw(); 139. }; 140. //+------------------------------------------------------------------+
Fragmento del código del indicador de posición
Observa con atención el procedimiento OnChartEvent, que forma parte del código principal. No se han realizado demasiados cambios, aunque puedes comprobar que fue necesario añadir el nombre del símbolo a todos los constructores definidos aquí. Sin embargo, no es eso lo que quiero destacar. Presta atención a las líneas 133 y 134. En ellas, el quinto parámetro no representa exactamente lo que muchos podrían imaginar, ya que estamos utilizando una referencia cruzada. Si examinas esas dos líneas junto con el fragmento anterior, comprenderás por qué se ha implementado el código de esta manera.
Observa que, aunque hemos eliminado de la clase C_ElementsTrade las llamadas destinadas a obtener datos de la posición, esas mismas llamadas aparecen ahora en el código principal. La diferencia es que ahora controlamos mejor cómo se utilizarán. Gracias a ello, conseguiremos adaptar este mismo indicador para utilizarlo con el servicio de repetición/simulador.
Muy bien. En este punto, el indicador vuelve a funcionar de la misma manera que antes de las modificaciones. Sin embargo, hay otra cuestión que hasta ahora había pasado inadvertida. No sé por qué tardé tanto en detectar este fallo. Quizá no lo había notado porque durante las pruebas utilizaba siempre el conjunto completo y no intentaba añadir el indicador manualmente. En cualquier caso, vamos a corregirlo ahora. Observa el siguiente fragmento.
30. //+------------------------------------------------------------------+ 31. C_ElementsTrade *Open = NULL, *Stop = NULL, *Take = NULL; 32. //+------------------------------------------------------------------+ 33. bool CheckCatch(ulong ticket) 34. { 35. double vv; 36. 37. ZeroMemory(m_Infos); 38. m_Infos.szShortName = StringFormat("%I64u", m_Infos.ticket = ticket); 39. if (!PositionSelectByTicket(m_Infos.ticket)) return false; 40. if (ChartWindowFind(0, m_Infos.szShortName) >= 0) 41. { 42. m_Infos.ticket = 0; 43. return false; 44. } 45. m_Infos.szSymbol = PositionGetString(POSITION_SYMBOL); 46. m_Infos.digits = (char)SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); 47. m_Infos.tickSize = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); 48. vv = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); 49. m_Infos.var = m_Infos.tickSize / vv; 50. IndicatorSetString(INDICATOR_SHORTNAME, m_Infos.szShortName); 51. EventChartCustom(0, evUpdate_Position, ticket, 0, ""); 52. 53. return true; 54. } 55. //+------------------------------------------------------------------+
Fragmento del código del indicador de posición
Sí, todavía estamos trabajando con el código principal del indicador. Fíjate en la línea 40. Incluso en el código del artículo anterior, la llamada utilizada allí era ObjectFind. No me preguntes por qué, porque no sabría explicar el motivo de haber utilizado específicamente esa llamada. La llamada correcta es precisamente la que estás viendo ahora. ¿Cuál es el objetivo de la línea 40? Evitar que el operador o usuario intente añadir dos indicadores al gráfico cuando uno solo es suficiente. Si lo intenta, la llamada ChartWindowFind de la línea 40 comprobará si ya existe en el gráfico un indicador con el mismo nombre corto. En caso afirmativo, el indicador que el usuario intenta añadir se eliminará inmediatamente. De este modo, siempre habrá un único indicador en el gráfico vinculado a cada posición abierta. Es importante que tengas en cuenta que, en una cuenta de tipo HEDGING, puede haber más de un indicador de posición, pero cada uno estará vinculado a una posición distinta. Nunca habrá dos indicadores asociados a la misma posición.
Con esto, el indicador de posición está prácticamente terminado. Sin embargo, antes de comenzar a adaptar el código para utilizarlo en el sistema de repetición/simulador, quiero realizar otros dos cambios. Quizá los consideres innecesarios. Si no les encuentras utilidad, puedes ignorarlos. En cualquier caso, las aplicaciones compiladas que se incluirán en los archivos adjuntos ya incorporarán estas modificaciones. Para separar ambas cuestiones, pasemos a un nuevo apartado.
Indicar que el take profit y el stop loss son correctos
Quizá no lo sepas o nunca lo hayas visto, pero MetaTrader 5 utiliza un código de colores que aparece en el cuadro de mensajes, dentro de la pestaña Negociación. Estos colores normalmente indican el estado de la operación. Es bastante habitual que veas los códigos de colores cuando todo funciona correctamente. Sin embargo, también existe un color que aparece cuando se produce algún problema. Como esta situación es poco frecuente, resulta difícil mostrarla en la mayoría de los casos. Cuando el precio salta el nivel de stop loss o take profit, la posición aparece resaltada en amarillo. Esto indica que algo no funciona correctamente y que requiere una atención especial. Personalmente, solo lo he visto una vez. En más de cuatro años operando. Ocurrió durante un episodio de volatilidad extrema en el contrato del dólar, que hizo que el precio saltara la orden de salida. Lo que más me llamó la atención fue que esta situación apenas se comenta. Quizá se deba a que es relativamente raro que llegues a ver una posición resaltada en amarillo.
Mi intención ahora es implementar esto en el indicador de posición. De este modo, cuando lo utilices, notarás que se ha producido algún problema y tomar las medidas necesarias. No es una tarea complicada; solo requiere modificar ligeramente el código existente. Sin embargo, lo haré de una manera algo distinta a la utilizada en la pestaña Negociación del cuadro de mensajes de MetaTrader 5. Mi objetivo principal será avisar al operador o usuario cuando el stop loss o el take profit no se encuentren en un nivel coherente. Es decir, cuando estén situados de forma incorrecta y no formen un canal. Explicado únicamente con palabras, puede parecer algo difícil de comprender, pero enseguida quedará claro el objetivo. Para hacer lo que necesitamos, será necesario añadir algunos elementos a la clase C_ElementsTrade. El siguiente fragmento muestra dónde se realizaron los cambios.
026. //+------------------------------------------------------------------+ 027. class C_ElementsTrade : private C_Mouse 028. { 029. private : 030. //+------------------------------------------------------------------+ 031. struct stInfos 032. { 033. struct st_01 034. { 035. short Width, 036. Height, 037. digits; 038. }Text; 039. ulong ticket; 040. string szPrefixName, 041. szDescr, 042. szSymbol; 043. EnumEvents ev; 044. double price, 045. open, 046. volume, 047. var, 048. tickSize, 049. tpsl, 050. limit; 051. bool bClick, 052. bIsBuy; 053. char weight; 054. color _color; 055. int sizeText; 056. enum e1 {eValue, eFinance, eTicks, ePercentage} ViewMode; 057. }m_Info; 058. //+------------------------------------------------------------------+ . . . 164. //+------------------------------------------------------------------+ 165. inline void ChartChange(void) 166. { 167. UpdateViewPort(MathAbs(m_Info.price)); 168. m_Info.limit = (m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 169. if (m_Info.ev != evMsgClosePositionEA) 170. ViewValue(m_Info.limit); 171. } 172. //+------------------------------------------------------------------+ . . . 218. //+------------------------------------------------------------------+ 219. void ViewValue(const double profit, const bool Enabled = true) 220. { 221. string szTxt; 222. color _cor; 223. static double memSL, memTP; 224. 225. if (Enabled) 226. { 227. switch (m_Info.ViewMode) 228. { 229. case stInfos::eValue: 230. szTxt = StringFormat("%." + (string)m_Info.Text.digits + "f", MathAbs(profit)); 231. break; 232. case stInfos::eFinance: 233. szTxt = StringFormat("$ %." + (string)m_Info.Text.digits + "f", (MathAbs(profit) / m_Info.var) * m_Info.volume); 234. break; 235. case stInfos::eTicks: 236. szTxt = StringFormat("%d", (uint)MathRound(MathAbs(profit) / m_Info.tickSize)); 237. break; 238. case stInfos::ePercentage: 239. szTxt = StringFormat("%.2f%%", NormalizeDouble((MathAbs(profit) / (m_Info.open ? m_Info.open : m_Info.price)) * 100, 2)); 240. break; 241. } 242. ObjectSetString(0, def_NameObjLabel, OBJPROP_TEXT, szTxt); 243. if (StringLen(szTxt) != m_Info.sizeText) 244. { 245. AdjustDinamic(def_NameObjLabel, szTxt); 246. m_Info.sizeText = StringLen(szTxt); 247. } 248. } 249. _cor = (m_Info.limit >= 0 ? clrPaleGreen : clrCoral); 250. switch (m_Info.ev) 251. { 252. case evMsgCloseTakeProfit: 253. memTP = (Enabled ? memTP : profit); 254. _cor = (m_Info.limit < memTP ? clrYellow : _cor); 255. break; 256. case evMsgCloseStopLoss: 257. memSL = (Enabled ? memSL : profit); 258. _cor = (m_Info.limit > memSL ? clrYellow : _cor); 259. break; 260. case evMsgClosePositionEA: 261. _cor = (profit >= 0 ? clrPaleGreen : clrCoral); 262. break; 263. } 264. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, _cor); 265. } 266. //+------------------------------------------------------------------+ 267. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 268. { 269. string sz0; 270. long _lparam = lparam; 271. double _dparam = dparam; 272. 273. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 274. switch (id) 275. { . . . 307. case CHARTEVENT_MOUSE_MOVE: 308. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 309. if (m_Info.weight > 1) 310. { 311. UpdateViewPort(_dparam = GetPositionsMouse().Position.Price); 312. ViewValue(m_Info.limit = (m_Info.bIsBuy ? _dparam - m_Info.open : m_Info.open - _dparam)); 313. if (m_Info.bClick) 314. { 315. if ((m_Info.ev == evMsgCloseTakeProfit) || (m_Info.ev == evMsgCloseStopLoss)) 316. EventChartCustom(0, (ushort)(m_Info.ev == evMsgCloseTakeProfit ? evMsgNewTakeProfit : evMsgNewStopLoss), m_Info.ticket, GetPositionsMouse().Position.Price, m_Info.szSymbol); 317. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 318. } 319. } 320. break;
Fragmento de C_ElementsTrade
Sé que puede parecer confuso, pero todo comienza con la nueva variable declarada en la línea 50. Esta variable recibe un valor en dos lugares. El primero es la línea 168. Su funcionamiento es bastante sencillo: el valor dependerá de si estamos en el lado de compra o de venta, pero también del segmento que utilice la clase C_ElementsTrade. Por tanto, tendremos un valor para el stop loss y otro para el take profit. Por ahora dejaremos de lado el procedimiento ViewValue, situado en la línea 219, y veremos el segundo punto en el que se asigna un valor a la variable declarada en la línea 50. Este segundo punto se encuentra en la línea 312. Presta atención a la diferencia: en la línea 168, el valor depende de la posición registrada en el servidor de trading; en la línea 312, depende del movimiento del ratón. Esto se debe a que en ese momento estaremos modificando la posición, ya sea su take profit o su stop loss.
Antes de analizar el procedimiento ViewValue de la línea 219, será necesario revisar el código principal del indicador. Mostraré únicamente el fragmento que se ha modificado, que aparece a continuación.
55. //+------------------------------------------------------------------+ 56. inline void ProfitNow(void) 57. { 58. double ask, bid, value; 59. 60. ask = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_ASK); 61. bid = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_BID); 62. if (Open != NULL) 63. { 64. (*Open).ViewValue(value = (m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask)); 65. (*Take).ViewValue(value, false); 66. (*Stop).ViewValue(value, false); 67. } 68. } 69. //+------------------------------------------------------------------+
Fragmento del código del indicador de posición
Observa que en las líneas 65 y 66 llamamos al procedimiento de la línea 219 del archivo de cabecera que contiene la clase C_ElementsTrade. Sin embargo, fíjate en un detalle: el valor que se pasa es exactamente el mismo que recibe el segmento correspondiente a la línea del precio de apertura. Por tanto, cada vez que se actualice el valor mostrado como resultado de la operación, también actualizaremos, cuando sea necesario, el color del take profit y del stop loss. Observa además que, en ambos casos, el segundo argumento se establece en false. Ahora sí podemos volver a la línea 219 del fragmento anterior, porque lo que explicaré tendrá sentido. Para que prestes especial atención a la explicación, el resultado aparece en la siguiente animación.

Comencemos por la línea 223. Allí encontramos dos variables estáticas. El principio es casi el mismo que utilizamos en el procedimiento UpdateViewPort. Sin embargo, como el procedimiento ViewValue será utilizado por dos segmentos —el take profit y el stop loss—, necesitamos dos variables estáticas. Podríamos haber declarado otra variable junto a la que creamos en la línea 50. Sin embargo, muchas veces asignar nombres a las variables es una tarea complicada. En realidad, el problema no es elegir el nombre, sino evitar utilizar la variable en un lugar incorrecto. Pero este es solo un detalle. Una vez declaradas las variables estáticas, observa que en la línea 225 comprobamos el segundo argumento del procedimiento. Recuerda que solo será false en los dos puntos mostrados en el código principal. En cualquier otro caso será true. Si es true, realizamos el mismo trabajo que antes. Si es false, no ejecutamos ninguna acción. Sin embargo, la verdadera tarea comienza en la línea 249. Es en ese punto donde empezamos a determinar si el precio se encuentra dentro del canal formado por las líneas de stop loss y take profit. Cuando el precio queda fuera de ese canal, comienza a ocurrir la magia.
Veamos cómo funciona. En la línea 249 utilizamos el valor límite para determinar si el color de fondo debe ser verde o rojo, independientemente del segmento que esté ejecutando el código. En la línea 250 realizamos una selección entre los segmentos para definir correctamente los colores. Quizá ahora te preguntes por qué utilizamos la línea 249 para establecerlos si después volvemos a definirlos mediante esa selección. La respuesta está en las líneas 254 y 258. Sin la línea 249, tendríamos que repetir en ambas el mismo código utilizado para asignar la variable _cor. Veamos ahora qué ocurre entre las líneas 252 y 259, ya que la línea 260 es bastante sencilla.
Explicaré el caso de evMsgCloseTakeProfit, aunque el mismo razonamiento se aplica a evMsgCloseStopLoss. En las líneas 253 y 257 realizamos la misma comprobación: verificamos si estamos recibiendo el valor procedente de las líneas 65 y 66 del código principal. Cuando así sea, lo almacenaremos en la variable estática correspondiente. En caso contrario, reutilizaremos el valor almacenado para poder emplearlo en el operador ternario. Después, también mediante operadores ternarios, en las líneas 254 y 258 comprobamos si el precio del símbolo se encuentra dentro del canal. Ten en cuenta que este precio incluye el spread. Por ese motivo, aunque visualmente pueda parecer que todo está correcto, es posible que no sea así. Si el precio se encuentra fuera del canal correspondiente, el fondo se volverá amarillo. Si está en la zona adecuada, podrá mostrarse en verde o rojo, según el caso. Esto puedes verlo en la animación anterior. Cuando intentamos colocar el stop loss en una posición incorrecta, el fondo pasa a mostrarse en amarillo en cuanto su línea entra en una zona no válida.
Cuando la línea vuelve a una posición válida, la indicación cambia de amarillo a verde o rojo, según si ese nivel de precio representa una ganancia o una pérdida. Por último, para aplicar la actualización, se ejecuta la línea 264, que establece el color correspondiente en el fondo del objeto OBJ_EDIT, donde se muestra el resultado en ese nivel.
Consideraciones finales
Sé que todo esto puede parecer bastante confuso y complicado. Sin embargo, si pruebas el sistema en una cuenta DEMO —que es precisamente lo que recomiendo—, observarás algo interesante. Siempre que el fondo aparezca en amarillo e intentes colocar la línea de precio en ese nivel, el resultado de la solicitud de negociación mostrará un error comunicado por el servidor de trading. Normalmente será el error 4756, como puedes ver en la siguiente imagen.

Aunque este error no procede realmente del servidor y aparece entre los errores de tiempo de ejecución, lo cual resulta bastante curioso, indica que no se ha podido enviar la solicitud de negociación. Su etiqueta es ERR_TRADE_SEND_FAILED. Por tanto, siempre que el fondo del objeto OBJ_EDIT aparezca en amarillo, ten en cuenta que este error se mostrará en el cuadro de mensajes si, como usuario u operador, intentas colocar el precio en ese nivel.
En cualquier caso, la implementación ya está terminada. Espero que hayas comprendido su finalidad y su funcionamiento. Aunque en condiciones normales de negociación no es habitual que el fondo se muestre en amarillo, esta situación puede producirse. Si ocurre, deberás tomar alguna medida para resolverla. Esta implementación no pretende alarmarte ni causarte preocupación. Su único objetivo es hacer que el indicador de posición resulte mucho más práctico y seguro.
En los archivos adjuntos dejaré disponibles las aplicaciones necesarias para que puedas probar el sistema localmente, en caso de que no sepas cómo utilizar la información de los artículos para crear los códigos fuente. Por ahora me despido. Nos veremos en el próximo artículo de esta serie sobre la creación de un sistema de repetición/simulador. Hasta la próxima.
| Archivo | Descripción |
|---|---|
| Experts\Expert Advisor.mq5 | Muestra la interacción entre Chart Trade y el Asesor Experto. Es necesario usar Mouse Study para la interacción. |
| Indicators\Chart Trade.mq5 | Crea la ventana para configurar la orden que se va a enviar. Es necesario usar Mouse Study para la interacción. |
| Indicators\Market Replay.mq5 | Crea los controles que permiten interactuar con el servicio de repetición/simulador. Es necesario usar 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 operar el sistema de repetición/simulador como en el mercado real. |
| Indicators\Order Indicator.mq5 | Se encarga de indicar las órdenes de mercado y permite interactuar con ellas y controlarlas. |
| Indicators\Position View.mq5 | Se encarga de indicar 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 (archivo principal de todo el sistema). |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/13483
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.
Introducción a MQL5 (Parte 27): Cómo dominar las API y la función WebRequest() en MQL5
Del básico al intermedio: Colas, listas y árboles (VIII)
Particularidades del trabajo con números del tipo double en MQL4
Características del Wizard MQL5 que debe conocer (Parte 71): Uso de los patrones del MACD y del OBV
- 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