Servicio de señales de trading y página web de prueba

El servicio de señales de trading es técnicamente idéntico al servicio de chat; sin embargo, sus usuarios (o más bien las conexiones de los clientes) deben desempeñar uno de estos dos papeles:

  • Proveedor de mensajes
  • Consumidor de mensajes

Además, la información no debe estar disponible para todo el mundo, sino funcionar según algún sistema de suscripción.

Para garantizarlo, al conectarse al servicio, se pedirá a los usuarios que faciliten ciertos datos identificativos que difieren en función del papel o rol.

El proveedor debe especificar un identificador de señal pública (PUB_ID) que sea único entre todas las señales. Básicamente, una misma persona podría generar potencialmente más de una señal y, por tanto, debería poder obtener múltiples identificadores. En este sentido, no complicaremos el servicio introduciendo por separado identificadores del proveedor (como una persona concreta) e identificadores de sus señales. En lugar de ello, sólo se admitirán identificadores de señal. Para un verdadero servicio de señalización, hay que resolver este problema, junto con el de la autorización, que dejamos fuera de este libro.

El identificador será necesario para publicitarlo o simplemente transmitirlo a las personas interesadas en abonarse a esta señal. Pero no «todo el mundo» debería poder acceder a la señal conociendo sólo el identificador público. En el caso más sencillo, esto sería aceptable para la supervisión de cuentas abiertas, pero demostraremos la opción de restringir el acceso específicamente en el contexto de las señales.

Para ello, el proveedor debe proporcionarle al servidor una clave secreta (PUB_KEY) conocida sólo por él, pero no por el público en general. Esta clave será necesaria para generar la clave de acceso de un suscriptor concreto.

El consumidor (suscriptor) también debe tener un identificador único (SUB_ID, y aquí también lo haremos sin autorización). Para suscribirse a la señal deseada, el usuario debe indicarle al proveedor de la señal el identificador (en la práctica, se entiende que en la misma fase es necesario confirmar el pago, y normalmente todo esto está automatizado por el servidor). El proveedor forma una instantánea compuesta por el identificador del proveedor, el identificador del suscriptor y la clave secreta. En nuestro servicio, esto se hará calculando el hash SHA256 a partir de la cadena PUB_ID:PUB_KEY:SUB_ID, tras lo cual los bytes resultantes se convertirán a una cadena en formato hexadecimal. Será la clave de acceso (SUB_KEY o ACCESS_KEY) a la señal de un determinado proveedor para un determinado suscriptor. El proveedor (y en los sistemas reales, el propio servidor automáticamente) reenvía esta clave al suscriptor.

Así, al conectarse al servicio, el suscriptor deberá especificar el identificador del suscriptor (SUB_ID), el identificador de la señal deseada (PUB_ID) y la clave de acceso (SUB_KEY). Como el servidor conoce la clave secreta del proveedor, puede recalcular la clave de acceso para la combinación dada de PUB_ID y SUB_ID, y compararla con la SUB_KEY proporcionada. Una coincidencia significa que el proceso normal de mensajería continúa. La diferencia dará lugar a un mensaje de error y a la desconexión del pseudosuscriptor del servicio.

Es importante señalar que en nuestra demostración, en aras de la simplicidad, no existe un registro normal de usuarios y señales, por lo que la elección de los identificadores es arbitraria. Sólo es importante para nosotros hacer un seguimiento de la unicidad de los identificadores para saber a quién y de quién enviar información en línea. Así pues, nuestro servicio no garantiza que el identificador, por ejemplo, «Super Trend» pertenezca al mismo usuario ayer, hoy y mañana. La reserva de nombres se hace según el principio de que a quien madruga Dios le ayuda. Mientras un proveedor esté conectado continuamente con el identificador dado, la señal se entrega. Si el proveedor se desconecta, el identificador pasa a estar disponible para su selección en cualquier conexión siguiente.

El único identificador que siempre estará ocupado es «Server»: el servidor lo utiliza para enviar sus mensajes de estado de conexión.

Para generar claves de acceso en la carpeta del servidor, existe un sencillo JavaScript access.js. Cuando lo ejecute en la línea de comandos, deberá pasar como único parámetro una cadena del tipo PUB_ID:PUB_KEY:SUB_ID (identificadores y la clave secreta entre ellos, conectados por el símbolo ':')

Si no se especifica el parámetro, el script genera una clave de acceso para algunos identificadores de demostración (PUB_ID_001, SUB_ID_100) y un secreto (PUB_KEY_FFF).

// JavaScript
const args = process.argv.slice(2);
const input = args.length > 0 ? args[0] : 'PUB_ID_001:PUB_KEY_FFF:SUB_ID_100';
console.log('Hashing "', input, '"');
const crypto = require('crypto');
console.log(crypto.createHash('sha256').update(input).digest('hex'));

Al ejecutar el script con el comando

node access.js PUB_ID_001:PUB_KEY_FFF:SUB_ID_100

obtenemos este resultado:

fd3f7a105eae8c2d9afce0a7a4e11bf267a40f04b7c216dd01cf78c7165a2a5a

Por cierto: puede comprobar y repetir este algoritmo en MQL5 puro utilizando la función CryptEncode.

Una vez analizada la parte conceptual, pasemos a la aplicación práctica.

El script de servidor del servicio de señalización se colocará en el archivo MQL5/Experts/MQL5Book/p7/Web/wspubsub.js. Configurar servidores en él es lo mismo que hicimos antes. No obstante, tendrá además que conectar el mismo módulo «crypto» que se utilizó en access.js. La página de inicio se llamará wspubsub.htm.

// JavaScript
const crypto = require('crypto');
...
http1.createServer(optionsfunction (reqres)
{
   ...
   if(req.url == '/')
   {
      req.url = "wspubsub.htm";
   }
   ...
});

En lugar de un mapa de clientes conectados, definiremos dos mapas, por separado para proveedores y consumidores de señales.

// JavaScript
const publishers = new Map();
const subscribers = new Map();

En ambos mapas, la clave es el ID del proveedor, pero el primero almacena los objetos de los proveedores, y el segundo almacena los objetos de los suscriptores suscritos a cada proveedor (arrays de objetos).

Para transferir identificadores y claves durante el «handshake», utilizaremos un encabezado especial permitida por la especificación WebSockets, concretamente Sec-Websocket-Protocol. Acordemos que los identificadores y las claves se pegarán con el símbolo '-': en el caso de un proveedor, se espera una cadena como X-MQL5-publisher-PUB_ID-PUB_KEY, y en el caso de un suscriptor, esperamos X-MQL5-subscriber-SUB_ID-PUB_ID-SUB_KEY.

Cualquier intento de conexión a nuestro servicio sin el encabezado Sec-Websocket-Protocol: X-MQL5-... se detendrá por cierre inmediato.

En el nuevo objeto cliente (en el parámetro del manejador de eventos de «conexión» onConnect(client)) este título es fácil de extraer de la propiedad client.protocol.

Vamos a mostrar el procedimiento de registro y envío de mensajes del proveedor de señales de forma simplificada, sin tratamiento de errores (se adjunta el código completo). Es importante tener en cuenta que el texto del mensaje se genera en formato JSON (del que hablaremos con más detalle en la siguiente sección). En particular, el remitente del mensaje se pasa en la propiedad «origin» (además, cuando el mensaje es enviado por el propio servicio, este campo contiene la cadena «Server») y los datos de la aplicación del proveedor se colocan en la propiedad «msg», y esto puede ser no solo texto, sino también una estructura anidada de cualquier contenido.

// JavaScript
const wsServer = new WebSocket.Server({ server });
wsServer.on('connection', function onConnect(client)
{
   console.log('New user:', ++countclient.protocol);
   if(client.protocol.startsWith('X-MQL5-publisher'))
   {
      const parts = client.protocol.split('-');
      client.id = parts[3];
      client.key = parts[4];
      publishers.set(client.idclient);
      client.send('{"origin":"Server", "msg":"Hello, publisher ' + client.id + '"}');
      client.on('message', function(message)
      {
         console.log('%s : %s', client.idmessage);
         
         if(subscribers.get(client.id))
            subscribers.get(client.id).forEach(function(elem)
         {
            elem.send('{"origin":"publisher ' + client.id + '", "msg":'
               + message + '}');
         });
      });
      client.on('close', function()
      {
         console.log('Publisher disconnected:', client.id);
         if(subscribers.get(client.id))
            subscribers.get(client.id).forEach(function(elem)
         {
            elem.close();
         });
         publishers.delete(client.id);
      });
   }
   ...

La mitad del algoritmo para los suscriptores es similar, pero aquí tenemos el cálculo de la clave de acceso y su comparación con lo que transmitió el cliente que se conecta, como añadido.

// JavaScript
   else if(client.protocol.startsWith('X-MQL5-subscriber'))
   {
      const parts = client.protocol.split('-');
      client.id = parts[3];
      client.pub_id = parts[4];
      client.access = parts[5];
      const id = client.pub_id;
      var p = publishers.get(id);
      if(p)
      {
         const check = crypto.createHash('sha256').update(id + ':' + p.key + ':'
            + client.id).digest('hex');
         if(check != client.access)
         {
            console.log(`Bad credentials: '${client.access}' vs '${check}'`);
            client.send('{"origin":"Server", "msg":"Bad credentials, subscriber '
               + client.id + '"}');
            client.close();
            return;
         }
   
         var list = subscribers.get(id);
         if(list == undefined)
         {
            list = [];
         }
         list.push(client);
         subscribers.set(idlist);
         client.send('{"origin":"Server", "msg":"Hello, subscriber '
            + client.id + '"}');
         p.send('{"origin":"Server", "msg":"New subscriber ' + client.id + '"}');
      }
      
      client.on('close', function()
      {
         console.log('Subscriber disconnected:', client.id);
         const list = subscribers.get(client.pub_id);
         if(list)
         {
            if(list.length > 1)
            {
               const filtered = list.filter(function(el) { return el !== client; });
               subscribers.set(client.pub_idfiltered);
            }
            else
            {
               subscribers.delete(client.pub_id);
            }
         }
      });
   }

La interfaz de usuario de la página de cliente wspubsub.htm invita simplemente a seguir un enlace a una de las dos páginas con formularios para proveedores (wspublisher.htm + wspublisher_client.js) o suscriptores (wssubscriber.htm + wssubscriber_client.js).

Páginas web de clientes de prueba del servicio de señales

Páginas web de clientes de prueba del servicio de señales

Su implementación hereda las características de los clientes JavaScript considerados con anterioridad, pero con respecto a la personalización del encabezado Sec-Websocket-Protocol: X-MQL5- y un matiz más.

Hasta ahora, hemos intercambiado mensajes de texto sencillos. Pero para un servicio de señalización, necesitará transferir mucha información estructurada, y lo más adecuado para ello es JSON. Por lo tanto, los clientes pueden analizar JSON, aunque no lo utilizan para el fin previsto, porque aunque en JSON se encuentre una orden para comprar o vender un ticker específico con una cantidad determinada, el navegador no sabe cómo hacerlo.

Necesitaremos añadir compatibilidad JSON a nuestro cliente de servicio de señales en MQL5. Mientras tanto, puede ejecutar en el servidor wspubsub.js y probar la conexión selectiva de los proveedores de señal y los consumidores de acuerdo con los detalles especificados por ellos. Le sugerimos que lo haga usted mismo, por su propio beneficio.