English Русский Português
preview
Desarrollamos un gestor de terminales (Parte 2): Inicio de varias instancias

Desarrollamos un gestor de terminales (Parte 2): Inicio de varias instancias

MetaTrader 5 — Trading |
15 0
Yuriy Bykov
Yuriy Bykov

Introducción

En la primera parte de la serie «Desarrollamos un gestor de terminales», nos propusimos crear una interfaz web para controlar el inicio y la parada de las instancias de MetaTrader 5 en el ordenador local. Hemos definido la arquitectura del sistema, comenzando por el desarrollo de un producto mínimo viable (MVP): un servidor web capaz de recibir solicitudes HTTP y gestionar una instancia de terminal. De paso, hemos analizado conceptos clave como API, REST, solicitudes HTTP y decoradores.

Para llevar a cabo el proyecto se eligió el lenguaje Python, gracias a su amplio ecosistema de bibliotecas y a la facilidad de integración con API externas —en este caso, con la biblioteca de MetaTrader 5—.

El framework FastAPI permite crear aplicaciones web con un mínimo de código, ya que ofrece de serie compatibilidad con el tipado, la asincronía y la documentación automática a través de Swagger UI.

Junto con él se utiliza Uvicorn, un servidor ASGI ligero y rápido que garantiza un alto rendimiento incluso con un gran número de solicitudes simultáneas.

Para trabajar con los procesos de los terminales se usan las bibliotecas psutil y subprocess, lo que permite obtener fácilmente información sobre las instancias en ejecución y gestionarlas.

En el futuro, está previsto incorporar el uso de SQLite u otra base de datos para almacenar el estado de las instancias, así como una capa de front-end en JavaScript o Vue.js para lograr una interfaz más interactiva.

Ahora ha llegado el momento de ampliar la funcionalidad y pasar a las siguientes fases: la implementación de funciones más complejas, como la gestión de varias instancias, el almacenamiento del estado, la integración con la API de MetaTrader 5 y una interfaz web con información completa sobre los terminales.


Trazamos el camino

Vamos a intentar delimitar el alcance aproximado de la siguiente fase de implementación. En primer lugar, mientras no tengamos una separación real de la funcionalidad general entre dos servidores web distintos, deberemos añadir los componentes de la interfaz, al menos en una versión mínima, al servidor web que ya hemos empezado a desarrollar. Este trabajo no habrá sido en vano, ya que más adelante podremos trasladar el código que ya tenemos a otro servidor web con algunas modificaciones. Por eso, empezaremos creando archivos adicionales para alojar las distintas partes del código del proyecto, dándoles así un cierto aire de organización.

En segundo lugar, daremos el siguiente paso más obvio: añadiremos la posibilidad de trabajar con varias instancias del terminal MetaTrader 5. Es decir, implementaremos la creación, el inicio y la parada de instancias, así como la obtención de información sobre el conjunto de instancias que estén en ejecución en ese momento. Dejaremos para más adelante la obtención de información detallada sobre la cuenta de trading.

Esto requerirá adoptar previamente algunas decisiones arquitectónicas, lo cual no es tan sencillo como podría parecer a primera vista. Ahora recae sobre nosotros la responsabilidad del desarrollo futuro del proyecto. De lo acertadas que resulten las decisiones que se tomen ahora dependerá la facilidad con la que se lleve a cabo la implementación posterior y los posibles límites de ampliación de la funcionalidad. No nos gustaría llegar a una determinada fase del proyecto y descubrir que, para seguir avanzando, tenemos que volver al principio y rehacerlo todo. Sí, claro, eso también puede pasar, pero intentaremos evitarlo prestando más atención a nuestra elección.

Por otra parte, es importante no perderse en un mar de posibilidades. Por eso, en ciertos asuntos, es mejor tomar al menos alguna decisión ahora mismo que pasarse el tiempo preguntándose sin cesar qué repercusiones tendrá en el futuro una u otra elección. Además, en algunos casos resulta más eficaz optar ahora por algo sencillo. Algo que luego, sin duda, se reelaborará en otro nivel de implementación más complejo. Pero, a cambio, no nos quedaremos estancados en la implementación de la arquitectura ideal de una parte concreta del proyecto y podremos avanzar más rápido en su conjunto.


División del código

Vamos a añadir algunos archivos nuevos a nuestro proyecto. Trasladaremos al archivo mt5_control.py todas las funciones del archivo main.py que se encargan de la lógica de control de los terminales. De esta manera, en el archivo principal de la aplicación solo quedarán las funciones que actúan como controladores de ruta (puntos finales) y la creación del objeto FastAPI con la configuración necesaria.

Para almacenar las plantillas de código HTML de las páginas que deben generarse al procesar determinados puntos finales, crearemos una carpeta llamada templates. Por ahora, colocaremos en ella un único archivo index.html para generar el código HTML de la página de inicio (ruta GET /).

Para almacenar el código estático, que siempre se enviará al navegador sin que el servidor web lo procese, crearemos una carpeta llamada static. Entre estos archivos se pueden incluir, por ejemplo, los archivos de hojas de estilo CSS o de código JavaScript a los que se hace referencia desde el código HTML de las páginas. Vamos a añadir ahora mismo un par de archivos vacíos de este tipo para la página de inicio.

A continuación, el árbol de archivos del proyecto tendrá este aspecto:

mt5_manager/
│
├── main.py # Archivo principal de FastAPI
├── mt5_control.py # Lógica de control del inicio y la parada de los terminales
├── templates/
│ └── index.html # Plantilla de la página principal
└── static/
├── styles.css # Estilos CSS
└── script.js # Código JavaScript

En adelante, seguiremos las convenciones establecidas para la ubicación de los archivos del proyecto. Si es necesario, se podrán crear nuevas carpetas anidadas dentro de las ya existentes, siempre que ello contribuya a la organización del código.

Para poder usar el mecanismo de plantillas y los archivos estáticos en nuestra aplicación, es necesario importar las bibliotecas correspondientes en el archivo main.py:

from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

A continuación, tras crear el objeto de la aplicación (app), registramos un nuevo punto de conexión GET /static; al procesarlo, cualquier nombre que aparezca en la URL después de static/ se interpretará como un archivo de la carpeta static de nuestro proyecto:

# Creamos un objeto de aplicación
app = FastAPI()

# Incorporamos los archivos estáticos
app.mount("/static", StaticFiles(directory="static"), name="static")

Observe que, en este caso, no creamos un controlador de ruta independiente para GET /static, sino que llamamos a un método del objeto de la aplicación que creará automáticamente la función necesaria con el contenido adecuado.

Ahora crearemos un objeto que se usará cuando sea necesario enviar como respuesta código HTML generado a partir de una plantilla concreta de la carpeta templates:

# Creamos un objeto para trabajar con plantillas de código HTML
templates = Jinja2Templates(directory="templates")

Reescribiremos el controlador de ruta GET / de la siguiente manera:

@app.get('/', response_class=HTMLResponse)
async def index(request: Request):
    '''Manejador GET / (directorio raíz)'''
    instances = load_instances()
    return templates.TemplateResponse("index.html", {"request": request, "instances": instances})

En él obtenemos el diccionario instances, que contiene información sobre las instancias de terminales registradas y su estado actual (en ejecución o paradas). A continuación, devolvemos la respuesta de plantilla obtenida del archivo index.html, teniendo en cuenta la información del diccionario instances. Más adelante veremos qué información concreta aparecerá en el diccionario instances y cómo se transformará en código HTML.

También introduciremos pequeños cambios en los dos controladores de ruta restantes. Los cambios serán del mismo tipo; es decir, para el controlador de inicio de la instancia haremos lo mismo que para el controlador de parada. La necesidad de estos cambios se debe a que ahora hay que pasar al controlador la información sobre qué instancia queremos iniciar o detener en este momento. Cuando solo había una instancia, no se planteaba ese problema.

Añadiremos a la URL de estas rutas una parte variable, denominada {name}. Esto nos obliga a añadir a la lista de argumentos de la función controladora un nuevo argumento con el nombre name y de tipo str (ya que el nombre de la instancia es una cadena):

@app.post('/start/{name}')
async def start_instance(name: str):
    '''Manejador POST /start - inicio del terminal'''
    result = start_mt5(name)
    return JSONResponse(result)


@app.post('/stop/{name}')
async def stop_instance(name: str):
    '''Manejador POST /stop - interrumpir el terminal'''
    result = stop_mt5(name)
    return JSONResponse(result)
Esto hará que, cuando se invoque el controlador, se le pase como argumento la cadena extraída de la parte de la URL correspondiente a {name}. Y a partir de ahí ya podemos trabajar con ella dentro de las funciones controladoras.

Actualicemos nuestra tabla de rutas para reflejar los cambios realizados:

# Método URL Descripción, parámetros Formato
de respuesta
1 GET / Muestra la página principal con la interfaz web para gestionar las instancias de MetaTrader 5
Por ahora, que solo se muestre el título del proyecto.
HTML
2 POST /start/{name} Inicia una instancia del terminal MetaTrader 5 con el nombre {name} JSON
3 POST
/stop/{name} Detiene el proceso de MetaTrader 5 en ejecución con el nombre {name} JSON

Aunque el proyecto se encuentre todavía en fase de desarrollo activo, es importante empezar ya a crear una arquitectura sólida. Lo ideal es que el código de la aplicación sea fácilmente ampliable, de modo que añadir nuevas funciones, rutas o lógica de control no requiera reescribir las partes existentes.

Precisamente por eso tiene sentido seguir los principios de separación de responsabilidades y de arquitectura limpia, de modo que:

  • El módulo mt5_control.py se encarga únicamente de gestionar los procesos de los terminales.
  • El archivo main.py se centra en las rutas y en la interfaz web.
  • Las carpetas «templates» y «static» aíslan la parte visual de la aplicación.

Esta división hace que el proyecto sea más resistente a los cambios y facilita las pruebas. En el futuro, si fuera necesario, estas partes se podrán convertir en microservicios independientes.


Instancias de terminales

Para poder ejecutar varias instancias de terminales MetaTrader 5 en el servidor, estas deben estar físicamente presentes en él. Es decir, en el servidor debe haber varias carpetas, cada una de ellas con el archivo ejecutable del terminal. Hay diferentes formas de conseguirlo, pero, para simplificar, supongamos por ahora que el usuario realizará esta parte del trabajo manualmente.

Vamos a realizar esta operación. Si aún no tiene instalado el terminal en el ordenador, debe descargarlo del sitio web del bróker o de www.metatrader5.com e instalarlo. Crearemos una carpeta llamada MT5 con tres subcarpetas con nombres del tipo MetaTrader5.x y copiaremos en ellas los archivos ejecutables del terminal (terminal64.exe) y del editor (metaeditor64.exe) desde cualquier copia instalada del terminal MetaTrader 5:

La elección de nombres concretos para las carpetas de los terminales y su carpeta raíz no tiene ninguna importancia. Hemos elegido estos nombres porque son bastante breves y claros.

En la carpeta de cada terminal, ejecutaremos el archivo terminal64.exe en modo portable y nos conectaremos a alguna cuenta de trading. Aquí usaremos cuentas de demostración de la empresa MetaQuotes. Si es necesario instalar actualizaciones, esperaremos a que finalice este proceso. Bien, ahora tenemos tres instancias del terminal instaladas en carpetas con las siguientes rutas completas:

C:/MT5/MetaTrader5.1/terminal64.exe
C:/MT5/MetaTrader5.2/terminal64.exe
C:/MT5/MetaTrader5.3/terminal64.exe

Son precisamente estas instancias las que vamos a iniciar y detener a través del servidor web que estamos creando. Además, por el momento no abordaremos las cuestiones relacionadas con el cambio de cuentas de trading a las que están conectados los terminales; es decir, nos centraremos únicamente en el inicio y la parada.

En el futuro, lo que ahora hemos hecho manualmente se automatizará de alguna forma. Pero ahora no tenemos por qué distraernos con la implementación: una vez realizado este trabajo preparatorio, no tendremos que volver a él durante mucho tiempo y podremos centrar nuestros esfuerzos en la parte esencial del proyecto.


Creación de una lista de terminales

La última vez implementamos el inicio y la parada de una única instancia del terminal, ubicada en una ruta fija de nuestro servidor. Ahora vamos a implementar el inicio y la parada selectivos de cada instancia de una lista fija. Como ya hemos mencionado, hemos trasladado este código y seguiremos escribiéndolo en el nuevo archivo mt5_control.py.

Por ahora no necesitaremos nuevas bibliotecas, aparte de las que ya se han añadido: subprocess y psutil. Hemos creado tres carpetas con instancias de los terminales, que solo se diferencian en el nombre de la última carpeta de la ruta de acceso al archivo terminal64.exe. Por lo tanto, definamos dos constantes que contengan aquellas partes de la ruta que son iguales para todas las instancias:

# Ruta a la carpeta que contiene las instancias de los terminales
MT5_FOLDER = 'C:/MT5'

# Nombre del archivo ejecutable del terminal
MT5_EXE = 'terminal64.exe'

Ahora se puede crear una función que, a partir del nombre, genere la ruta completa al archivo ejecutable de la instancia del terminal deseada. Entonces solo tenemos que unir las tres partes de la ruta completa: la carpeta común, el nombre de la instancia y el nombre del archivo ejecutable:

def instance_path(name: str) -> str:
    '''
    Genera la ruta completa a partir del nombre de la instancia del terminal

    Args:
        name (str): nombre de la instancia del terminal.

    Returns:
        path (str): Ruta completa al archivo ejecutable de la instancia del terminal.
    '''
    return f'{MT5_FOLDER}/{name}/{MT5_EXE}'

Para almacenar la lista de nombres de las instancias, utilizaremos, por el momento, una lista normal en la que figurarán los nombres de las carpetas que hayamos seleccionado:

# Lista de nombres de instancias (por ahora fija)
instances_names = ['MetaTrader5.1', 'MetaTrader5.2', 'MetaTrader5.3']

A partir de la lista de nombres ya preparada, podemos obtener de forma inequívoca las rutas completas a los archivos ejecutables de cada instancia del terminal. Como, por el momento, también serán fijas, se pueden crear de antemano dos diccionarios auxiliares que se utilizarán para obtener la ruta a partir de un nombre dado y el nombre a partir de una ruta dada:

# Diccionario de correspondencias «nombre» -> «ruta»
instances_paths = {name: instance_path(name) for name in instances_names}

# Diccionario de correspondencias «ruta» -> «nombre»
paths_instances = {instance_path(name): name for name in instances_names}

Tras una larga reflexión, hemos decidido no volver a utilizar la información sobre el estado de las instancias que se obtuvo al iniciar el servidor. En su lugar, actualizaremos la lista de instancias en ejecución cada vez que se realice una operación. Para ello, escribiremos la función load_instances(), que creará el diccionario instances. En este diccionario, la clave será el nombre de la instancia y el valor, otro diccionario con dos claves: pid y status. En ellas almacenaremos el identificador único del proceso en ejecución (PID) y el estado de la instancia con el nombre especificado.

Es posible que el almacenamiento de estos dos tipos de información resulte redundante, ya que, en principio, a partir del valor del PID podemos determinar si una instancia concreta está en ejecución o no. Si PID > 0, el proceso está en ejecución. Pero por ahora no está del todo claro cuál es la mejor forma de hacerlo, así que conservaremos ambos campos. Más adelante, lo más probable es que ampliemos el diccionario con información sobre una instancia concreta.

En primer lugar, inicializamos el diccionario instances con los mismos valores para todas las instancias:

{'pid': 0, 'status': 'stopped'}

lo que corresponde a una instancia que no se ha iniciado. A continuación, utilizando la biblioteca psutil, vamos a recorrer todos los procesos que se están ejecutando en el ordenador. Para cada uno de ellos, comprobaremos si ese proceso es una de las instancias de nuestro terminal. Si es así, determinamos su nombre y su PID, y los guardamos en el elemento correspondiente del diccionario instances.

def load_instances() -> dict:
    '''Obtener información sobre el estado de las instancias 
    del terminal MetaTrader 5 de las carpetas establecidas.

    Returns:
        instances (dict): Información sobre las instancias del terminal.
    '''

    # Creamos un diccionario para todas las instancias a partir de la lista de nombres
    instances = {name: {'pid': 0, 'status': 'stopped'}
                 for name in instances_names}

    # Iteramos todos los procesos en ejecución
    for proc in psutil.process_iter(['pid', 'name', 'exe']):
        try:
            # Obtenemos la ruta al archivo ejecutable del proceso
            exe_path = str(proc.info['exe']).replace('\\', '/')

            # Si se encuentra entre las rutas de las instancias del terminal
            if exe_path and exe_path in paths_instanses:
                # Determinamos el nombre de la instancia para esta ruta
                name = paths_instanses[exe_path]

                # Seleccionamos la instancia que necesitamos
                instance = instances[name]

                # Anotamos el identificador y el estado del proceso
                instance['pid'] = proc.info['pid']
                instance['status'] = 'running'
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue

    return instances

Esta función se usará en el controlador de ruta GET / para generar el estado actual de todas las instancias registradas. Pero también se utilizará en cada operación de inicio y parada. Gracias a ella, podremos evitar iniciar una instancia que ya esté en ejecución y parar una que ya esté parada.


Inicio de los terminales

Veamos ahora cómo se implementa la función para el inicio de una instancia de terminal con un nombre determinado, que se pasa como argumento. En primer lugar, obtenemos información actualizada sobre el estado de todos los terminales registrados. Si el nombre facilitado figura entre los nombres existentes, comprobaremos si hay algún proceso en ejecución para esa instancia.

Si no es así, iniciamos un nuevo proceso utilizando la función Popen() de la biblioteca subprocess. Como argumento, hay que pasarle la lista de argumentos de la línea de comandos para el inicio del archivo deseado. El primer elemento de esta lista es la ruta completa al archivo que se va a ejecutar y, a continuación, la opción /portable para utilizar la carpeta actual del terminal como carpeta de trabajo.

Tras el inicio correcto, obtenemos el valor del identificador único del proceso en el sistema operativo (PID) y, junto con el estado «running», lo guardamos en la entrada correspondiente del diccionario de instancias para devolverlo desde la función:

def start_mt5(name: str) -> dict:
    '''
    Iniciando instancia del terminal

    Args:
        name (str): Nombre de la instancia del terminal.

    Returns:
        instance (dict): Información sobre el terminal iniciado.
    '''

    # Obtenemos información sobre las instancias del terminal
    instances = load_instances()

    # Si el nombre de la instancia figura entre los disponibles
    if name in instances:
        # Seleccionamos la instancia que necesitamos
        instance = instances[name]

        # Si no hay ningún proceso en ejecución para esa instancia, entonces
        if not instance['pid']:
            # Iniciamos un nuevo proceso
            process = subprocess.Popen([instanсes_paths[name], '/portable'])

            # Identificador del proceso iniciado
            pid = process.pid

            # Anotamos el identificador y el estado del proceso
            instance['pid'] = pid
            instance['status'] = 'running'

        # Devolvemos el resultado: inicio correcto
        return {name: instance}

    # Devolvemos el resultado: no se ha encontrado el nombre
    return {name: {'status': 'not found'}}

Si, por el contrario, en la solicitud aparece el nombre de una instancia que no figura entre los nombres registrados, no hacemos nada y devolvemos un mensaje indicando que no se ha encontrado dicha instancia.


Parada de los terminales

La función de parada de una instancia del terminal funciona de manera similar. También se le pasa el nombre de la instancia obtenido de la solicitud. Asimismo, se elabora una lista actualizada de todas las instancias registradas con su estado actual. Si el nombre de la instancia es correcto, obtenemos su PID y creamos un objeto que permite gestionar el proceso con ese identificador. Enviamos al proceso un comando de parada y esperamos un máximo de 10 segundos a que finalice su ejecución:
def stop_mt5(name: str) -> dict:
    '''
    Interrumpir el terminal

    Args:
        name (str): Nombre de la instancia del terminal.

    Returns:
        instance (dict): Información sobre el terminal iniciado.
    '''

    # Obtenemos información sobre las instancias del terminal
    instances = load_instances()

    # Si el nombre de la instancia figura entre los disponibles
    if name in instances:
        # Seleccionamos la instancia que necesitamos
        instance = instances[name]

        # Obtenemos el identificador de su proceso
        pid = instance['pid']

        # Si el terminal ya se había iniciado
        if pid:
            try:
                # Obtenemos el objeto del proceso del terminal
                p = psutil.Process(pid)

                # Detenemos el proceso
                p.terminate()
                p.wait(10)
            except psutil.NoSuchProcess:
                pass

        # Eliminamos la información sobre el proceso iniciado anteriormente
        instance['pid'] = 0
        instance['status'] = 'stopped'

        # Devolvemos el resultado: parada realizada con éxito
        return {name: instance}

    # Devolvemos el resultado: no se ha encontrado el proceso
    return {name: {'status': 'not found'}}

Si el proceso ha finalizado correctamente, devolvemos la información correspondiente. Si no es así, lo consideramos finalizado de todos modos. En las circunstancias actuales, todavía no hemos tenido ningún problema a la hora de finalizar los procesos, por lo que mantendremos esta función tal y como está. Cuando surjan problemas, ya pensaremos en seguir perfeccionándola.

Y si resulta que estamos intentando detener un proceso que ya ha finalizado (por ejemplo, si hemos cerrado manualmente la ventana de un terminal en ejecución), simplemente se devolverá un mensaje indicando que no se ha encontrado el proceso.


Página principal

Ahora nos toca ocuparnos de la plantilla HTML para generar la página principal. En ella tenemos que hacer varias cosas. En primer lugar, vamos a incluir los archivos estáticos externos con código CSS y JS. En segundo lugar, añadiremos a la página una lista sin numerar, en la que cada elemento contendrá información sobre una instancia del terminal.

Para crearlo, usaremos el motor de plantillas Jinja2. En el controlador de ruta GET /, hemos pasado al objeto de plantilla el diccionario que hemos creado con información sobre las instancias registradas. Tiene más o menos este aspecto:

{
    "MetaTrader5.1": {
        "pid": 23820,
        "status": "running"
    },
    "MetaTrader5.2": {
        "pid": 24888,
        "status": "running"
    },
    "MetaTrader5.3": {
        "pid": 0,
        "status": "stopped"
    }
}

Utilizando la estructura {% for name, data in instances.items() %} ... {% endfor %}, podemos crear un bucle que genere fragmentos de código HTML del mismo tipo, insertando en los lugares adecuados los valores de las variables del bucle. En este caso, la variable name irá almacenando, uno tras otro, los nombres de nuestras instancias del terminal: 'MetaTrader5.1', 'MetaTrader5.2', 'MetaTrader5.3'. Y en la variable data se almacenará un diccionario con las claves pid y status para cada instancia del diccionario instances.

Además, si es necesario, podemos utilizar la estructura del operador condicional {% if data.pid > 0 %} ... {% else %} ... {% endif %}. Gracias a ella, mostramos para cada instancia un único botón para realizar la acción correspondiente: el inicio de una instancia parada o la parada de una instancia en ejecución.

<!DOCTYPE html>
<html>

<head>
  <title>MT5 Manager</title>
  <link rel="stylesheet" href="/static/styles.css">
</head>

<body>
  <h1>MT5 Manager</h1>
  <div class="instances">
    <h2>Instances</h2>
    <ul id="instanceList">
      {% for name, data in instances.items() %}
      <li class="{{ data.status }}">
        <strong>{{ name }}</strong>
        {% if data.pid > 0 %}
        (PID: {{ data.pid }})
        <button onclick="stopInstance('{{ name }}')">Stop</button>
        {% else %}
        <button onclick="startInstance('{{ name }}')">Start</button>
        {% endif %}
      </li>
      {% endfor %}
    </ul>
  </div>
  
  <script src="/static/script.js"></script>
</body>

</html>

Al pulsar los botones de la página principal, se ejecutará el código JavaScript de las funciones startInstance() y stopInstance().


Gestión de pulsaciones

Las funciones que gestionan los eventos de pulsación de los botones «Start» y «Stop» se ubicarán en un archivo independiente, script.js, que hemos guardado en la carpeta /static. Su código es prácticamente idéntico y solo se diferencia en el punto de conexión al que la función enviará la solicitud POST. El nombre de la instancia del terminal, pasado como argumento, se inserta en la URL de la solicitud y, a continuación, se espera recibir una respuesta de nuestro propio servidor web. Una vez recibida la respuesta, esta se muestra en la consola de JavaScript del navegador y la página actual se recarga:

/**
 * Iniciando el terminal
 * @param {string} name - Nombre de la instancia
 */
async function startInstance(name) {
  // Enviamos una solicitud a la ruta correspondiente
  const res = await fetch(`/start/${name}`, { method: "POST" });

  // Recibimos la respuesta
  const data = await res.json();

  // Aquí se pueden añadir acciones adicionales 
  console.log(data);

  // Recargamos la página
  location.reload();
}
/**
 * Interrumpir el terminal
 * @param {string} name - Nombre de la instancia
 */
async function stopInstance(name) {
  // Enviamos una solicitud a la ruta correspondiente
  const res = await fetch(`/stop/${name}`, { method: "POST" });

  // Recibimos la respuesta
  const data = await res.json();

  // Aquí se pueden añadir acciones adicionales
  console.log(data);

  // Recargamos la página
  location.reload();
}

Posteriormente, la información obtenida en la variable «data» se puede utilizar para mostrar mensajes adicionales o actualizar el código de la página sin necesidad de recargarla por completo. Pero, como por ahora en nuestra página de inicio se muestra muy poca información, podemos recargar la página por completo.

Para mejorar el aspecto de la página de inicio, añadiremos algunos estilos al archivo styles.css, que se encuentra en la carpeta /static. De momento, no vamos a incorporar potentes frameworks de estilos para HTML como Bootstrap; siempre podremos utilizarlos cuando el proyecto adquiera una forma más definida. Por ahora, solo estamos explorando cómo podría ser la interfaz de usuario, por lo que no conviene prestar demasiada atención al aspecto visual en esta fase.

Pero un poco… ¡sí que se puede! Por ejemplo, hemos añadido a cada elemento de la lista una clase CSS adicional que coincide con el estado de la instancia. Y en el archivo styles.css hemos añadido reglas que establecen un color de fondo diferente para el elemento en función de si tiene o no la clase running:

.instances li.running {
    background: #d1ffd1;
    border: 1px solid #89fd85;
}

Ahora se podrá distinguir claramente entre las instancias en ejecución y las que no lo están.


Pruebas

Iniciemos el servidor web con el siguiente comando desde la carpeta de trabajo del proyecto:

uvicornmain:app--reload--no-use-colors

y abriremos en el navegador la página http://127.0.0.1:8000.

Mientras no se haya iniciado ninguna instancia del terminal, veremos algo parecido a esto:

Hacemos clic en el botón «Start» para la primera y la segunda instancia. Al cabo de un instante veremos que han pasado al estado «en ejecución», lo cual se confirma en el Administrador de tareas de Windows, donde vemos dos procesos con los mismos identificadores que en la página principal de nuestra interfaz web:

También se puede comprobar que el sistema de documentación automática ha recogido los comentarios y las precisiones que hemos aportado sobre las rutas y sus parámetros:

Como podemos ver, para la ruta POST /start/{name} hay una descripción tanto de la propia ruta como del parámetro {name}, con un ejemplo de un posible valor.


Conclusión

En esta fase, el proyecto MetaTrader 5 Manager ha pasado de ser una idea y un simple prototipo a convertirse en una aplicación completa, capaz ya de gestionar varias instancias de terminales de trading MetaTrader 5 a través de una cómoda interfaz web. Paso a paso, hemos separado la lógica de gestión de procesos de la parte del servidor, hemos añadido rutas para gestionar las instancias, hemos implementado una visualización sencilla del estado de los terminales y hemos dado los primeros pasos para estructurar el código del proyecto.

A pesar de que la solución actual aún está lejos de ser definitiva, tal y como está ya cumple su función principal: permite iniciar y detener los terminales de forma centralizada, así como obtener información sobre su estado actual. De este modo, contamos con una base sólida sobre la que podemos desarrollar niveles de funcionalidad más complejos.

En el futuro, desarrollaremos el proyecto en varias líneas:

  • la automatización del registro de nuevas instancias de terminales y almacenamiento de la información sobre ellas en una base de datos;
  • la ampliación de la integración con la API de Python de MetaTrader 5 para obtener estadísticas, órdenes e historiales de operaciones;
  • la mejora de la interfaz: incorporación de actualizaciones dinámicas sin necesidad de recargar la página y de elementos de control visuales;
  • la posible separación de la parte de interfaz en un servicio front-end independiente.

También es necesario diseñar un sistema de control de acceso. Para ello, se puede añadir la autenticación básica o utilizar un canal seguro HTTPS.

Además, conviene prever el mantenimiento de un registro de operaciones que indique quién ha iniciado o detenido los terminales y cuándo. Estos datos pueden resultar útiles para la auditoría y el análisis del uso del sistema.

Pero lo más importante, y que ya se puede apreciar, es que la arquitectura y el conjunto de tecnologías elegidos (Python + FastAPI + Jinja2 + psutil) permiten desarrollar el proyecto con flexibilidad, sin perder control ni transparencia. Este es un excelente ejemplo de cómo, a partir de una sencilla API REST, se puede desarrollar, paso a paso, un sistema completo de gestión de procesos de trading reales.

¡Gracias por su atención y hasta la próxima!


Contenido del archivo

#
Nombre Versión Descripción Últimas modificaciones
  mt5-manager/   Carpeta de trabajo del proyecto del servidor web de terminales  
1 ├─ main.py 0.1.0
Aplicación web para el servidor web de terminales
Parte 2
2 ├─ mt5_control.py 0.1.0 Lógica de control del inicio y la parada de los terminales Parte 2

├─ templates/      
3 │ └─ index.html
0.1.0 Plantilla de la página principal Parte 2
  └─ static/      
4 ├─ styles.css
0.1.0 Estilos CSS
Parte 2
5 └─ script.js 0.1.0 Código JavaScript
Parte 2

Además, el código fuente del proyecto está disponible en el repositorio mt5-manager.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/19852

Archivos adjuntos |
mt5-manager.zip (4.38 KB)
Análisis de la influencia de los ciclos solares y lunares en los tipos de cambio Análisis de la influencia de los ciclos solares y lunares en los tipos de cambio
¿Y si los ciclos lunares y los patrones estacionales influyesen en los mercados de divisas? Este artículo muestra cómo traducir los conceptos astrológicos al lenguaje de las matemáticas y el aprendizaje automático. Hemos creado un sistema en Python con 88 características basadas en ciclos astronómicos, he entrenado CatBoost con 15 años de datos del EUR/USD y he obtenido unos resultados interesantes. El código es abierto, los métodos se pueden comprobar, las conclusiones son inesperadas: la sabiduría ancestral se une al «gradient boosting».
Guía de aprendizaje automático para MetaTrader 5 (Parte 3): Método de etiquetado mediante exploración de tendencias Guía de aprendizaje automático para MetaTrader 5 (Parte 3): Método de etiquetado mediante exploración de tendencias
Hemos creado un sólido proceso de ingeniería de características utilizando barras de ticks adecuadas para eliminar la fuga de datos y hemos resuelto el problema crítico del etiquetado mediante señales de triple barrera metaetiquetadas. En esta entrega se aborda la exploración de tendencias, una técnica avanzada de etiquetado para horizontes adaptativos. Tras explicar la teoría, se muestra un ejemplo de cómo se pueden utilizar las etiquetas de exploración de tendencias junto con el metaetiquetado para mejorar la estrategia clásica del cruce de medias móviles.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Características del Wizard MQL5 que debe conocer (Parte 85): Uso de patrones del oscilador estocástico y el FrAMA con aprendizaje por inferencia mediante beta-VAE Características del Wizard MQL5 que debe conocer (Parte 85): Uso de patrones del oscilador estocástico y el FrAMA con aprendizaje por inferencia mediante beta-VAE
Este artículo es la continuación de la «Parte 84», en la que presentamos la combinación del indicador estocástico y la media móvil adaptativa fractal. Ahora centraremos nuestra atención en el aprendizaje por inferencia para analizar si es posible mejorar los resultados de los patrones de bajo rendimiento del artículo anterior. El indicador estocástico y el FrAMA forman un binomio complementario de impulso y tendencia. Para nuestro aprendizaje por inferencia, volvemos a examinar el algoritmo beta de un autoencoder variacional. Además, como siempre, nos encargamos de la implementación de una clase de señal personalizada diseñada para integrarse con el Asistente de MQL5.