- Normas generales para trabajar con proyectos locales
- Plan de proyecto de un servicio web de copia de operaciones y señales
- Servidor web basado en Nodejs
- Fundamentos teóricos del protocolo WebSockets
- Componente servidor de servicios web basados en el protocolo WebSocket
- Protocolo WebSocket en MQL5
- Programas cliente para servicios echo y chat en MQL5
- Servicio de señales de trading y página web de prueba
- Programa cliente de servicios de señales en MQL5
Programa cliente de servicios de señales en MQL5
Así pues, conforme a lo decidido, el texto de los mensajes de servicio estará en formato JSON.
En la versión más común, JSON es una descripción en texto de un objeto, similar a como se hace para las estructuras en MQL5. El objeto se encierra entre llaves, dentro de las cuales se escriben sus propiedades separadas por comas: cada propiedad tiene un identificador entre comillas, seguido de dos puntos y el valor de la propiedad. Aquí se admiten propiedades de varios tipos primitivos: cadenas, números enteros y reales, booleanos true/false y valor vacío null. Además, el valor de la propiedad puede ser, a su vez, un objeto o un array. Los arrays se describen mediante corchetes, dentro de los cuales los elementos se separan por comas. Por ejemplo:
{
|
Básicamente, el array del nivel superior también es JSON válido. Por ejemplo:
[
|
Para reducir el tráfico en los protocolos de aplicación que utilizan JSON, es habitual abreviar los nombres de los campos a varias letras (a menudo a una).
Los nombres de las propiedades y los valores de las cadenas van entre comillas dobles. Si desea especificar una cotización dentro de una cadena, debe escaparse con una barra invertida.
El uso de JSON hace que el protocolo sea versátil y extensible. Por ejemplo, para el servicio que se está diseñando (señales de trading y, en un caso más general, copia del estado de la cuenta), puede suponerse la siguiente estructura de mensajes:
{
|
Algunas de estas características pueden o no admitir implementaciones específicas de programas cliente (todo lo que no «entiendan», simplemente lo ignorarán). Además, con la condición de que no haya conflictos en los nombres de las propiedades del mismo nivel, cada proveedor de información puede añadir sus propios datos específicos a JSON. El servicio de mensajería se limitará a reenviar esta información. Por supuesto, el programa en el lado receptor debe ser capaz de interpretar estos datos específicos.
El libro viene con un analizador JSON llamado ToyJson («toy» JSON, archivo toyjson.mqh) que es pequeño e ineficiente y no admite todas las capacidades de la especificación del formato (por ejemplo, en términos de procesamiento de secuencias escape). Fue escrito específicamente para este servicio de demostración, ajustado a la estructura esperada, no muy compleja, de la información sobre las señales de trading. No lo describiremos en detalle aquí, y los principios de su uso quedarán claros en el código fuente del cliente MQL del servicio de señales.
Para sus proyectos y para el desarrollo posterior de este proyecto, puede elegir otros analizadores JSON disponibles en la base de código del sitio mql5.com.
El objeto de clase JsValue describe un elemento (contenedor o propiedad) por ToyJson. Se han definido varias sobrecargas del método put(key, value), que se pueden utilizar para la adición de propiedades internas con nombre como en un objeto JSON o put(value), para añadir un valor como en un array JSON. Además, este objeto puede representar un único valor de un tipo primitivo. Para leer las propiedades de un objeto JSON, puede aplicar a JsValue una notación del operador [] seguida del nombre de la propiedad requerida entre paréntesis. Obviamente, los índices enteros se admiten para el acceso dentro de un array JSON.
Una vez formada la configuración necesaria de objetos relacionados JsValue, puede serializarla en texto JSON utilizando el método stringify(string&buffer).
La segunda clase de toyjson.mqh -JsParser- permite realizar la operación inversa: convertir el texto con la descripción JSON en una estructura jerárquica de objetos JsValue.
Teniendo en cuenta las clases para trabajar con JSON, vamos a empezar a escribir un Asesor Experto MQL5/Experts/MQL5Book/p7/wsTradeCopier/wstradecopier.mq5, que podrá desempeñar ambos papeles en el servicio de copia de operaciones: un proveedor de información sobre las operaciones de trading realizadas en la cuenta, o un receptor de esta información del servicio para reproducir dichas operaciones.
El volumen y contenido de la información enviada queda, desde un punto de vista político, a discreción del proveedor y puede diferir significativamente en función del escenario (propósito) de uso del servicio. En concreto, es posible copiar sólo las operaciones en curso o todo el saldo de la cuenta junto con las órdenes pendientes y los niveles de protección. En nuestro ejemplo, sólo indicaremos la implementación técnica de la transferencia de información, y luego podrá elegir un conjunto específico de objetos y propiedades a su discreción.
En el código, describiremos 3 estructuras que se heredan de las estructuras integradas y que proporcionan «empaquetado» de información en JSON:
- MqlTradeRequestWeb MqlTradeRequest
- MqlTradeResultWeb MqlTradeResult
- DealMonitorWeb DealMonitor*
La última estructura de la lista, en sentido estricto, no está integrada, sino que la definimos nosotros en el archivo DealMonitor.mqh, pero se rellena en el conjunto estándar de propiedades de la transacción.
El constructor de cada una de las estructuras derivadas rellena los campos basándose en la fuente primaria transmitida (solicitud de operación, su resultado o transacción). Cada estructura implementa el método asJsValue, que devuelve un puntero al objeto JsValue que refleja todas las propiedades de la estructura: se añaden al objeto JSON mediante el método JsValue::put. Por ejemplo, he aquí cómo se hace en el caso de MqlTradeRequest:
struct MqlTradeRequestWeb: public MqlTradeRequest
|
Transferimos todas las propiedades a JSON (esto es adecuado para el servicio de supervisión de cuentas), pero puede dejar sólo un conjunto limitado.
Para las propiedades que son enumeraciones, hemos proporcionado dos formas de representarlas en JSON: como un número entero y como un nombre de cadena de un elemento de enumeración. La elección del método se realiza mediante el parámetro de entrada VerboseJson (lo ideal es que no se escriba en el código de la estructura directamente, sino a través de un parámetro del constructor).
input bool VerboseJson = false; |
Pasar sólo números simplificaría la codificación porque, en el lado receptor, basta con convertirlos al tipo de enumeración deseado para realizar acciones «espejo». Sin embargo, los números dificultan la percepción de la información por parte de una persona, que puede necesitar analizar la situación (mensaje). Por lo tanto, tiene sentido apoyar una opción para la representación de cadena, por ser más «fácil de usar», aunque requiere operaciones adicionales en el algoritmo de recepción.
Los parámetros de entrada también especifican la dirección del servidor, el rol de la aplicación y los detalles de conexión por separado para el proveedor y el suscriptor.
enum TRADE_ROLE
|
Los parámetros SymbolFilter y MagicFilter del grupo de proveedores le permiten limitar la actividad de trading supervisada a un símbolo y un número mágico determinados. Un valor vacío en SymbolFilter significa controlar solo el símbolo actual del gráfico; para interceptar cualquier operación de trading, introduzca el símbolo '*'. El proveedor de señales utilizará para ello la función FilterMatched, que acepta el símbolo y el número mágico de la transacción.
bool FilterMatched(const string s, const ulong m)
|
El parámetro SymbolSubstitute del grupo de entrada del suscriptor permite sustituir el símbolo recibido en los mensajes por otro, que se utilizará para la operación de copia. Esta función es útil si los nombres de los tickers del mismo instrumento financiero difieren entre los distintos brókeres. Pero este parámetro también cumple la función de filtro permisivo para señales repetitivas: sólo se negociarán los símbolos aquí especificados. Por ejemplo, para permitir la negociación de señales para el símbolo EURUSD (incluso sin sustitución de ticker), debe establecer la cadena «EURUSD=EURUSD» en el parámetro. El símbolo de los mensajes de señalización se indica a la izquierda del signo «=», y el símbolo de trading, a la derecha.
La lista de sustitución de caracteres es procesada por la función FillSubstitutes durante la inicialización y luego utilizada para sustituir y resolver la operación de trading mediante la función FindSubstitute.
string Substitutes[][2];
|
Para comunicarnos con el servicio, definimos una clase derivada de WebSocketClient. Se necesita, en primer lugar, para empezar a el trading con una señal cuando llega un mensaje al manejador onMessage. Volveremos sobre esta cuestión un poco más adelante, después de considerar la formación y el envío de señales en el lado del proveedor.
class MyWebSocket: public WebSocketClient<Hybi>
|
La inicialización en OnInit activa el temporizador (para una llamada periódica wss.checkMessages(false)) y la preparación de encabezados personalizados con detalles del usuario, dependiendo del rol seleccionado. A continuación, abrimos la conexión con la llamada a wss.open(custom).
int OnInit()
|
El mecanismo de copia, es decir, interceptar transacciones y enviar información sobre ellas a un servicio web, se pone en marcha en el manejador OnTradeTransaction. Como sabemos, esta no es la única forma y sería posible analizar la «instantánea» del estado de la cuenta en OnTrade.
void OnTradeTransaction(const MqlTradeTransaction &transaction,
|
Hacemos un seguimiento de los eventos sobre solicitudes de operaciones de trading completadas con éxito que satisfacen las condiciones de los filtros especificados. A continuación, las estructuras de la solicitud, el resultado de la solicitud y la transacción se convierten en objetos JSON. Todos ellos se colocan en un contenedor común msg bajo los nombres «req», «res» y «deal», respectivamente. Recuerde que el propio contenedor se incluirá en el mensaje del servicio web como la propiedad «msg».
// container to attach to service message will be visible as "msg" property: // {"origin" : "this_publisher_id", "msg" : { our data is here }}
|
Una vez lleno, el contenedor se muestra como una cadena en buffer, se imprime en el registro y se envía al servidor.
Podemos añadir otra información a este contenedor: el estado de la cuenta (reducción, carga), el número y las propiedades de las órdenes pendientes, etc. Así, sólo para demostrar las posibilidades de ampliar el contenido de los mensajes, hemos añadido el número de posiciones abiertas más arriba. Para seleccionar las posiciones según los filtros, utilizamos el objeto de clase PositionFilter (PositionFilter.mqh):
PositionFilter Positions;
|
Básicamente, para aumentar la fiabilidad, tiene sentido que los copiadores analicen el estado de las posiciones, y no sólo intercepten las transacciones.
Esto concluye la consideración de la parte del Asesor Experto que está involucrada en el papel de proveedor de señales.
Como suscriptor, como ya hemos anunciado, el Asesor Experto recibe mensajes en el método MyWebSocket::onMessage. Aquí el mensaje entrante es analizado con JsParser::jsonify, y el contenedor que se formó por el lado transmisor es recuperado de la propiedad obj["msg"].
class MyWebSocket: public WebSocketClient<Hybi>
|
La función RemoteTrade ejecuta las operaciones de trading y análisis de señales. Aquí se da con abreviaturas, sin manejar posibles errores. La función ofrece compatibilidad con ambas formas de representar enumeraciones: como valores enteros o como nombres de elementos de cadena. El objeto JSON entrante se «examina» en busca de las propiedades necesarias (comandos y atributos de señal) aplicando el operador [], incluso varias veces consecutivas (para acceder a objetos JSON anidados).
bool RemoteTrade(JsValue *obj)
|
Esta implementación no analiza el precio de transacción, las posibles restricciones en el lote, los niveles de stop ni otros momentos. Simplemente repetimos la operación al precio local actual. Además, al cerrar una posición, se comprueba la igualdad exacta del volumen, lo que es adecuado para las cuentas de cobertura, pero no para la compensación, donde es posible un cierre parcial si el volumen de la transacción es inferior a la posición (y quizá más, en caso de una inversión, pero la opción DEAL_ENTRY_INOUT no se admite aquí). Todos estos puntos deben ultimarse para su aplicación real.
Vamos a iniciar el servidor node.exe wspubsub.js y dos copias del Asesor Experto wstradecopier.mq5 en diferentes gráficos, en el mismo terminal. El escenario habitual supone que el Asesor Experto necesita ser lanzado en diferentes cuentas, pero una opción «paradójica» también es adecuada para comprobar el rendimiento: copiaremos señales de un símbolo a otro.
En una copia del Asesor Experto, dejaremos la configuración por defecto, con el papel del editor. Debe colocarse en el gráfico EURUSD. En la segunda copia que se ejecuta en el gráfico GBPUSD, cambiamos el rol a suscriptor. La cadena «EURUSD=GBPUSD» en el parámetro de entrada SymbolSubstitute permite operar GBPUSD con señales EURUSD.
Los datos de conexión se registrarán, con las cabeceras HTTP y los saludos que ya hemos visto, así que los omitiremos.
Compremos EURUSD y asegurémonos de que se «duplica» en el mismo volumen para GBPUSD.
A continuación se muestran fragmentos del registro (tenga en cuenta que debido al hecho de que ambos Asesores Expertos trabajan en la misma copia del terminal, los mensajes de transacción se enviarán a ambos gráficos y por lo tanto, para facilitar el análisis del registro, puede establecer alternativamente los filtros «EURUSD» y « USDUSD»):
(EURUSD,H1) TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 0.99886, #=1461313378 (EURUSD,H1) DONE, D=1439023682, #=1461313378, V=0.01, @ 0.99886, Bid=0.99886, Ask=0.99886, Req=2 (EURUSD,H1) {"req" : {"a" : "TRADE_ACTION_DEAL", "s" : "EURUSD", "t" : "ORDER_TYPE_BUY", "v" : 0.01, » "f" : "ORDER_FILLING_FOK", "p" : 0.99886, "o" : 1461313378}, "res" : {"code" : 10009, "d" : 1439023682, » "o" : 1461313378, "v" : 0.01, "p" : 0.99886, "b" : 0.99886, "a" : 0.99886}, "deal" : {"d" : 1439023682, » "o" : 1461313378, "t" : "2022.09.19 16:45:50", "tmsc" : 1663605950086, "type" : "DEAL_TYPE_BUY", » "entry" : "DEAL_ENTRY_IN", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01, "p" : 0.99886, » "s" : "EURUSD"}, "pos" : {"n" : 1}}
|
Esto muestra el contenido de la solicitud ejecutada y su resultado, así como un búfer con una cadena JSON enviada al servidor.
Casi instantáneamente, en el lado receptor, en el gráfico GBPUSD, se muestra una alerta con un mensaje del servidor en forma «en bruto» y formateado tras un análisis sintáctico satisfactorio en JsParser. En la forma «en bruto», se almacena la propiedad «origen», en la que el servidor nos permite saber quién es la fuente de la señal.
(GBPUSD,H1) Alert: {"origin":"publisher PUB_ID_001", "msg":{"req" : {"a" : "TRADE_ACTION_DEAL", » "s" : "EURUSD", "t" : "ORDER_TYPE_BUY", "v" : 0.01, "f" : "ORDER_FILLING_FOK", "p" : 0.99886, » "o" : 1461313378}, "res" : {"code" : 10009, "d" : 1439023682, "o" : 1461313378, "v" : 0.01, » "p" : 0.99886, "b" : 0.99886, "a" : 0.99886}, "deal" : {"d" : 1439023682, "o" : 1461313378, » "t" : "2022.09.19 16:45:50", "tmsc" : 1663605950086, "type" : "DEAL_TYPE_BUY", » "entry" : "DEAL_ENTRY_IN", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01, » "p" : 0.99886, "s" : "EURUSD"}, "pos" : {"n" : 1}}} (GBPUSD,H1) { (GBPUSD,H1) req = (GBPUSD,H1) { (GBPUSD,H1) a = TRADE_ACTION_DEAL (GBPUSD,H1) s = EURUSD (GBPUSD,H1) t = ORDER_TYPE_BUY (GBPUSD,H1) v = 0.01 (GBPUSD,H1) f = ORDER_FILLING_FOK (GBPUSD,H1) p = 0.99886 (GBPUSD,H1) o = 1461313378 (GBPUSD,H1) } (GBPUSD,H1) res = (GBPUSD,H1) { (GBPUSD,H1) code = 10009 (GBPUSD,H1) d = 1439023682 (GBPUSD,H1) o = 1461313378 (GBPUSD,H1) v = 0.01 (GBPUSD,H1) p = 0.99886 (GBPUSD,H1) b = 0.99886 (GBPUSD,H1) a = 0.99886 (GBPUSD,H1) } (GBPUSD,H1) deal = (GBPUSD,H1) { (GBPUSD,H1) d = 1439023682 (GBPUSD,H1) o = 1461313378 (GBPUSD,H1) t = 2022.09.19 16:45:50 (GBPUSD,H1) tmsc = 1663605950086 (GBPUSD,H1) type = DEAL_TYPE_BUY (GBPUSD,H1) entry = DEAL_ENTRY_IN (GBPUSD,H1) pid = 1461313378 (GBPUSD,H1) r = DEAL_REASON_CLIENT (GBPUSD,H1) v = 0.01 (GBPUSD,H1) p = 0.99886 (GBPUSD,H1) s = EURUSD (GBPUSD,H1) } (GBPUSD,H1) pos = (GBPUSD,H1) { (GBPUSD,H1) n = 1 (GBPUSD,H1) } (GBPUSD,H1) } (GBPUSD,H1) Alert: Trade by subscription: market entry ORDER_TYPE_BUY 0.01 GBPUSD - Successful |
La última de las entradas anteriores indica una transacción exitosa en GBPUSD. En la pestaña de trading de la cuenta, deberían aparecer 2 posiciones.
Transcurrido algún tiempo, cerramos la posición EURUSD, y la posición GBPUSD debería cerrarse automáticamente.
(EURUSD,H1) TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_SELL, V=0.01, ORDER_FILLING_FOK, @ 0.99881, #=1461315206, P=1461313378 (EURUSD,H1) DONE, D=1439025490, #=1461315206, V=0.01, @ 0.99881, Bid=0.99881, Ask=0.99881, Req=4 (EURUSD,H1) {"req" : {"a" : "TRADE_ACTION_DEAL", "s" : "EURUSD", "t" : "ORDER_TYPE_SELL", "v" : 0.01, » "f" : "ORDER_FILLING_FOK", "p" : 0.99881, "o" : 1461315206, "q" : 1461313378}, "res" : {"code" : 10009, » "d" : 1439025490, "o" : 1461315206, "v" : 0.01, "p" : 0.99881, "b" : 0.99881, "a" : 0.99881}, » "deal" : {"d" : 1439025490, "o" : 1461315206, "t" : "2022.09.19 16:46:52", "tmsc" : 1663606012990, » "type" : "DEAL_TYPE_SELL", "entry" : "DEAL_ENTRY_OUT", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", » "v" : 0.01, "p" : 0.99881, "m" : -0.05, "s" : "EURUSD"}, "pos" : {"n" : 0}} |
Si la transacción tenía un tipo DEAL_ENTRY_IN la primera vez, ahora es DEAL_ENTRY_OUT. La alerta confirma la recepción del mensaje y el cierre con éxito de la posición duplicada.
(GBPUSD,H1) Alert: {"origin":"publisher PUB_ID_001", "msg":{"req" : {"a" : "TRADE_ACTION_DEAL", » "s" : "EURUSD", "t" : "ORDER_TYPE_SELL", "v" : 0.01, "f" : "ORDER_FILLING_FOK", "p" : 0.99881, » "o" : 1461315206, "q" : 1461313378}, "res" : {"code" : 10009, "d" : 1439025490, "o" : 1461315206, » "v" : 0.01, "p" : 0.99881, "b" : 0.99881, "a" : 0.99881}, "deal" : {"d" : 1439025490, » "o" : 1461315206, "t" : "2022.09.19 16:46:52", "tmsc" : 1663606012990, "type" : "DEAL_TYPE_SELL", » "entry" : "DEAL_ENTRY_OUT", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01, » "p" : 0.99881, "m" : -0.05, "s" : "EURUSD"}, "pos" : {"n" : 0}}} ... (GBPUSD,H1) Alert: Trade by subscription: market exit ORDER_TYPE_SELL 0.01 GBPUSD - Successful |
Por último, junto al Asesor Experto wstradecopier.mq5, creamos un archivo de proyecto wstradecopier.mqproj para añadirle una descripción y los archivos de servidor necesarios (en el antiguo directorio MQL5/Experts/p7/MQL5Book/Web/).
En resumen: hemos organizado un sistema multiusuario técnicamente extensible para intercambiar información de trading a través de un servidor de sockets. Debido a las características técnicas de los web sockets (conexión abierta permanente), esta implementación del servicio de señales es más adecuada para el trading a corto plazo y de alta frecuencia, así como para el control de situaciones de arbitraje en las cotizaciones.
Resolver el problema exigía combinar varios programas en distintas plataformas y conectar un gran número de dependencias, que es lo que suele caracterizar la transición al nivel de proyecto. También se amplía el entorno de desarrollo, que va más allá del compilador y el editor de código fuente. En concreto, la presencia en el proyecto de las partes cliente o servidor suele implicar el trabajo de distintos programadores responsables de ellas. En este caso, los proyectos compartidos en la nube y con control de versiones se hacen indispensables.
Tenga en cuenta que al desarrollar un proyecto en la carpeta MQL5/Shared Projects a través de MetaEditor, los archivos de encabezado del directorio estándar MQL5/Include no se incluyen en el almacenamiento compartido. Por otro lado, crear una carpeta dedicada Include dentro de su proyecto y transferir a ella los archivos mqh estándar necesarios dará lugar a la duplicación de información y a posibles discrepancias en las versiones de los archivos de encabezado. Es probable que este comportamiento se mejore en MetaEditor.
Otro punto para los proyectos públicos es la necesidad de administrar usuarios y autorizarlos. En nuestro último ejemplo, este problema sólo se identificó, pero no se aplicó. No obstante, el sitio mql5.com ofrece una solución ya preparada basada en el conocido protocolo OAuth. Cualquiera que tenga una cuenta en mql5.com puede familiarizarse con el principio de OAuth y configurarlo para su servicio web: sólo tiene que encontrar la sección Applications (enlace parecido a https://www.mql5.com/es/users/<login> /apps) en su perfil. Al registrar un servicio web en las aplicaciones de mql5.com, podrá autorizar usuarios a través del sitio web de mql5.com.