Simulación de mercado: Position View (XVIII)
Introducción
Hola a todos y bienvenidos a otro 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 (XVII), mostré cómo hacer que el indicador de posición presentara distintos tipos de datos. De este modo, el operador puede visualizar el estado de la posición en términos de variación porcentual, variación financiera, número de ticks o diferencia entre valores. Tú, mi querido lector, puedes ampliarlo o incluso mejorarlo con facilidad.
Aunque el indicador de posición ya se encuentra en un estado casi adecuado para utilizarlo en el sistema de repetición/simulación para probar el sistema y poner a prueba nuestra capacidad de lectura del mercado, todavía no podemos hacerlo debido a algunas cuestiones pendientes en el propio indicador.
Además, debemos corregir ciertos fallos menores. Si el operador comete accidentalmente ciertos errores durante una operación, se quedará sin poder utilizar el indicador de posición, ya que este no funcionará correctamente. En la mayoría de los casos, el indicador obligará al operador a realizar acciones poco intuitivas.
Por tanto, en este artículo seguiremos mejorando un poco más las cosas para acercarnos un paso más a nuestro objetivo final. Comencemos con el primer tema.
Mostrar el volumen
Lo primero que haremos será mostrar el volumen de la posición abierta. ¿Por qué necesitamos esta información? Porque permite que el operador tenga una noción adecuada del volumen abierto y pueda planificar distintas estrategias para aumentar el beneficio o reducir el riesgo, por ejemplo, mediante otra operación simultánea. Como estos planteamientos quedan completamente fuera del alcance del artículo, no entraré en detalles sobre ese tipo de estrategias. Nuestro foco aquí es la programación, no explicar cómo operar en el mercado.
Para mostrar el volumen de la posición abierta solo necesitamos realizar una pequeña adición al código. Consiste en incorporar al indicador un objeto OBJ_EDIT o OBJ_LABEL. ¿Por qué no utilizar OBJ_TEXT? La razón es su sistema de posicionamiento. A diferencia de OBJ_EDIT y OBJ_LABEL, el objeto OBJ_TEXT se coloca mediante coordenadas de precio y tiempo. Esto no impide utilizarlo, pero resulta considerablemente más sencillo trabajar con los otros tipos de objeto, ya que emplearemos coordenadas cartesianas X e Y.
Muy bien. Como programadores, nos corresponde decidir cuál de estos objetos resulta más adecuado. Personalmente, no encuentro grandes diferencias entre utilizar uno u otro, aunque OBJ_EDIT permite una presentación algo más cuidada. Esto se debe a que podemos controlar mejor sus propiedades y evitar que el texto quede simplemente suelto en el indicador. Por ese motivo, utilizaremos OBJ_EDIT.
El primer cambio que debemos realizar aparece en el siguiente fragmento:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameHLine m_Info.szPrefixName + "#HLINE" 05. #define def_NameBtnClose m_Info.szPrefixName + "#CLOSE" 06. #define def_NameBtnMove m_Info.szPrefixName + "#MOVE" 07. #define def_NameInfoDirect m_Info.szPrefixName + "#DIRECT" 08. #define def_NameObjLabel m_Info.szPrefixName + "#PROFIT" 09. #define def_NameBackGround m_Info.szPrefixName + "#BACKGROUND" 10. #define def_NameVolume m_Info.szPrefixName + "#VOLUME" 11. //+------------------------------------------------------------------+ 12. #define macro_LineInFocus(A) ObjectSetInteger(0, def_NameHLine, OBJPROP_YSIZE, m_Info.weight = (A ? 3 : 1)); 13. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
Observa que en la línea 10 añadimos una nueva definición, que establece el nombre del objeto que crearemos en breve. En el código original ya disponemos de una llamada que permite crear un objeto OBJ_EDIT. Modificaremos esa llamada como se muestra a continuación.
117. //+------------------------------------------------------------------+ 118. inline void CreateObjectInfoText(const string szObj, const color _color) 119. { 120. CreateObjectGraphics(szObj, OBJ_EDIT, clrNONE, (EnumPriority)(ePriorityDefault)); 121. ObjectSetString(0, szObj, OBJPROP_FONT, def_FontName); 122. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, def_FontSize); 123. ObjectSetInteger(0, szObj, OBJPROP_COLOR, clrBlack); 124. ObjectSetInteger(0, szObj, OBJPROP_BORDER_COLOR, _color); 125. ObjectSetInteger(0, szObj, OBJPROP_ALIGN, ALIGN_CENTER); 126. ObjectSetInteger(0, szObj, OBJPROP_READONLY, true); 127. } 128. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
Perfecto. Ahora debemos corregir el código original mediante el siguiente fragmento.
171. //+------------------------------------------------------------------+ 172. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 173. { 174. m_Info.sizeText = 0; 175. ObjectsDeleteAll(0, m_Info.szPrefixName); 176. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 177. m_Info.var = (var > 0 ? var : m_Info.var); 178. if (price > 0) 179. { 180. CreateLinePrice(); 181. CreateButtonClose(); 182. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 183. } 184. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 185. m_Info.open = open; 186. UpdateViewPort(m_Info.price = (price > 0 ? price : open)); 187. if (m_Info.ev != evMsgClosePositionEA) 188. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 189. } 190. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
La modificación se realiza en la línea 182. Tras esta actualización, el código anterior sigue funcionando exactamente de la misma manera. El cambio introducido en la línea 118 y la nueva definición de la línea 10 nos permiten comenzar a crear el elemento que necesitamos. Ahora llega la parte divertida. Antes, realizaremos otra pequeña modificación para reutilizar mejor el código existente. El cambio aparece en el siguiente fragmento.
137. //+------------------------------------------------------------------+ 138. inline void AdjustDinamic(const string szObj, const string szTxt) 139. { 140. uint w, h; 141. 142. TextSetFont(def_FontName, def_FontSize * -10); 143. TextGetSize(szTxt, w, h); 144. m_Info.Text.Height = (uchar) h + 4; 145. m_Info.Text.Width = (uchar) w + 4; 146. m_Info.Text.Width = UpdateViewPort(0, m_Info.Text.Width, h = 32); 147. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, m_Info.Text.Width); 148. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, m_Info.Text.Height); 149. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XSIZE, m_Info.Text.Width + h + (m_Info.ev == evMsgClosePositionEA ? 8 : 0)); 150. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YSIZE, m_Info.Text.Height + 5); 151. } 152. //+------------------------------------------------------------------+ . . . 195. //+------------------------------------------------------------------+ 196. void ViewValue(const double profit) 197. { 198. string szTxt; 199. 200. switch (m_Info.ViewMode) 201. { 202. case stInfos::eValue: 203. szTxt = StringFormat("%." + (string)m_Info.Text.digits + "f", MathAbs(profit)); 204. break; 205. case stInfos::eFinance: 206. szTxt = StringFormat("$ %." + (string)m_Info.Text.digits + "f", (MathAbs(profit) / m_Info.var) * m_Info.volume); 207. break; 208. case stInfos::eTicks: 209. szTxt = StringFormat("%d", (uint)MathRound(MathAbs(profit) / m_Info.tickSize)); 210. break; 211. case stInfos::ePercentage: 212. szTxt = StringFormat("%.2f%%", NormalizeDouble((MathAbs(profit) / (m_Info.open ? m_Info.open : m_Info.price)) * 100, 2)); 213. break; 214. } 215. ObjectSetString(0, def_NameObjLabel, OBJPROP_TEXT, szTxt); 216. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, (profit >= 0 ? clrPaleGreen : clrCoral)); 217. if (StringLen(szTxt) != m_Info.sizeText) 218. { 219. AdjustDinamic(def_NameObjLabel, szTxt); 220. m_Info.sizeText = StringLen(szTxt); 221. } 222. } 223. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
Observa que he modificado el número de parámetros del procedimiento de la línea 138. De este modo, en las líneas 147 y 148 podemos reutilizar ese mismo código para mostrar el volumen. Como hemos modificado este procedimiento, también debemos actualizar inmediatamente la llamada al procedimiento. Esto se realiza en la línea 219. Con este ajuste aumentamos considerablemente la reutilización del código y reducimos la cantidad de código nuevo que tendremos que escribir.
Ya podemos crear y colocar el objeto OBJ_EDIT que recibirá el volumen abierto. No es una tarea complicada; solo debemos ajustar y posicionar correctamente los objetos creados. Para crear el objeto y asignarle desde el principio el valor correcto, modificaremos el procedimiento UpdatePrice como se muestra a continuación.
171. //+------------------------------------------------------------------+ 172. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 173. { 174. m_Info.sizeText = 0; 175. ObjectsDeleteAll(0, m_Info.szPrefixName); 176. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 177. m_Info.var = (var > 0 ? var : m_Info.var); 178. if (price > 0) 179. { 180. CreateLinePrice(); 181. CreateButtonClose(); 182. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 183. } 184. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 185. m_Info.open = open; 186. m_Info.price = (price > 0 ? price : open); 187. if (m_Info.ev != evMsgClosePositionEA) 188. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 189. else 190. { 191. CreateObjectInfoText(def_NameVolume, clrBlack); 192. ObjectSetInteger(0, def_NameVolume, OBJPROP_BGCOLOR, clrViolet); 193. ObjectSetString(0, def_NameVolume, OBJPROP_TEXT, DoubleToString(m_Info.volume, (MathRound(m_Info.volume) != m_Info.volume ? 2 : 0))); 194. AdjustDinamic(def_NameVolume, "88888" + (MathRound(m_Info.volume) != m_Info.volume ? ".88" : "")); 195. }; 196. UpdateViewPort(m_Info.price); 197. } 198. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
Perfecto. Con esto ya estamos creando el objeto. Veamos ahora qué sucede en el código. En la línea 187 comprobamos si la clase se utiliza para el segmento correspondiente al precio de apertura o en otro segmento. Como queremos mostrar el volumen en el segmento del precio de apertura, esta condición debe cumplirse. Cuando la ejecución alcanza la línea 189, comienza el trabajo propiamente dicho. En la línea 191 creamos el objeto OBJ_EDIT que recibirá el valor del volumen. En la línea 192 definimos su color de fondo. Las líneas 193 y 194 son las que realmente requieren una explicación. ¿Qué tienen de especial? Estas líneas permiten que el indicador de posición se adapte al tipo de información que debe mostrar.
Es muy probable que todavía no entiendas qué está ocurriendo, pero el funcionamiento es sencillo. Cuando debamos mostrar un valor de coma flotante, como 0.02, el código adaptará el formato. En cambio, si el valor puede representarse como un entero, se omitirá la parte decimal. Por ejemplo, se mostrará 234, ya que en ese caso no tendría sentido añadir un punto seguido de dos ceros. ¿Por qué hacerlo de este modo? Si operas con un símbolo o en un mercado que no admite fracciones, no tiene sentido mostrar un valor fraccionario. Por ejemplo, si el indicador se utiliza en un mercado como B3, la bolsa brasileña, donde no se negocian fracciones de un símbolo, el volumen se mostrará únicamente con números enteros.
En cambio, si operas en una bolsa o mercado donde sí se permiten fracciones, como el mercado estadounidense o FOREX, el indicador mostrará el volumen con decimales. Eso es precisamente lo que hace este código, aunque aplica un criterio algo diferente. En realidad, no comprueba el símbolo ni el mercado utilizado, sino únicamente el valor del volumen. Si el volumen contiene una parte decimal, por ejemplo 10.25, el indicador mostrará ese valor. Sin embargo, si en el mismo símbolo el volumen es 10.00, el indicador mostrará solo 10 y omitirá la parte fraccionaria. Eso es exactamente lo que hace el código tan extraño de las líneas 193 y 194.
Observa otro detalle: UpdateViewPort ha cambiado de ubicación y ahora aparece en la línea 196. Antes, al actualizar el volumen, el objeto no acompañaba al segmento y quedaba desplazado respecto de su posición correcta. Esto ocurría porque la actualización no se ejecutaba en el momento adecuado. Ahora, como UpdateViewPort se llama después, la actualización se realiza correctamente.
Muy bien. Ya hemos asignado el valor al objeto OBJ_EDIT, pero todavía debemos mostrarlo en el gráfico. Para ello, debemos acudir al código encargado de posicionar todos los objetos, es decir, al procedimiento UpdateViewPort. Antes, debemos modificar un pequeño detalle en el procedimiento AdjustDinamic, como se muestra a continuación.
137. //+------------------------------------------------------------------+ 138. inline void AdjustDinamic(const string szObj, const string szTxt) 139. { 140. uint w, h; 141. 142. TextSetFont(def_FontName, def_FontSize * -10); 143. TextGetSize(szTxt, w, h); 144. m_Info.Text.Height = (uchar) h + 4; 145. m_Info.Text.Width = (uchar) w + 4; 146. m_Info.Text.Width = UpdateViewPort(0, m_Info.Text.Width, h = 32); 147. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, m_Info.Text.Width); 148. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, m_Info.Text.Height); 149. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XSIZE, m_Info.Text.Width + h + (m_Info.ev == evMsgClosePositionEA ? m_Info.Text.Width + 8 : 0)); 150. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YSIZE, m_Info.Text.Height + 5); 151. } 152. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
Observa que fue necesario ajustar ligeramente el cálculo de la línea 149 para que el fondo tuviera las dimensiones correctas. Una vez realizado este cambio, podemos pasar a UpdateViewPort, cuyo código aparece en el siguiente fragmento.
55. //+------------------------------------------------------------------+ 56. short UpdateViewPort(const double price, short size = 0, uint ui = 0) 57. { 58. static short _SizeControls; 59. static short _Width; 60. uint x, y; 61. 62. if (size > 0) 63. { 64. size += (short)(ui + 8); 65. _SizeControls = (_SizeControls > size ? _SizeControls : size); 66. size = (short)(_SizeControls - ui - 12); 67. _Width = (_Width > size ? _Width : size); 68. }else 69. { 70. ChartTimePriceToXY(0, 0, 0, price, x, y); 71. x = 125 + (m_Info.ev == evMsgClosePositionEA ? 0 : _Width + (m_Info.ev == evMsgCloseTakeProfit ? _SizeControls : (_SizeControls * 2))); 72. ObjectSetInteger(0, def_NameHLine, OBJPROP_XDISTANCE, x); 73. ObjectSetInteger(0, def_NameHLine, OBJPROP_YDISTANCE, y - (m_Info.weight > 1 ? (int)(m_Info.weight / 2) : 0)); 74. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_XDISTANCE, x); 75. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_YDISTANCE, y); 76. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_XDISTANCE, x + 10 + (m_Info.ev == evMsgClosePositionEA ? _Width + 2 : 0)); 77. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 78. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_XDISTANCE, x + (_Width * 2) + 20); 79. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_YDISTANCE, y); 80. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_XDISTANCE, x + _Width + 20); 81. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_YDISTANCE, y); 82. ObjectSetInteger(0, def_NameBackGround, OBJPROP_XDISTANCE, x - 10); 83. ObjectSetInteger(0, def_NameBackGround, OBJPROP_YDISTANCE, y - ((m_Info.Text.Height + 5) / 2)); 84. ObjectSetInteger(0, def_NameVolume, OBJPROP_XDISTANCE, x + 10); 85. ObjectSetInteger(0, def_NameVolume, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 86. } 87. return _Width; 88. } 89. //+------------------------------------------------------------------+
Fragmento de C_ElementsTrade
Ahora sí disponemos de el código correcto para que se muestren todos los segmentos. El resultado puede verse en la siguiente animación.

Observa que el indicador se adapta para evitar colisiones entre los objetos. Aunque por ahora este enfoque funciona bien con cuentas de tipo NETTING, no resulta especialmente adecuado para cuentas de tipo HEDGING, ya que se producirán colisiones entre objetos que representan posiciones distintas. Incluso en cuentas NETTING sigue existiendo un problema: a medida que aumenta el número de objetos, estos se desplazan cada vez más sobre el eje X. En algún momento, esto acabará convirtiéndose en un problema, pero por ahora no nos ocuparemos de ese aspecto.
Como el fragmento anterior es bastante sencillo y solo contiene una serie de ajustes de posición, no considero necesario explicar cada detalle. Basta con dibujarlo en papel para comprender fácilmente cómo se distribuyen los objetos. Con esto concluimos este tema y podemos pasar al siguiente.
Evitar que los objetos desaparezcan
Uno de los problemas más delicados al trabajar con objetos en un gráfico surge cuando se elimina alguno que resulta necesario para utilizar correctamente una aplicación en MetaTrader 5. Hasta ahora no nos habíamos ocupado de esta cuestión, ya que estábamos centrados en codificar las funciones básicas del indicador de posición. Ahora que ya tenemos mucho más código, debemos tener en cuenta que, si el operador o usuario elimina un objeto crítico, el indicador acabará en una situación incómoda respecto de su correcto funcionamiento. Esto se debe a que todo el control se realiza mediante los objetos presentes en el gráfico.
Por tanto, debemos garantizar que, si alguno de estos objetos se elimina accidentalmente, se vuelva a crear y colocar en el gráfico. De este modo, el usuario u operador conservará el acceso a las funciones disponibles mediante el indicador de posición.
Asegurar la presencia de determinados objetos no es una tarea complicada. Sin embargo, debemos tener cuidado al volver a colocarlos, ya que es necesario respetar un orden concreto. De lo contrario, quedarán ocultos detrás de otros objetos pertenecientes al mismo segmento de la clase C_ElementsTrade. Probablemente, el objeto más crítico sea el botón de cierre. Sin él no podemos enviar directamente solicitudes al servidor de trading, ya que es el encargado de disparar un evento personalizado para el Asesor Experto. Aun así, no nos limitaremos a este botón. Garantizaremos que todos los objetos se mantengan correctamente en el gráfico para evitar que el operador o usuario tenga dificultades al utilizar el indicador de posición.
Para lograrlo, utilizaremos un evento. MetaTrader 5 lo dispara cada vez que se elimina un objeto del gráfico. Como la clase C_Terminal ya indica a MetaTrader 5 que queremos recibir este evento, no será necesario declararlo nuevamente en el código, pues esto ya está incluido en el código mediante herencia.
Muy bien. Comencemos a añadir el código necesario a la clase C_ElementsTrade. No es especialmente complicado, pero requiere cierta atención. En el código existen básicamente dos puntos en los que pueden eliminarse los objetos. Antes de comenzar, debemos garantizar que los objetos se eliminen en esos dos puntos. Evidentemente, los dos utilizan una llamada a ObjectsDeleteAll u otra llamada destinada a eliminar objetos del gráfico. Como esto solo ocurre en dos puntos del código, podemos limitar los cambios al fragmento que aparece a continuación.
168. //+------------------------------------------------------------------+ 169. ~C_ElementsTrade() 170. { 171. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 172. ObjectsDeleteAll(0, m_Info.szPrefixName); 173. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 174. } 175. //+------------------------------------------------------------------+ 176. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 177. { 178. m_Info.sizeText = 0; 179. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 180. ObjectsDeleteAll(0, m_Info.szPrefixName); 181. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 182. m_Info.volume = (vol > 0 ? vol : m_Info.volume);
Fragmento de C_ElementsTrade
Este fragmento impide que MetaTrader 5 dispare el evento cuando sea necesario eliminar los objetos del gráfico. Como el código continúa actualizándose, la numeración de las líneas también cambia. Sin embargo, si has aplicado todos los cambios del tema anterior, podrás localizar los puntos indicados en el fragmento. Observa que se han añadido cuatro líneas nuevas. Las líneas 171 y 179 desactivan la solicitud del evento, mientras que las líneas 173 y 181 vuelven a activarla. No obstante, conviene hacerlo de otra manera para reducir la posibilidad de cometer errores al programar.
Aunque el fragmento anterior cumple el objetivo, lo ideal es agrupar esas tres líneas de una forma más adecuada. Una opción sería utilizar una macro; otra, crear un procedimiento dentro de la clase. Como una macro tendría que eliminarse explícitamente al final del archivo de cabecera y no quiero confundirte, crearemos un procedimiento. Puedes verlo en el siguiente fragmento.
154. //+------------------------------------------------------------------+ 155. inline void RemoveAllsObjects(void) 156. { 157. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 158. ObjectsDeleteAll(0, m_Info.szPrefixName); 159. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 160. } 161. //+------------------------------------------------------------------+ 162. public : 163. //+------------------------------------------------------------------+ . . . 175. //+------------------------------------------------------------------+ 176. ~C_ElementsTrade() 177. { 178. RemoveAllsObjects(); 179. } 180. //+------------------------------------------------------------------+ 181. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 182. { 183. m_Info.sizeText = 0; 184. RemoveAllsObjects(); 185. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 186. m_Info.var = (var > 0 ? var : m_Info.var); 187. if (price > 0) 188. {
Fragmento de C_ElementsTrade
En este fragmento te muestro dónde se ha creado el procedimiento y cómo se modificó el código anterior para utilizarlo dentro de la clase C_ElementsTrade. Observa que ahora el código es mucho más sencillo. Quizá pienses que el procedimiento de la línea 155 es innecesario, puesto que el fragmento anterior ya resolvía el problema. Sin embargo, a medida que aumenta la complejidad del código, es muy habitual olvidarse de hacer algo. Para evitar problemas futuros, conviene agrupar siempre el código común dentro de una función o procedimiento.
De este modo, si posteriormente es necesario modificar algo, bastará con cambiar una sola parte del código. Esto resulta mucho mejor que tener que actualizar varios puntos distribuidos por todo el código. Una vez explicado por qué se creó el procedimiento de la línea 155, podemos analizar finalmente el procedimiento DispatchMessage, que aparece a continuación.
235. //+------------------------------------------------------------------+ 236. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 237. { 238. string sz0; 239. long _lparam = lparam; 240. double _dparam = dparam; 241. 242. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 243. switch (id) 244. { . . . 294. case CHARTEVENT_OBJECT_DELETE: 295. if (StringFind(sparam, m_Info.szPrefixName) < 0) break; 296. UpdatePrice(m_Info.open, m_Info.price); 297. break; 298. } 299. } 300. //+------------------------------------------------------------------+ 301.
Fragmento de C_ElementsTrade
Observa que solo estoy destacando lo que realmente nos interesa. Probablemente ya te estés preguntando si esto funciona. La respuesta es: más o menos. Antes de explicar por qué no podemos responder simplemente sí o no, veamos cómo funciona este fragmento. En este apartado mencioné que MetaTrader 5 dispara un evento cuando se elimina un objeto del gráfico. En ese momento, la línea 294 captura el evento enviado por MetaTrader 5. El nombre del objeto eliminado se recibe mediante el argumento sparam. Consulta la documentación para obtener más información.
Lo que debemos hacer es comprobar si el nombre proporcionado por MetaTrader 5 coincide con alguno de los nombres creados por el indicador de posición. Existen varias formas de realizar esta comprobación, pero aquí utilizamos una de las más sencillas. En la línea 295 utilizamos una llamada de la biblioteca de MQL5 para comprobar si el prefijo del nombre del objeto creado por el indicador coincide con el nombre informado por MetaTrader 5. Si no coincide, ignoramos el resto del procedimiento de reconstrucción. Si coincide, en la línea 296 solicitamos a la clase C_ElementsTrade que vuelva a crear los objetos.
Ahora presta atención a lo siguiente: el procedimiento UpdatePrice vuelve a crear los objetos, por lo que el código funciona como se esperaba. Sin embargo, existe un pequeño problema. Este aparece cuando una posición carece de alguno de sus segmentos, por ejemplo, el take profit. En ese caso, solo estará presente en el gráfico el objeto de movimiento. Si el usuario u operador elimina ese objeto, la llamada de la línea 296 volverá a crear todos los demás objetos, no solo el objeto de movimiento. Por eso no podemos afirmar que el código funcione correctamente; en realidad, funciona solo a medias. Para corregir este problema, debemos actualizar el código. No te preocupes, el cambio es sencillo y aparece en el siguiente fragmento.
180. //+------------------------------------------------------------------+ 181. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 182. { 183. m_Info.sizeText = 0; 184. RemoveAllsObjects(); 185. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 186. m_Info.var = (var > 0 ? var : m_Info.var); 187. if (price > 0) 188. { 189. CreateLinePrice(); 190. CreateButtonClose(); 191. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 192. } 193. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 194. m_Info.open = open; 195. m_Info.price = (price > 0 ? price : -open); 196. if (m_Info.ev != evMsgClosePositionEA) 197. ViewValue(m_Info.bIsBuy ? MathAbs(m_Info.price) - m_Info.open : m_Info.open - MathAbs(m_Info.price)); 198. else 199. { 200. CreateObjectInfoText(def_NameVolume, clrBlack); 201. ObjectSetInteger(0, def_NameVolume, OBJPROP_BGCOLOR, clrViolet); 202. ObjectSetString(0, def_NameVolume, OBJPROP_TEXT, DoubleToString(m_Info.volume, (MathRound(m_Info.volume) != m_Info.volume ? 2 : 0))); 203. AdjustDinamic(def_NameVolume, "88888" + (MathRound(m_Info.volume) != m_Info.volume ? ".88" : "")); 204. }; 205. UpdateViewPort(MathAbs(m_Info.price)); 206. } 207. //+------------------------------------------------------------------+ . . . 235. //+------------------------------------------------------------------+ 236. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 237. { 238. string sz0; 239. long _lparam = lparam; 240. double _dparam = dparam; 241. 242. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 243. switch (id) 244. { 245. case (CHARTEVENT_KEYDOWN): 246. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 247. _lparam = (long) m_Info.ticket; 248. _dparam = 0; 249. EventChartCustom(0, evUpdate_Position, _lparam, 0, ""); 250. case CHARTEVENT_CUSTOM + evMsgSetFocus: 251. if ((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)) 252. UpdatePrice(m_Info.open, GetPositionsMouse().Position.Price); 253. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 254. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 255. m_Info.bClick = false; 256. case CHARTEVENT_CHART_CHANGE: 257. UpdateViewPort(MathAbs(m_Info.price)); 258. if (m_Info.ev != evMsgClosePositionEA) 259. ViewValue(m_Info.bIsBuy ? MathAbs(m_Info.price) - m_Info.open : m_Info.open - MathAbs(m_Info.price)); 260. break; 261. case CHARTEVENT_CUSTOM + evMsgSwapViewModePosition:
Fragmento de C_ElementsTrade
Presta mucha atención, porque se trata de un detalle muy sutil. Observa la línea 195. En ella establecemos que el valor de m_Info.price será negativo cuando el parámetro price sea menor o igual que cero. Esto solo ocurrirá cuando el código principal indique que el segmento no tiene ningún valor. Para comprenderlo mejor, recuerda que, cuando el código principal del indicador —mostrado en los artículos anteriores— intenta obtener el valor de la línea de take profit o stop loss y esa línea no existe en la posición, el parámetro price será igual a cero. Si existe, su valor será distinto de cero.
Aquí es donde ocurre la magia. Si el valor es mayor que cero, el fragmento anterior puede restaurar los objetos del segmento. En cambio, si es igual a cero, asignamos un valor negativo a m_Info.price. Ese valor negativo corresponde precisamente al precio de apertura de la posición. Como no existe ninguna línea que deba volver a crearse, el indicador mostrará únicamente el objeto que permite mover el precio, de modo que podamos crear la línea que falta. Por tanto, si el usuario u operador intenta eliminar precisamente ese objeto, MetaTrader 5 disparará el evento CHARTEVENT_OBJECT_DELETE y la línea 296 llamará a UpdatePrice. En ese momento se volverá a crear únicamente el objeto de movimiento, a diferencia de lo que ocurría antes.
¿Por qué ocurre esto? Es posible que todavía no hayas comprendido la razón. Lo único que tuvimos que hacer fue actualizar el código para que m_Info.price pudiera adoptar un valor negativo. Para entenderlo, observa la línea 296 y fíjate en que el segundo argumento que se pasa a UpdatePrice es precisamente m_Info.price. Ahora dirige la atención a la línea 187. Ese valor corresponde al segundo argumento de la llamada que aparece en la línea 181. Como es negativo, la condición de la línea 187 devuelve false e impide que se vuelvan a crear los demás objetos.
Sin embargo, permitir que m_Info.price sea negativo tiene ciertas implicaciones, por lo que debemos realizar nuevos ajustes en el código. Observa que en las líneas 197, 205, 257 y 259 fue necesario añadir una llamada de la biblioteca de MQL5. Esto se debe a que no podemos realizar esos cálculos directamente con valores negativos. MathAbs resuelve el problema al devolver el valor absoluto. De este modo, con un esfuerzo mínimo, corregimos el código y hacemos posible que el indicador de posición impida que el usuario u operador elimine del gráfico los objetos creados por el propio indicador.
Antes de terminar, resolvamos otro pequeño detalle. Observa que en el fragmento anterior existe un bloque de código duplicado. La duplicación se aprecia con facilidad debido al recorte mostrado: las líneas 257 a 259 contienen código que también aparece entre las líneas 196 y 205. Aunque a primera vista no parezca duplicado, en realidad sí lo está. Lo resolveremos del mismo modo que antes: crearemos un procedimiento para eliminar la duplicación. El fragmento actualizado aparece a continuación.
161. //+------------------------------------------------------------------+ 162. inline void ChartChange(void) 163. { 164. UpdateViewPort(MathAbs(m_Info.price)); 165. if (m_Info.ev != evMsgClosePositionEA) 166. ViewValue(m_Info.bIsBuy ? MathAbs(m_Info.price) - m_Info.open : m_Info.open - MathAbs(m_Info.price)); 167. } 168. //+------------------------------------------------------------------+ 169. public : 170. //+------------------------------------------------------------------+ . . . 187. //+------------------------------------------------------------------+ 188. inline void UpdatePrice(const double open, const double price, const double vol = 0, const double var = 0) 189. { 190. m_Info.sizeText = 0; 191. RemoveAllsObjects(); 192. m_Info.volume = (vol > 0 ? vol : m_Info.volume); 193. m_Info.var = (var > 0 ? var : m_Info.var); 194. if (price > 0) 195. { 196. CreateLinePrice(); 197. CreateButtonClose(); 198. CreateObjectInfoText(def_NameObjLabel, m_Info._color); 199. } 200. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 201. m_Info.open = open; 202. m_Info.price = (price > 0 ? price : -open); 203. if (m_Info.ev == evMsgClosePositionEA) 204. { 205. CreateObjectInfoText(def_NameVolume, clrBlack); 206. ObjectSetInteger(0, def_NameVolume, OBJPROP_BGCOLOR, clrViolet); 207. ObjectSetString(0, def_NameVolume, OBJPROP_TEXT, DoubleToString(m_Info.volume, (MathRound(m_Info.volume) != m_Info.volume ? 2 : 0))); 208. AdjustDinamic(def_NameVolume, "88888" + (MathRound(m_Info.volume) != m_Info.volume ? ".88" : "")); 209. }; 210. ChartChange(); 211. } 212. //+------------------------------------------------------------------+ . . . 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:
Fragmento de C_ElementsTrade
Ahora disponemos de un nuevo procedimiento en la línea 162. Este procedimiento es privado de la clase C_ElementsTrade. Hay un detalle importante: aunque en la clase C_Terminal existe otro procedimiento con el mismo nombre, ambos no generan ninguna ambigüedad para el compilador, ya que cada uno es privado de la clase en la que se declara. Aunque C_ElementsTrade hereda procedimientos de C_Terminal, cuando el código de la línea 162 se invoca desde las líneas 210 y 262, el compilador puede determinar que no se trata del procedimiento definido en C_Terminal. Esto se debe a que aquel procedimiento no es accesible fuera de dicha clase. Por tanto, el hecho de que C_ElementsTrade herede de C_Terminal no provoca ningún conflicto en este caso. Así conseguimos eliminar por completo otro bloque de código duplicado del sistema.
Consideraciones finales
En este artículo mostré, o al menos intenté mostrar de la forma más didáctica posible, cómo modificar y desarrollar un código capaz de cumplir determinados objetivos alterando lo mínimo posible un código ya existente. Nada de esto sería posible si el sistema no se hubiera diseñado de forma modular. Aunque al principio desarrollar sistemas de esta manera pueda parecer complicado, en realidad no resulta tan complejo. Sin embargo, exige que sigas pensando, practicando y estudiando para aprovechar plenamente las posibilidades que puede ofrecer un determinado lenguaje de programación.
En los archivos adjuntos de este artículo dejaré disponible esta versión ya compilada del código, junto con las demás aplicaciones necesarias para probar el indicador de posición. Así podrás verlo en funcionamiento antes de intentar seguir el código presentado en los artículos. No obstante, todavía no hemos terminado el indicador. Aún contiene algunos puntos que deben codificarse mejor antes de que podamos utilizarlo realmente en el sistema de repetición/simulador. No se trata de problemas graves, sino de pequeños inconvenientes que debemos corregir para que el sistema de repetición/simulador pueda utilizar el indicador correctamente. Cada vez estamos más cerca de disponer de un sistema realmente funcional. Sigan acompañando esta serie de artículos. Nos veremos en el próximo, donde continuaremos avanzando hacia la creación de un sistema de repetición/simulador plenamente funcional.
| 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/simulación del 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/13457
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.
Utilizando redes neuronales en MetaTrader
Del básico al intermedio: Colas, listas y árboles (VII)
Particularidades del trabajo con números del tipo double en MQL4
De novato a experto: Trading con filtrado temporal
- 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