preview
Разрабатываем менеджер терминалов (Часть 3): Получаем информацию о счёте и добавляем конфигурацию

Разрабатываем менеджер терминалов (Часть 3): Получаем информацию о счёте и добавляем конфигурацию

MetaTrader 5Трейдинг |
113 3
Yuriy Bykov
Yuriy Bykov

Введение

В предыдущей части мы заложили фундамент проекта, создав минимально жизнеспособный веб-сервер, способный управлять запуском и остановкой нескольких экземпляров терминалов MetaTrader 5 через простой веб-интерфейс. Были выбраны FastAPI как основа для серверной логики и Jinja2 для генерации HTML, а работа с процессами реализована через psutil и subprocess. Проект был структурирован: логика управления вынесена в отдельный модуль, добавлены шаблоны и статические файлы, настроены маршруты для запуска, остановки и отображения состояния терминалов. Работоспособность была проверена — через веб-интерфейс нам удалось успешно запускать и останавливать терминалы, расположенные в заданных папках.

Теперь пришло время расширять функциональность и пользовательские возможности. В этой части мы добавим создание и выбор файла конфигурации при запуске приложения, а также реализуем отображение основных характеристик каждого терминала, таких как номер счёта, баланс, прибыль и другие данные, на главной странице. Для получения этих данных мы воспользуемся Python-библиотекой MetaTrader5, позволяющей общаться с запущенными терминалами из программ на Python. Какое-то внимание уделим архитектурным деталям и внешнему виду интерфейса, подключив мощные библиотеки JQuery и Bootstrap. Это будет уже заметный шаг в сторону полноценного менеджера терминалов.


Намечаем путь

В прошлый раз мы остановились на том, что создали три фиксированные папки с терминалами и указали пути к ним прямо в исходном коде нашего приложения. Причём полные пути формировались из двух константных значений — пути к корневой папки терминалов в константе MT5_FOLDER и названию запускаемого файла терминала в константе MT5_EXE, и списка названий папок разных экземпляров терминалов, расположенных внутри одной корневой папки.

Но было понятно, что задавать эти параметры таким жестким образом будет очень неудобно, когда проект будет использоваться по прямому назначению. Поэтому нам необходимо определиться с тем, какая информация будет требоваться при запуске веб-приложения и при этом будет относительно редко изменяться или дополняться. Определившись с её составом, нам потребуется структурировать её и обеспечить передачу веб-приложению. С другой стороны, потребуется эту информацию корректно принять и использовать в веб-приложении.

Когда терминалы будут работать, мы будем собирать с них информацию. Поскольку наше веб-приложение пишется на Python, то мы сможем использовать модуль MetaTrader для интеграции с Python (Python-библиотекой MetaTrader5). С её помощью мы будем программно подключаться к каждому терминалу, получать от него все данные о торговом счёте и использовать их для отправки ответа на соответствующие запросы клиентов веб-сервера. Это потребует расширения команд API нашего веб-приложения.

Также же надо подумать о реализации автоматического обновления информации обо всех терминалах на главной странице веб-приложения, чтобы пользователю не приходилось каждый раз вручную нажимать F5 для обновления страницы в браузере. Здесь можно реализовывать разные схемы взаимодействия между браузером и веб-приложением, но мы пока остановимся на самой простой.

И конечно же, чтобы разрабатываемым веб-приложением было удобно пользоваться, понадобится подумать над разумной компоновкой информационных блоков главной страницы и их внутреннего наполнения.


Подопытные счета

Ранее мы уже создали три экземпляра терминалов и подключили их к новым демо-счетам. Чтобы нам было проще их различать между собой, были выбраны неодинаковые значения начального депозита: $100 000, $200 000 и $300 000. Для отработки процессов запуска/остановки терминалов нам было не важно, ведётся ли на них какая-либо торговля. Поэтому в процессе работы над предыдущей частью, баланс этих демо-счетов оставался равным начальному. Но сейчас время пойти дальше.

Теперь на эти терминалы мы поставим работать какие-нибудь советники, чтобы можно было видеть изменяющиеся с течением времени значения баланса, прибыли и средств. Так будет, конечно, интереснее, чем наблюдать за торговыми счётами без каких-либо торговых операций. Советники поставим на разные группы символов, чтобы их результаты не выглядели одинаково.

Четвёртый отдельный терминал мы подключим к тому же демо-счёту, что и первый терминал. На него пока не будем устанавливать никакой советник. Этот терминал пригодится нам для дальнейшей отладки процесса остановки и запуска терминала, чтобы лишний раз не дёргать терминалы, на которых будут работать советники.

Старт работы советников был дан 2025-10-17. По техническим причинам одну неделю (с 10-26 по 11-04) терминалы были выключены, бросив на произвол судьбы все открытые позиции. Затем их работа была возобновлена. Сохраним здесь это замечание, так как оно, возможно, пригодится нам в последующем, когда будет добавлена возможность работы с историей сделок.

Подготовив таким образом тестовый стенд, приступим к реализации намеченного.


Новый маршрут

Начнем работу с создания нового маршрута, предназначенного для обработки запросов на получение информации об одном экземпляре терминала. Добавим его в нашу таблицу маршрутов.

# Метод URL Описание, параметры Формат
ответа 
1 GET / Отображает главную страницу с веб-интерфейсом для управления экземплярами MetaTrader 5
Пока что пусть отображается только заголовок с названием проекта.
HTML
POST /start/{name}  Запускает экземпляр терминала MetaTrader 5 с именем {name} JSON
3 POST
/stop/{name} Останавливает запущенный процесс MetaTrader 5 с именем {name} JSON
POST /instances/{name}  Получение информации об экземпляре терминала с именем {name} JSON

В соответствии с принятым соглашением, всю логику обработки запроса для каждого маршрута мы будем выносить в отдельные функции. Более того, эти функции теперь будут являться методами объекта нового класса, который мы добавим для организации всех функций обработчиков в отдельном пространстве имён.

Если на глобальном уровне будет существовать объект с именем control, то добавив к его классу метод instance_info(), мы можем написать обработчик нового маршрута примерно так: 

@app.post('/instances/{name}')
async def info_instance(name: str = Path(..., description="Информация об экземпляре", 
                                         example="MetaTrader5.1")):
    '''Получение информации об экземпляре терминала'''
    result = control.instance_info(name)
    return JSONResponse(result)

С детальной реализацией этого метода мы познакомимся ниже. А на уровне обработки маршрутов наш проект пока больше ни в чём не нуждается.


Модуль MetaTrader5 для Python

Итак, мы добрались до момента, когда нам уже нужно вплотную заняться получением информации от терминала. В этом нам поможет модуль MetaTrader 5 для интеграции с Python. Как сказано в справке:

Пакет MetaTrader для Python предназначен для удобного и быстрого получения биржевой информации через межпроцессное взаимодействие прямо из терминала MetaTrader 5. Полученные таким образом данные можно дальше использовать для статистических вычислений и машинного обучения.

Помимо биржевой информации (цен, позиций, ордеров), этот модуль позволяет получать и всю информацию о торговом счёте, к которому подключен терминал. Также он может быть использован для подключения к разным терминалам, установленным на компьютере. Но обо всём по порядку.

Для начала нам нужно этот модуль установить на тот компьютер, который будет играть роль сервера терминалов, так как при установке терминала этот модуль не ставится автоматически. На это есть вполне понятная причина — его применение возможно только в программах на Python, а далеко не на всяком компьютере, на который ставится терминал, присутствует интерпретатор языка Python.

Интерпретатор Python у нас уже есть (так как именно на нём уже частично написан код нашего веб-приложения), поэтому для использования этого модуля нам нужно только установить его с помощью менеджера пакетов pip, выполнив такую команду в консоли:

pip install MetaTrader5

К сожалению, на момент написания статьи установить этот модуль для последней версии Python v. 3.14 не удалось, поэтому пришлось переключиться на использование более ранней версии Python v. 3.12. В этом нет ничего страшного, так как для потребностей нашего проекта вряд ли понадобятся какие-то специфические возможности, добавленные в последней версии. Скорее всего, мы с тем же успехом могли применять и ещё более ранние версии Python. Со временем MetaQuotes обновит этот модуль, чтобы он мог устанавливаться и с более поздними версиями Python.


Схема работы с модулем

В какой бы программе мы ни решили использовать модуль интеграции, схема работы с ним будет включать в себя выполнение трёх обязательных действий:

  • Импорт модуля. Используя стандартный механизм импорта модулей в Python, мы подключаем к программе модуль MetaTrader 5 с коротким именем mt5:
    import MetaTrader5 as mt5
    После этого все функции данного модуля становятся доступны в программе на Python с префиксом mt5.

  • Подключение к терминалу. Все действия будет в итоге выполнять какой-то терминал MetaTrader 5, поэтому нам необходимо установить с ним связь из программы на Python. Для этого предназначена функция initialize():
    mt5.initialize()
    Эта функция также может принимать дополнительные параметры, применение которых мы рассмотрим ниже. В результате она возвращает логическое значение, означающее успешность подключения к терминалу. В зависимости от полученного результата, мы можем либо выполнить дальнейшие необходимые действия, либо как-то сообщить об ошибке подключения.

  • Отключение от терминала. После завершения необходимых действий установленное подключение нужно закрыть. Для этого предназначена функция shutdown():
    mt5.shutdown()
    Её можно вызвать и в случае неудачного подключения на предыдущем шаге.

Поэтому в целом шаблон кода, использующего модуль интеграции, будет выглядеть, например, так:

# 1. Импорт модуля 
import MetaTrader5 as mt5
 
# 2. Подключение к терминалу MetaTrader 5
if mt5.initialize():
    # Подключение успешно
    # Выполнение основной работы
    # ...
    pass
else:
    # Подключение не установлено
    # Обработка ошибок
    print("initialize() failed")

# 3. Отключение от терминала
mt5.shutdown()

Если программа на Python становится довольно объёмной, то второй и третий шаг обычно помещаются не в глобальной области, а внутри отдельных функций или методов классов.


Подключение к терминалу

Рассмотрим подробнее функцию подключения к терминалу initialize() применительно к контексту нашей задачи.

Если она вызывается без параметров, то терминал для подключения выбирается автоматически. Это означает, что безопасно и предсказуемо можно применять такой формат вызова только в том случае, когда на компьютере был установлен единственный терминал, и он уже был ранее подключен к нужному торговому счёту с сохранением параметров подключения.

Если на компьютере было установлено несколько терминалов, то нет гарантии, что такой вызов функции подключится именно к нужному терминалу. Поэтому в нашем случае, когда мы изначально планируем работать с несколькими терминалами на одном компьютере, необходимо строго указывать через дополнительные параметры, к какому терминалу мы хотим сейчас подключиться.

Мы будем делать это, используя первый неименованный параметр функции initialize(), в котором можно передать полный путь к запускаемому файлу нужного экземпляра терминала. Так как эти пути у нас в программе хранятся в отдельном списке, то нет никаких проблем с тем, чтобы передать их в функцию подключения. Если нужный путь мы предварительно присвоим переменной path, то вызов функции подключения будет выглядеть так:

# 2. Подключение к терминалу MetaTrader 5
if mt5.initialize(path):
    # Подключение успешно
    # Выполнение основной работы
    # ...
    pass

Также отметим разницу в поведении этой функции в зависимости от того, был ли ранее запущен тот терминал, к которому следует подключиться, или ещё нет. В первом случае (терминал уже запущен) произойдет почти мгновенное подключение к работающему терминалу. Во втором случае (терминал ещё не запущен) вызов этой функции инициирует сначала запуск терминала и только потом будет пытаться к нему подключаться.

На запуск терминала, как правило, требуется некоторое время (порядка нескольких секунд). Это время программа на Python будет ждать, так как вызов функции initialize() продолжает выполняться, ожидая запуска терминала. Но когда терминал запущен, или был запущен ранее, то ожидание на этом не заканчивается. На следующем этапе вызов функции initialize() будет пытаться дождаться установления подключения к торговому счёту. Только после успешного соединения терминала с сервером брокера и не менее успешного подключения к торговому счёту, функция initialize() будет считать свою миссию выполненной и вернёт true.

Такое поведение в принципе можно использовать и для задачи запуска экземпляра терминала. Но такой способ обладает меньшим богатством возможностей, чем запуск терминала через выполнение консольной команды, реализованный в прошлых частях. Поэтому оставим уже имеющуюся схему запуска/остановки терминалов без изменений, а с помощью модуля интеграции будем обращаться только к уже запущенным экземплярам.

Если же на каком-то этапе что-то пошло не так, функция initialize() возвращает false. Однако, если причина кроется в отсутствии связи с сервером брокера, то такой результат мы получим не мгновенно, а только спустя время ожидания, которое по умолчанию равно 60 секундам. В планируемой архитектуре это недопустимо большое время задержки ответа, поэтому мы воспользуемся именованным параметром timeout, через который мы можем задать допустимое время ожидания подключения:

# 2. Подключение к терминалу MetaTrader 5
if mt5.initialize(path, timeout=200):
    # Подключение успешно
    # Выполнение основной работы
    # ...
    pass

Такое небольшое время ожидания (200 мс = 0.2 с) с одной стороны достаточно велико для подключения к запущенному терминалу с уже установленным подключением к торговому счёту, а с другой стороны — достаточно мало, чтобы не ожидая слишком долго, понять, что что-то пошло не так, и к этому терминалу пока полноценно подключиться нельзя.

Последний параметр, которым мы воспользуемся при подключении к терминалу, будет параметр portable. С его помощью можно указать, что терминал для подключения должен запускаться или быть запущенным в режиме Portable. Это как раз то, что нужно, так как в дальнейшем нам придётся работать с рабочими папками терминалов (папки данных MQL5). Для этого нам нужно будет точно знать их расположение на компьютере.

# 2. Подключение к терминалу MetaTrader 5
if mt5.initialize(path, timeout=200, portable=True):
    # Подключение успешно
    # Выполнение основной работы
    # ...
    pass

Функция подключения позволяет ещё задать в дополнительных именованных параметрах данные для подключения к торговому счёту (логин, пароль и сервер). Если эти параметры не указаны, то используются те данные, которые были сохранены экземпляром терминала при последнем подключении. Мы пока не дошли до смены торговых счетов, поэтому такое поведение нас вполне устраивает.


Отключение от терминала

По сравнению с функцией подключения, противоположная по смыслу функция отключения shutdown() гораздо проще. Она не имеет никаких параметров и выполняет только программное отключение установленного ранее канала связи с терминалом. При этом сам терминал продолжает оставаться запущенным, то есть воспользоваться функцией shutdown() для остановки терминала нельзя.

В справке прямо не указано, является ли её вызов обязательным при неудачном подключении к терминалу, но во многих примерах эта функция вызывается в обоих случаях: и при успешном подключении, и при неуспешном. Поэтому поступим таким же образом, разместив вызов функции отключения после условного оператора, обрабатывающего оба случая.


Основная работа

В случае успешного подключения к нужному экземпляру терминала, у нас появляется возможность вызвать любую доступную функцию из описанных в справке. В рамках поставленной задачи мы воспользуемся только тремя из них.

С помощью функции terminal_info() мы будем получать состояние и настройки подключенного клиентского терминала MetaTrader 5 в виде именованного кортежа, те есть набора значений, каждому из которых присвоено определённое имя. Далее такой кортеж можно легко преобразовать в словарь, то есть структуру данных в виде множества пар <ключ>=<значение>. В таком виде данные уже можно сохранить в формате JSON и отправлять в составе ответа веб-сервера на запросы клиентов.

С помощью функции account_info() будем получать информацию о текущем торговом счете, к которому подключен терминал. Формат возвращаемого значения у этой функции такой же, как у terminal_info().

В случае возникновения каких-либо ошибок, поучить дополнительную информацию о случившемся можно с помощью функции last_error(). Эта функция возвращает кортеж из двух значений: числового кода ошибки и её строкового описания. В случае отсутствия ошибок возвращается кортеж вида (0, 'Success'), так что результат этой функции мы тоже всегда будем включать в состав данных возвращаемых клиенту веб-сервером.

Другие функции из модуля интеграции позволяют получать цены различных торговых инструментов, списки открытых позиций, историю сделок, а также совершать сами сделки на торговом счёте терминала. Фактически, с помощью этого модуля можно писать полноценные торговые советники, реализуя логику принятия решений о совершаемых торговых операциях на уровне программы на Python, а в терминал отправлять только команды на открытие и закрытие позиций. Но в рамках данной статьи эти возможности нам пока не понадобятся. 

 

Изменения главной страницы

В прошлый раз мы генерировали HTML-код главной страницы, сразу включая в него информацию обо всех зарегистрированных терминалах. Эта информация включала в себя только статус терминалов (запущен/остановлен) и PID (идентификатор запущенного процесса терминала). Теперь мы изменим эту схему следующим образом. На главной странице будут выделяться отдельные блоки, в которые в дальнейшем будет добавляться информация о каждом терминале. Для её получения будут использоваться отдельные запросы по маршрутам /instances/{name} с разными именами терминалов. Эти запросы будет раз в 10 секунд генерировать и отправлять JS-код на главной странице. Информация из ответов на эти запросы будет обрабатываться в другом JS-коде, который будет подставлять полученные данные в нужные места блоков на главной странице.

Таким образом, если раньше нам требовалось вручную перезагрузить главную страницу, чтобы увидеть изменения в составе запущенных экземпляров терминалов, то теперь это будет происходить без перезагрузки главной страницы и автоматически, с заданной периодичностью.

Интервал 10 секунд между последовательными обновлениями выбран, исходя из того чтобы, с одной стороны, быть не слишком большим и достаточно часто обновлять данные о терминалах, а с другой стороны, быть не слишком маленьким, чтобы не создавать большую нагрузку на сервер, позволяя ему успевать обрабатывать все поступающие запросы.

Упомянутый JS-код мы разместим в файле /static/script.js.


Класс MT5_Control

Для лучшей структуризации кода мы решили объединить разрозненные функции из файла mt5_control.py в виде методов одного класса с именем MT5_Control. Те параметры, которые раньше у нас были в виде констант или глобальных переменных мы сделали членами этого класса и добавили их инициализацию в конструкторе:

class MT5_Control:
    def __init__(self, terminals: dict, mt5_folder: str, mt5_exe: str = 'terminal64.exe'):
        '''
        Инициализирует экземпляр класса MT5_Control.

        Args:
            terminals (dict): Словарь с информацией о терминалах.
            mt5_folder (str): Путь к папке с экземплярами терминалов.
            mt5_exe (str): Имя исполняемого файла терминала (по умолчанию 'terminal64.exe').
        '''
        # Путь к папке с экземплярами терминалов
        self.mt5_folder = mt5_folder

        # Имя исполняемого файла терминала
        self.mt5_exe = mt5_exe

        # Список имён экземпляров
        self.instances_folders = list(terminals.keys())

        # Словарь соответствия имя -> путь
        self.instanсes_paths = {folder: self.instance_path(
            folder) for folder in self.instances_folders}

        # Словарь соответствия путь -> имя
        self.paths_instanсes = {self.instance_path(
            folder): folder for folder in self.instances_folders}

        self.instances = terminals.copy()

Код этого класса сохраним в уже существующем файле mt5_control.py. В нём ещё будут присутствовать несколько методов, но их мы рассмотрим позднее.


Создаём конфигурацию

Итак, нам понадобилось хранить некоторую информацию как конфигурацию, то есть набор параметров, которые нужны при каждом запуске приложения. Что может предложить фреймворк FastAPI для этого? К сожалению, FastAPI не предоставляет встроенного механизма для хранения и загрузки конфигурации, но он отлично сочетается с популярными решениями для конфигурации в Python. Поскольку FastAPI — это веб-фреймворк, а не приложение с "жёсткой" архитектурой конфигурации, поэтому мы можем использовать любые удобные инструменты для этих целей.

От FastAPI нам понадобится единственная вещь — возможность назначить определённую функцию в качестве обработчика событий жизненного цикла веб-приложения (lifespan). В этой функции должен присутствовать оператор yield, который служит разделителем кода, выполняющегося до старта и после остановки веб-приложения. Поэтому чтение конфигурации и инициализацию объекта класса MT5_Control мы выполним именно там.

Сама конфигурация в рамках данного проекта будет существовать в нескольких ипостасях. Во-первых, она будет находиться в каком-то JSON-файле. Пока веб-приложение не запущено, конфигурация существует только в таком виде. Во-вторых, в момент запуска веб-приложения будет создаваться специальный объект config класса Config, единственной задачей которого будет чтение конфигурации из JSON-файла и создание структуры данных для хранения прочитанной информации. И в-третьих, после создания объекта веб-приложения app класса FastAPI, данные конфигурации будут переноситься в его поле state. Оттуда их можно будет получить при необходимости в любом из обработчиков маршрутов. После этого объект config больше не нужен.

Рассмотрим последовательно все упомянутые вещи.


JSON-файл конфигурации

Структуру JSON-файла мы выбираем самостоятельно, исходя из имеющийся информации, которую хотели бы там хранить. После нескольких итераций мы пришли к следующему варианту конфигурации, которая была сохранена в файле с именем config.json:

{
  "terminals": {
    "MetaTrader5.1": {
      "name": "MetaTrader5.1",
      "login": 12345671,
      "server": "MetaQuotes-Demo"
    },
    "MetaTrader5.2": {
      "name": "MetaTrader5.2",
      "login": 12345672,
      "server": "MetaQuotes-Demo"
    },
    "MetaTrader5.3": {
      "name": "MetaTrader5.3",
      "login": 12345673,
      "server": "MetaQuotes-Demo"
    },
    "MetaTrader5.4": {
      "name": "MetaTrader5.4",
      "login": 12345671,
      "server": "MetaQuotes-Demo"
    }
  },
  "mt5_folder": "C:/MT5",
  "mt5_exe": "terminal64.exe"
}

Как видно, в этом файле хранится словарь (называемый объектом в нотации JSON). В нём есть три элемента с ключами terminals, mt5_folder и mt5_exe. Элемент terminals в свою очередь тоже является словарём, в котором представлена вся информация об экземплярах терминалов. В нём сейчас четыре элемента с ключами, которые соответствуют названиям папок экземпляров терминалов.

Каждый такой элемент хранит информацию об одном экземпляре терминала, расположенного в соответствующей папке и включает три элемента с ключами name, login и server. Назначение поля name — это имя, которое будет отображаться для данного экземпляра терминала на веб-странице. В простейшем случае оно может совпадать с названием папки. Назначение двух оставшихся полей понятно из их названий — в них хранится номер торгового счёта и название сервера брокера. Но эти параметры пока ещё не будут использоваться в коде, их обработка будет добавлена позднее. Также в будущем мы точно расширим список параметров, которые будут храниться для каждого экземпляра терминала. Но сейчас не будем забегать вперёд.

В элементы mt5_folder и mt5_exe мы перенесли значения, которые раньше в коде были объявлены как константы MT5_FOLDER и MT5_EXE. Они хранят название корневой папки для всех экземпляров терминалов и название запускаемого файла терминала MetaTrader 5.

Файл config.json добавлен в состав репозитория проекта, поэтому он может использоваться только в качестве примера конфигурационного файла. Для создания своей персональной конфигурации можно сделать копию файла config.json, назвав её, например config.server1.json или как-то иначе. Далее мы добавим возможность указать при запуске приложения имя конфигурационного файла, из которого нужно прочесть данные.

В .gitignore мы добавили такое исключение:

config.*.json

Поэтому имена файлов, соответствующие такому формату, будут игнорироваться системой контроля версий. Такой файл будет спокойно находиться в папке репозитория, не мешая дальнейшим обновлениям. Вносить изменения в него понадобится в том случае, если будет как-то меняться формат конфигурационной информации.


Объект config

Для чтения JSON-файла конфигурации воспользуемся возможностями, предоставляемыми модулем Python, который называется Pydantic Settings. В простейшем случае мы могли бы создать свой класс Config, унаследовав его от готового класса BaseSettings и перечислив в области свойств класса названия, типы и значения той информации, которую хотели бы использовать в качестве конфигурации. Например: 

from pydantic_settings import BaseSettings

class Config(BaseSettings):
    folders: list[str] = ["MetaTrader5.1", "MetaTrader5.2"]
    mt5_folder: str = "C:/MT5/"
    mt5_exe: str = "terminal64.exe"

В нашем случае, класс Config получился немного сложнее, так как потребовалось обеспечить возможность чтения данных из определённого JSON-файла, а не простое указание их в исходном коде:

from pydantic_settings import BaseSettings, JsonConfigSettingsSource, SettingsConfigDict
import os


class Config(BaseSettings):
    terminals: dict
    mt5_folder: str
    mt5_exe: str

    model_config = SettingsConfigDict()

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls,
        init_settings,
        env_settings,
        dotenv_settings,
        file_secret_settings,
    ):
        # Получаем путь к файлу из переменной окружения
        config_file = os.getenv("MT5_MANAGER_CONFIG_FILE", "config.json")
        return (
            init_settings,
            JsonConfigSettingsSource(settings_cls, json_file=config_file),
            env_settings,
            dotenv_settings,
            file_secret_settings,
        )

Он был добавлен в проект как файл config.py.


Передача конфигурации объекту веб-приложения

Для последнего шага загрузки конфигурации мы импортируем в основном файле веб-приложения main.py наши новые классы Config и MT5_Manager:

from config import Config
from mt5_control import MT5_Control

Далее создадим функцию lifespan(), которая будет принимать в качестве параметра объект класса FastAPI. Через этот параметр ей будет передаваться наш объект веб-приложения app. Добавив к этой функции декоратор @asynccontextmanager, мы превращаем её в упомянутую выше функцию-обработчик событий жизненного цикла веб-приложения. Внутри неё мы будем создавать объект config, читающий конфигурацию из JSON-файла, переносить прочитанное в нужные места и создавать глобальный объект control для управления терминалами:

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Создаем объект, читающий конфигурацию из JSON-файла
    config = Config(_env_file=None)

    # Переносим прочитанное в объект app 
    app.state.terminals = config.terminals
    app.state.instances = {}

    # Обеспечиваем наличие имени экземпляра терминала.
    # При отсутсвии его в конфигурации подставляется имя папки
    for folder in app.state.terminals:
        app.state.terminals[folder]['name'] = app.state.terminals[folder].get(
            'name', folder)

    # Создаём глобальный объект управления терминалами
    global control
    control = MT5_Control(app.state.terminals,
                          config.mt5_folder, config.mt5_exe)
    yield

Остается внести небольшое дополнение в процесс создания объекта веб-приложения app. Нам нужно передать в конструктор через параметр lifespan имя функции, которая будет играть роль обработчика событий жизненного цикла. Она у нас тоже названа lifespan, поэтому строка создания теперь будет выглядеть так:

# Создаём объект приложения
app = FastAPI(lifespan=lifespan)

Сохраним сделанные изменения в файле main.py.


Добавляем внешний запуск

Следующим шагом стало добавление другого способа запуска нашего веб-приложения. Раньше мы использовали такую команду:

uvicorn main:app --reload --no-use-colors

То есть мы запускали отдельное приложение uvicorn, которое обрабатывало наш исходный код на Python. При запуске мы могли передать ему только тот набор параметров, который предусмотрен этим приложением. Чтобы иметь возможность передать свои параметры, мы создадим новый скрипт на Python, который будет принимать нужные параметры и изнутри запускать веб-сервер приложения uvicorn:

import argparse
import uvicorn
import os


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--config-file", default="config.json",
                        help="Путь к файлу конфигурации")
    parser.add_argument("--host", default="0.0.0.0", help="IP-адрес сервера")
    parser.add_argument("--port", default=8000,
                        help="Порт сервера", type=int)
    args = parser.parse_args()

    # Устанавливаем переменную окружения
    os.environ["MT5_MANAGER_CONFIG_FILE"] = args.config_file

    # Запускаем uvicorn
    uvicorn.run("main:app", reload=True, host=args.host, port=args.port)


if __name__ == "__main__":
    main()

Таким образом мы добавили возможность предать имя JSON-файла с конфигурацией, которое будет запомнено в переменной окружения MT5_MANAGER_CONFIG_FILE. Оттуда это имя сможет достать при создании объект config:

# Получаем путь к файлу из переменной окружения
config_file = os.getenv("MT5_MANAGER_CONFIG_FILE", "config.json")

В результате наше веб-приложение сможет стартовать, используя нужную конфигурацию, с помощью такой команды:

python run.py --config-file=config.json --host=0.0.0.0 --port=8000

Вместо указанных значений можно подставить свои, если мы хотим запускать веб-сервер не на всех доступных на компьютере IP-адресах или с использованием другого номера порта.


Метод получения информации

После долгого отступления вернёмся снова к классу MT5_Control и рассмотрим его метод, отвечающий за получение информации об экземпляре терминала. Вначале посмотрим, каким должен быть результат работы этого метода, а потом и сам метод.

Каждый запрос по маршруту /instances/{name} будет возвращать нам данные в виде JSON-представления словаря (множества пар ключ-значение). Пары в этом словаре можно разбить на несколько блоков, в зависимости от источника получения данных для каждого.

Первым будут идти данные о конкретном экземпляре терминала, получаемые по сути из конфигурации нашего приложения: имя экземпляра, логин торгового счёта и имя сервера брокера.

{
  "name": "MetaTrader5.1",
  "login": 1234567,
  "server": "MetaQuotes-Demo",
  ...
}

Пока что указываемый логин никак не используется, так как мы договорились подключаться только к терминалам, в которых уже была выполнена ранее процедура подключения к нужному торговому счёту, и информация об этих подключениях сохранена для последующих запусков терминалов. Поэтому тут значение логина не совпадает с реально используемым, но это несоответствие мы в скором времени устраним.

Следующий блок содержит информацию о значении PID и статусе процесса терминала на компьютере. Если PID не равен 0, то это означает, что данный терминал запущен, а значение статуса это просто подтверждает.

{
  
    ... 
  "pid": 11560,
  "status": "running",
  ...
}

Далее будет идти информация об ошибке в виде вложенного словаря с числовым кодом и описанием ошибки. В большинстве случаев это будет сообщение об успешном получении данных от терминала:

{
  ...
  "last_error": {
    "code": 1,
    "description": "Success"
  },
  ...
}

Следующие блоки — это вся информация, которую модуль интеграции MetaTrader 5 предоставляет о запущенном терминале и подключенном торговом счёте:

{
  ...
  "terminal": {
    "community_account": true,
    "community_connection": true,
    "connected": true,
    // ...
  },
  "account": {
    "login": 5041102414,
    "trade_mode": 0,
    "leverage": 100,
    // ...
  },
  
    ... 
}

И последним указывается время получения всей предшествующей информации:

{
  ...
  "last_update": "2025-11-18 09:46:14.076863"
}

Полностью ответ сервера выглядит примерно так:

{
  "name": "MetaTrader5.1",
  "login": 1234567,
  "server": "MetaQuotes-Demo",
  "pid": 11560,
  "status": "running",
  "last_error": {
    "code": 1,
    "description": "Success"
  },
  "terminal": {
    "community_account": true,
    "community_connection": true,
    "connected": true,
    // ...
  },
  "account": {
    "login": 5041102414,
    "trade_mode": 0,
    "leverage": 100,
    // ...
  },
  "last_update": "2025-11-18 09:46:14.076863"
}

Для формирования такого ответа, мы будем сначала проверять, что терминал с запрошенным именем существует и запущен. Если всё в порядке, то подключаемся к нему, используя модуль Python-интеграции по схеме, изложенной выше. При успешном подключении мы запрашиваем у терминала необходимые данные и помещаем их в словарь. Туда же будет добавляться и информация об ошибках, текущем времени и статусе терминала. Наполненный словарь возвращается как результат этого метода.

def instance_info(self, folder: str) -> dict:
        '''
        Получает информацию о терминале по имени экземпляра терминала

        Args:
            folder (str): Имя экземпляра терминала.

        Returns:
            info (dict): Информация о терминале или обнаруженных ошибках
        '''
        # Словарь для результатов
        info = {}

        # Если терминал с таким именем папки присутствует в конфигурации, то
        if folder in self.instances:
            # Сохраняем начальную имеющуюся информацию из конфигурации
            info = self.instances[folder]

            # Формируем полный путь к терминалу
            path = self.instanсes_paths[folder]

            # Если указанный терминал запущен, то
            if 'pid' in self.instances[folder] and self.instances[folder]['pid']:
                # print(f'Start mt5.initialize[{folder}]')
                # Если подключение выполнено успешно, то
                if mt5.initialize(path, timeout=200, portable=True):
                    # Добавим в результат информацию о состоянии терминала
                    terminal_info = mt5.terminal_info()
                    if terminal_info != None:
                        info['terminal'] = terminal_info._asdict()

                    # Добавим в результат информацию о торговом счёте
                    account_info = mt5.account_info()
                    if account_info != None:
                        info['account'] = account_info._asdict()

                    # Добавим текущее время
                    info['last_update'] = str(datetime.now())

                    # Запомним код ошибки, сообщение и актуальный статус
                    code, description = mt5.last_error()
                    status = 'running'

                else:
                    # Если подключиться быстро не удалось, то
                    # если терминал числится в процессе запуска
                    if info['status'] == 'starting':
                        # Запомним код ошибки, сообщение и актуальный статус
                        # для терминала в процессе запуска
                        code, description = 0, 'Starting'
                        status = 'starting'
                    else:
                        # Иначе запомним код ошибки, сообщение и актуальный статус
                        # для запущенного терминала
                        code, description = mt5.last_error()
                        status = 'running'

                mt5.shutdown()
                # print(f'End mt5.initialize[{folder}]')
            else:
                # Иначе запомним код ошибки, сообщение и актуальный статус
                # для остановленного терминала
                code, description = 0, 'Stopped'
                status = 'stopped'
        else:
            # Иначе установим код ошибки, сообщение и актуальный статус
            # для отсутствующего терминала
            code, description = -1, 'Manager: Terminal not found'
            status = 'not found'

        # Добавим в результат информацию об ошибках и актуальном статусе
        info['last_error'] = {'code': code, 'description': description}
        info['status'] = status
        
        return info

Остальные изменения, сделанные в этом классе, не настолько принципиальны, поэтому не будем их здесь детально описывать. Как всегда, полный код проекта доступен в приложенном файле и в публичном репозитории, где его можно посмотреть.


Тестирование

Посмотрим, как выглядит теперь отображение информации о запущенных экземплярах терминалов. Мы немного поработали над внешним видом главной страницы и её информационных блоков. Добавлено главное меню, через которое в дальнейшем можно будет переходить на страницу редактирования конфигурации прямо в браузере.

Рис. 1. Главная страница веб-приложения, отображающая информацию о четырёх экземплярах терминала MetaTarder 5

Рассмотрим внимательнее отдельный блок для одного экземпляра терминала. Как видно, для него отображается иконка сервера брокера, последние цифры номера торгового счёта и его имя. Библиотека иконок пока не наполнена, поэтому если для какого-то сервера брокера её нет, то будет показываться иконка по умолчанию (как для MeatQuotes-Demo).

Далее показывается дата/время последнего получения информации и статус. В нормальном режиме показывается только время последнего соединения, так как оно должно быть относительно недавним: при отсутствии проблем со связью и отправке запросов каждые 10 секунд, это время отличается от текущего не более чем на 10 секунд. Поэтому нет необходимости показывать рядом ещё и сегодняшнюю дату. Но если какой-то терминал был остановлен, то это время перестаёт меняться и показывает, по сути, момент остановки. Если этот момент приходится уже не на текущие сутки, то тогда перед временем начинает показываться и дата.

На рис. 1 мы специально остановили последний терминал MetaTrader5.4. В тот момент, когда был сделан скриншот (2025-11-06 18:23:10), с момента остановки прошло чуть больше минуты, поэтому новый день ещё не наступил, и время показывается без даты. На следующий день (2025-11-07) эта страница будет выглядеть следующим образом:

Рис. 2. Главная страница веб-приложения на следующий день

Как видно, у первых трёх терминалов по-прежнему показывается только время, так как они работают и регулярно обновляют свои данные, а у последнего терминала стала показываться и дата, и время (2025-11-06 18:21:59).

В правом верхнем углу блока информации об экземпляре терминала расположены кнопки запуска и остановки данного терминала. В зависимости от текущего состояния, доступной для нажатия будет только одна из этих двух кнопок. Ниже показываются значения текущего баланса торгового счёта данного терминала, прибыль по открытым позициям, значение средств с процентом отличия от баланса и задействованная маржа. Больше пока мы ничего не выводим.

Но даже при таком составе выводимой информации, у нас получился уже достаточно мощный и полезный инструмент, облегчающий одновременное наблюдение за несколькими торговыми счетами без присутствия рядом с тем компьютером, где они работают.


Заключение

В этой части мы значительно расширили возможности нашего приложения. Благодаря библиотеке Python-интеграции MetaTrader 5, мы научились получать информацию о торговых счетах и выводить её на главную страницу веб-интерфейса. Также была реализована гибкая система конфигурации, позволяющая управлять параметрами приложения через внешний JSON-файл. Внедрение асинхронного обновления данных на клиентской стороне с помощью JavaScript сделало интерфейс более интерактивным и удобным. Это позволяет уже прямо сейчас использовать систему для удобного мониторинга нескольких торговых счётов.

В следующих частях мы продолжим развивать его функциональность. Например, добавим вывод более детальной информации о каждом экземпляре, улучшим процесс редактирования конфигурации, расширим работу на несколько серверов, обеспечим возможность объединять торговые счета в группы с подсчётом суммарных показателей и снова улучшим пользовательский интерфейс. В общем, здесь открывается достаточно широкое поле для дальнейшего развития.

Спасибо за внимание и до новых встреч!


Предупреждение. Стоит подчеркнуть важность осторожности при использовании проекта. На данном этапе он находится в начальной стадии реализации, поэтому в нём никак не учитываются возможные угрозы утечки чувствительной информации при работе разрабатываемого веб-приложения в общедоступном режиме. В целях безопасности приложение должно запускаться только в защищённой или локальной среде. В текущем виде оно не предусматривает шифрования трафика, аутентификации пользователей или защиты от несанкционированного доступа. Поэтому пока обеспечение приватности передаваемой информации, как и любая прочая защита, ложится на плечи пользователя и обеспечивается внешними по отношению к проекту средствами.


Содержание архива

#
Имя Версия Описание Последние изменения
  mt5-manager/   Рабочая папка проекта веб-сервера терминалов  
  ├─ config.py
0.1.0  Класс работы с конфигурацией приложения Часть 3 
1 ├─ main.py 0.2.0
Веб-приложение для веб-сервера терминалов
Часть 3
2 ├─ mt5_control.py 0.2.0 Логика управления запуском и остановкой терминалов  Часть 3
  ├─ run.py
0.1.0
Файл запуска веб-приложения для веб-сервера терминалов с заданной конфигурацией и параметрами Часть 3

├─ templates/      
3 │   └─ index.html
0.2.0 Шаблон основной страницы Часть 3
  └─ static/      
       ├─ ...
  Дополнительные файлы для стилей и иконок  
4      ├─ styles.css
0.2.0 CSS-стили
Часть 3
5      └─ script.js   0.2.0 JavaScript-код для главной страницы
Часть 3

Также исходный код проекта доступен в репозитории mt5-manager.

Прикрепленные файлы |
mt5-manager.zip (152.25 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Yevgeniy Koshtenko
Yevgeniy Koshtenko | 21 нояб. 2025 в 15:19
Классный и очень полезный проект.
Roman Shiredchenko
Roman Shiredchenko | 22 нояб. 2025 в 01:03
Спасибо - Юрий. Очень интересная статья. И полезное содержимое....  Сам буду нечто подобное ваять в контексте арбитражных торгов forex - moex с двух терминалов MT 5 с разных брокеров! 
Yuriy Bykov
Yuriy Bykov | 22 нояб. 2025 в 06:28

Изначально это будет инструмент только для наблюдения за терминалами с возможностью их запуска и остановки. Но смотря вперед, не вижу препятствий для расширения его функциональности и на совершение торговых операций. Например, можно будет давать команду открыть позицию BUY EURUSD 1.00 одновременно на всех счетах заданной группы. Или закрывать открытые позиции прямо из браузера на любом наблюдаемом терминале.

Но и это ещё не все. Можно будет через браузер загрузить свой советник (то есть не из Маркета) и дать команду поставить его работать на нужный терминал или наоборот, удалить работающий советник. Тут, правда, пока ещё не ясно, насколько это получится легко сделать, но со временем, думаю, разберёмся.

Ещё в планах показ в браузере логов со всех терминалов. Либо по отдельности, либо сведённые в один общий лог.

В общем, возможностей можно добавить сюда очень много.

Моделирование рынка (Часть 14): Сокеты (VIII) Моделирование рынка (Часть 14): Сокеты (VIII)
Многие программисты могут предположить, что нам следует отказаться от использования Excel и перейти непосредственно на Python, используя некоторые пакеты, позволяющие Python создавать Excel-файл, чтобы потом проанализировать результаты. Но, как уже говорилось в предыдущей статье, хотя это решение и является наиболее простым для многих программистов, оно не будет воспринято некоторыми пользователями. И в данном вопросе пользователь всегда прав. Мы, как программисты, должны найти способ заставить всё работать.
Нейросети в трейдинге: Спайковая архитектура пространственно-временного анализа рынка (Энкодер) Нейросети в трейдинге: Спайковая архитектура пространственно-временного анализа рынка (Энкодер)
В статье представлена адаптация фреймворка SDformerFlow, обеспечивающая высокую адаптивность за счёт интеграции спайкового внимания с многооконной свёрткой и взвешенным суммированием элементов Query. Архитектура позволяет каждой голове внимания обучать собственные параметры, что повышает точность и чувствительность модели к структуре анализируемых данных.
Трейдинг с экономическим календарем MQL5 (Часть 6): Автоматизация входа в сделку с анализом новостей и таймерами обратного отсчета Трейдинг с экономическим календарем MQL5 (Часть 6): Автоматизация входа в сделку с анализом новостей и таймерами обратного отсчета
В этой статье мы реализуем автоматизированный вход в торговлю с использованием экономического календаря MQL5, применив настраиваемые фильтры и временные смещения для поиска новостей. Мы сравниваем прогнозные и предыдущие значения, чтобы определить, следует ли открывать сделку на покупку или продажу. Динамические таймеры обратного отсчета отображают оставшееся время до выхода новостей и автоматически сбрасываются после совершения сделки.
Моделирование рынка (Часть 09): Сокеты (III) Моделирование рынка (Часть 09): Сокеты (III)
Сегодняшняя статья является продолжением предыдущей. В ней мы рассмотрим, как будет реализован советник, сосредоточившись в основном на том, как выполняется серверный код. Кода, приведенного в предыдущей статье, недостаточно для того, чтобы всё работало как надо, поэтому необходимо немного углубиться в него. Поэтому нужно прочитать обе статьи, чтобы лучше понять то, что произойдет.