Simulación de mercado: Position View (XV)
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 (XIV), mostré cómo añadir los detalles necesarios para que las líneas de take profit y stop loss mostraran el resultado asociado a su activación. Es decir, cuando el precio alcanzara uno de esos niveles, ya fuera el de stop loss o el de take profit, podríamos saber si habría una ganancia o una pérdida y cuál sería su magnitud. Por ahora, expresamos ese resultado en puntos, pues todavía no trabajamos con importes monetarios.
En ese mismo artículo prometí explicar los detalles de cómo el intercambio de mensajes y la programación basada en eventos hacen posible que funcione un código como el que estoy presentando, como muchos pueden comprobar claramente. Incluyo los ejecutables en el archivo adjunto cuando son suficientemente estables y también muestro el código completo, para que otros programadores tengan acceso a la información y a lo que se está implementando.
Aunque el código presentado hasta el artículo anterior funciona perfectamente, podemos ir mucho más allá. Para simplificar la explicación, decidí publicar de nuevo el mismo código, pero con las llamadas actualizadas. Su funcionamiento es idéntico al de la versión anterior, aunque la implementación ha cambiado ligeramente, no es necesario explicarla por separado: si comprendes lo que explicaré en este artículo, también podrás entender el código, si has venido siguiendo la serie hasta este punto. Comencemos.
Una actualización rápida
A continuación puedes ver el código actualizado. Compáralo con la versión del artículo anterior y comprobarás que sigue siendo el mismo, aunque a primera vista pueda parecer diferente. El código principal aparece a continuación.
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.127" 008. #property link "https://www.mql5.com/pt/articles/13375" 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) break; 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
A continuación se muestra el código de la clase.
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 : private 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. EventChartCustom(0, evUpdate_Position, _lparam, 0, ""); 179. case CHARTEVENT_CUSTOM + evMsgSetFocus: 180. if ((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)) 181. UpdatePrice(m_Info.open, GetPositionsMouse().Position.Price); 182. macro_LineInFocus((m_Info.ticket == (ulong)(_lparam)) && ((EnumEvents)(_dparam) == m_Info.ev)); 183. EventChartCustom(0, (ushort)(_dparam ? evHideMouse : evShowMouse), 0, 0, ""); 184. m_Info.bClick = false; 185. case CHARTEVENT_CHART_CHANGE: 186. UpdateViewPort(m_Info.price); 187. if (m_Info.ev != evMsgClosePositionEA) 188. ViewValue(m_Info.bIsBuy ? m_Info.price - m_Info.open : m_Info.open - m_Info.price); 189. break; 190. case CHARTEVENT_OBJECT_CLICK: 191. sz0 = GetPositionsMouse().szObjNameClick; 192. if (m_Info.bClick) 193. { 194. if (sz0 == def_NameBtnMove) 195. EventChartCustom(0, evMsgSetFocus, m_Info.ticket, m_Info.ev, ""); 196. if (sz0 == def_NameBtnClose) 197. EventChartCustom(0, (ushort) m_Info.ev, m_Info.ticket, PositionGetDouble(m_Info.ev == evMsgCloseTakeProfit ? POSITION_SL : POSITION_TP), PositionGetString(POSITION_SYMBOL)); 198. } 199. m_Info.bClick = false; 200. break; 201. case CHARTEVENT_MOUSE_MOVE: 202. m_Info.bClick = (CheckClick(C_Mouse::eClickLeft) ? true : m_Info.bClick); 203. if (m_Info.weight > 1) 204. { 205. UpdateViewPort(_dparam = GetPositionsMouse().Position.Price); 206. if (m_Info.ev != evMsgClosePositionEA) 207. ViewValue(m_Info.bIsBuy ? _dparam - m_Info.open : m_Info.open - _dparam); 208. if (m_Info.bClick) 209. { 210. if ((m_Info.ev == evMsgCloseTakeProfit) || (m_Info.ev == evMsgCloseStopLoss)) 211. EventChartCustom(0, (ushort)(m_Info.ev == evMsgCloseTakeProfit ? evMsgNewTakeProfit : evMsgNewStopLoss), m_Info.ticket, GetPositionsMouse().Position.Price, PositionGetString(POSITION_SYMBOL)); 212. EventChartCustom(0, evMsgSetFocus, 0, 0, ""); 213. } 214. } 215. break; 216. } 217. } 218. //+------------------------------------------------------------------+ 219. }; 220. //+------------------------------------------------------------------+ 221. #undef macro_LineInFocus 222. //+------------------------------------------------------------------+ 223. #undef def_Btn_Close 224. #undef def_PathBtns 225. #undef def_FontName 226. #undef def_FontSize 227. //+------------------------------------------------------------------+ 228. #undef def_NameObjLabel 229. #undef def_NameInfoDirect 230. #undef def_NameBtnMove 231. #undef def_NameBtnClose 232. #undef def_NameHLine 233. //+------------------------------------------------------------------+
C_ElementsTrade
Una vez compilado, este código funciona de la misma manera que el del artículo anterior, por lo que no merece ninguna mención especial. Sin embargo, lo utilizaré como base para la explicación que sigue, ya que facilita ligeramente la comprensión de algunos de los conceptos que necesitaré utilizar. En determinados momentos tendrás que consultar los artículos anteriores para poder seguir toda la explicación y comprender cómo la programación basada en eventos y el intercambio de mensajes entre aplicaciones permiten que este código, aparentemente sin sentido, funcione. Así que ponte cómodo y veamos cómo utilizar este modelo de implementación. Intentaré explicarlo de la forma más didáctica posible.
Por qué funciona el sistema
Para comprender todo el sistema, comenzaremos por lo esencial. La estructura inicial y más básica aparece en la siguiente imagen:

Empecemos a analizar el intercambio de mensajes a partir de esta imagen. Llevamos bastante tiempo utilizando este modelo de programación, pero hasta ahora no me había detenido a explicarlo en detalle. Observa que del indicador Chart Trade salen tres flechas. Cada una representa un evento personalizado que se disparará cuando el usuario interactúe con Chart Trade mediante el puntero del mouse.
Aunque los eventos se capturan y tratan en el código del Asesor Experto, Chart Trade no los dispara directamente hacia él. En realidad, los dispara mediante MetaTrader 5, que posteriormente los distribuye al gráfico correspondiente. Presta atención a este detalle. Puedes indicar a MetaTrader 5 que distribuya el evento al gráfico actual o a cualquier otro gráfico.
Como resultado, MetaTrader 5 distribuirá el evento personalizado y ejecutará OnChartEvent en las aplicaciones vinculadas al gráfico. De este modo, el Asesor Experto podrá capturar y tratar el evento que Chart Trade haya disparado a partir de la solicitud del usuario. Una vez interpretada correctamente, el Asesor Experto remitirá una solicitud al servidor de trading para abrir o cerrar una posición, o modificar su volumen. Esta es la primera parte del sistema. Cuando el servidor responde a la solicitud, se inicia el flujo de mensajes y eventos que aparece a continuación. El flujo representado arriba se produce cada vez que se envía una solicitud al servidor.
No obstante, observa que el diagrama incluye una flecha MORADA. Esta flecha indica que el Asesor Experto creará el indicador de posición. En esta fase, no se dispara ningún evento que las demás aplicaciones puedan capturar.

Sin embargo, en cuanto el indicador se encuentra en el gráfico, el Asesor Experto dispara un evento personalizado mediante MetaTrader 5. MetaTrader 5 lo distribuye al gráfico, y el indicador de posición lo captura, con lo que obtiene los datos más recientes de la posición. Observa que el indicador de posición desconoce la presencia del Asesor Experto. Del mismo modo, el Asesor Experto no sabe si el indicador de posición está presente en el gráfico.
Ambas aplicaciones ignoran por completo la existencia de la otra. Lo único que hacen es disparar un evento personalizado mediante MetaTrader 5, que lo distribuye al gráfico indicado por la aplicación remitente. El evento solo será capturado y tratado si alguna aplicación implementa el tratamiento correspondiente. De lo contrario, se perderá.
Sin embargo, la imagen anterior no muestra qué sucede cuando el usuario interactúa con el indicador de posición, ya que en ella no aparece el indicador de mouse. Cuando se produce una interacción entre ambos indicadores, el funcionamiento cambia ligeramente y obtenemos un esquema como el siguiente.

Al observar ahora el esquema de funcionamiento, puede parecer que todo se ha vuelto mucho más complicado, pero no es así. La imagen anterior representa por completo lo que ocurre entre el indicador de mouse, el indicador de posición y el Asesor Experto cuando los tres están presentes en el gráfico. Llegados a este punto, podemos volver al código mostrado al principio del artículo para comprender cada una de las flechas del diagrama.
Observa un detalle curioso. Aquí aparece una flecha GRIS que, en apariencia, representa un intercambio de información entre el indicador de mouse y el indicador de posición. Sin embargo, esa no es la relación que muestra. La flecha GRIS representa la herencia entre clases: en la línea 25, la clase C_ElementsTrade hereda de forma privada de la clase C_Mouse. No obstante, debes tener cuidado al interpretar este esquema.
Podrías pensar que no es necesario mantener el indicador de mouse en el gráfico porque la clase C_ElementsTrade hereda de la clase C_Mouse, pero la herencia solo relaciona las clases y no sustituye al indicador. Es imprescindible que el indicador de mouse esté presente en el gráfico. De lo contrario, el indicador de posición no podrá capturar los eventos del ratón. Ahora presta mucha atención.
En este esquema vemos una flecha que parte de MetaTrader 5 y se dirige al indicador de posición. Esta flecha representa el flujo de los eventos procedentes de MetaTrader 5: el procedimiento OnChartEvent del código principal captura el evento y DispatchMessage, dentro de la clase, realiza su tratamiento. Observa también que en el diagrama he separado el evento Update de los demás eventos. ¿Por qué lo hice? Porque necesitas comprender que aquí existen dos tipos de eventos. El primero lo dispara el Asesor Experto: son los eventos Update. El segundo lo dispara el indicador de posición o MetaTrader 5. En ambos casos, el indicador de mouse y el Asesor Experto también capturan este segundo tipo de evento. Sin embargo, sus códigos no implementan el tratamiento del evento procedente del indicador de posición, por lo que lo ignoran por completo. Ese mismo evento, en cambio, sí es importante para el indicador de posición.
Cuando este indicador dispara un evento personalizado, MetaTrader 5 llama a OnChartEvent en las aplicaciones vinculadas al gráfico. Todas capturan el evento disparado por el indicador de posición y distribuido por MetaTrader 5. No obstante, solo DispatchMessage del indicador de posición implementa el tratamiento correspondiente.
Uno de los momentos en que esto ocurre es precisamente cuando se ejecuta la línea 178. Observa que allí se dispara el evento personalizado evUpdate_Position. El procedimiento OnChartEvent del indicador de posición lo captura en la línea 85 y lo entrega a DispatchMessage para su tratamiento. En principio, el código podría interpretar que el evento lo disparó el Asesor Experto, pero no es así. Lo dispara el propio indicador de posición mediante MetaTrader 5, que lo distribuye de nuevo a las aplicaciones vinculadas al gráfico. De este modo, se obliga al indicador de posición a actualizar los datos de la posición. Como el evento se dispara después de pulsar la tecla ESC, podemos tratar esa condición sin necesidad de conservar ningún dato en memoria.
Sin embargo, este no es el aspecto más interesante. ¿Recuerdas el artículo anterior, en que expliqué cómo modificar el código para crear una línea de stop loss o take profit, si aún no existía, mediante la interacción entre el indicador de mouse y el indicador de posición? Pues bien, esa tarea está representada en la imagen anterior, aunque no resulte tan evidente. Sigue el razonamiento. La clase C_ElementsTrade accede a los miembros heredados de C_Mouse mediante herencia privada, mientras que el indicador de posición obtiene las actualizaciones del ratón principalmente a través de los eventos procedentes de MetaTrader 5.
En un primer momento, la variable m_Info.bClick tendrá el valor false. Por tanto, cuando MetaTrader 5 dispare el evento CHARTEVENT_OBJECT_CLICK, la línea 192 impedirá que el código continúe, ya que allí se comprueba el valor de m_Info.bClick. Sin embargo, si el indicador de mouse informa de que el clic es válido, la línea 202 asignará el valor true a m_Info.bClick. Como el evento CHARTEVENT_OBJECT_CLICK se dispara después de CHARTEVENT_MOUSE_MOVE, tal como vimos en los artículos anteriores, la comprobación de la línea 192 se superará. Así, si se hace clic en el objeto de movimiento, la línea 195 disparará un evento personalizado mediante MetaTrader 5.
Ahora es cuando el funcionamiento se vuelve interesante, aunque pueda parecer algo confuso. Mantén siempre el diagrama como referencia. Ejecutar la línea 195 equivale, en la práctica, a provocar una ejecución equivalente a la de la línea 179. Sin embargo, aunque la llamada se origina dentro del mismo código, para la aplicación, la llamada parece proceder de fuera del código del indicador. En ese momento, el código actualiza algunos valores y a continuación, retorna. Paralelamente, se ejecuta la línea 199, con lo que finaliza el tratamiento de los eventos de las líneas 179 y 190. Si hay más de un indicador de posición en el gráfico, todos capturarán el mismo evento personalizado distribuido por MetaTrader 5, con independencia de dónde se haya disparado. Todos responderán de una forma muy similar. De este modo, la línea que debe recibir el foco recibirá el foco del mouse, mientras que las demás lo perderán.
La próxima vez que el indicador de posición capture un evento CHARTEVENT_MOUSE_MOVE, comprobaremos en la línea 203 si tiene el foco. En caso afirmativo, las líneas 205 y 207 mostrarán el desplazamiento de la línea, ya sea la de take profit o la de stop loss. Ninguna otra línea se verá afectada por ese movimiento. Sin embargo, el desplazamiento todavía no es real. Es decir, el desplazamiento no se produce realmente a medida que mueves el mouse. El movimiento es virtual y queda a la espera de un nuevo evento que lo confirme o lo descarte. Si se descarta, lo que sucede al pulsar la tecla ESC, MetaTrader 5 dispara un evento que se captura en la línea 174. Como consecuencia, se revisan todos los datos, ya que en la línea 178 se dispara un evento Update. De este modo, el indicador de posición vuelve a mostrar los datos anteriores, es decir, los que permanecen registrados en el servidor de trading.
Si, por el contrario, se aceptan los datos correspondientes al movimiento del ratón porque el operador hace clic en la posición de precio deseada, en la línea 211 se dispara un evento personalizado dirigido al Asesor Experto. Al capturarlo y tratarlo, el Asesor Experto remite una solicitud al servidor para actualizar la posición de la línea de take profit o de stop loss. A continuación, en la línea 212, se dispara otro evento dirigido a todos los indicadores de posición, incluido el que lo originó, para que las líneas de take profit y stop loss pierdan el foco. Mientras tanto, el Asesor Experto espera la respuesta del servidor de trading. Cuando llega la respuesta, el Asesor Experto dispara un evento Update dirigido al indicador de posición cuyo ticket coincida con el valor incluido en el evento. Así regresamos de nuevo a la línea 85 del procedimiento OnChartEvent del indicador de posición.
Esta es la gran ventaja del intercambio de mensajes y de la programación basada en eventos. No necesitamos controlar estrictamente el orden en que suceden las cosas, siempre que cada evento se dispare, se capture y se trate correctamente. Observa, sin embargo, que el indicador de mouse también captura un evento. El indicador de posición lo dispara en la línea 183 y el indicador de mouse lo trata para ocultar su línea de precio. También podríamos modificar por completo este comportamiento. Bastaría con indicar al indicador de mouse qué debe hacer, y este ejecutaría la acción correspondiente. Incluso podría activar algún tipo de estudio u otra acción similar mientras desplazamos el puntero para posicionar la línea de stop loss o de take profit.
Como vengo señalando desde hace tiempo, no existen límites para lo que podemos hacer. Por eso dedico este artículo a explicarte, estimado lector, cómo utilizar la programación basada en eventos y el intercambio de mensajes. Si necesitas manipular o analizar cualquier elemento, podrás hacerlo de una forma muy sencilla, rápida y directa desde el gráfico. No es necesario recurrir a métodos ni recursos innecesariamente complejos.
Por ejemplo, varias personas me han hecho una petición cuyo objetivo me parece bastante razonable. Aunque no expliqué cómo hacerlo ni las animé a pensar de esa manera, es algo que podemos implementar fácilmente mediante el intercambio de mensajes entre aplicaciones.
La petición es la siguiente: quieren que el Asesor Experto que están creando indique al servicio de repetición/simulador que deje de emitir nuevos ticks o cotizaciones. A primera vista, esto parece extremadamente complicado de implementar, ya que no podemos enviar una solicitud o un mensaje ni disparar un evento directamente al servicio. Recuerda que los eventos personalizados disparados y los mensajes enviados mediante este mecanismo siempre están vinculados al gráfico. Es decir, no podemos transferir mensajes directamente a un servicio porque este no está vinculado a ningún gráfico específico. Sin embargo, sí existe una forma de transferir o reenviar mensajes a un servicio. Para separar los conceptos y mostrarte cómo proceder, pasemos a un nuevo tema.
Pausar el servicio de repetición/simulador directamente desde el Asesor Experto
Para alcanzar este objetivo necesitaremos una pequeña ayuda. O, mejor dicho, necesitamos algo. Para que el servicio pueda obtener o recibir información procedente de eventos o mensajes, debe monitorizar algún elemento presente en el gráfico. Una vez más, los mensajes y los eventos se dirigen al gráfico. NUNCA OLVIDES ESTO cuando programes en MQL5. Afortunadamente, el servicio de repetición/simulador dispone de un indicador en el gráfico: el indicador de control. Al interactuar con él mediante el indicador de mouse, podemos indicar al servicio que cambie al modo pausa o al modo reproducción.
Sin embargo, el sistema no se diseñó originalmente para modelar o probar estrategias automatizadas, es decir, para usar un Asesor Experto que te ayude en las operaciones de mercado. Por ese motivo, hasta ahora no dispone de ningún evento ni mensaje que permita cambiar el servicio al modo pausa o al modo reproducción a petición del Asesor Experto, de otro indicador o de un script en MQL5 vinculado al gráfico. Para conseguirlo, tendremos que modificar ligeramente el código.
No te preocupes: son cambios muy sencillos que podrás incorporar con facilidad. Solo tendrás que añadir una línea al Asesor Experto, indicador o script que estés desarrollando. Esto permitirá alternar el servicio de repetición/simulador entre el modo reproducción y el modo pausa.
Veamos ahora qué debemos modificar en el código original del sistema de repetición/simulador. Para ello, localiza la clase C_Controls y dirígete al procedimiento DispatchMessage Aquí debemos añadir el tratamiento de los eventos personalizados que permitirán alternar el servicio entre el modo reproducción y el modo pausa. El fragmento original que modificaremos aparece a continuación:
179. //+------------------------------------------------------------------+ 180. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 181. { 182. short x, y; 183. static ushort iPinPosX = 0; 184. static short six = -1, sps; 185. uCast_Double info; 186. 187. switch (id) 188. { 189. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 190. info.dValue = dparam; 191. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 192. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 193. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 194. break; 195. case CHARTEVENT_OBJECT_DELETE: 196. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 197. { 198. if (sparam == def_ObjectCtrlName(ePlay)) 199. { 200. delete m_Section[ePlay].Btn; 201. m_Section[ePlay].Btn = NULL; 202. SetPlay(m_Section[ePlay].state); 203. }else 204. { 205. RemoveCtrlSlider(); 206. CreateCtrlSlider(); 207. } 208. } 209. break; 210. case CHARTEVENT_MOUSE_MOVE: 211. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 212. { 213. case ePlay: 214. SetPlay(!m_Section[ePlay].state); 215. if (m_Section[ePlay].state) 216. { 217. RemoveCtrlSlider(); 218. m_Slider.Minimal = iPinPosX; 219. }else CreateCtrlSlider(); 220. break; 221. case eLeft: 222. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - 1 : m_Slider.Minimal)); 223. break; 224. case eRight: 225. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider)); 226. break; 227. case ePin: 228. if (six == -1) 229. { 230. six = x; 231. sps = (short)iPinPosX; 232. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 233. } 234. iPinPosX = sps + x - six; 235. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); 236. break; 237. }else if (six > 0) 238. { 239. six = -1; 240. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 241. } 242. break; 243. } 244. ChartRedraw(GetInfoTerminal().ID); 245. } 246. //+------------------------------------------------------------------+
Fragmento original de C_Controls
Observa que en la línea 213 hay un filtro, que determina dónde se ha hecho clic. Si pulsamos el botón de reproducción, el servicio cambiará del modo pausa al modo reproducción. Si ya está en modo reproducción, ocurrirá lo contrario. Este cambio se realiza al ejecutar la línea 214. Por tanto, solo necesitamos disparar un evento personalizado desde este procedimiento. Cuando OnChartEvent del indicador de control lo capture, DispatchMessage podrá tratarlo e indicar al servicio de repetición/simulador si debe cambiar al modo reproducción o al modo pausa. Así de sencillo.
Para hacerlo, abre el archivo Defines.mqh y añade los dos nuevos eventos que aparecen a continuación:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_VERSION_DEBUG 05. //+------------------------------------------------------------------+ 06. #ifdef def_VERSION_DEBUG 07. #define macro_DEBUG_MODE(A) \ 08. Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A)); 09. #else 10. #define macro_DEBUG_MODE(A) 11. #endif 12. //+------------------------------------------------------------------+ 13. #define def_SymbolReplay "RePlay" 14. #define def_MaxPosSlider 400 15. #define def_MaskTimeService 0xFED00000 16. #define def_IndicatorTimeFrame (_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))) 17. #define def_IndexTimeFrame 4 18. //+------------------------------------------------------------------+ 19. union uCast_Double 20. { 21. double dValue; 22. long _long; // 1 Information 23. datetime _datetime; // 1 Information 24. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 25. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 26. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 27. }; 28. //+------------------------------------------------------------------+ 29. enum EnumEvents { 30. evTicTac, //Event of tic-tac 31. evHideMouse, //Hide mouse price line 32. evShowMouse, //Show mouse price line 33. evHideBarTime, //Hide bar time 34. evShowBarTime, //Show bar time 35. evHideDailyVar, //Hide daily variation 36. evShowDailyVar, //Show daily variation 37. evHidePriceVar, //Hide instantaneous variation 38. evShowPriceVar, //Show instantaneous variation 39. evCtrlReplayInit, //Initialize replay control 40. evChartTradeBuy, //Market buy event 41. evChartTradeSell, //Market sales event 42. evChartTradeCloseAll, //Event to close positions 43. evChartTrade_At_EA, //Event to communication 44. evEA_At_ChartTrade, //Event to communication 45. evChatWriteSocket, //Event to Mini Chat 46. evChatReadSocket, //Event To Mini Chat 47. evUpdate_Position, //Event to communication 48. evMsgClosePositionEA, //Event to communication 49. evMsgCloseTakeProfit, //Event to communication 50. evMsgCloseStopLoss, //Event to communication 51. evMsgNewTakeProfit, //Event to communication 52. evMsgNewStopLoss, //Event to communication 53. evMsgSetFocus, //Event to communication 54. evMsgServiceSwapStatus //Event to stop the replay/simulator service 55. }; 56. //+------------------------------------------------------------------+ 57. enum EnumPriority { //Priority list on objects 58. ePriorityNull = -1, 59. ePriorityDefault = 0 60. }; 61. //+------------------------------------------------------------------+
Defines.mqh
En la línea 54 se encuentra el identificador del nuevo evento personalizado. Cuando lo utilices en tu código, recuerda incluir el archivo Defines.mqh para que el compilador conozca el valor correcto. Esta línea permite alternar el estado del servicio de repetición/simulador.
EventChartCustom(0, evMsgServiceSwapStatus, 0, 0, "");
Por tanto, en algún punto de tu código, deberás añadir la siguiente línea: Si el servicio está en modo reproducción, cambiará al modo pausa y esperará la autorización para continuar. Si está en modo pausa, recibirá permiso para cambiar al modo reproducción. Como ves, es muy sencillo. Sin embargo, el lugar exacto donde debes insertar esta línea y la condición que activará su ejecución dependen de lo que estés implementando, por lo que no es posible mostrar aquí un ejemplo práctico aquí. Aun así, quienes necesiten realizar esta tarea sabrán dónde y cómo utilizarla. Recuerda también que no necesariamente tendrás que disparar tanto el evento de modo pausa como el de modo reproducción. Lo más probable es que utilices principalmente el evento de modo pausa, ya que el sistema puede volver al modo reproducción directamente mediante la interacción entre el indicador de mouse y el indicador de control.
Veamos ahora qué debemos añadir o modificar en el fragmento anterior para que el indicador de control pueda indicar al servicio de repetición/simulador qué debe hacer. El nuevo fragmento del procedimiento DispatchMessage de la clase C_Controls aparece a continuación:
179. //+------------------------------------------------------------------+ 180. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 181. { 182. short x, y; 183. static ushort iPinPosX = 0; 184. static short six = -1, sps; 185. uCast_Double info; 186. 187. switch (id) 188. { 189. case (CHARTEVENT_CUSTOM + evMsgServiceSwapStatus): 190. SetPlay(!m_Section[ePlay].state); 191. if (m_Section[ePlay].state) 192. { 193. RemoveCtrlSlider(); 194. m_Slider.Minimal = iPinPosX; 195. }else CreateCtrlSlider(); 196. break; 197. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 198. info.dValue = dparam; 199. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 200. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 201. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 202. break; 203. case CHARTEVENT_OBJECT_DELETE: 204. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 205. { 206. if (sparam == def_ObjectCtrlName(ePlay)) 207. { 208. delete m_Section[ePlay].Btn; 209. m_Section[ePlay].Btn = NULL; 210. SetPlay(m_Section[ePlay].state); 211. }else 212. { 213. RemoveCtrlSlider(); 214. CreateCtrlSlider(); 215. } 216. } 217. break; 218. case CHARTEVENT_MOUSE_MOVE: 219. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 220. { 221. case ePlay: 222. EventChartCustom(0, evMsgServiceSwapStatus, 0, 0, ""); 223. break; 224. case eLeft: 225. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - 1 : m_Slider.Minimal)); 226. break; 227. case eRight: 228. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider)); 229. break; 230. case ePin: 231. if (six == -1) 232. { 233. six = x; 234. sps = (short)iPinPosX; 235. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 236. } 237. iPinPosX = sps + x - six; 238. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); 239. break; 240. }else if (six > 0) 241. { 242. six = -1; 243. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 244. } 245. break; 246. } 247. ChartRedraw(GetInfoTerminal().ID); 248. } 249. //+------------------------------------------------------------------+
Novo fragmento de C_Controls
Observa atentamente y comprobarás que aplico el mismo mecanismo utilizado durante todo el artículo: el indicador de control dispara un evento personalizado que él mismo captura y trata. El evento se dispara en la línea 222; OnChartEvent lo captura y DispatchMessage lo trata en la línea 189. Esto garantiza que, cuando tu Asesor Experto dispare ese mismo evento, el indicador de control siga la misma ruta de captura y tratamiento. No obstante, conviene tener en cuenta un detalle. Si el Asesor Experto dispara el evento justo cuando haces clic en el botón de pausa del indicador de control, se acumularán dos eventos en la cola. Como consecuencia, el servicio cambiará al modo pausa y, acto seguido, volverá al modo reproducción. También puede ocurrir a la inversa. Puedes modificar este comportamiento si lo consideras necesario.
Sin embargo, como no estoy mostrando cómo desarrollar este sistema para convertirlo en una alternativa al simulador de estrategias de MetaTrader 5, no considero que este aspecto plantee demasiados problemas. Solo debes actuar con calma y tener presente que podrás mejorar o modificar el sistema de la forma que te parezca más interesante.
Consideraciones finales
En este artículo intenté explicar, de la forma más sencilla posible, cómo utilizar el intercambio de mensajes entre aplicaciones. El objetivo es que puedas desarrollar algo funcional de la manera más simple y eficaz, siempre que sea posible. No sé si logré transmitir la idea que sustenta este concepto, pues no resulta fácil de asimilar para quien lo encuentra por primera vez. También mostré cómo modificar el sistema de repetición/simulador para depurar un Asesor Experto o cualquier otro código que estés desarrollando. Lo hice de una forma igualmente sencilla y directa, aplicando la misma técnica descrita a lo largo del artículo para que todo resultara aún más interesante.
Como esta serie sobre el desarrollo de un sistema de repetición/simulador tiene una finalidad exclusivamente didáctica, no podía dejar pasar la oportunidad de explicar el mecanismo que el código utiliza desde hace tiempo. Espero que esto anime a quienes acaban de incorporarse a consultar los artículos anteriores. Tal vez entre estos entusiastas y aspirantes a programadores surja alguien que llegue a destacar realmente. Lo que explico aquí no es algo que pueda dominarse de la noche a la mañana. Será necesario dedicar un largo periodo al estudio y al estudio y al trabajo práctico con este mecanismo. Sin embargo, te aseguro que estudiar este concepto y esforzarte por comprenderlo merecerá mucho la pena. Nos veremos en el próximo artículo, donde continuaremos desarrollando nuestro sistema de repetición/simulador. Les deseo a todos mucho provecho en sus estudios. 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 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 (Es necesario tanto para operar el sistema de repetición/simulador como en el mercado real) |
| Indicators\Order Indicator.mq5 | Muestra las órdenes de mercado y permite interactuar con las órdenes de mercado y controlar dichas órdenes. |
| Indicators\Position View.mq5 | Muestra las posiciones de mercado y permite interactuar con las posiciones de mercado y controlar dichas posiciones. |
| Services\Market Replay.mq5 | Crea y mantiene el servicio de repetición y simulación de mercado (Archivo principal del sistema) |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/13375
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.
De novato a experto: Períodos del mercado Forex
Dominando los registros (Parte 8): Registros de errores con traducción automática
Aprendizaje automático y Data Science (Parte 44): Pronóstico de series temporales OHLC de Forex mediante autorregresión vectorial (VAR)
Optimización por Comunidad de Científicos: Teoría
- 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