mt5-manager/docs/articles/part-03/publish/article.html
604 lines
87 KiB
HTML
<!doctype html>
<meta charset="utf-8">
<h1>Разрабатываем менеджер терминалов (Часть 3): Получаем информацию о счёте и добавляем конфигурацию</h1>
<div class="inner formatted-content">
<h3>Введение</h3> <p>В <a href="https://www.mql5.com/ru/articles/19852" title="https://chat.qwen.ai/c/link-to-part-1" target="_blank" rel="nofollow">предыдущей части</a> мы заложили фундамент проекта, создав минимально жизнеспособный веб-сервер, способный управлять запуском и остановкой нескольких экземпляров терминалов MetaTrader 5 через простой веб-интерфейс. Были выбраны FastAPI как основа для серверной логики и Jinja2 для генерации HTML, а работа с процессами реализована через psutil и subprocess. Проект был структурирован: логика управления вынесена в отдельный модуль, добавлены шаблоны и статические файлы, настроены маршруты для запуска, остановки и отображения состояния терминалов. Работоспособность была проверена — через веб-интерфейс нам удалось успешно запускать и останавливать терминалы, расположенные в заданных папках.</p> <p>Теперь пришло время расширять функциональность и пользовательские возможности. В этой части мы добавим создание и выбор файла конфигурации при запуске приложения, а также реализуем отображение основных характеристик каждого терминала, таких как номер счёта, баланс, прибыль и другие данные, на главной странице. Для получения этих данных мы воспользуемся Python-библиотекой MetaTrader5, позволяющей общаться с запущенными терминалами из программ на Python. Какое-то внимание уделим архитектурным деталям и внешнему виду интерфейса, подключив мощные библиотеки JQuery и Bootstrap. Это будет уже заметный шаг в сторону полноценного менеджера терминалов.</p> <p><br></p> <h3>Намечаем путь<br></h3> <p>В прошлый раз мы остановились на том, что создали три фиксированные папки с терминалами и указали пути к ним прямо в исходном коде нашего приложения. Причём полные пути формировались из двух константных значений — пути к корневой папки терминалов в константе <i>MT5_FOLDER</i> и названию запускаемого файла терминала в константе <i>MT5_EXE</i>, и списка названий папок разных экземпляров терминалов, расположенных внутри одной корневой папки.</p> <p>Но было понятно, что задавать эти параметры таким жестким образом будет очень неудобно, когда проект будет использоваться по прямому назначению. Поэтому нам необходимо определиться с тем, какая информация будет требоваться при запуске веб-приложения и при этом будет относительно редко изменяться или дополняться. Определившись с её составом, нам потребу<D0B1>
<span class="keyword">async</span> <span class="keyword">def</span> info_instance(name: <span class="built_in">str</span> = Path(..., description=<span class="string">"Информация об экземпляре"</span>,
example=<span class="string">"MetaTrader5.1"</span>)):
    <span class="string">'''Получение информации об экземпляре терминала'''</span>
    result = <span style="background-color:rgb(255, 246, 200);">control</span>.<span style="background-color:rgb(216, 232, 194);">instance_info(name)</span>
    <span class="keyword">return</span> JSONResponse(result)</pre> <p>С детальной реализацией этого метода мы познакомимся ниже. А на уровне обработки маршрутов наш проект пока больше ни в чём не нуждается.</p> <p><br></p> <h3>Модуль MetaTrader5 для Python<br></h3> <p></p> <p>Итак, мы добрались до момента, когда нам уже нужно вплотную заняться получением информации от терминала. В этом нам поможет <a href="https://www.mql5.com/ru/docs/python_metatrader5" target="_blank">модуль</a> MetaTrader 5 для интеграции с Python. Как сказано в справке:</p> <div class="fquote"> Пакет MetaTrader для Python предназначен для удобного и быстрого получения биржевой информации через межпроцессное взаимодействие прямо из терминала MetaTrader 5. Полученные таким образом данные можно дальше использовать для статистических вычислений и машинного обучения. </div> <p>Помимо биржевой информации (цен, позиций, ордеров), этот модуль позволяет получать и всю информацию о торговом счёте, к которому подключен терминал. Также он может быть использован для подключения к разным терминалам, установленным на компьютере. Но обо всём по порядку.</p> <p>Для начала нам нужно этот модуль установить на тот компьютер, который будет играть роль сервера терминалов, так как при установке терминала этот модуль не ставится автоматически. На это есть вполне понятная причина — его применение возможно только в программах на Python, а далеко не на всяком компьютере, на который ставится терминал, присутствует интерпретатор языка Python. </p> <p>Интерпретатор Python у нас уже есть (так как именно на нём уже частично написан код нашего веб-приложения), поэтому для использования этого модуля нам нужно только установить его с помощью менеджера пакетов pip, выполнив такую команду в консоли:</p> <div class="code"> <p>pip install MetaTrader5</p> </div> <p>К сожалению, на момент написания статьи установить этот модуль для последней версии Python v. 3.14 не удалось, поэтому пришлось переключиться на использование более ранней версии Python v. 3.12. В этом нет ничего страшного, так как для потребностей нашего проекта вряд ли понадобятся какие-то специфические возможности, добавленные в последней версии. Скорее всего, мы с тем же успехом могли применять и ещё более ранние версии Python. Со временем MetaQuotes обновит этот модуль, чтобы он мог устанавливаться и с более поздними версиями Python.</p> <p><br></p> <h3>Схема работы с модулем<br></h3> <p>В какой бы программе м
<span class="keyword">import</span> MetaTrader5 <span class="keyword">as</span> mt5
<span class="comment"># 2. Подключение к терминалу MetaTrader 5</span>
<span class="keyword">if</span> mt5.initialize():
    <span class="comment"># Подключение успешно</span>
    <span class="comment"># Выполнение основной работы</span>
    <span class="comment"># ...</span>
    <span class="keyword">pass</span>
<span class="keyword">else</span>:
    <span class="comment"># Подключение не установлено</span>
    <span class="comment"># Обработка ошибок</span>
    <span class="built_in">print</span>(<span class="string">"initialize() failed"</span>)
<span class="comment"># 3. Отключение от терминала</span>
mt5.shutdown()</pre> <p>Если программа на Python становится довольно объёмной, то второй и третий шаг обычно помещаются не в глобальной области, а внутри отдельных функций или методов классов.</p> <p><br></p> <h3>Подключение к терминалу<br></h3> <p>Рассмотрим подробнее функцию подключения к терминалу <i>initialize()</i> применительно к контексту нашей задачи. </p> <p>Если она вызывается без параметров, то терминал для подключения выбирается автоматически. Это означает, что безопасно и предсказуемо можно применять такой формат вызова только в том случае, когда на компьютере был установлен единственный терминал, и он уже был ранее подключен к нужному торговому счёту с сохранением параметров подключения.</p> <p>Если на компьютере было установлено несколько терминалов, то нет гарантии, что такой вызов функции подключится именно к нужному терминалу. Поэтому в нашем случае, когда мы изначально планируем работать с несколькими терминалами на одном компьютере, необходимо строго указывать через дополнительные параметры, к какому терминалу мы хотим сейчас подключиться.</p> <p>Мы будем делать это, используя первый неименованный параметр функции <i>initialize()</i>, в котором можно передать полный путь к запускаемому файлу нужного экземпляра терминала. Так как эти пути у нас в программе хранятся в отдельном списке, то нет никаких проблем с тем, чтобы передать их в функцию подключения. Если нужный путь мы предварительно присвоим переменной <i><span style="background-color:rgb(216, 232, 194);">path</span></i>, то вызов функции подключения будет выглядеть так:</p> <pre class="code"><span class="comment"># 2. Подключение к терминалу MetaTrader 5</span>
<span class="keyword">if</span> mt5.initialize(<span style="background-color:rgb(216, 232, 194);">path</span>):
    <span class="comment"># Подключение успешно</span>
    <span class="comment"># Выполнение основной работы</span>
    <span class="comment"># ...</span>
    <span class="keyword">pass</span></pre> <p>Также отметим разницу в поведении этой функции в зависимости от того, был ли ранее запущен тот терминал, к которому следует подключиться, или ещё нет. В первом случае (терминал уже запущен) произойдет почти мгновенное подключение к работающему терминалу. Во втором случае (терминал ещё не запущен) вызов этой функции инициирует сначала запуск терминала и только потом будет пытаться к нему подключаться.</p> <p>На запуск терминала, как правило, требуется некоторое время (порядка нескольких секунд). Это время программа на Python будет ждать, так как вызов функции <i>initialize()</i> продолжает выполняться, ожидая запуска терминала. Но когда терминал запущен, или был запущен ранее, то ожидание на этом не заканчивается. На следующем этапе вызов функции <i>initialize()</i> будет пытаться дождаться установления подключения к торговому счёту. Только после успешного соединения терминала с сервером брокера и не менее успешного подключения к торговому счёту, функция <i>initialize()</i> будет считать свою миссию выполненной и вернёт <i>true</i>.</p> <p>Такое поведение в принципе можно использовать и для задачи запуска экземпляра терминала. Но такой способ обладает меньшим богатством возможностей, чем запуск терминала через выполнение консольной команды, реализованный в прошлых частях. Поэтому оставим уже имеющуюся схему запуска/остановки терминалов без изменений, а с помощью модуля интеграции будем обращаться только к уже запущенным экземплярам.</p> <p>Если же на каком-то этапе что-то пошло не так, функция <i>initialize()</i> возвращает <i>false</i>. Однако, если причина кроется в отсутствии связи с сервером брокера, то такой результат мы получим не мгновенно, а только спустя время ожидания, которое по умолчанию равно 60 секундам. В планируемой архитектуре это недопустимо большое время задержки ответа, поэтому мы воспользуемся именованным параметром <i><span style="background-color:rgb(216, 232, 194);">timeout</span></i>, через который мы можем задать допустимое время ожидания подключения:</p> <pre class="code"><span class="comment"># 2. Подключение к терминалу MetaTrader 5</span>
<span class="keyword">if</span> mt5.initialize(path, <span style="background-color:rgb(216, 232, 194);">timeout=<span class="number">200</span></span>):
    <span class="comment"># Подключение успешно</span>
    <span class="comment"># Выполнение основной работы</span>
    <span class="comment"># ...</span>
    <span class="keyword">pass</span></pre> <p>Такое небольшое время ожидания (200 мс = 0.2 с) с одной стороны достаточно велико для подключения к запущенному терминалу с уже установленным подключением к торговому счёту, а с другой стороны — достаточно мало, чтобы не ожидая слишком долго, понять, что что-то пошло не так, и к этому терминалу пока полноценно подключиться нельзя.</p> <p>Последний параметр, которым мы воспользуемся при подключении к терминалу, будет параметр <i><span style="background-color:rgb(216, 232, 194);">portable</span>.</i> С его помощью можно указать, что терминал для подключения должен запускаться или быть запущенным в режиме Portable. Это как раз то, что нужно, так как в дальнейшем нам придётся работать с рабочими папками терминалов (папки данных MQL5). Для этого нам нужно будет точно знать их расположение на компьютере.</p> <pre class="code"><span class="comment"># 2. Подключение к терминалу MetaTrader 5</span>
<span class="keyword">if</span> mt5.initialize(path, timeout=<span class="number">200</span>, <span style="background-color:rgb(216, 232, 194);">portable=<span class="literal">True</span></span>):
    <span class="comment"># Подключение успешно</span>
    <span class="comment"># Выполнение основной работы</span>
    <span class="comment"># ...</span>
    <span class="keyword">pass</span></pre> <p>Функция подключения позволяет ещё задать в дополнительных именованных параметрах данные для подключения к торговому счёту (логин, пароль и сервер). Если эти параметры не указаны, то используются те данные, которые были сохранены экземпляром терминала при последнем подключении. Мы пока не дошли до смены торговых счетов, поэтому такое поведение нас вполне устраивает.</p> <p><br></p> <h3>Отключение от терминала<br></h3> <p>По сравнению с функцией подключения, противоположная по смыслу функция отключения <i>shutdown()</i> гораздо проще. Она не имеет никаких параметров и выполняет только программное отключение установленного ранее канала связи с терминалом. При этом сам терминал продолжает оставаться запущенным, то есть воспользоваться функцией <i>shutdown()</i> для остановки терминала нельзя.</p> <p>В справке прямо не указано, является ли её вызов обязательным при неудачном подключении к терминалу, но во многих примерах эта функция вызывается в обоих случаях: и при успешном подключении, и при неуспешном. Поэтому поступим таким же образом, разместив вызов функции отключения после условного оператора, обрабатывающего оба случая.</p> <p><br></p> <h3>Основная работа<br></h3> <p>В случае успешного подключения к нужному экземпляру терминала, у нас появляется возможность вызвать любую доступную функцию из описанных в <a href="https://www.mql5.com/ru/docs/python_metatrader5" target="_blank">справке</a>. В рамках поставленной задачи мы воспользуемся только тремя из них.</p> <p>С помощью функции <a href="https://www.mql5.com/ru/docs/python_metatrader5/mt5terminalinfo_py" target="_blank"><i>terminal_info()</i></a> мы будем получать состояние и настройки подключенного клиентского терминала MetaTrader 5 в виде именованного кортежа, те есть набора значений, каждому из которых присвоено определённое имя. Далее такой кортеж можно легко преобразовать в словарь, то есть структуру данных в виде множества пар &lt;ключ&gt;=&lt;значение&gt;. В таком виде данные уже можно сохранить в формате JSON и отправлять в составе ответа веб-сервера на запросы клиентов.</p> <p>С помощью функции <a href="https://www.mql5.com/ru/docs/python_metatrader5/mt5accountinfo_py" target="_blank"><i>account_info()</i></a> будем получать информацию о текущем торговом счете, к которому подключен терминал. Формат возвращаемого значения у этой функции такой же, как у <i>terminal_info()</i>.</p> <p>В случае возникновения каких-либо о
    <span class="keyword">def</span> __init__(self, terminals: <span class="built_in">dict</span>, mt5_folder: <span class="built_in">str</span>, mt5_exe: <span class="built_in">str</span> = <span class="string">'terminal64.exe'</span>):
        <span class="string">'''
        Инициализирует экземпляр класса MT5_Control.
        Args:
            terminals (dict): Словарь с информацией о терминалах.
            mt5_folder (str): Путь к папке с экземплярами терминалов.
            mt5_exe (str): Имя исполняемого файла терминала (по умолчанию 'terminal64.exe').
        '''</span>
        <span class="comment"># Путь к папке с экземплярами терминалов</span>
        self.mt5_folder = mt5_folder
        <span class="comment"># Имя исполняемого файла терминала</span>
        self.mt5_exe = mt5_exe
        <span class="comment"># Список имён экземпляров</span>
        self.instances_folders = <span class="built_in">list</span>(terminals.keys())
        <span class="comment"># Словарь соответствия имя -&gt; путь</span>
        self.instanсes_paths = {folder: self.instance_path(
            folder) <span class="keyword">for</span> folder <span class="keyword">in</span> self.instances_folders}
        <span class="comment"># Словарь соответствия путь -&gt; имя</span>
        self.paths_instanсes = {self.instance_path(
            folder): folder <span class="keyword">for</span> folder <span class="keyword">in</span> self.instances_folders}
        self.instances = terminals.copy()</pre> <p>Код этого класса сохраним в уже существующем файле <i>mt5_control.py</i>. В нём ещё будут присутствовать несколько методов, но их мы рассмотрим позднее.</p> <p><br></p> <h3>Создаём конфигурацию<br></h3> <p>Итак, нам понадобилось хранить некоторую информацию как конфигурацию, то есть набор параметров, которые нужны при каждом запуске приложения. Что может предложить фреймворк FastAPI для этого? К сожалению, FastAPI не предоставляет встроенного механизма для хранения и загрузки конфигурации, но он отлично сочетается с популярными решениями для конфигурации в Python. Поскольку FastAPI — это веб-фреймворк, а не приложение с "жёсткой" архитектурой конфигурации, поэтому мы можем использовать любые удобные инструменты для этих целей.</p> <p>От FastAPI нам понадобится единственная вещь — возможность назначить определённую функцию в качестве обработчика событий жизненного цикла веб-приложения (lifespan). В этой функции должен присутствовать оператор <i>yield</i>, который служит разделителем кода, выполняющегося до старта и после остановки веб-приложения. Поэтому чтение конфигурации и инициализацию объекта класса <i>MT5_Control</i> мы выполним именно там.</p> <p>Сама конфигурация в рамках данного проекта будет существовать в нескольких ипостасях. Во-первых, она будет находиться в каком-то JSON-файле. Пока веб-приложение не запущено, конфигурация существует только в таком виде. Во-вторых, в момент запуска веб-приложения будет создаваться специальный объект <i>config</i> класса <i>Config</i>, единственной задачей которого будет чтение конфигурации из JSON-файла и создание структуры данных для хранения прочитанной информации. И в-третьих, после создания объекта веб-приложения <i>app</i> класса <i>FastAPI,</i> данные конфигурации будут переноситься в его поле <i>state</i>. Оттуда их можно будет получить при необходимости в любом из обработчиков маршрутов. После этого объект <i>config</i> больше не нужен.</p> <p>Рассмотрим последовательно все упомянутые вещи.</p> <p><br></p> <h3>JSON-файл конфигурации<br></h3> <p>Структуру JSON-файла мы выбираем самостоятельно, исходя из имеющийся информации, которую хотели бы там хранить. После нескольких итераций мы пришли к следующему варианту конфигурации, которая была сохранена в файле с именем <i>config.json</i>:</p> <pre class="code">{
  <span class="string">"<span style="background-color:rgb(255, 246, 200);">terminals</span>"</span>: {
    <span class="string">"<span style="background-color:rgb(216, 232, 194);">MetaTrader5.1</span>"</span>: {
      <span class="string">"<span style="background-color:rgb(164, 192, 228);">name</span>"</span>: <span class="string">"MetaTrader5.1"</span>,
      <span class="string">"<span style="background-color:rgb(164, 192, 228);">login</span>"</span>: <span class="number">12345671</span>,
      <span class="string">"<span style="background-color:rgb(164, 192, 228);">server</span>"</span>: <span class="string">"MetaQuotes-Demo"</span>
    },
    <span class="string">"<span style="background-color:rgb(216, 232, 194);">MetaTrader5.2</span>"</span>: {
      <span class="string">"name"</span>: <span class="string">"MetaTrader5.2"</span>,
      <span class="string">"login"</span>: <span class="number">12345672</span>,
      <span class="string">"server"</span>: <span class="string">"MetaQuotes-Demo"</span>
    },
    <span class="string">"<span style="background-color:rgb(216, 232, 194);">MetaTrader5.3</span>"</span>: {
      <span class="string">"name"</span>: <span class="string">"MetaTrader5.3"</span>,
      <span class="string">"login"</span>: <span class="number">12345673</span>,
      <span class="string">"server"</span>: <span class="string">"MetaQuotes-Demo"</span>
    },
    <span class="string">"<span style="background-color:rgb(216, 232, 194);">MetaTrader5.4</span>"</span>: {
      <span class="string">"name"</span>: <span class="string">"MetaTrader5.4"</span>,
      <span class="string">"login"</span>: <span class="number">12345671</span>,
      <span class="string">"server"</span>: <span class="string">"MetaQuotes-Demo"</span>
    }
  },
  <span class="string">"<span style="background-color:rgb(255, 246, 200);">mt5_folder</span>"</span>: <span class="string">"C:/MT5"</span>,
  <span class="string">"<span style="background-color:rgb(255, 246, 200);">mt5_exe</span>"</span>: <span class="string">"terminal64.exe"</span>
}</pre> <p>Как видно, в этом файле хранится словарь (называемый объектом в нотации JSON). В нём есть три элемента с ключами <i><span style="background-color:rgb(255, 246, 200);">terminals</span></i>, <i><span style="background-color:rgb(255, 246, 200);">mt5_folder</span></i> и <i><span style="background-color:rgb(255, 246, 200);">mt5_exe</span></i>. Элемент <i>terminals</i> в свою очередь тоже является словарём, в котором представлена вся информация об экземплярах терминалов. В нём сейчас четыре элемента с <span style="background-color:rgb(216, 232, 194);">ключами</span>, которые соответствуют <span style="background-color:rgb(216, 232, 194);">названиям папок</span> экземпляров терминалов.</p> <p>Каждый такой элемент хранит информацию об одном экземпляре терминала, расположенного в соответствующей папке и включает три элемента с ключами <i><span style="background-color:rgb(164, 192, 228);">name</span></i>, <i><span style="background-color:rgb(164, 192, 228);">login</span></i> и <i><span style="background-color:rgb(164, 192, 228);">server</span></i>. Назначение поля <i>name </i>— это имя, которое будет отображаться для данного экземпляра терминала на веб-странице. В простейшем случае оно может совпадать с названием папки. Назначение двух оставшихся полей понятно из их названий — в них хранится номер торгового счёта и название сервера брокера. Но эти параметры пока ещё не будут использоваться в коде, их обработка будет добавлена позднее. Также в будущем мы точно расширим список параметров, которые будут храниться для каждого экземпляра терминала. Но сейчас не будем забегать вперёд.</p> <p>В элементы <i>mt5_folder</i> и <i>mt5_exe</i> мы перенесли значения, которые раньше в коде были объявлены как константы <i>MT5_FOLDER</i> и <i>MT5_EXE</i>. Они хранят название корневой папки для всех экземпляров терминалов и название запускаемого файла терминала MetaTrader 5.</p> <p>Файл <i>config.json</i> добавлен в состав репозитория проекта, поэтому он может использоваться только в качестве примера конфигурационного файла. Для создания своей персональной конфигурации можно сделать копию файла <i>config.json</i>, назвав её, например <i>config.server1.json</i> или как-то иначе. Далее мы добавим возможность указать при запуске приложения имя конфигурационного файла, из которого нужно прочесть данные.</p> <p>В <i>.gitignore</i> мы добавили такое исключение:</p> <pre class="code">config.*.json</pre> <p>Поэтому имена файлов, соответствующие такому формату, будут игнорироваться системой контроля версий. Такой файл будет спокойно находиться в папке репозитория, не мешая дальнейш<D0B9>
<span class="keyword">class</span> Config(BaseSettings):
    folders: <span class="built_in">list</span>[<span class="built_in">str</span>] = [<span class="string">"MetaTrader5.1"</span>, <span class="string">"MetaTrader5.2"</span>]
    mt5_folder: <span class="built_in">str</span> = <span class="string">"C:/MT5/"</span>
    mt5_exe: <span class="built_in">str</span> = <span class="string">"terminal64.exe"</span></pre> <p>В нашем случае, класс <i>Config</i> получился немного сложнее, так как потребовалось обеспечить возможность чтения данных из определённого JSON-файла, а не простое указание их в исходном коде:</p> <pre class="code"><span class="keyword">from</span> pydantic_settings <span class="keyword">import</span> BaseSettings, JsonConfigSettingsSource, SettingsConfigDict
<span class="keyword">import</span> os
<span class="keyword">class</span> Config(BaseSettings):
    terminals: <span class="built_in">dict</span>
    mt5_folder: <span class="built_in">str</span>
    mt5_exe: <span class="built_in">str</span>
    model_config = SettingsConfigDict()
    @<span class="built_in">classmethod</span>
    <span class="keyword">def</span> settings_customise_sources(
        cls,
        settings_cls,
        init_settings,
        env_settings,
        dotenv_settings,
        file_secret_settings,
    ):
        <span class="comment"># Получаем путь к файлу из переменной окружения</span>
        config_file = os.getenv(<span class="string">"MT5_MANAGER_CONFIG_FILE"</span>, <span class="string">"config.json"</span>)
        <span class="keyword">return</span> (
            init_settings,
            JsonConfigSettingsSource(settings_cls, json_file=config_file),
            env_settings,
            dotenv_settings,
            file_secret_settings,
        )</pre> <p>Он был добавлен в проект как файл <i>config.py.</i></p> <p><br></p> <h3>Передача конфигурации объекту веб-приложения<br></h3> <p>Для последнего шага загрузки конфигурации мы импортируем в основном файле веб-приложения main.py наши новые классы Config и MT5_Manager:</p> <pre class="code"><span class="keyword">from</span> config <span class="keyword">import</span> Config
<span class="keyword">from</span> mt5_control <span class="keyword">import</span> MT5_Control</pre> <p>Далее создадим функцию <i>lifespan()</i>, которая будет принимать в качестве параметра объект класса FastAPI. Через этот параметр ей будет передаваться наш объект веб-приложения <i>app</i>. Добавив к этой функции декоратор <i>@asynccontextmanager</i>, мы превращаем её в упомянутую выше функцию-обработчик событий жизненного цикла веб-приложения. Внутри неё мы будем создавать объект <i><span style="background-color:rgb(249, 204, 202);">config</span></i>, читающий конфигурацию из JSON-файла, <span style="background-color:rgb(255, 246, 200);">переносить</span> прочитанное в нужные места и создавать глобальный объект <i><span style="background-color:rgb(216, 232, 194);">control</span></i> для управления терминалами:</p> <pre class="code">@asynccontextmanager
<span class="keyword">async</span> <span class="keyword">def</span> lifespan(app: FastAPI):
    <span class="comment"># Создаем объект, читающий конфигурацию из JSON-файла</span>
    <span style="background-color:rgb(249, 204, 202);">config</span> = Config(_env_file=<span class="literal">None</span>)
    <span class="comment"># Переносим прочитанное в объект app </span>
    <span style="background-color:rgb(255, 246, 200);">app.state.terminals = config.terminals</span>
    app.state.instances = {}
    <span class="comment"># Обеспечиваем наличие имени экземпляра терминала.</span>
    <span class="comment"># При отсутсвии его в конфигурации подставляется имя папки</span>
    <span class="keyword">for</span> folder <span class="keyword">in</span> app.state.terminals:
        app.state.terminals[folder][<span class="string">'name'</span>] = app.state.terminals[folder].get(
            <span class="string">'name'</span>, folder)
    <span class="comment"># Создаём глобальный объект управления терминалами</span>
    <span class="keyword">global</span> control
    <span style="background-color:rgb(216, 232, 194);">control</span> = MT5_Control(app.state.terminals,
                          <span style="background-color:rgb(255, 246, 200);">config.mt5_folder, config.mt5_exe</span>)
    <span class="keyword">yield</span></pre> <p>Остается внести небольшое дополнение в процесс создания объекта веб-приложения app. Нам нужно <span style="background-color:rgb(216, 232, 194);">передать</span> в конструктор через параметр <i>lifespan</i> имя функции, которая будет играть роль обработчика событий жизненного цикла. Она у нас тоже названа <i>lifespan</i>, поэтому строка создания теперь будет выглядеть так:</p> <pre class="code"><span class="comment"># Создаём объект приложения</span>
app = FastAPI(<span style="background-color:rgb(216, 232, 194);">lifespan=lifespan</span>)</pre> <p>Сохраним сделанные изменения в файле <i>main.py</i>.</p> <p><br></p> <h3>Добавляем внешний запуск<br></h3> <p>Следующим шагом стало добавление другого способа запуска нашего веб-приложения. Раньше мы использовали такую команду:</p> <pre class="code">uvicorn main:app --reload --no-use-colors</pre> <p>То есть мы запускали отдельное приложение uvicorn, которое обрабатывало наш исходный код на Python. При запуске мы могли передать ему только тот набор параметров, который предусмотрен этим приложением. Чтобы иметь возможность передать свои параметры, мы создадим новый скрипт на Python, который будет принимать нужные параметры и изнутри запускать веб-сервер приложения uvicorn:</p> <pre class="code"><span class="keyword">import</span> argparse
<span class="keyword">import</span> uvicorn
<span class="keyword">import</span> os
<span class="keyword">def</span> main():
    parser = argparse.ArgumentParser()
    parser.add_argument(<span class="string">"--config-file"</span>, default=<span class="string">"config.json"</span>,
                        <span class="built_in">help</span>=<span class="string">"Путь к файлу конфигурации"</span>)
    parser.add_argument(<span class="string">"--host"</span>, default=<span class="string">"0.0.0.0"</span>, <span class="built_in">help</span>=<span class="string">"IP-адрес сервера"</span>)
    parser.add_argument(<span class="string">"--port"</span>, default=<span class="number">8000</span>,
                        <span class="built_in">help</span>=<span class="string">"Порт сервера"</span>, <span class="built_in">type</span>=<span class="built_in">int</span>)
    args = parser.parse_args()
    <span class="comment"># Устанавливаем переменную окружения</span>
    os.environ[<span class="string">"MT5_MANAGER_CONFIG_FILE"</span>] = args.config_file
    <span class="comment"># Запускаем uvicorn</span>
    uvicorn.run(<span class="string">"main:app"</span>, reload=<span class="literal">True</span>, host=args.host, port=args.port)
<span class="keyword">if</span> __name__ == <span class="string">"__main__"</span>:
    main()</pre> <p>Таким образом мы добавили возможность предать имя JSON-файла с конфигурацией, которое будет запомнено в переменной окружения <i>MT5_MANAGER_CONFIG_FILE</i>. Оттуда это имя сможет достать при создании объект <i>config</i>:</p> <pre class="code"><span class="comment"># Получаем путь к файлу из переменной окружения</span>
config_file = os.getenv(<span class="string">"MT5_MANAGER_CONFIG_FILE"</span>, <span class="string">"config.json"</span>)</pre> <p>В результате наше веб-приложение сможет стартовать, используя нужную конфигурацию, с помощью такой команды:</p> <pre class="code">python run.py --config-file=config.json --host=<span class="number">0.0</span><span class="number">.0</span><span class="number">.0</span> --port=<span class="number">8000</span></pre> <p>Вместо указанных значений можно подставить свои, если мы хотим запускать веб-сервер не на всех доступных на компьютере IP-адресах или с использованием другого номера порта.</p> <p><br></p> <h3>Метод получения информации</h3> <p>После долгого отступления вернёмся снова к классу <i>MT5_Control</i> и рассмотрим его метод, отвечающий за получение информации об экземпляре терминала. Вначале посмотрим, каким должен быть результат работы этого метода, а потом и сам метод.</p> <p>Каждый запрос по маршруту <i>/instances/{name}</i> будет возвращать нам данные в виде JSON-представления словаря (множества пар ключ-значение). Пары в этом словаре можно разбить на несколько блоков, в зависимости от источника получения данных для каждого.</p> <p>Первым будут идти данные о конкретном экземпляре терминала, получаемые по сути из конфигурации нашего приложения: имя экземпляра, логин торгового счёта и имя сервера брокера.</p> <pre class="code">{
  <span class="string">"name"</span>: <span class="string">"MetaTrader5.1"</span>,
  <span class="string">"login"</span>: <span class="number">1234567</span>,
  <span class="string">"server"</span>: <span class="string">"MetaQuotes-Demo",
</span> ...
}</pre> <p>Пока что указываемый логин никак не используется, так как мы договорились подключаться только к терминалам, в которых уже была выполнена ранее процедура подключения к нужному торговому счёту, и информация об этих подключениях сохранена для последующих запусков терминалов. Поэтому тут значение логина не совпадает с реально используемым, но это несоответствие мы в скором времени устраним.</p> <p>Следующий блок содержит информацию о значении PID и статусе процесса терминала на компьютере. Если PID не равен 0, то это означает, что данный терминал запущен, а значение статуса это просто подтверждает.</p> <pre class="code">{
...
  <span class="string">"pid"</span>: <span class="number">11560</span>,
  <span class="string">"status"</span>: <span class="string">"running"</span>,
  ...
}</pre> <p>Далее будет идти информация об ошибке в виде вложенного словаря с числовым кодом и описанием ошибки. В большинстве случаев это будет сообщение об успешном получении данных от терминала:</p> <pre class="code">{
  ...
  <span class="string">"last_error"</span>: {
    <span class="string">"code"</span>: <span class="number">1</span>,
    <span class="string">"description"</span>: <span class="string">"Success"</span>
  },
...
}</pre> <p>Следующие блоки — это вся информация, которую модуль интеграции MetaTrader 5 предоставляет о запущенном терминале и подключенном торговом счёте:</p> <pre class="code">{
  ...
  <span class="string">"terminal"</span>: {
    <span class="string">"community_account"</span>: <span class="keyword">true</span>,
    <span class="string">"community_connection"</span>: <span class="keyword">true</span>,
    <span class="string">"connected"</span>: <span class="keyword">true</span>,
    <span class="comment">// ...</span>
  },
  <span class="string">"account"</span>: {
    <span class="string">"login"</span>: <span class="number">5041102414</span>,
    <span class="string">"trade_mode"</span>: <span class="number">0</span>,
    <span class="string">"leverage"</span>: <span class="number">100</span>,
    <span class="comment">// ...</span>
  },
...
}</pre> <p></p> <p>И последним указывается время получения всей предшествующей информации:</p> <pre class="code">{
...
  <span class="string">"last_update"</span>: <span class="string">"2025-11-18 09:46:14.076863"</span>
}</pre> <p>Полностью ответ сервера выглядит примерно так:</p> <pre class="code">{
  <span class="string">"name"</span>: <span class="string">"MetaTrader5.1"</span>,
  <span class="string">"login"</span>: <span class="number">1234567</span>,
  <span class="string">"server"</span>: <span class="string">"MetaQuotes-Demo"</span>,
  <span class="string">"pid"</span>: <span class="number">11560</span>,
  <span class="string">"status"</span>: <span class="string">"running"</span>,
  <span class="string">"last_error"</span>: {
    <span class="string">"code"</span>: <span class="number">1</span>,
    <span class="string">"description"</span>: <span class="string">"Success"</span>
  },
  <span class="string">"terminal"</span>: {
    <span class="string">"community_account"</span>: <span class="keyword">true</span>,
    <span class="string">"community_connection"</span>: <span class="keyword">true</span>,
    <span class="string">"connected"</span>: <span class="keyword">true</span>,
    <span class="comment">// ...</span>
  },
  <span class="string">"account"</span>: {
    <span class="string">"login"</span>: <span class="number">5041102414</span>,
    <span class="string">"trade_mode"</span>: <span class="number">0</span>,
    <span class="string">"leverage"</span>: <span class="number">100</span>,
    <span class="comment">// ...</span>
  },
  <span class="string">"last_update"</span>: <span class="string">"2025-11-18 09:46:14.076863"</span>
}</pre> <p>Для формирования такого ответа, мы будем сначала проверять, что терминал с запрошенным именем существует и запущен. Если всё в порядке, то подключаемся к нему, используя модуль Python-интеграции по схеме, изложенной выше. При успешном подключении мы запрашиваем у терминала необходимые данные и помещаем их в словарь. Туда же будет добавляться и информация об ошибках, текущем времени и статусе терминала. Наполненный словарь возвращается как результат этого метода.</p> <pre class="code"><span class="keyword">def</span> instance_info(self, folder: <span class="built_in">str</span>) -&gt; <span class="built_in">dict</span>:
        <span class="string">'''
        Получает информацию о терминале по имени экземпляра терминала
        Args:
            folder (str): Имя экземпляра терминала.
        Returns:
            info (dict): Информация о терминале или обнаруженных ошибках
        '''</span>
        <span class="comment"># Словарь для результатов</span>
        info = {}
        <span class="comment"># Если терминал с таким именем папки присутствует в конфигурации, то</span>
        <span class="keyword">if</span> folder <span class="keyword">in</span> self.instances:
            <span class="comment"># Сохраняем начальную имеющуюся информацию из конфигурации</span>
            info = self.instances[folder]
            <span class="comment"># Формируем полный путь к терминалу</span>
            path = self.instanсes_paths[folder]
            <span class="comment"># Если указанный терминал запущен, то</span>
            <span class="keyword">if</span> <span class="string">'pid'</span> <span class="keyword">in</span> self.instances[folder] <span class="keyword">and</span> self.instances[folder][<span class="string">'pid'</span>]:
                <span class="comment"># print(f'Start mt5.initialize[{folder}]')</span>
                <span class="comment"># Если подключение выполнено успешно, то</span>
                <span class="keyword">if</span> mt5.initialize(path, timeout=<span class="number">200</span>, portable=<span class="literal">True</span>):
                    <span class="comment"># Добавим в результат информацию о состоянии терминала</span>
                    terminal_info = mt5.terminal_info()
                    <span class="keyword">if</span> terminal_info != <span class="literal">None</span>:
                        info[<span class="string">'terminal'</span>] = terminal_info._asdict()
                    <span class="comment"># Добавим в результат информацию о торговом счёте</span>
                    account_info = mt5.account_info()
                    <span class="keyword">if</span> account_info != <span class="literal">None</span>:
                        info[<span class="string">'account'</span>] = account_info._asdict()
                    <span class="comment"># Добавим текущее время</span>
                    info[<span class="string">'last_update'</span>] = <span class="built_in">str</span>(datetime.now())
                    <span class="comment"># Запомним код ошибки, сообщение и актуальный статус</span>
                    code, description = mt5.last_error()
                    status = <span class="string">'running'</span>
                <span class="keyword">else</span>:
                    <span class="comment"># Если подключиться быстро не удалось, то</span>
                    <span class="comment"># если терминал числится в процессе запуска</span>
                    <span class="keyword">if</span> info[<span class="string">'status'</span>] == <span class="string">'starting'</span>:
                        <span class="comment"># Запомним код ошибки, сообщение и актуальный статус</span>
                        <span class="comment"># для терминала в процессе запуска</span>
                        code, description = <span class="number">0</span>, <span class="string">'Starting'</span>
                        status = <span class="string">'starting'</span>
                    <span class="keyword">else</span>:
                        <span class="comment"># Иначе запомним код ошибки, сообщение и актуальный статус</span>
                        <span class="comment"># для запущенного терминала</span>
                        code, description = mt5.last_error()
                        status = <span class="string">'running'</span>
                mt5.shutdown()
                <span class="comment"># print(f'End mt5.initialize[{folder}]')</span>
            <span class="keyword">else</span>:
                <span class="comment"># Иначе запомним код ошибки, сообщение и актуальный статус</span>
                <span class="comment"># для остановленного терминала</span>
                code, description = <span class="number">0</span>, <span class="string">'Stopped'</span>
                status = <span class="string">'stopped'</span>
        <span class="keyword">else</span>:
            <span class="comment"># Иначе установим код ошибки, сообщение и актуальный статус</span>
            <span class="comment"># для отсутствующего терминала</span>
            code, description = -<span class="number">1</span>, <span class="string">'Manager: Terminal not found'</span>
            status = <span class="string">'not found'</span>
        <span class="comment"># Добавим в результат информацию об ошибках и актуальном статусе</span>
        info[<span class="string">'last_error'</span>] = {<span class="string">'code'</span>: code, <span class="string">'description'</span>: description}
        info[<span class="string">'status'</span>] = status
        <span class="keyword">return</span> info</pre> <p>Остальные изменения, сделанные в этом классе, не настолько принципиальны, поэтому не будем их здесь детально описывать. Как всегда, полный код проекта доступен в приложенном файле и в публичном репозитории, где его можно посмотреть.</p> <p><br></p> <h3>Тестирование<br></h3> <p>Посмотрим, как выглядит теперь отображение информации о запущенных экземплярах терминалов. Мы немного поработали над внешним видом главной страницы и её информационных блоков. Добавлено главное меню, через которое в дальнейшем можно будет переходить на страницу редактирования конфигурации прямо в браузере.</p> <p style="text-align:center;"> <img width="825" height="607" src="../images/655cc7eb-6007318317274.png" loading="lazy" alt="" style="vertical-align:middle;"></p> <p style="text-align:center;"><span class="small">Рис. 1. Главная страница веб-приложения, отображающая информацию о четырёх экземплярах терминала MetaTarder 5</span></p> <p>Рассмотрим внимательнее отдельный блок для одного экземпляра терминала. Как видно, для него отображается иконка сервера брокера, последние цифры номера торгового счёта и его имя. Библиотека иконок пока не наполнена, поэтому если для какого-то сервера брокера её нет, то будет показываться иконка по умолчанию (как для MeatQuotes-Demo).</p> <p>Далее показывается дата/время последнего получения информации и статус. В нормальном режиме показывается только время последнего соединения, так как оно должно быть относительно недавним: при отсутствии проблем со связью и отправке запросов каждые 10 секунд, это время отличается от текущего не более чем на 10 секунд. Поэтому нет необходимости показывать рядом ещё и сегодняшнюю дату. Но если какой-то терминал был остановлен, то это время перестаёт меняться и показывает, по сути, момент остановки. Если этот момент приходится уже не на текущие сутки, то тогда перед временем начинает показываться и дата.</p> <p>На рис. 1 мы специально остановили последний терминал MetaTrader5.4. В тот момент, когда был сделан скриншот (2025-11-06 18:23:10), с момента остановки прошло чуть больше минуты, поэтому новый день ещё не наступил, и время показывается без даты. На следующий день (2025-11-07) эта страница будет выглядеть следующим образом:</p> <p style="text-align:center;"> <img width="825" height="575" src="../images/dfed2c4c-4922359171575.png" loading="lazy" alt="" style="vertical-align:middle;"></p> <p style="text-align:center;"><span class="small">Рис. 2. Главная страница веб-прилож
</div>