Simulación de mercado: Position View (XIV)
Introducción
Hola a todos y bienvenidos a un nuevo artículo de la serie sobre cómo construir un sistema de repetición/simulación.
En el artículo anterior, Simulación de mercado: Position View (XIII), mostré cómo consultar el resultado de una operación abierta y comprobar si una posición está generando ganancias o pérdidas. Sin embargo, podemos ampliar y mejorar ese mismo sistema para convertir el indicador de posición en una herramienta práctica y útil para el uso diario. En este artículo lo adaptaremos a distintas situaciones. Por eso, te invito a seguir el desarrollo de este artículo, ya que las mismas técnicas también resultarán útiles en otros proyectos desarrollados con MQL5.
Implementando un dimensionamiento de altura
Quizá una de las tareas más laboriosas al crear objetos mediante código sea dimensionarlos con precisión. Muchas veces necesitamos configurar su altura y anchura, compilar el código y ejecutar la aplicación en MetaTrader 5 para comprobar si las dimensiones son adecuadas.
Con determinados tipos de objetos, esta tarea resulta sencilla. Aun así, la tarea resulta tediosa y requiere tiempo, especialmente cuando todavía no sabemos qué dimensiones necesitará el objeto en una fase más avanzada del desarrollo. Pues bien, uno de los objetos que utilizaremos con frecuencia es OBJ_EDIT. Lo usaremos para mostrar valores al operador. Este objeto es más adecuado cuando necesitamos presentar textos que deben permanecer contenidos dentro de un fondo predefinido. En el artículo anterior, el valor de ganancias o pérdidas aparece sobre un fondo que permite identificar de inmediato si estamos en positivo o en negativo.
Sin embargo, aquí tenemos un problema: dimensionar correctamente la altura y la anchura del objeto OBJ_EDIT. Dimensionar el objeto se complica porque algunos valores requieren cuatro caracteres y otros necesitan más. La cuestión no es ajustar manualmente las dimensiones actuales, sino qué ocurre cuando tú, mi querido lector, entusiasta e interesado en aprender a programar en MQL5, quieres utilizar una fuente diferente o un tamaño de fuente distinto. Al utilizar el mismo código, quizá descubras que el texto aparece cortado o no se muestra como esperabas. La falta de correspondencia entre el tamaño de la fuente y las dimensiones de OBJ_EDIT resulta frustrante cuando estamos aprendiendo a programar y no sabemos qué debemos modificar para corregirla.
Afortunadamente, contamos con una solución práctica y eficaz para dimensionar el objeto, aunque pocos programadores la utilizan. Esta solución resulta útil al diseñar una interfaz para uso personal, ya que evita ajustar manualmente la altura y la anchura de OBJ_EDIT según la cantidad de caracteres.
Para automatizar correctamente el dimensionamiento, iremos a la clase C_ElementsTrade y añadiremos nuevas definiciones, variables y una estructura de datos. Implementaré los cambios en C_ElementsTrade de forma gradual para que tú, mi querido lector, comprendas cómo estructuramos la clase y puedas perfeccionar el código con mayor facilidad. Así, a partir de lo visto en el artículo anterior, modificaremos la clase C_ElementsTrade. A continuación muestro el resultado.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_NameHLine m_Info.szPrefixName + "#HLINE" 005. #define def_NameBtnClose m_Info.szPrefixName + "#CLOSE" 006. #define def_NameBtnMove m_Info.szPrefixName + "#MOVE" 007. #define def_NameInfoDirect m_Info.szPrefixName + "#DIRECT" 008. #define def_NameObjLabel m_Info.szPrefixName + "#PROFIT" 009. //+------------------------------------------------------------------+ 010. #define macro_LineInFocus(A) ObjectSetInteger(0, def_NameHLine, OBJPROP_YSIZE, m_Info.weight = (A ? 3 : 1)); 011. //+------------------------------------------------------------------+ 012. #define def_PathBtns "Images\\Market Replay\\Orders\\" 013. #define def_Btn_Close def_PathBtns + "Btn_Close.bmp" 014. #resource "\\" + def_Btn_Close; 015. //+------------------------------------------------------------------+ 016. #include "..\Auxiliar\C_Mouse.mqh" 017. //+------------------------------------------------------------------+ 018. #ifdef def_FontName 019. "Why are you trying to do this?" 020. #else 021. #define def_FontName "Lucida Console" 022. #define def_FontSize 10 023. #endif 024. //+------------------------------------------------------------------+ 025. class C_ElementsTrade : public C_Mouse 026. { 027. private : 028. //+------------------------------------------------------------------+ 029. struct st00 030. { 031. struct st_01 032. { 033. uchar Width, 034. Height, 035. digits; 036. }Text; 037. ulong ticket; 038. string szPrefixName, 039. szDescr; 040. EnumEvents ev; 041. double price, 042. open; 043. bool bClick, 044. bIsBuy; 045. char weight; 046. color _color; 047. }m_Info; 048. //+------------------------------------------------------------------+ 049. void UpdateViewPort(const double price) 050. { 051. int x, y; 052. 053. ChartTimePriceToXY(0, 0, 0, price, x, y); 054. x = (m_Info.ev == evMsgClosePositionEA ? 150 : (m_Info.ev == evMsgCloseTakeProfit ? 220 : 290)); 055. ObjectSetInteger(0, def_NameHLine, OBJPROP_XDISTANCE, x); 056. ObjectSetInteger(0, def_NameHLine, OBJPROP_YDISTANCE, y - (m_Info.weight > 1 ? (int)(m_Info.weight / 2) : 0)); 057. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_XDISTANCE, x); 058. ObjectSetInteger(0, def_NameBtnClose, OBJPROP_YDISTANCE, y); 059. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_XDISTANCE, x + 10); 060. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_YDISTANCE, y - (m_Info.Text.Height / 2)); 061. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_XDISTANCE, x + m_Info.Text.Width + 20); 062. ObjectSetInteger(0, def_NameInfoDirect, OBJPROP_YDISTANCE, y); 063. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_XDISTANCE, x + m_Info.Text.Width + 20); 064. ObjectSetInteger(0, def_NameBtnMove, OBJPROP_YDISTANCE, y); 065. } 066. //+------------------------------------------------------------------+ 067. inline void CreateLinePrice(void) 068. { 069. string szObj; 070. 071. CreateObjectGraphics(szObj = def_NameHLine, OBJ_RECTANGLE_LABEL, m_Info._color, (EnumPriority)(ePriorityDefault)); 072. ObjectSetInteger(0, szObj, OBJPROP_BGCOLOR, m_Info._color); 073. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH)); 074. ObjectSetInteger(0, szObj, OBJPROP_BORDER_TYPE, BORDER_FLAT); 075. ObjectSetInteger(0, szObj, OBJPROP_CORNER, CORNER_LEFT_UPPER); 076. ObjectSetString(0, szObj, OBJPROP_TOOLTIP, m_Info.szDescr); 077. macro_LineInFocus(false); 078. } 079. //+------------------------------------------------------------------+ 080. inline void CreateBoxInfo(const bool bMove) 081. { 082. string szObj; 083. const char c[] = {(char)(bMove ? 'u' : (m_Info.bIsBuy ? 236 : 238)), 0}; 084. 085. CreateObjectGraphics(szObj = (bMove ? def_NameBtnMove : def_NameInfoDirect), OBJ_LABEL, clrNONE, (EnumPriority)(ePriorityDefault)); 086. ObjectSetString(0, szObj, OBJPROP_FONT, "Wingdings"); 087. ObjectSetString(0, szObj, OBJPROP_TEXT, CharArrayToString(c)); 088. ObjectSetInteger(0, szObj, OBJPROP_COLOR, (bMove ? m_Info._color : (m_Info.bIsBuy ? clrForestGreen : clrFireBrick))); 089. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, (bMove ? 17 : 15)); 090. ObjectSetInteger(0, szObj, OBJPROP_ANCHOR, ANCHOR_CENTER); 091. } 092. //+------------------------------------------------------------------+ 093. inline void CreateObjectInfoText(void) 094. { 095. string szObj; 096. 097. CreateObjectGraphics(szObj = def_NameObjLabel, OBJ_EDIT, clrNONE, (EnumPriority)(ePriorityDefault)); 098. ObjectSetString(0, szObj, OBJPROP_FONT, def_FontName); 099. ObjectSetInteger(0, szObj, OBJPROP_FONTSIZE, def_FontSize); 100. ObjectSetInteger(0, szObj, OBJPROP_COLOR, clrBlack); 101. ObjectSetInteger(0, szObj, OBJPROP_BORDER_COLOR, m_Info._color); 102. ObjectSetInteger(0, szObj, OBJPROP_ALIGN, ALIGN_CENTER); 103. ObjectSetInteger(0, szObj, OBJPROP_READONLY, true); 104. ObjectSetInteger(0, szObj, OBJPROP_YSIZE, m_Info.Text.Height); 105. ObjectSetInteger(0, szObj, OBJPROP_XSIZE, m_Info.Text.Width); 106. } 107. //+------------------------------------------------------------------+ 108. inline void CreateButtonClose(void) 109. { 110. string szObj; 111. 112. CreateObjectGraphics(szObj = def_NameBtnClose, OBJ_BITMAP_LABEL, clrNONE, (EnumPriority)(ePriorityDefault)); 113. ObjectSetString(0, szObj, OBJPROP_BMPFILE, 0, "::" + def_Btn_Close); 114. ObjectSetInteger(0, szObj, OBJPROP_ANCHOR, ANCHOR_CENTER); 115. } 116. //+------------------------------------------------------------------+ 117. public : 118. //+------------------------------------------------------------------+ 119. C_ElementsTrade(const ulong ticket, const EnumEvents ev, color _color, char digits, string szDescr = "\n", const bool IsBuy = true) 120. :C_Mouse(0, "") 121. { 122. uint w, h; 123. 124. ZeroMemory(m_Info); 125. m_Info.szPrefixName = StringFormat("%I64u@%03d", m_Info.ticket = ticket, (int)(m_Info.ev = ev)); 126. m_Info._color = _color; 127. m_Info.szDescr = szDescr; 128. m_Info.bIsBuy = IsBuy; 129. m_Info.Text.digits = digits; 130. TextSetFont(def_FontName, def_FontSize * -10); 131. TextGetSize(StringFormat("%." + (string)digits + "f", 8888.88888), w, h); 132. m_Info.Text.Width = (uchar) w + 4; 133. m_Info.Text.Height = (uchar) h + 4; 134. } 135. //+------------------------------------------------------------------+ 136. ~C_ElementsTrade() 137. { 138. ObjectsDeleteAll(0, m_Info.szPrefixName); 139. } 140. //+------------------------------------------------------------------+ 141. inline void UpdatePrice(const double open, const double price) 142. { 143. if (price > 0) 144. { 145. CreateLinePrice(); 146. CreateButtonClose(); 147. }else 148. ObjectsDeleteAll(0, m_Info.szPrefixName); 149. CreateBoxInfo(m_Info.ev != evMsgClosePositionEA); 150. if (price > 0) 151. CreateObjectInfoText(); 152. m_Info.open = open; 153. UpdateViewPort(m_Info.price = (price > 0 ? price : open)); 154. if (m_Info.ev != evMsgClosePositionEA) 155. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 156. } 157. //+------------------------------------------------------------------+ 158. void ViewValue(const double profit) 159. { 160. ObjectSetString(0, def_NameObjLabel, OBJPROP_TEXT, StringFormat("%." + (string)m_Info.Text.digits + "f", (profit < 0 ? -(profit) : profit))); 161. ObjectSetInteger(0, def_NameObjLabel, OBJPROP_BGCOLOR, (profit >= 0 ? clrPaleGreen : clrCoral)); 162. ChartRedraw(); 163. } 164. //+------------------------------------------------------------------+ 165. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 166. { 167. string sz0; 168. long _lparam = lparam; 169. double _dparam = dparam; 170. 171. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 172. switch (id) 173. { 174. case (CHARTEVENT_KEYDOWN): 175. if (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) break; 176. _lparam = (long) m_Info.ticket; 177. _dparam = 0; 178. case CHARTEVENT_CUSTOM + evMsgSetFocus: 179. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 180. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 181. m_Info.bClick = false; 182. case CHARTEVENT_CHART_CHANGE: 183. UpdateViewPort(m_Info.price); 184. if (m_Info.ev != evMsgClosePositionEA) 185. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 186. break; 187. case CHARTEVENT_OBJECT_CLICK: 188. sz0 = GetPositionsMouse().szObjNameClick; 189. if (m_Info.bClick) switch (m_Info.ev) 190. { 191. case evMsgClosePositionEA: 192. if (sz0 == def_NameBtnClose) 193. EventChartCustom(0, evMsgClosePositionEA, m_Info.ticket, 0, ""); 194. break; 195. case evMsgCloseTakeProfit: 196. if (sz0 == def_NameBtnClose) 197. EventChartCustom(0, evMsgCloseTakeProfit, m_Info.ticket, PositionGetDouble(POSITION_SL), PositionGetString(POSITION_SYMBOL)); 198. else if (sz0 == def_NameBtnMove) 199. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, evMsgCloseTakeProfit, ""); 200. break; 201. case evMsgCloseStopLoss: 202. if (sz0 == def_NameBtnClose) 203. EventChartCustom(0, evMsgCloseStopLoss, m_Info.ticket, PositionGetDouble(POSITION_TP), PositionGetString(POSITION_SYMBOL)); 204. else if (sz0 == def_NameBtnMove) 205. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, evMsgCloseStopLoss, ""); 206. break; 207. } 208. m_Info.bClick = false; 209. break; 210. case CHARTEVENT_MOUSE_MOVE: 211. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 212. if (m_Info.weight > 1) 213. { 214. UpdateViewPort(_dparam = GetPositionsMouse().Position.Price); 215. if (m_Info.ev != evMsgClosePositionEA) 216. ViewValue(m_Info.bIsBuy ? _dparam - m_Info.open : m_Info.open - _dparam); 217. if (m_Info.bClick) 218. { 219. switch (m_Info.ev) 220. { 221. case evMsgCloseTakeProfit: 222. EventChartCustom(0, evMsgNewTakeProfit, m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 223. break; 224. case evMsgCloseStopLoss: 225. EventChartCustom(0, evMsgNewStopLoss, m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 226. break; 227. } 228. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 229. } 230. } 231. break; 232. } 233. } 234. //+------------------------------------------------------------------+ 235. }; 236. //+------------------------------------------------------------------+ 237. #undef macro_LineInFocus 238. //+------------------------------------------------------------------+ 239. #undef def_Btn_Close 240. #undef def_PathBtns 241. #undef def_FontName 242. #undef def_FontSize 243. //+------------------------------------------------------------------+ 244. #undef def_NameObjLabel 245. #undef def_NameInfoDirect 246. #undef def_NameBtnMove 247. #undef def_NameBtnClose 248. #undef def_NameHLine 249. //+------------------------------------------------------------------+
C_ElementsTrade.mqh
Bien. Lo primero que notarás es una construcción que, a primera vista, parece extraña al observar la línea 18. Tú, mi querido lector, quizá estés pensando: ¿Por qué utiliza el código esta condición de preprocesamiento? La razón es que, al trabajar en C/C++ con bibliotecas de funciones u otros módulos reutilizables, distintos archivos pueden definir los mismos nombres y generar conflictos. Conviene distinguir las fases: primero, el preprocesador evalúa las directivas y prepara el código fuente activo; después, el compilador traduce ese código. En C/C++, #error es una directiva de diagnóstico del preprocesador. Cuando aparece en una rama activa, genera un diagnóstico y detiene la traducción. Su finalidad es señalar de forma explícita una configuración o condición no válida detectada antes de generar el programa ejecutable.
MQL5 no ofrece una directiva #error equivalente. Por eso, el código utiliza una directiva no reconocida dentro de una rama condicional para provocar deliberadamente un error si def_FontName ya estaba definida. La directiva no reconocida actúa como salvaguarda frente a una redefinición o un conflicto de configuración, aunque no reproduce la semántica formal ni el mensaje personalizable de #error en C/C++. Veamos qué ocurre.
Durante el preprocesamiento, la línea 18 comprueba mediante #ifdef si def_FontName ya está definida. Si lo está, la rama activa incluye la línea 19. Como MQL5 no admite #error, el preprocesador encuentra una directiva no reconocida y la traducción se interrumpe antes de generar el archivo ejecutable. El texto escrito después de #error no debe interpretarse como un mensaje producido por una directiva compatible con MQL5. Si def_FontName no está definida, esa rama se descarta y el preprocesador conserva el bloque situado entre #else y #endif para que el compilador lo traduzca.
Si def_FontName no estaba definida, el preprocesador conserva el bloque situado entre #else y #endif, que define el nombre y el tamaño de la fuente. Centralizar ambas definiciones permite modificarlas sin cambiar varios puntos del código. Después de definir el nombre y el tamaño de la fuente, presta atención a la línea 25. Observa que la herencia, que antes era privada, ahora pasa a ser pública. En este momento, cambiar la herencia de privada a pública no parece importante, pero al final del artículo este cambio permitirá la implementación posterior. La línea 31 declara una estructura que almacena las dimensiones y los datos de formato necesarios para mostrar el texto.
La función UpdateViewPort, situada en la línea 49, utiliza las nuevas variables para aplicar al campo OBJ_EDIT la anchura y la altura calculadas. La línea 93 reutiliza esas dimensiones al configurar el objeto gráfico. En ambos puntos, la función lee valores ya almacenados; el constructor de la clase los inicializa a partir de la línea 119. Ahora presta atención a cómo inicializamos las dimensiones, porque quizá debas cambiar este código si amplías la funcionalidad.
Observa que, en la línea 122, declaramos dos nuevas variables locales y que, hasta la línea 130, el código anterior permanece tal como ya lo conocías. Cuando creamos la instancia, el constructor ejecuta las líneas 130 y 131, que configuran la fuente y calculan la anchura y la altura del texto en píxeles. El compilador se limita a validar y traducir esas llamadas. Después, las líneas 132 y 133 suman cuatro píxeles a cada dimensión para dejar un margen de dos píxeles por lado. También introduje otros cambios menores en la clase que no requieren una explicación detallada. Sin embargo, quiero destacar una incorporación importante.
Las líneas 184 y 215 comprueban si la instancia actual de C_ElementsTrade corresponde a una línea de take profit o stop loss antes de calcular su distancia respecto del precio de apertura. Ahora presta atención. En el artículo anterior mostré que el campo OBJ_EDIT situado sobre la línea del precio de apertura permite visualizar las ganancias o pérdidas actuales. En esta ampliación reutilizaremos la misma lógica para que otros campos OBJ_EDIT muestren la distancia, en puntos, entre el precio de apertura y las líneas de take profit o stop loss. No es necesario volver a programar toda la lógica de visualización.
Del mismo modo que el campo OBJ_EDIT de la línea de apertura muestra las ganancias o pérdidas actuales, los campos vinculados al take profit y al stop loss muestran la distancia, en puntos, respecto del precio de apertura. Las líneas 185 y 216 calculan la distancia en puntos y escriben el resultado en los campos OBJ_EDIT del take profit y del stop loss. Como la misma función también procesa la instancia asociada al precio de apertura, las condiciones de las líneas 184 y 215 omiten esa instancia y evitan sobrescribir el valor de ganancias o pérdidas mostrado en la línea de apertura.
Esta versión compila, pero todavía se comporta de forma incorrecta durante la ejecución. Por eso, debemos actualizar el código del indicador. A continuación muestro la versión modificada completa.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property icon "/Images/Market Replay/Icons/Positions.ico" 004. #property description "Indicator for tracking an open position on the server." 005. #property description "This should preferably be used together with an Expert Advisor." 006. #property description "For more details see the same article." 007. #property version "1.126" 008. #property link "https://www.mql5.com/pt/articles/13361" 009. #property indicator_chart_window 010. #property indicator_plots 0 011. //+------------------------------------------------------------------+ 012. #define def_ShortName "Position View" 013. //+------------------------------------------------------------------+ 014. #include <Market Replay\Order System\C_ElementsTrade.mqh> 015. #include <Market Replay\Defines.mqh> 016. //+------------------------------------------------------------------+ 017. input ulong user00 = 0; //For Expert Advisor use 018. //+------------------------------------------------------------------+ 019. struct st00 020. { 021. ulong ticket; 022. string szShortName, 023. szSymbol; 024. double priceOpen; 025. char digits; 026. bool bIsBuy; 027. }m_Infos; 028. //+------------------------------------------------------------------+ 029. C_ElementsTrade *Open = NULL, *Stop = NULL, *Take = NULL; 030. //+------------------------------------------------------------------+ 031. bool CheckCatch(ulong ticket) 032. { 033. ZeroMemory(m_Infos); 034. m_Infos.szShortName = StringFormat("%I64u", m_Infos.ticket = ticket); 035. if (!PositionSelectByTicket(m_Infos.ticket)) return false; 036. if (ObjectFind(0, m_Infos.szShortName) >= 0) 037. { 038. m_Infos.ticket = 0; 039. return false; 040. } 041. m_Infos.szSymbol = PositionGetString(POSITION_SYMBOL); 042. m_Infos.digits = (char)SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); 043. IndicatorSetString(INDICATOR_SHORTNAME, m_Infos.szShortName); 044. EventChartCustom(0, evUpdate_Position, ticket, 0, ""); 045. 046. return true; 047. } 048. //+------------------------------------------------------------------+ 049. inline void ProfitNow(void) 050. { 051. double ask, bid; 052. 053. ask = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_ASK); 054. bid = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_BID); 055. if (Open != NULL) 056. (*Open).ViewValue((m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask)); 057. } 058. //+------------------------------------------------------------------+ 059. int OnInit() 060. { 061. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 062. if (!CheckCatch(user00)) 063. { 064. ChartIndicatorDelete(0, 0, def_ShortName); 065. return INIT_FAILED; 066. } 067. 068. return INIT_SUCCEEDED; 069. } 070. //+------------------------------------------------------------------+ 071. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 072. { 073. ProfitNow(); 074. 075. return rates_total; 076. } 077. //+------------------------------------------------------------------+ 078. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 079. { 080. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 081. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 082. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 083. switch (id) 084. { 085. case CHARTEVENT_CUSTOM + evUpdate_Position: 086. if (lparam != m_Infos.ticket) return; 087. if (!PositionSelectByTicket(m_Infos.ticket)) 088. { 089. ChartIndicatorDelete(0, 0, m_Infos.szShortName); 090. return; 091. }; 092. if (Open == NULL) Open = new C_ElementsTrade(m_Infos.ticket, evMsgClosePositionEA, clrRoyalBlue, m_Infos.digits, StringFormat("%I64u : Position opening price.", m_Infos.ticket), m_Infos.bIsBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)); 093. if (Take == NULL) Take = new C_ElementsTrade(m_Infos.ticket, evMsgCloseTakeProfit, clrForestGreen, m_Infos.digits, StringFormat("%I64u : Take Profit price.", m_Infos.ticket), m_Infos.bIsBuy); 094. if (Stop == NULL) Stop = new C_ElementsTrade(m_Infos.ticket, evMsgCloseStopLoss, clrFireBrick, m_Infos.digits, StringFormat("%I64u : Stop Loss price.", m_Infos.ticket), m_Infos.bIsBuy); 095. (*Open).UpdatePrice(0, m_Infos.priceOpen = PositionGetDouble(POSITION_PRICE_OPEN)); 096. (*Take).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_TP)); 097. (*Stop).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_SL)); 098. ProfitNow(); 099. break; 100. } 101. ChartRedraw(); 102. }; 103. //+------------------------------------------------------------------+ 104. void OnDeinit(const int reason) 105. { 106. delete Open; 107. delete Take; 108. delete Stop; 109. } 110. //+------------------------------------------------------------------+
Indicador de posición
Las modificaciones del código quedan como ejercicio para que estudies su funcionamiento. Como son cambios menores, no entraré en detalles aquí. Al ejecutar el código actualizado, obtendrás el resultado de la siguiente animación:

Observa que, cuando movemos la línea, ya sea la de take profit o la de stop loss, el indicador nos mostrará cuántos puntos existen entre el precio de apertura y la línea que estamos ajustando. Un detalle: si mueves la línea de take profit de forma que el valor quede negativo, el indicador lo reflejará y pasará a mostrarse en rojo, como si fuera la línea de stop loss. El cambio de color indica que, si el take profit aparece en rojo, al cerrar la posición en ese punto tendrás pérdidas. Lo mismo se aplica al stop loss. Cuando entre en la región en la que pase a mostrarse en verde, del mismo color que el take profit, significará que, si la posición se cierra en ese punto, obviamente obtendrás una ganancia equivalente a la cantidad de puntos indicada. El funcionamiento del indicador es sencillo y directo. No tiene ningún secreto.
Sin embargo, hay un detalle. Observa que tanto la línea de take profit como la de stop loss están presentes en la posición. Pero ¿qué ocurre si intentas crear una de ellas mediante los controles de Position View? ¿El campo gráfico de distancia mostrará la cantidad de puntos durante la creación? Bien. Sin realizar nuevas modificaciones en el código, obtendrás el resultado de la siguiente animación.

Observa que el campo OBJ_EDIT de distancia solo aparece después de confirmar con un clic el precio en el que se creará el take profit o el stop loss. A partir de ese momento, el campo muestra la separación, en puntos, entre la línea confirmada y el precio de apertura. El cálculo funciona para ambas líneas. Durante el arrastre inicial todavía no existe una línea de take profit o stop loss confirmada; solo movemos el objeto gráfico de interacción. Aunque esto no impide crear la línea, obliga a confirmar primero el precio y ajustar después su ubicación, como vimos en la primera animación. El comportamiento resulta poco natural, así que debemos mejorarlo. Modificar el indicador parece complejo, como si exigiera un doctorado en programación o una mente prodigiosa con un nivel intelectual de cinco millones. ¿La modificación exige ese nivel de complejidad? Para responder a esta pregunta, pasemos al siguiente tema, ya que así podremos separar mejor ambos temas.
Añadiendo una interacción más intuitiva
Ahora implementaremos esta solución porque MQL5 sigue el mismo principio que la programación basada en eventos. Los desarrolladores utilizan este modelo con frecuencia al crear DLL. Sé que, al principio, el funcionamiento basado en eventos parecerá confuso y carente de lógica. Pero créeme, mi querido lector: llevo tiempo trabajando con programación basada en eventos y afirmo que comprenderla y dominarla requiere práctica y dedicación. Tendrás que programar varias aplicaciones basadas en eventos para comprender por completo el mecanismo que intentaré explicar aquí.
La idea es modificar lo menos posible el código para ofrecer una interacción más intuitiva. Al intentar crear una línea de take profit o stop loss, debemos actualizar el campo OBJ_EDIT de distancia durante el arrastre con el precio correspondiente a la coordenada vertical actual del cursor. Así mostraremos una distancia provisional antes de confirmar la línea. Mientras el take profit o el stop loss todavía no existe en el servidor, solo está presente el objeto gráfico de interacción, que permite seleccionar el tipo de línea, desplazar el cursor hasta el precio deseado y confirmar la creación con un clic. La selección y el arrastre funcionan correctamente, pero el campo OBJ_EDIT no calcula la separación entre ese precio provisional y el precio de apertura. Nuestro primer problema consiste en actualizar el campo durante el arrastre.
El segundo problema surge cuando mostramos temporalmente el campo OBJ_EDIT de distancia antes de crear la línea. Después de iniciar la creación, el usuario cancela la operación con la tecla ESC. En ese caso, la línea de stop loss o take profit no se crea. La cancelación ya funciona, pero también exige ocultar o restablecer el campo gráfico de distancia. De lo contrario, ese campo permanecerá visible en el gráfico con un valor correspondiente a una línea inexistente y proporcionará información falsa al operador.
Debemos resolver dos problemas con el menor número posible de cambios: actualizar el campo OBJ_EDIT durante el arrastre y ocultarlo después de cancelar la operación. Muchos comenzarían a añadir y reescribir código sin criterio y modificarían gran parte de C_ElementsTrade para actualizar y ocultar el campo de distancia. Esa sería una opción válida, pero aquí modificaremos únicamente el código del indicador de posición.
A continuación aparece el nuevo código.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property icon "/Images/Market Replay/Icons/Positions.ico" 004. #property description "Indicator for tracking an open position on the server." 005. #property description "This should preferably be used together with an Expert Advisor." 006. #property description "For more details see the same article." 007. #property version "1.126" 008. #property link "https://www.mql5.com/pt/articles/13361" 009. #property indicator_chart_window 010. #property indicator_plots 0 011. //+------------------------------------------------------------------+ 012. #define def_ShortName "Position View" 013. //+------------------------------------------------------------------+ 014. #include <Market Replay\Order System\C_ElementsTrade.mqh> 015. #include <Market Replay\Defines.mqh> 016. //+------------------------------------------------------------------+ 017. input ulong user00 = 0; //For Expert Advisor use 018. //+------------------------------------------------------------------+ 019. struct st00 020. { 021. ulong ticket; 022. string szShortName, 023. szSymbol; 024. double priceOpen; 025. char digits; 026. bool bIsBuy; 027. }m_Infos; 028. //+------------------------------------------------------------------+ 029. C_ElementsTrade *Open = NULL, *Stop = NULL, *Take = NULL; 030. //+------------------------------------------------------------------+ 031. bool CheckCatch(ulong ticket) 032. { 033. ZeroMemory(m_Infos); 034. m_Infos.szShortName = StringFormat("%I64u", m_Infos.ticket = ticket); 035. if (!PositionSelectByTicket(m_Infos.ticket)) return false; 036. if (ObjectFind(0, m_Infos.szShortName) >= 0) 037. { 038. m_Infos.ticket = 0; 039. return false; 040. } 041. m_Infos.szSymbol = PositionGetString(POSITION_SYMBOL); 042. m_Infos.digits = (char)SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); 043. IndicatorSetString(INDICATOR_SHORTNAME, m_Infos.szShortName); 044. EventChartCustom(0, evUpdate_Position, ticket, 0, ""); 045. 046. return true; 047. } 048. //+------------------------------------------------------------------+ 049. inline void ProfitNow(void) 050. { 051. double ask, bid; 052. 053. ask = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_ASK); 054. bid = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_BID); 055. if (Open != NULL) 056. (*Open).ViewValue((m_Infos.bIsBuy ? bid - m_Infos.priceOpen : m_Infos.priceOpen - ask)); 057. } 058. //+------------------------------------------------------------------+ 059. int OnInit() 060. { 061. IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); 062. if (!CheckCatch(user00)) 063. { 064. ChartIndicatorDelete(0, 0, def_ShortName); 065. return INIT_FAILED; 066. } 067. 068. return INIT_SUCCEEDED; 069. } 070. //+------------------------------------------------------------------+ 071. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 072. { 073. ProfitNow(); 074. 075. return rates_total; 076. } 077. //+------------------------------------------------------------------+ 078. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 079. { 080. switch (id) 081. { 082. case CHARTEVENT_CUSTOM + evMsgSetFocus: 083. if (lparam != m_Infos.ticket) break; 084. switch ((EnumEvents)dparam) 085. { 086. case evMsgCloseTakeProfit: 087. (*Take).UpdatePrice(m_Infos.priceOpen, (*Take).GetPositionsMouse().Position.Price); 088. break; 089. case evMsgCloseStopLoss: 090. (*Stop).UpdatePrice(m_Infos.priceOpen, (*Stop).GetPositionsMouse().Position.Price); 091. break; 092. } 093. break; 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(m_Infos.ticket, evMsgClosePositionEA, clrRoyalBlue, m_Infos.digits, StringFormat("%I64u : Position opening price.", m_Infos.ticket), m_Infos.bIsBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)); 102. if (Take == NULL) Take = new C_ElementsTrade(m_Infos.ticket, evMsgCloseTakeProfit, clrForestGreen, m_Infos.digits, StringFormat("%I64u : Take Profit price.", m_Infos.ticket), m_Infos.bIsBuy); 103. if (Stop == NULL) Stop = new C_ElementsTrade(m_Infos.ticket, evMsgCloseStopLoss, clrFireBrick, m_Infos.digits, StringFormat("%I64u : Stop Loss price.", m_Infos.ticket), m_Infos.bIsBuy); 104. (*Open).UpdatePrice(0, m_Infos.priceOpen = PositionGetDouble(POSITION_PRICE_OPEN)); 105. (*Take).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_TP)); 106. (*Stop).UpdatePrice(m_Infos.priceOpen, PositionGetDouble(POSITION_SL)); 107. ProfitNow(); 108. break; 109. } 110. if (Open != NULL) (*Open).DispatchMessage(id, lparam, dparam, sparam); 111. if (Take != NULL) (*Take).DispatchMessage(id, lparam, dparam, sparam); 112. if (Stop != NULL) (*Stop).DispatchMessage(id, lparam, dparam, sparam); 113. ChartRedraw(); 114. }; 115. //+------------------------------------------------------------------+ 116. void OnDeinit(const int reason) 117. { 118. delete Open; 119. delete Take; 120. delete Stop; 121. } 122. //+------------------------------------------------------------------+
Código do Indicador
En la siguiente animación, presionamos la tecla ESC y cancelamos la creación de la línea.

La animación siguiente muestra el resultado cuando creamos la línea.

Al examinar el código del indicador, el comportamiento puede parecer difícil de justificar. Compáralo con la versión anterior que vimos en este mismo artículo. Verás que sí existen diferencias. Pero, aunque existan, ¿por qué la versión modificada del indicador, con tan pocos cambios respecto a lo que cabría esperar, consigue prácticamente completar su implementación? Que unas pocas modificaciones completen casi toda la implementación del indicador parece magia o brujería, como si alguien hubiera hecho un pacto. Pero no, mi querido lector. Obtenemos este resultado porque aplicamos correctamente los mecanismos de eventos, mensajes y reutilización de código que ofrece MQL5.
Veamos por qué bastó con modificar el código del indicador para obtener el resultado deseado. Presta atención. La línea 49 declara una función auxiliar que concentra la lógica de actualización que antes estaba escrita dentro de OnCalculate. Cuando MetaTrader 5 invoque OnCalculate, la línea 73 ejecutará esa función para recalcular y actualizar la representación de la posición. La palabra inline de la línea 49 no convierte la función en una macro ni obliga al compilador de MQL5 a copiar su cuerpo en cada punto de llamada. En MQL5, inline se conserva por compatibilidad con C++, pero no modifica el programa generado; el compilador decide automáticamente si aplica expansión en línea como optimización. Desde el punto de vista del código, sigue siendo una función con llamadas y parámetros normales.
La función auxiliar permite reutilizar la misma lógica desde dos manejadores. La línea 73 la ejecuta cuando MetaTrader 5 llama a OnCalculate; la línea 107 la ejecuta desde OnChartEvent para actualizar la representación después de recibir un evento del gráfico. Ahora deja de lado las demás secciones del código y centrémonos en OnChartEvent, donde se coordina la respuesta a los eventos del gráfico. Comprender el flujo de eventos y llamadas marca una gran diferencia en tu formación como programador, especialmente al trabajar con aplicaciones basadas en eventos.
El flujo depende del evento que genere el usuario o MetaTrader 5. Cuando el terminal detecta ese evento, invoca OnChartEvent durante la ejecución y entrega al manejador los datos asociados. Si interpretas esta función como si el programa ejecutara sus instrucciones durante la compilación, no entenderás el mecanismo. Pero, si sigues el flujo de eventos y llamadas, la secuencia comenzará a tener sentido. Dentro de OnChartEvent, trasladamos las instrucciones de las líneas 110 a 112 desde el comienzo hasta el final del manejador. De este modo, el manejador procesa primero el mensaje recibido y después actualiza la representación general.
La línea 178 de C_ElementsTrade define evMsgSetFocus como identificador del mensaje que OnChartEvent comprueba en la línea 82. Cuando el manejador recibe ese identificador, la línea 83 valida que el ticket pertenezca a la posición que gestiona Position View. A continuación, la línea 84 distribuye la ejecución según el tipo de línea seleccionado: la rama que llega a la línea 87 calcula y muestra la distancia provisional del take profit, y la rama que llega a la línea 90 hace lo mismo para el stop loss. Con estas dos ramas, Position View mantiene sincronizada la indicación gráfica de distancia durante la creación o el ajuste de cada línea.
Pero aún queda la pregunta: ¿cómo produce el código modificado del indicador el comportamiento que vemos en las animaciones? La conexión con C_ElementsTrade está en las llamadas de las líneas 87 y 90, que pasan el precio de apertura y el precio provisional de la línea a la función que calcula y actualiza el campo OBJ_EDIT de distancia.
En efecto, mi querido lector. El mecanismo exige seguir con atención el flujo de eventos. Seguramente esperabas que fuera necesario modificar el código de la clase C_ElementsTrade para que lo mostrado en las animaciones tuviera sentido. Sin embargo, sí modificamos la clase, aunque de forma sutil. ¿Recuerdas que, en el tema anterior, mencioné que habíamos cambiado la herencia? Ese cambio permite que el indicador acceda a la función heredada que utilizan las líneas 87 y 90. Cada llamada convierte la coordenada vertical del cursor en un precio del gráfico, calcula la diferencia respecto del precio de apertura y escribe la distancia resultante en el campo OBJ_EDIT correspondiente. La función recibe dos precios: el precio de apertura de la posición y el precio de referencia de la línea. Cuando la línea ya está confirmada, el segundo valor es su precio real; durante el arrastre, es el precio provisional derivado de la posición del cursor. Las líneas 105 y 106 ejecutan el mismo cálculo con los precios de take profit y stop loss ya confirmados en el servidor.
Las líneas 105 y 106 actualizan la indicación gráfica con el precio de take profit o stop loss registrado en el servidor. En cambio, las líneas 87 y 90 utilizan un precio provisional calculado a partir de la coordenada vertical del cursor. En esta fase todavía no existe en el servidor una línea de take profit o stop loss con ese precio; solo existen el objeto gráfico de interacción y la indicación local de distancia. Virgen María. Ahora sí que el análisis se complicó. Entonces, cuando seleccionamos el objeto de interacción y movemos el cursor, ¿son las líneas 87 y 90 las que desplazan ese objeto en las animaciones? NO.
En la versión anterior no existían las llamadas de las líneas 87 y 90. Estas llamadas NO DESPLAZAN EL OBJETO GRÁFICO: reciben el precio provisional asociado al cursor, calculan su distancia respecto del precio de apertura y actualizan el campo OBJ_EDIT correspondiente antes de que la línea quede confirmada. Así se reutiliza la misma lógica de cálculo y visualización sin afirmar que la línea exista todavía en el servidor.
Consideraciones finales
Sé que el código puede parecer confuso al principio. Sin embargo, no resulta fácil visualizar su funcionamiento observando solo el código, y el intercambio de mensajes y eventos resulta difícil de entender sin comprender la lógica implicada. No obstante, necesito un poco más de espacio para explicar con detalle lo que está ocurriendo aquí. En el próximo artículo detendré temporalmente el desarrollo del sistema de repetición/simulación para explicar mejor cómo las aplicaciones y MetaTrader 5 intercambian mensajes y eventos.
Así, quienes estén interesados en aprender a utilizar el modelo de programación basada en eventos sabrán por dónde empezar. Comprender el funcionamiento de este mecanismo me exigió mucho tiempo de estudio. Cuando lo entendí, empecé a diseñar y escribir los programas de otra manera.
Quiero que tú, mi querido lector y amigo, entiendas cómo se coordinan los eventos, los mensajes y las actualizaciones gráficas. Muchos programadores invierten varias horas en soluciones que pueden implementarse con mayor rapidez y claridad mediante programación basada en eventos e intercambio de mensajes.
Para comprobar el funcionamiento del sistema de mensajes y eventos, en el archivo adjunto dejaré disponibles los archivos ya compilados. Así evitarás compilar todos los archivos y reducirás el riesgo de cometer errores durante la compilación. De esta forma, me despido por ahora. Nos vemos en el próximo artículo, donde prometo explicar el mecanismo de intercambio de mensajes y la programación basada en eventos. 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 repetición.mq5 | Crea los controles para interactuar con el servicio de repetición y simulación de mercado (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 (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 repetición.mq5 | Crea y mantiene el servicio de repetición y simulación de mercado (Archivo principal del sistema de repetición y simulación de mercado) |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/13361
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 25): Creación de un asesor experto (EA) que opere a partir de objetos del gráfico (II)
Del básico al intermedio: Clases (I)
Optimización por Comunidad de Científicos: Teoría
Simulación de mercado: Position View (XIII)
- 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