Desarrollamos un gestor de terminales (Parte 2): Inicio de varias instancias
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
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.