mt5-manager/docs/articles/part-02/publish/article.html
Yuriy Bykov Yuriy Bykov 930b46b3c1 Добавить создание терминалов, обновление сервиса и документацию
- реализовать создание portable-экземпляров и настройку счетов
- обновить интерфейс и добавить обновление проекта через Git
- добавить сборку и распространение сервиса
- организовать материалы статей и описать актуальную версию
- запрашивать тестовый пароль интерактивно и расширить проверки
2026-10-29 14:39:39 +03:00
468 lines
60 KiB
HTML
This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<meta charset="utf-8">
<h1>Разрабатываем менеджер терминалов (Часть 2): Запуск нескольких экземпляров</h1>
<div class="inner formatted-content">
<h3>Введение</h3><p>В первой <a href="https://www.mql5.com/ru/articles/19804" target="_blank">части</a> серии "Менеджер терминалов своими руками" мы поставили перед собой задачу — создать веб-интерфейс для управления запуском и остановкой экземпляров MetaTrader 5 на локальном компьютере. Мы определили архитектуру системы, начав с разработки минимально жизнеспособного продукта (MVP) — веб-сервера, который может принимать HTTP-запросы и управлять одним экземпляром терминала. Заодно мы рассмотрели такие ключевые понятия как API, REST, HTTP-запросы, декораторы.</p><p>Для реализации проекта был выбран язык Python, благодаря его богатой экосистеме библиотек и простоте интеграции с внешними API — в данном случае с библиотекой MetaTrader 5.</p><p>Фреймворк FastAPI позволяет создавать веб-приложения с минимальными затратами кода, предоставляя из коробки поддержку типизации, асинхронности и автоматической документации через Swagger UI.</p><p>В связке с ним используется Uvicorn, лёгкий и быстрый ASGI-сервер, обеспечивающий высокую производительность даже при большом количестве одновременных запросов.</p><p>Для работы с процессами терминалов применяются библиотеки psutil и subprocess, что позволяет удобно получать сведения о запущенных экземплярах и управлять ими.</p><p>В дальнейшем планируется добавить использование SQLite или другой базы данных для хранения состояния экземпляров, а также фронтенд-слой на JavaScript или Vue.js для более интерактивного интерфейса.</p><p>Теперь пришло время расширять функциональность и переходить к следующим этапам — реализации более сложных возможностей, таких как управление несколькими экземплярами, хранение состояния, интеграция с MetaTrader5 API и веб-интерфейс с полной информацией о терминалах.</p><p><br></p><h3>Намечаем путь<br></h3><p>Попробуем очертить примерные границы следующего кусочка реализации. Во-первых, пока у нас нет реального разделения общей функциональности на два разных веб-сервера, придётся в каком-то минимальном виде добавлять интерфейсные части в тот веб-сервер, который мы уже начали делать. Эта работа не пропадёт зря, так как в дальнейшем мы сможем перенести уже имеющийся код на другой веб-сервер с некоторыми правками. Поэтому начнём с того, что создадим дополнительные файлы для размещения разных частей кода проекта, придав им некоторый налёт организованности.</p><p>Во-вторых, сделаем самый напрашивающийся следующий шаг, добавив возможность работать с несколькими экземплярами терминала MetaTrader 5. То есть реализуем создание, запуск, остановку и получение информации о составе запущенных в данный момент экземпляров. Получение детальной информации о торговом счёте оставим на будущее. </p><p>Это потребует предварительного принятия некоторых архитектурных решений, что не так просто, как может показаться на первый взгляд. Сейчас на нас лежит ответственность за будущее развитие проекта. От того, насколько удачными окажутся принятые сейчас решения, зависит легкость дальнейшей реализации и возможные пределы расширения функциональности. Очень не хотелось бы, дойдя до какого-то этапа проекта, выяснить, что для дальнейшего развития нужно вернуться к началу и всё переделать. Так, конечно, тоже бывает, но постараемся этого избежать, за счёт более внимательного отношения к своему выбору.</p><p>С другой стороны, важно не утонуть в море возможностей. Поэтому в каких-то вопросах лучше сделать хоть какой-то выбор прямо сейчас, чем заниматься бесконечным загадыванием, как тот или иной выбор отзовётся в будущем. А ещё в некоторых вопросах бывает более эффективно выбрать сейчас что-то простое. То, что потом точно будет переделано на другом, более сложном уровне реализации. Но зато мы не погрязнем в реализации идеальной архитектуры отдельной части проекта и сможем быстрее продвинуть его в целом.</p><p><br></p><h3>Разделение кода<br></h3><p>Добавим несколько новых файлов в наш проект. В файл <i>mt5_control.py</i> вынесем все функции из файла <i>main.py</i>, отвечающие за логику управления терминалами. Таким образом, сейчас в главном файле приложения останутся только функции-обработчики конечных точек (маршрутов) и создание объекта FastAPI с необходимыми настройками.</p><p>Для хранения шаблонов HTML-кода страниц, которые должны генерироваться при обработке некоторых конечных точек, создадим папку <i>templates</i>. Пока что разместим в ней один файл <i>index.html</i> для формирования HTML-кода главной страницы (маршрут GET /).</p><p>Для хранения статического кода, который будет всегда отправляться браузеру без какой-либо обработки веб-сервером, сделаем папку <i>static</i>. К таким файлам можно отнести, например, файлы таблиц CSS-стилей или JavaScript-кода, на которые есть ссылки из HTML-кода страниц. Добавим сразу парочку таких пустых файлов для главной страницы.</p><p>После этого дерево файлов проекта будет выглядеть так:</p><div class="code"><p>mt5_manager/<br>│<br>├── main.py                 # Основной файл FastAPI<br>├── mt5_control.py          # Логика управления запуском и остановкой терминалов<br>├── templates/<br>│   └── index.html          # Шаблон основной страницы<br>└── static/<br>    ├── styles.css          # CSS-стили<br>    └── script.js           # JavaScript-код</p></div><p>Будем в дальнейшем придерживаться принятых соглашений по размещению файлов проекта. При необходимости можно будет создавать новые вложенные папки внутри уже имеющихся, если это будет помогать структурированию кода.</p><p>Чтобы в нашем приложении можно было воспользоваться механизмом шаблонов и статическими файлами, нужно подключить соответствующие библиотеки в файле <i>main.py</i>:</p><pre class="code"><span class="keyword">from</span> fastapi.staticfiles <span class="keyword">import</span> StaticFiles
<span class="keyword">from</span> fastapi.templating <span class="keyword">import</span> Jinja2Templates</pre><p>Затем, после создания объекта приложения (<i>app</i>), мы подключаем новую конечную точку <i>GET /static</i>, при обработке которой любые имена, идущие в URL после <i>static/</i>, будут восприниматься как файл из папки <i>static</i> нашего проекта:</p><pre class="code"><span class="preprocessor"># </span>Создаём объект приложения
app = FastAPI()
<span class="preprocessor"># </span>Подключаем статические файлы
app.mount(<span class="string">"/static"</span>, StaticFiles(directory=<span class="string">"static"</span>), name=<span class="string">"static"</span>)
</pre><p>Заметьте, что в данном случае мы не создаём отдельную функцию-обработчик маршрута <i>GET /static</i>, а вызываем метод объекта приложения, который сам создаст необходимую функцию с нужным содержимым.</p><p>Теперь создадим объект, который будет использоваться при необходимости передать в качестве ответа HTML-код, сформированный по определённому шаблону из папки <i>templates</i>:</p><pre class="code"><span class="preprocessor"># </span>Создаём объект для работы с шаблонами HTML-кода
templates = Jinja2Templates(directory=<span class="string">"templates"</span>)</pre><p>Обработчик маршрута GET / перепишем таким образом:</p><pre class="code">@app.get(<span class="string">'/'</span>, response_class=HTMLResponse)
<span class="keyword">async</span> <span class="keyword">def</span> index(request: Request):
    <span class="string">'''Обработчик GET / (корневой каталог)'''</span>
    instances = load_instances()
    <span class="keyword">return</span> templates.TemplateResponse(<span class="string">"index.html"</span>, {<span class="string">"request"</span>: request, <span class="string">"instances"</span>: instances})</pre><p>В нём мы получаем словарь <i>instances</i> с информацией о зарегистрированных экземплярах терминалов и их текущем статусе (запущен/остановлен). Далее мы возвращаем шаблонный ответ, получаемый из файла <i>index.html</i> с учётом информации из словаря <i>instances</i>. Какая именно информация будет присутствовать в словаре <i>instances,</i> и как она будет трансформироваться в HTML-код, мы посмотрим чуть позже.</p><p>В два оставшихся обработчика маршрутов мы тоже внесем небольшие изменения. Они будут однотипны, то есть для обработчика запуска экземпляра мы сделаем то же самое, что и для обработчика остановки. Необходимость изменений вызвана тем, что теперь нужно в обработчик передавать информацию о том, какой экземпляр мы хотим сейчас запустить или остановить. Когда экземпляр был единственный, такой проблемы не возникало.</p><p>Добавим в URL этих маршрутов изменяемую часть, обозначенную как <i><span style="background-color:rgb(255, 246, 200);">{name}</span></i>. Это обязывает нас добавить в список аргументов функции-обработчика новый аргумент с именем <i><span style="background-color:rgb(216, 232, 194);">name</span></i> и типом <i><span style="background-color:rgb(216, 232, 194);">str</span></i> (так как имя экземпляра — это строка):</p><pre class="code">@app.post(<span class="string">'/start/<span style="background-color:rgb(255, 246, 200);">{name}</span>'</span>)
<span class="keyword">async</span> <span class="keyword">def</span> start_instance(<span style="background-color:rgb(216, 232, 194);">name: <span class="built_in">str</span></span>):
    <span class="string">'''Обработчик POST /start - запуск терминала'''</span>
    result = start_mt5(<span style="background-color:rgb(164, 192, 228);">name</span>)
    <span class="keyword">return</span> JSONResponse(result)
@app.post(<span class="string">'/stop/<span style="background-color:rgb(255, 246, 200);">{name}</span>'</span>)
<span class="keyword">async</span> <span class="keyword">def</span> stop_instance(<span style="background-color:rgb(216, 232, 194);">name: <span class="built_in">str</span></span>):
    <span class="string">'''Обработчик POST /stop - остановка терминала'''</span>
    result = stop_mt5(<span style="background-color:rgb(164, 192, 228);">name</span>)
    <span class="keyword">return</span> JSONResponse(result)
</pre><p>Это приведёт к тому, что при запуске обработчика ему в качестве аргумента будет передаваться строка, вырезанная из той части URL, которая соответствует положению <i><span style="background-color:rgb(255, 246, 200);">{name}</span></i>. А далее мы уже можем с <span style="background-color:rgb(164, 192, 228);">ней</span> работать внутри функций-обработчиков.</p><p>Обновим нашу таблицу маршрутов в связи со сделанными изменениями:</p><table cellspacing="0" cellpadding="3" width="100%" class="standart"><thead><tr><th>#</th><th>Метод</th><th>URL</th><th>Описание, параметры</th><th>Формат<br>ответа </th></tr></thead><tbody><tr><td>1</td><td>GET</td><td>/</td><td>Отображает главную страницу с веб-интерфейсом для управления экземплярами MetaTrader 5<br>Пока что пусть отображается только заголовок с названием проекта.</td><td>HTML</td></tr><tr><td>2 </td><td>POST</td><td style="background-color:rgb(216, 232, 194);">/start/{name} </td><td>Запускает экземпляр терминала MetaTrader 5 с именем {name}</td><td>JSON</td></tr><tr><td>3</td><td>POST<br></td><td style="background-color:rgb(216, 232, 194);">/stop/{name}</td><td>Останавливает запущенный процесс MetaTrader 5 с именем {name}</td><td>JSON</td></tr></tbody></table><p>Даже несмотря на то, что проект пока находится на стадии активной разработки, важно уже сейчас формировать устойчивую архитектуру. В идеале код приложения должен быть легко расширяемым — чтобы добавление новых функций, маршрутов или логики управления не требовало переписывания существующих частей.</p><p>Именно поэтому имеет смысл придерживаться принципов разделения ответственности и чистой архитектуры, когда:</p><ul><li>Модуль mt5_control.py отвечает только за работу с процессами терминалов.</li><li>Файл main.py концентрируется на маршрутах и веб-интерфейсе.</li><li>Папки templates и static изолируют визуальную часть приложения.</li></ul><p>Такое разделение делает проект более устойчивым к изменениям и облегчает тестирование. В будущем, при необходимости, эти части можно будет вынести в отдельные микросервисы.</p><p><br></p><p></p><h3>Экземпляры терминалов<br></h3><p>Чтобы можно было запускать несколько экземпляров терминалов MetaTrader 5 на сервере, он там должны физически присутствовать. То есть на сервере должны быть несколько папок, в каждой из располагается запускаемый файл терминала. Существуют разные способы, как можно этого добиться, но сейчас для упрощения предположим, что эту часть работы пользователь будет пока выполнять вручную. </p><p>Проделаем эту операцию. Если на компьютере ещё нет установленного терминала, то необходимо скачать его с сайта брокера или <a href="https://www.metatrader5.com/" title="https://www.metatrader5.com/" target="_blank">www.metatrader5.com</a> и установить. Создадим папку <i>MT5</i> с тремя вложенными папками с именами вида <i>MetaTrader5.x</i> и скопируем в них запускаемые файлы терминала (<i>terminal64.exe</i>) и редактора (<i>metaeditor64.exe</i>) из любой имеющийся установленной копии терминала MetaTrader 5:</p><p style="text-align:center;"><img width="694" height="279" src="../images/bd845337-2895060216714.png" loading="lazy" alt="" style="vertical-align:middle;"></p><p>Выбор конкретных названий для папок терминалов и их корневой папки не играет никакой роли. Мы взяли именно такие, потому что они достаточно короткие и понятные.</p><p>В папке каждого терминала запустим файл <i>terminal64.exe</i> в режиме portable и подключимся к какому-то торговому счёту. Здесь мы будем использовать демо-счета от компании MetaQuotes. При необходимости установки обновлений, дождемся завершения этого процесса. Итак, теперь у нас есть три экземпляра терминала, установленные в папки с такими полными путями:</p><div class="code"><p>C:/MT5/MetaTrader5.1/terminal64.exe<br>C:/MT5/MetaTrader5.2/terminal64.exe<br>C:/MT5/MetaTrader5.3/terminal64.exe</p></div><p></p><p>Именно их мы будем запускать и останавливать через создаваемый веб-сервер. Также мы пока не будем касаться вопросов смены торговых счетов, к которым подключены терминалы, то есть сфокусируемся только на запуске и остановке.</p><p>В дальнейшем то, что мы сейчас проделали вручную, будет каким-то образом автоматизировано. Но сейчас мы можем не отвлекаться на реализацию: проделав такую подготовительную работу один раз, мы можем теперь долго к ней не возвращаться, сосредоточив усилия на содержательной части проекта.</p><p><br></p><h3>Формирование списка терминалов</h3><p>В прошлый раз мы реализовали запуск и остановку одного единственного экземпляра терминала, находившегося по фиксированному пути на нашем сервере. Теперь приступим к реализации выборочного запуска и остановки каждого экземпляра из фиксированного списка. Как уже было сказано, этот код мы перенесли и продолжим писать в новом файле <i>mt5_control.py</i>.</p><p>Новых библиотек, кроме уже добавленных subprocess и psutil, нам пока не понадобится. Мы создали три папки с экземплярами терминалов, которые отличаются только названием последней папки в пути к файлу <i>terminal64.exe</i>. Поэтому давайте объявим две константы, содержащие те части пути, которые одинаковы для всех экземпляров:</p><pre class="code"><span class="comment"># Путь к папке с экземплярами терминалов</span>
MT5_FOLDER = <span class="string">'C:/MT5'</span>
<span class="comment"># Имя запускаемого файла терминала</span>
MT5_EXE = <span class="string">'terminal64.exe'</span></pre><p>Теперь можно сделать функцию, которая по имени будет формировать полный путь к запускаемому файлу нужного экземпляра терминала. Сейчас мы просто будем соединять вместе три части полного пути: общую папку, имя экземпляра и имя запускаемого файла:</p><pre class="code"><span class="keyword">def</span> instance_path(name: <span class="built_in">str</span>) -&gt; <span class="built_in">str</span>:
    <span class="string">'''
    Формирует полный путь по имени экземпляра терминала
    Args:
        name (str): Имя экземпляра терминала.
    Returns:
        path (str): Полый путь к запускаемому файлу экземпляра терминала.
    '''</span>
    <span class="keyword">return</span> <span class="string">f'<span class="subst">{MT5_FOLDER}</span>/<span class="subst">{name}</span>/<span class="subst">{MT5_EXE}</span>'</span></pre><p>Для хранения списка имён экземпляров будем использовать пока обычный список, в котором будут находиться выбранные нами названия папок:</p><pre class="code"># Список имён экземпляров (пока фиксированный)
instances_names = ['MetaTrader5.1', 'MetaTrader5.2', 'MetaTrader5.3']
</pre><p>По готовому списку имён мы можем однозначно получить полные пути к исполняемым файлам каждого экземпляра терминала. Поскольку они пока тоже будут постоянными, то можно заранее сформировать два вспомогательных словаря, которые будут использованы для получения пути по заданному имени и имени по заданному пути:</p><pre class="code"><span class="comment"># Словарь соответствия имя -&gt; путь</span>
instances_paths = {name: instance_path(name) <span class="keyword">for</span> name <span class="keyword">in</span> instances_names}
<span class="comment"># Словарь соответствия путь -&gt; имя</span>
paths_instances = {instance_path(name): name <span class="keyword">for</span> name <span class="keyword">in</span> instances_names}</pre><p>После длительных размышлений мы решили отказаться от повторного использования информации о состоянии экземпляров, которая была получена при запуске сервера. Вместо этого, мы при каждой операции будем актуализировать состав запущенных экземпляров. Для этого напишем функцию <i>load_instances()</i>, которая будет формировать словарь <i>instances.</i> В этом словаре ключом будет имя экземпляра, а значением — ещё один словарь с двумя ключами <i>pid</i> и <i>status</i>. В них мы будем хранить уникальный идентификатор запущенного процесса (PID) и статус экземпляра с заданным именем.</p><p>Возможно, что хранение этих двух видов информации является избыточным, так как в принципе, по значению PID мы можем судить о том, запущен ли данный экземпляр или нет. Если PID &gt; 0, то процесс выполняется. Но сейчас не совсем понятно, как сделать лучше, поэтому пусть будут храниться оба этих поля. Далее мы, скорее всего, будем расширять словарь с информацией об одном экземпляре.</p><p>Вначале мы инициализируем словарь <i>instances</i> для экземпляров одинаковыми значениями:</p><pre class="code">{<span class="string">'pid'</span>: <span class="number">0</span>, <span class="string">'status'</span>: <span class="string">'stopped'</span>}</pre><p>что соответствует незапущенному экземпляру. Затем, используя библиотеку psutil, организуем перебор всех запущенных на компьютере процессов. Для каждого из них мы будем смотреть, не является ли данный процесс одним из экземпляров нашего терминала. Если да, то определяем его имя, PID, и сохраняем их в соответствующем элементе словаря <i>instances</i>.</p><pre class="code"><span class="keyword">def</span> load_instances() -&gt; <span class="built_in">dict</span>:
    <span class="string">'''Получение информации о статусе экземпляров
    терминала MetaTrader 5 из заданных папок.
    Returns:
        instances (dict): Информация об экземплярах терминала.
    '''</span>
    <span class="comment"># Формируем словарь для всех экземпляров по списку имён</span>
    instances = {name: {<span class="string">'pid'</span>: <span class="number">0</span>, <span class="string">'status'</span>: <span class="string">'stopped'</span>}
                 <span class="keyword">for</span> name <span class="keyword">in</span> instances_names}
    <span class="comment"># Перебираем все запущенные процессы</span>
    <span class="keyword">for</span> proc <span class="keyword">in</span> psutil.process_iter([<span class="string">'pid'</span>, <span class="string">'name'</span>, <span class="string">'exe'</span>]):
        <span class="keyword">try</span>:
            <span class="comment"># Получаем путь к запускаемому файлу процесса</span>
            exe_path = <span class="built_in">str</span>(proc.info[<span class="string">'exe'</span>]).replace(<span class="string">'\\'</span>, <span class="string">'/'</span>)
            <span class="comment"># Если он есть среди путей экземпляров терминала</span>
            <span class="keyword">if</span> exe_path <span class="keyword">and</span> exe_path <span class="keyword">in</span> paths_instanses:
                <span class="comment"># Определяем имя экземпляра для данного пути</span>
                name = paths_instanses[exe_path]
                <span class="comment"># Берём нужный экземпляр</span>
                instance = instances[name]
                <span class="comment"># Запоминаем идентификатор и статус процесса</span>
                instance[<span class="string">'pid'</span>] = proc.info[<span class="string">'pid'</span>]
                instance[<span class="string">'status'</span>] = <span class="string">'running'</span>
        <span class="keyword">except</span> (psutil.NoSuchProcess, psutil.AccessDenied):
            <span class="keyword">continue</span>
    <span class="keyword">return</span> instances</pre><p>Эта функция будет использована в обработчике маршрута GET / для формирования актуального состояния всех зарегистрированных экземпляров. Но также она будет использована и при каждой операции запуска и остановки. С её помощью мы сможем не запускать уже запущенный ранее экземпляр и не останавливать уже остановленный.</p><p><br></p><h3>Запуск терминалов<br></h3><p>Посмотрим теперь на реализацию функции запуска экземпляра терминала с определённым именем, передаваемым в качестве аргумента. Сначала мы получаем актуальную информацию о состоянии всех зарегистрированных терминалов. Если переданное имя нашлось среди существующих имён, то проверим, существует ли запущенный процесс для этого экземпляра.</p><p>Если нет, то тогда запускаем новый процесс, используя функцию <i>Popen()</i> из библиотеки subprocess. В качестве аргумента ей надо передать список аргументов командной строки запуска нужного исполняемого файла. Первым в этом списке идёт полный путь к запускаемому файлу, а дальше — ключ <i>/portable</i> для использования текущей папки терминала в качестве рабочей.</p><p>После успешного запуска, мы получаем значение уникального идентификатора процесса в операционной системе (PID) и вместе со статусом "running" сохраняем его в элементе словаря экземпляров для возврата из функции:</p><pre class="code"><span class="keyword">def</span> start_mt5(name: <span class="built_in">str</span>) -&gt; <span class="built_in">dict</span>:
    <span class="string">'''
    Запуск экземпляра терминала
    Args:
        name (str): Имя экземпляра терминала.
    Returns:
        instance (dict): Информация о запущенном терминале.
    '''</span>
    <span class="comment"># Получаем информацию об экземплярах терминала</span>
    instances = load_instances()
    <span class="comment"># Если имя экземпляра есть среди имеющихся</span>
    <span class="keyword">if</span> name <span class="keyword">in</span> instances:
        <span class="comment"># Берём нужный экземпляр</span>
        instance = instances[name]
        <span class="comment"># Если для него нет запущенного процесса, то</span>
        <span class="keyword">if</span> <span class="keyword">not</span> instance[<span class="string">'pid'</span>]:
            <span class="comment"># Запускаем новый процесс</span>
            process = subprocess.Popen([instanсes_paths[name], <span class="string">'/portable'</span>])
            <span class="comment"># Идентификатор запущенного процесса</span>
            pid = process.pid
            <span class="comment"># Запоминаем идентификатор и статус процесса</span>
            instance[<span class="string">'pid'</span>] = pid
            instance[<span class="string">'status'</span>] = <span class="string">'running'</span>
        <span class="comment"># Возвращаем результат - успешный запуск</span>
        <span class="keyword">return</span> {name: instance}
    <span class="comment"># Возвращаем результат - имя не найдено</span>
    <span class="keyword">return</span> {name: {<span class="string">'status'</span>: <span class="string">'not found'</span>}}</pre><p>Если же в запросе пришло имя экземпляра, которого нет среди зарегистрированных имён, то ничего не делаем и возвращаем информацию о том, что такой экземпляр не найден.</p><p><br></p><h3>Остановка терминалов</h3>Функция остановки экземпляра терминалов работает похожим образом. Ей также передаётся имя экземпляра, полученное из запроса. Также формируется актуальный состав всех зарегистрированных экземпляров с их текущим статусом. Если имя экземпляра корректное, то берём его PID, и создаём объект, позволяющий управлять процессом с заданным идентификатором. Посылаем процессу команду остановки и ждем не более 10 секунд завершения его работы:<pre class="code"><span class="keyword">def</span> stop_mt5(name: <span class="built_in">str</span>) -&gt; <span class="built_in">dict</span>:
    <span class="string">'''
    Остановка терминала
    Args:
        name (str): Имя экземпляра терминала.
    Returns:
        instance (dict): Информация о запущенном терминале.
    '''</span>
    <span class="comment"># Получаем информацию об экземплярах терминала</span>
    instances = load_instances()
    <span class="comment"># Если имя экземпляра есть среди имеющихся</span>
    <span class="keyword">if</span> name <span class="keyword">in</span> instances:
        <span class="comment"># Берём нужный экземпляр</span>
        instance = instances[name]
        <span class="comment"># Берём идентификатор его процесса</span>
        pid = instance[<span class="string">'pid'</span>]
        <span class="comment"># Если ранее терминал был запущен</span>
        <span class="keyword">if</span> pid:
            <span class="keyword">try</span>:
                <span class="comment"># Получаем объект процесса терминала</span>
                p = psutil.Process(pid)
                <span class="comment"># Останавливаем процесс</span>
                p.terminate()
                p.wait(<span class="number">10</span>)
            <span class="keyword">except</span> psutil.NoSuchProcess:
                <span class="keyword">pass</span>
        <span class="comment"># Удаляем информацию о запущенном ранее процессе</span>
        instance[<span class="string">'pid'</span>] = <span class="number">0</span>
        instance[<span class="string">'status'</span>] = <span class="string">'stopped'</span>
        <span class="comment"># Возвращаем результат - успешная остановка</span>
        <span class="keyword">return</span> {name: instance}
    <span class="comment"># Возвращаем результат - процесс не найден</span>
    <span class="keyword">return</span> {name: {<span class="string">'status'</span>: <span class="string">'not found'</span>}}</pre><p>Если процесс благополучно завершился, то возвращаем информацию об этом. Если нет, то всё равно считаем его завершённым. В текущих условиях мы пока не столкнулись с какими-либо проблемами при завершении процессов, поэтому оставим эту функцию именно в таком виде. Когда же проблемы возникнут, тогда и подумаем о дальнейшей доработке.</p><p>Ну и если так оказалось, что мы пытаемся остановить уже завершённый процесс (например, мы могли вручную закрыть окно запущенного терминала), то просто вернётся сообщение о том, что процесс не найден.</p><p><br></p><h3>Главная страница<br></h3><p>Теперь настала очередь заняться HTML-шаблоном формирования главной страницы. В нём нам надо сделать несколько вещей. Во-первых, <span style="background-color:rgb(164, 192, 228);">подключим</span> внешние статические файлы с CSS и JS-кодом. Во-вторых, добавим на страницу ненумерованный список, каждый элемент которого будет представлять собой информацию об одном экземпляре терминала.</p><p>Для его формирования воспользуемся движком шаблонов Jinja2. В обработчике маршрута GET / мы передали объекту шаблона наш сформированный словарь с информацией о зарегистрированных экземплярах. Он выглядит примерно так:</p><pre class="code">{
    <span class="string">"MetaTrader5.1"</span>: {
        <span class="string">"pid"</span>: <span class="number">23820</span>,
        <span class="string">"status"</span>: <span class="string">"running"</span>
    },
    <span class="string">"MetaTrader5.2"</span>: {
        <span class="string">"pid"</span>: <span class="number">24888</span>,
        <span class="string">"status"</span>: <span class="string">"running"</span>
    },
    <span class="string">"MetaTrader5.3"</span>: {
        <span class="string">"pid"</span>: <span class="number">0</span>,
        <span class="string">"status"</span>: <span class="string">"stopped"</span>
    }
}</pre><p>Используя конструкцию <span style="background-color:rgb(222, 188, 133);">{% for name, data in instances.items() %}</span> ... <span style="background-color:rgb(222, 188, 133);">{% endfor %}</span>, мы можем организовать цикл, который будет генерировать однотипные куски HTML-кода, подставляя в <span style="background-color:rgb(216, 232, 194);">нужные места</span> значения переменных цикла. В данном случае в переменную <i>name</i> будут попадать по очереди названия наших экземпляров терминалов: 'MetaTrader5.1', 'MetaTrader5.2', 'MetaTrader5.3'. А в переменную <i>data</i> будет попадать словарь с ключами <i>pid</i> и <i>status</i> для каждого экземпляра из словаря <i>instances</i>.</p><p>Также при необходимости мы можем использовать конструкцию условного оператора <span style="background-color:rgb(255, 246, 200);">{% if data.pid &gt; 0 %}</span> ... <span style="background-color:rgb(255, 246, 200);">{% else %}</span> ... <span style="background-color:rgb(255, 246, 200);">{% endif %}</span>. Благодаря ей мы отображаем для каждого экземпляра только одну кнопку для выполнения возможного действия: запуска для остановленного экземпляра и остановки для запущенного.</p><pre class="code"><span class="doctype">&lt;!DOCTYPE html&gt;</span>
<span class="tag">&lt;<span class="keyword">html</span>&gt;</span>
<span class="tag">&lt;<span class="keyword">head</span>&gt;</span>
  <span class="tag">&lt;<span class="keyword">title</span>&gt;</span>MT5 Manager<span class="tag">&lt;/<span class="keyword">title</span>&gt;</span>
  <span class="tag" style="background-color:rgb(164, 192, 228);">&lt;<span class="keyword">link</span><span class="attribute"> rel=<span class="value">"stylesheet"</span></span><span class="attribute"> href=<span class="value">"/static/styles.css"</span></span>&gt;</span>
<span class="tag">&lt;/<span class="keyword">head</span>&gt;</span>
<span class="tag">&lt;<span class="keyword">body</span>&gt;</span>
  <span class="tag">&lt;<span class="keyword">h1</span>&gt;</span>MT5 Manager<span class="tag">&lt;/<span class="keyword">h1</span>&gt;</span>
  <span class="tag">&lt;<span class="keyword">div</span><span class="attribute"> class=<span class="value">"instances"</span></span>&gt;</span>
    <span class="tag">&lt;<span class="keyword">h2</span>&gt;</span>Instances<span class="tag">&lt;/<span class="keyword">h2</span>&gt;</span>
    <span class="tag">&lt;<span class="keyword">ul</span><span class="attribute"> id=<span class="value">"instanceList"</span></span>&gt;</span>
      <span style="background-color:rgb(222, 188, 133);">{% for name, data in instances.items() %}</span>
      <span class="tag">&lt;<span class="keyword">li</span><span class="attribute"> class=<span class="value">"<span style="background-color:rgb(216, 232, 194);">{{ data.status }}</span>"</span></span>&gt;</span>
        <span class="tag">&lt;<span class="keyword">strong</span>&gt;</span><span style="background-color:rgb(216, 232, 194);">{{ name }}</span><span class="tag">&lt;/<span class="keyword">strong</span>&gt;</span>
        <span style="background-color:rgb(255, 242, 153);">{% if data.pid &gt; 0 %}</span>
        (PID: <span style="background-color:rgb(216, 232, 194);">{{ data.pid }}</span>)
        <span class="tag">&lt;<span class="keyword">button</span><span class="attribute"> onclick=<span class="value">"stopInstance('<span style="background-color:rgb(216, 232, 194);">{{ name }}</span>')"</span></span>&gt;</span>Stop<span class="tag">&lt;/<span class="keyword">button</span>&gt;</span>
        <span style="background-color:rgb(255, 242, 153);">{% else %}</span>
        <span class="tag">&lt;<span class="keyword">button</span><span class="attribute"> onclick=<span class="value">"startInstance('<span style="background-color:rgb(216, 232, 194);">{{ name }}</span>')"</span></span>&gt;</span>Start<span class="tag">&lt;/<span class="keyword">button</span>&gt;</span>
        <span style="background-color:rgb(255, 242, 153);">{% endif %}</span>
      <span class="tag">&lt;/<span class="keyword">li</span>&gt;</span>
      <span style="background-color:rgb(222, 188, 133);">{% endfor %}</span>
    <span class="tag">&lt;/<span class="keyword">ul</span>&gt;</span>
  <span class="tag">&lt;/<span class="keyword">div</span>&gt;</span>
  <span style="background-color:rgb(164, 192, 228);"><span class="tag">&lt;<span class="keyword">script</span><span class="attribute"> src=<span class="value">"/static/script.js"</span></span>&gt;</span><span></span><span class="tag">&lt;/<span class="keyword">script</span>&gt;</span></span>
<span class="tag">&lt;/<span class="keyword">body</span>&gt;</span>
<span class="tag">&lt;/<span class="keyword">html</span>&gt;</span></pre><p>При нажатии на кнопки на главной странице, будет выполняться JS-код в функциях <i>startInstance()</i> и <i>stopInstance()</i>.</p><p><br></p><h3><h3>Обработка нажатий</h3></h3><p>Функции-обработчики событий нажатия на кнопки Start и Stop будут располагаться в отдельном файле <i>script.js</i>, который мы разместили в папке <i>/static</i>. Их код почти одинаков и отличается только тем, на какую конечную точку функция будет посылать POST-запрос. Переданное в аргументе имя экземпляра терминала подставляется в URL запроса и дальше ожидается получение ответа от нашего же веб-сервера. После получения ответа, он выводится в консоль JavaScript браузера, и текущая страница перезагружается:</p><pre class="code"><span class="comment">/**
* Запуск терминала
* @param {string} name - Имя экземпляра
*/</span>
async function startInstance(name) {
  <span class="comment">// Посылаем запрос по нужному маршруту</span>
  <span class="keyword">const</span> res = await fetch(`/start/${name}`, { method: <span class="string">"POST"</span> });
  <span class="comment">// Получаем ответ</span>
  <span class="keyword">const</span> data = await res.json();
  <span class="comment">// Здесь можно добавить дополнительные действия </span>
  console.<span class="functions">log</span>(data);
  <span class="comment">// Перезагружаем страницу</span>
  location.reload();
}</pre><pre class="code"><span class="comment">/**
* Остановка терминала
* @param {string} name - Имя экземпляра
*/</span>
async function stopInstance(name) {
  <span class="comment">// Посылаем запрос по нужному маршруту</span>
  <span class="keyword">const</span> res = await fetch(`/stop/${name}`, { method: <span class="string">"POST"</span> });
  <span class="comment">// Получаем ответ</span>
  <span class="keyword">const</span> data = await res.json();
  <span class="comment">// Здесь можно добавить дополнительные действия</span>
  console.<span class="functions">log</span>(data);
  <span class="comment">// Перезагружаем страницу</span>
  location.reload();
}</pre><p>В дальнейшем информацию, получаемую в переменной data, можно использовать для показа каких-либо дополнительных сообщений или обновления кода страницы без её полной перезагрузки. Но пока у нас на главной странице выводится очень мало информации, можно её перезагружать полностью. </p><p>Для улучшения внешнего вида главной страницы, добавим немного стилей в файл <i>styles.css</i>, располагающийся в папке <i>/static</i>. Не будем пока подтягивать мощные фреймворки стилизации HTML вроде Bootstrap, мы всегда сможем ими воспользоваться после того, как проект приобретёт более внятные очертания. Пока же мы только нащупываем, как мог бы выглядеть интерфейс взаимодействия с пользователем, и поэтому не стоит сразу уделять большого внимания внешнему виду. </p><p>Но немного — можно! Например, мы добавили к каждому элементу списка дополнительный CSS-класс, совпадающий со статусом экземпляра. А в файле <i>styles.css</i> добавили правила, задающие разный цвет фона для элемента в зависимости от наличия класса <i>running</i>:</p><pre class="code">.instances li.running {
    background: <span class="comment">#d1ffd1;</span>
    border: <span class="number">1</span>px solid <span class="comment">#89fd85;</span>
}</pre><p>Теперь запущенные и незапущенные экземпляры будут хорошо отличимы друг от друга.</p><p><br></p><h3>Тестирование<br></h3><p>Запустим веб-сервер командой из рабочей папки проекта:</p><div class="code"><p>uvicornmain:app--reload--no-use-colors</p></div><p>и откроем в браузере страницу по адресу http://127.0.0.1:8000. </p><p>Пока ни один экземпляр терминала не запущен, мы будем видеть примерно следующее:</p><p style="text-align:center;"><img width="745" height="400" src="../images/9955369d-5348011681570.png" loading="lazy" alt="" style="vertical-align:middle;"></p><p>Нажмём на кнопку Start для первого и второго экземпляра. Спустя мгновение мы увидим, что они перешли в состояние выполняющихся, что подтверждается Диспетчером задач Windows, где мы видим два процесса с такими же идентификаторами, как и на главной странице нашего веб-интерфейса:</p><p style="text-align:center;"><img width="745" height="400" src="../images/5dd3f466-3582312767553.png" loading="lazy" alt="" style="vertical-align:middle;"></p><p>Также можно проверить, что система автоматической документации подхватила сделанные нами комментарии и уточнения про маршруты и их параметры:</p><p style="text-align:center;"><img width="745" height="600" src="../images/2589a28e-3481539276901.png" loading="lazy" alt="" style="vertical-align:middle;"></p><p>Как видно, для маршрута POST /start/{name} есть описание как самого маршрута, так и параметра {name} с примером возможного значения. </p><p><br></p><p></p><h3>Заключение<br></h3><p></p><p>На данном этапе проект MetaTrader 5 Manager превратился из идеи и простого прототипа в полноценное приложение, которое уже способно управлять несколькими экземплярами торговых терминалов MetaTrader 5 через удобный веб-интерфейс. Мы шаг за шагом отделили логику работы с процессами от серверной части, добавили маршруты для управления экземплярами, реализовали простую визуализацию состояния терминалов и сделали первые шаги к структурированию кода проекта.</p><p>Несмотря на то, что текущее решение ещё далеко от окончательного, именно в таком виде оно уже выполняет свою основную задачу — позволяет централизованно запускать и останавливать терминалы, а также получать информацию об их текущем состоянии. Таким образом, у нас появился прочный фундамент, на котором можно строить более сложные уровни функциональности.</p><p>В дальнейшем мы будем развивать проект в нескольких направлениях:</p><ul><li>автоматизация регистрации новых экземпляров терминалов и хранение информации о них в базе данных;</li><li>расширение взаимодействия с Python MetaTrader 5 API для получения статистики, ордеров и торговых историй;</li><li>улучшение интерфейса — добавление динамических обновлений без перезагрузки страницы и визуальных элементов управления;</li><li>возможное выделение интерфейсной части в отдельный фронтенд-сервис.</li></ul><p>Также необходимо продумать систему ограничения доступа. Для этого можно добавить базовую авторизацию или работу через защищённый канал HTTPS.</p><p>Кроме того, стоит предусмотреть ведение журнала операций: когда и кто запускал или останавливал терминалы. Эти данные могут быть полезны для аудита и анализа использования системы.</p><p>Но главное, что уже сейчас можно видеть: выбранная архитектура и стек технологий (Python + FastAPI + Jinja2 + psutil) позволяют гибко развивать проект, не теряя управляемости и прозрачности. Это отличный пример того, как из простого REST API можно шаг за шагом вырастить полноценную систему управления реальными торговыми процессами.</p><p></p><p>Спасибо за внимание и до следующей встречи!</p><p><br></p><p><b>Содержание архива</b></p><table class="standart" width="100%" cellspacing="0" cellpadding="3"><thead><tr><th>#<br></th><th>Имя</th><th>Версия</th><th>Описание</th><th>Последние изменения</th></tr><tr><td> </td><td><b>mt5-manager/</b></td><td> </td><td>Рабочая папка проекта веб-сервера терминалов</td><td> </td></tr><tr><td>1</td><td>├─ main.py</td><td>0.1.0<br></td><td>Веб-приложение для веб-сервера терминалов<br></td><td><a href="https://www.mql5.com/ru/articles/19852" target="_blank">Часть 2</a></td></tr><tr><td>2</td><td>├─ mt5_control.py</td><td>0.1.0</td><td>Логика управления запуском и остановкой терминалов  </td><td><a href="https://www.mql5.com/ru/articles/19852" target="_blank">Часть 2</a><br></td></tr><tr><td><br></td><td><b>├─ templates/</b></td><td> </td><td> </td><td> </td></tr><tr><td>3</td><td>│   └─ index.html<br></td><td>0.1.0</td><td>Шаблон основной страницы</td><td><a href="https://www.mql5.com/ru/articles/19852" target="_blank">Часть 2</a><br></td></tr><tr><td> </td><td>└─ <b>static/</b></td><td> </td><td> </td><td> </td></tr><tr><td>4</td><td>     ├─ styles.css<br></td><td>0.1.0</td><td>CSS-стили<br></td><td><a href="https://www.mql5.com/ru/articles/19852" target="_blank">Часть 2</a><br></td></tr><tr><td>5</td><td>     └─ script.js  </td><td>0.1.0</td><td>JavaScript-код<br></td><td><a href="https://www.mql5.com/ru/articles/19852" target="_blank">Часть 2</a><br></td></tr></thead></table><p>Также исходный код проекта доступен в репозитории <a href="https://forge.mql5.io/antekov/mt5-manager" rel="noreferrer noopener nofollow" title="https://forge.mql5.io/antekov/mt5-manager" target="_blank">mt5-manager</a>.</p>
</div>