| # Часть 4 «Создание сервиса» — конспект проработки
|
|
|
| > Исторические рабочие заметки к четвёртой части. Описание актуальной версии
|
| > приложения находится в [документации](../../README.md); промежуточные выводы
|
| > ниже могут отличаться от текущего кода.
|
|
|
| ## Исправления по замечаниям модератора, 29 октября 2026
|
|
|
| Замечания: [комментарии к статье](https://www.mql5.com/ru/articles/20150/comments).
|
| Комментарии перед блоками MQL5 приведены к `//---`, первый уровень выровнен
|
| по левому краю, вложенные комментарии — по отступу блока. Функции снабжены
|
| непустыми рамками с названиями, комментарии к переменным выровнены по колонке.
|
| Изменения выполнены в исходнике и девяти MQL5-листингах статьи. Версия сервиса
|
| повышена до 1.01; исполняемые токены, кроме директивы версии, не изменились.
|
|
|
| Подготовлен `publish/MQL5.zip` с 18 файлами кода четвёртой части из тега `p4`
|
| и исправленным исходником сервиса. Проект находится в
|
| `MQL5/Shared Projects/mt5-manager`, как требуется для описанного развёртывания.
|
| EX5 и личная конфигурация в архив не включены, вместо неё используется пример
|
| без логинов, серверов и паролей. Архив собирается `publish/build_archive.py`.
|
|
|
| На сайте MQL5 все девять листингов перевставлены через мастер «Код» с языком MQL,
|
| архив заменён на `MQL5.zip`, версия сервиса в таблице обновлена.
|
| Сохранённая редакция: [278364](https://www.mql5.com/ru/articles/20150/278364#!tab=article).
|
| После сохранения проверено совпадение листингов с локальными файлами,
|
| сохранение всех 17 блоков кода и трёх изображений. Копия HTML тела статьи
|
| с подсветкой сайта находится в `publish/article4-site.html`.
|
|
|
| Далее идут исторические заметки разработки.
|
|
|
| Рабочие заметки к статье и коду. Составлены до первой компиляции сервиса,
|
| поэтому часть утверждений помечена как требующая проверки.
|
|
|
| Ветка: `article-20150`. Репозиторий: https://forge.mql5.io/antekov/mt5-manager
|
|
|
| ---
|
|
|
| ## 1. Суть изменения
|
|
|
| Сейчас веб-приложение получает данные о терминалах **по запросу**: метод
|
| `MT5_Control.instance_info()` вызывает `mt5.initialize()` для конкретного
|
| экземпляра, забирает `terminal_info()` и `account_info()`, затем
|
| `mt5.shutdown()`.
|
|
|
| Становится **наоборот**: в каждом терминале работает сервис на MQL5, который
|
| раз в 2–3 секунды пишет своё состояние в общую базу SQLite. Веб-приложение
|
| только читает эту базу.
|
|
|
| ### Зачем
|
|
|
| - `mt5.initialize()` работает на уровне процесса — из одного процесса Python
|
| нельзя одновременно держать соединения с несколькими терминалами. Опрос
|
| строго последовательный, с `shutdown()` между итерациями. При четырёх
|
| терминалах терпимо, при пятнадцати цикл перестаёт укладываться в интервал
|
| обновления, а один «залипший» терминал тормозит очередь для всех.
|
| - Сервисы пишут параллельно, каждый в своём процессе.
|
| - С пути чтения уходит зависимость от модуля MetaTrader5, а с ней и
|
| ограничение «Python не выше 3.12» (на 2026-10-28 оно уже снято: модуль 5.0.6231 работает на 3.14, сборок под 3.15 пока нет) (частично: `create_mt5()` пока держит
|
| эту зависимость).
|
| - Появляется задел под историю и журнал операций, обещанные во 2-й части.
|
|
|
| ---
|
|
|
| ## 2. Принятые решения
|
|
|
| ### Расположение базы
|
|
|
| Файл в общей папке терминалов, открывается с флагом `DATABASE_OPEN_COMMON`.
|
|
|
| **Проверено на практике:** общая папка остаётся общей и в portable-режиме,
|
| разные экземпляры терминалов работают с одной базой корректно. Это стоит
|
| подчеркнуть в статье — интуиция подсказывает обратное.
|
|
|
| Следствие: путь к базе одинаков для всех экземпляров, настройка при
|
| развёртывании не нужна, один и тот же `.ex5` разложен по терминалам.
|
|
|
| ### Конкурентный доступ
|
|
|
| - **WAL** (`PRAGMA journal_mode=WAL`) — читатели и писатель перестают
|
| блокировать друг друга. Записывается в заголовок файла базы один раз и
|
| сохраняется. Файлы-спутники `-wal` и `-shm` рядом с базой — это нормально.
|
| Ограничение: работает только в пределах одной машины (нужна разделяемая
|
| память), по сетевой шаре не работает. Для распределённой архитектуры из
|
| 1-й части это означает, что у каждого сервера должна быть своя база.
|
| - **`PRAGMA busy_timeout=3000`** — вместо мгновенной ошибки `SQLITE_BUSY`
|
| библиотека сама ждёт и повторяет. WAL этого не решает: одновременный
|
| писатель по-прежнему ровно один, сервисы выстраиваются в очередь.
|
| **Важно:** это свойство *соединения*, а не файла. Не сохраняется, надо
|
| выставлять после каждого `DatabaseOpen()`, в каждом сервисе. На стороне
|
| Python — параметр `timeout` в `sqlite3.connect()` (в секундах, по
|
| умолчанию 5).
|
| - **Явные транзакции** вокруг записи (`DatabaseTransactionBegin` /
|
| `DatabaseTransactionCommit`). Без них каждый `INSERT` — отдельная
|
| транзакция со сбросом на диск, окно занятости базы растёт.
|
| - **Разброс интервала**: сервисы стартуют одновременно при запуске
|
| терминалов, и циклы `Sleep()` долго идут синхронно. Случайная добавка в
|
| пределах интервала размазывает записи во времени.
|
| - **Ошибку занятости игнорируем**, считая пропуски. Пока в базе только
|
| текущее состояние, пропуск безвреден — через две секунды приедет свежее
|
| значение. С появлением истории это станет дыркой в ряду данных.
|
|
|
| ### Схема
|
|
|
| Две таблицы. Таблица истории **отложена до следующей части** — там же
|
| обсудить более редкий интервал (5–10 минут) и чистку старых записей.
|
|
|
| ```sql
|
| CREATE TABLE IF NOT EXISTS terminals (
|
| id INTEGER PRIMARY KEY,
|
| folder TEXT NOT NULL UNIQUE, -- ключ связи с config.json
|
| path TEXT, -- полный путь, для диагностики
|
| company TEXT,
|
| build INTEGER,
|
| first_seen INTEGER,
|
| last_seen INTEGER
|
| );
|
|
|
| CREATE TABLE IF NOT EXISTS states (
|
| terminal_id INTEGER PRIMARY KEY REFERENCES terminals(id),
|
| updated_at INTEGER NOT NULL,
|
| connected INTEGER,
|
| ping_last INTEGER,
|
| login INTEGER,
|
| server TEXT,
|
| company TEXT,
|
| holder_name TEXT,
|
| currency TEXT,
|
| leverage INTEGER,
|
| trade_mode INTEGER,
|
| trade_allowed INTEGER,
|
| balance REAL,
|
| equity REAL,
|
| profit REAL,
|
| margin REAL,
|
| margin_free REAL,
|
| margin_level REAL,
|
| credit REAL,
|
| positions INTEGER,
|
| error_code INTEGER DEFAULT 0,
|
| error_text TEXT
|
| );
|
| ```
|
|
|
| Пояснения к решениям:
|
|
|
| - Разделение на две таблицы: редко меняющееся (папка, брокер, билд) отдельно
|
| от постоянно меняющегося. Даёт список известных терминалов даже когда все
|
| выключены.
|
| - Размер базы не растёт со временем: 4 строки в каждой таблице.
|
| - Индексов сверх первичных ключей не нужно, на таких объёмах они мешают.
|
| - Время — `INTEGER`, секунды unix. Родной формат `datetime` в MQL5, кладётся
|
| напрямую из `TimeGMT()`. **Не `TimeCurrent()`** — последняя возвращает
|
| время сервера брокера, и терминалы у разных брокеров дадут расхождение в
|
| часы при вычислении свежести данных.
|
| **Не `TimeLocal()` тоже** — изначально был выбран именно он, но при первом
|
| прогоне обнаружилось: `TimeLocal()` возвращает цифры настенных часов
|
| машины, выданные за UTC-эпоху, а не настоящий UTC (проверено эмпирически,
|
| расхождение с `time.time()` в Python совпало с часовым поясом машины день
|
| в день). Сравнение с `time.time()` на стороне Python было бы systematически
|
| неверным на величину смещения пояса. `TimeGMT()` даёт настоящий UTC и
|
| сохраняет исходное преимущество перед `TimeCurrent()` — не зависит от
|
| брокера. Заменено в обеих функциях сервиса (`RegisterTerminal()`,
|
| `StoreState()`).
|
| - `margin_level` отдельным полем, хотя выводится из equity и margin: при
|
| нулевой марже не определено, пусть эту ветку обработает MQL5 один раз.
|
| - `pid` и `status` в схеме сознательно отсутствуют — сервис физически не
|
| может записать «я остановлен», это остаётся за `psutil`.
|
| - Открытые позиции построчно (символ, объём, прибыль) — **следующая часть**,
|
| сейчас только агрегат `positions`.
|
|
|
| ### Определение имени экземпляра
|
|
|
| Сервис берёт `TerminalInfoString(TERMINAL_DATA_PATH)` и отрезает последний
|
| компонент пути.
|
|
|
| Решение автора: сервис работает только в portable-режиме (менеджер всегда
|
| запускает терминалы с `/portable`), и в этом режиме папка данных совпадает
|
| с папкой терминала. Запуск без `/portable` не поддерживается; если он
|
| случится, в базе появится другое имя папки. Раньше использовался
|
| `TERMINAL_PATH` (результат тот же).
|
|
|
| Функции `StringFindLast()` в MQL5 нет, написана вручную через `StringFind()`
|
| в цикле.
|
|
|
| Открытые вопросы, отложенные осознанно:
|
|
|
| - **Регистр в путях.** Windows нечувствителен к регистру, `MetaTrader5.1` и
|
| `Metatrader5.1` — одна папка, но разные строки. Второстепенно: после
|
| реализации автоматической установки пользователь не будет вводить имя
|
| папки вручную в нескольких местах. В статье упомянуть, не заостряя.
|
| - **Папка, которой нет в конфигурации.** Пока ничего не делаем. В финальной
|
| реализации такого быть не должно. Вернуться в следующих частях.
|
|
|
| ### Способ записи
|
|
|
| `INSERT ... ON CONFLICT(terminal_id) DO UPDATE` каждый цикл — надёжнее
|
| связки «`UPDATE`, при нуле изменённых строк `INSERT`», переживает ручное
|
| удаление строки, не требует различать первый цикл и последующие.
|
|
|
| Регистрация в `terminals` вынесена из цикла — делается один раз при старте
|
| (и повторно при переподключении к базе). Гонять её каждые две секунды
|
| незачем.
|
|
|
| Идентификатор терминала после вставки забирается отдельным `SELECT`.
|
| `last_insert_rowid()` не годится: при срабатывании ветки обновления вставки
|
| не было. `RETURNING id` требует SQLite 3.35 — рискованнее, чем лишний запрос
|
| раз при старте.
|
|
|
| ---
|
|
|
| ## 3. Найденные нестыковки с текущим кодом
|
|
|
| Разобраны, требуют правки в коде проекта.
|
|
|
| ### Код успеха равен 1, а не 0
|
|
|
| В `static/script.js`:
|
|
|
| - `last_error.code == 1` → зелёная галочка (работает)
|
| - `last_error.code == 0` → серый значок (Stopped / Starting)
|
| - всё остальное → красный восклицательный знак
|
|
|
| Это следствие того, что `mt5.last_error()` возвращает `RES_S_OK`, равный 1.
|
| Ноль занят под статусы, проставляемые вручную в `instance_info()`.
|
|
|
| Принятое соглашение для сервиса:
|
|
|
| - `1` / `"Success"` — есть связь с торговым сервером
|
| - `-2` / `"Service: No connection to trade server"` — терминал работает,
|
| связи нет
|
|
|
| Отрицательные коды продолжают ряд ошибок менеджера (`-1` —
|
| `"Manager: Terminal not found"`), префикс в тексте показывает источник
|
| жалобы.
|
|
|
| ### Формат времени
|
|
|
| Сейчас Python кладёт в `last_update` результат `str(datetime.now())`, JS
|
| парсит через `new Date()`. Строка вида `2026-07-25 12:34:56.789012` без
|
| буквы `T` — нестандартный формат, Chrome его переваривает, спецификация не
|
| гарантирует.
|
|
|
| **Решено:** миллисекундная точность не нужна, наружу отдаётся то же целое
|
| число секунд Unix, что лежит в `updated_at` (`mt5_control.py`), без
|
| умножения на 1000. `formatDateTime()` в `static/script.js` сам умножает
|
| полученное значение на 1000 перед `new Date()`. Сделано.
|
|
|
| ### Недостающие поля
|
|
|
| Фронтенд обращается к `data.account.company`. Добавлены `company` и
|
| `holder_name` (`ACCOUNT_COMPANY`, `ACCOUNT_NAME`) в `states`, чтобы словарь
|
| `account` собирался из базы без потерь. Учесть, что `company` у счёта и
|
| `company` у терминала — разные вещи, хотя обычно совпадают.
|
|
|
| ### Мелочи
|
|
|
| - `config.json`: у `MetaTrader5.4` логин совпадает с `MetaTrader5.1`.
|
| **Не баг** — так задумано намеренно (терминал 4 подключается к тому же
|
| счёту, что и терминал 1). Логины из конфигурации пока нигде не
|
| используются, начнём использовать в следующих частях — тогда и
|
| проверится, что дублирование обрабатывается корректно.
|
| - `script.js`: в расчёте `profitPct` нет защиты от нулевого баланса.
|
| - `mt5_control.py`: `self.instances = terminals.copy()` — поверхностная
|
| копия, запись `pid` и `status` меняет вложенные словари самой
|
| конфигурации. Сейчас безобидно, аукнется при появлении перечитывания
|
| конфига.
|
| **Исправлено:** заменено на `copy.deepcopy(terminals)`.
|
| - `mt5_control.py`: в именах `instanсes_paths` и `paths_instanсes` буква «с»
|
| в слове `instanсes` кириллическая. Код рабочий (используется
|
| последовательно), но поиск по латинскому написанию ничего не найдёт.
|
| **Исправлено:** переименовано на латиницу (`instances_paths`,
|
| `paths_instances`).
|
| - `create_mt5()` и `POST /create/{name}` выглядят заготовкой: внутри
|
| `mt5.initialize()` без пути, возвращается `{folder: folder}`. В статьях
|
| цикла этого нет.
|
| **Решено:** оставляем как есть, не трогаем и не упоминаем в 4-й части —
|
| дойдём до этого функционала в 5-й части.
|
| - `main.py`: в обработчике `index()` был закомментированный черновой код
|
| (`for name in instances: ... instances[name]['info'] = ...`).
|
| **Исправлено:** убран — эта информация приходит на главную страницу
|
| через ajax-запросы (`POST /instances/{name}`), а не при первом рендере.
|
|
|
| ---
|
|
|
| ## 4. Совместимость API
|
|
|
| Структура ответа `POST /instances/{name}` — словарь с ключами `terminal`,
|
| `account`, `last_update`, `last_error`, `status` — воспроизводится из базы
|
| один в один.
|
|
|
| Значит `templates/index.html` и `static/script.js` не требуют изменений,
|
| кроме формата времени. **Для статьи это сильный аргумент**: механизм
|
| получения данных меняется полностью, интерфейс подмены не замечает. Стоит
|
| вынести в отдельный подзаголовок.
|
|
|
| ---
|
|
|
| ## 5. Что проверить при первой компиляции
|
|
|
| Код сервиса написан без доступа к MetaTrader, ниже места с наибольшим
|
| риском расхождения.
|
|
|
| 1. **`PRAGMA` через `DatabaseExecute()`.** И `journal_mode`, и
|
| `busy_timeout` возвращают строку результата. `DatabaseExecute()` на таких
|
| запросах может вернуть ошибку. Проверено 28.10.2026 на практике: оба PRAGMA
|
| через `DatabaseExecute()` выполняются без ошибок (`true`, код 0), поэтому
|
| функция `ExecutePragma()` убрана, сервис вызывает `DatabaseExecute()` напрямую.
|
| 2. **Выполнение `INSERT` через подготовленный запрос.** Возвращать нечего,
|
| `DatabaseRead()` вернёт `false` с кодом «нет данных». В коде результат
|
| `DatabaseRead()` игнорируется, проверяется `GetLastError()` с отсечением
|
| кода завершения выборки. **Константа 5126 приведена по памяти, сверить с
|
| фактическим поведением.**
|
| 3. **Поддержка `ON CONFLICT ... DO UPDATE`.** Требует SQLite 3.24+.
|
| Встроенная в MetaTrader библиотека почти наверняка новее, но проверить
|
| первым делом. Если нет — схема упрощается до `UPDATE`, при нуле
|
| изменённых строк `INSERT`.
|
| 4. **Нумерация в `DatabaseBind()` начинается с нуля**, а плейсхолдеры `?1`,
|
| `?2` в SQL — с единицы. Выглядит как ошибка, но так и задумано. В статье
|
| отметить явно, читатели будут спотыкаться.
|
| 5. **Поведение `DatabaseTransactionBegin()`** — какую форму `BEGIN` он
|
| использует. Документация умалчивает. Пока не критично: сервис только
|
| пишет, ничего не вычитывая в той же транзакции. Станет важно, если
|
| появится логика «прочитать предыдущее значение, сравнить, записать» —
|
| тогда нужен `BEGIN IMMEDIATE`, иначе `SQLITE_BUSY` возвращается
|
| немедленно, не выжидая таймаут.
|
|
|
| ---
|
|
|
| ## 6. Ограничения сервисов, которые стоит проговорить в статье
|
|
|
| - В сервисах **запрещены** `EventSetTimer()`, `EventSetMillisecondTimer()`,
|
| `EventKillTimer()`. Цикл строится на `Sleep()` с проверкой `IsStopped()`.
|
| - Сервисы **загружаются автоматически** после запуска терминала, если на
|
| момент его остановки были запущены. Сбор данных стартует сам, без участия
|
| менеджера. Хороший контраст с советником, которого пришлось бы вешать на
|
| график.
|
| - Каждый сервис работает в своём потоке, зацикленный сервис не мешает
|
| другим программам.
|
| - Экземпляр создаётся через Навигатор.
|
|
|
| ---
|
|
|
| ## 7. Развёртывание
|
|
|
| Сервис компилируется и раскладывается в `MQL5/Services` каждого терминала,
|
| экземпляр создаётся через Навигатор — вручную, четыре раза. Для статьи
|
| нормально, но честно назвать временным решением. Автоматизация установки
|
| логично ложится в следующую часть и смыкается с планом «загружать советника
|
| через браузер».
|
|
|
| ---
|
|
|
| ## 8. Что остаётся на стороне Python
|
|
|
| База скажет, что было в момент последней записи, но не скажет, что терминал
|
| упал. Определение «запущен / остановлен» через `psutil` никуда не уходит,
|
| архитектура становится гибридной.
|
|
|
| Комбинирование даёт **три статуса вместо нынешних двух**, и на странице их
|
| стоит различать визуально:
|
|
|
| | Процесс (psutil) | Свежесть `updated_at` | Статус |
|
| |---|---|---|
|
| | жив | свежее порога | работает |
|
| | жив | старше порога | запущен, данные не поступают |
|
| | нет процесса | — | остановлен |
|
|
|
| Второй случай покрывает загрузку терминала, потерю связи и незапустившийся
|
| сервис. Порог — порядка 30 секунд при интервале записи 2–3 секунды.
|
|
|
| Отдельно: терминал может работать, а связи с сервером брокера не быть —
|
| тогда цифры счёта в базе последние известные, а не актуальные. Это
|
| `error_code = -2`, веб-серверу стоит отрисовывать отдельно.
|
|
|
| ---
|
|
|
| ## 9. Незакрытые обещания из предыдущих частей
|
|
|
| Держать в виду при планировании, что войдёт в 4-ю часть, а что дальше:
|
|
|
| - SQLite как хранилище — закрывается этой частью (частично: только состояние)
|
| - авторизация и HTTPS — отложены
|
| - журнал операций — отложен вместе с историей
|
| - главный сервер + серверы терминалов (схема из 1-й части) — отложено
|
| - торговые операции из браузера, загрузка советников, сведение логов —
|
| из комментариев к 3-й части
|
| - автоматизация установки сервиса — **закрыта частично** находкой
|
| `config\services.ini` (раздел 10); программная раскладка самого `.ex5`
|
| по папкам новых терминалов и генерация `services.ini` под конкретную
|
| установку остаются на будущее
|
|
|
| ---
|
|
|
| ## 10. Результаты первого запуска
|
|
|
| Компиляция прошла без ошибок с первого раза. Ниже — что показал реальный
|
| прогон, по пунктам риска из раздела 5.
|
|
|
| ### Баг: `RegisterTerminal()` не учитывал код 5126
|
|
|
| Код 5126 подтвердился на практике (пункт риска 2), но именно это вскрыло
|
| реальную ошибку: `RegisterTerminal()` проверял успех как
|
| `DatabaseRead(request) || GetLastError() == 0`, не зная про 5126 — в отличие
|
| от `StoreState()`, где это уже было учтено. В журнале (`MQL5\Logs`, не
|
| корневой `Logs` терминала — это два разных лога) сервис раз в цикл писал
|
| «Не удалось зарегистрировать терминал», `g_terminal_id` оставался `-1`
|
| навсегда, и `StoreState()` не вызывался вообще — при этом сама запись в
|
| `terminals` через `ON CONFLICT DO UPDATE` реально проходила, `last_seen`
|
| продвигался. То есть SQL отработал, а приложенческая проверка кода ошибки —
|
| нет. Хороший пример для статьи: база подтверждает успех, а сервис думает,
|
| что провалился.
|
|
|
| Исправлено: вынесена именованная константа `ERR_DB_NO_MORE_DATA = 5126`,
|
| обе функции проверяют `error != 0 && error != ERR_DB_NO_MORE_DATA`
|
| одинаково. После исправления и перекомпиляции лог показывает мгновенную
|
| успешную регистрацию и первую запись в `states` уже на первом цикле.
|
|
|
| Остальные пункты риска из раздела 5 закрыты без сюрпризов: `PRAGMA` через
|
| `DatabaseExecute` работает (позже перепроверено), `journal_mode=wal` подтверждён
|
| прямо в файле базы, `ON CONFLICT DO UPDATE` поддерживается, нумерация
|
| `DatabaseBind()` с нуля отработала верно. Поведение
|
| `DatabaseTransactionBegin()`/`Commit()` подтвердилось косвенно: как только
|
| починили баг регистрации, `StoreState()` начал доходить до транзакции и
|
| писать `states` без ошибок.
|
|
|
| ### Находка: `TimeLocal()` — не UTC
|
|
|
| Описано в разделе 2 (схема). Коротко: `TimeLocal()` вернул значение,
|
| отличающееся от `time.time()` в Python ровно на часовой пояс машины
|
| (GMT+3 → расхождение 10800 секунд, проверено вычитанием). Заменено на
|
| `TimeGMT()` в обеих функциях сервиса.
|
|
|
| ### Проверено: сервис переживает жёсткий рестарт терминала
|
|
|
| Открытый вопрос был в том, что `stop_mt5()` останавливает терминал через
|
| `psutil.Process.terminate()`, а на Windows это алиас `kill()` →
|
| `TerminateProcess()` — жёсткое завершение, не штатное закрытие через GUI.
|
| Утверждение «сервис загружается сам, если был запущен на момент остановки
|
| терминала» (раздел 1, из справки MetaTrader) не уточняет, требует ли это
|
| именно штатного закрытия.
|
|
|
| Проверено вручную: `Stop-Process -Force` по PID терминала (эквивалент
|
| `TerminateProcess`), затем повторный запуск `terminal64.exe /portable` —
|
| то есть буквально то, что делают `stop_mt5()`/`start_mt5()`. Сервис стартовал
|
| сам через ~2 секунды после запуска терминала, без обращения к Навигатору,
|
| и `states.updated_at` продолжил расти. Жёсткий килл не ломает автозагрузку
|
| сервиса.
|
|
|
| ### Решено: автодобавление сервиса через команду запуска — не реализуемо штатно
|
|
|
| Проверено по официальной документации
|
| (metatrader5.com/ru/terminal/help/start_advanced/start) и статье про
|
| сервисы на mql5.com: секция `[StartUp]` конфиг-файла поддерживает `Expert`
|
| и `Script`, но не `Service`. Программного или конфигурационного способа
|
| добавить сервис в Навигатор при первом развёртывании терминала не существует
|
| задокументированным способом.
|
|
|
| Рассмотренная альтернатива — перейти со `Service` на `Script`, запускаемый
|
| через `/config` + `[StartUp] Script=...` — отклонена: это возвращает тот
|
| самый минус советника, который сервис был выбран избежать (нужен график,
|
| скрипт умирает при его закрытии). Решение: разовое ручное добавление через
|
| Навигатор на каждый из терминалов при разворачивании, дальше сервис
|
| переживает перезапуски терминала сам (см. проверку выше) без участия
|
| Python. Честно назвать в статье шагом разворачивания, а не ограничением.
|
|
|
| **Пересмотрено ниже** — найден рабочий обходной путь, см. следующий раздел.
|
|
|
| ### Найдено: `config\services.ini` — недокументированный обходной путь
|
|
|
| Экспериментально найден способ добавить сервис в Навигатор без ручного шага,
|
| минуя вывод предыдущего раздела о принципиальной невозможности.
|
|
|
| Файл `config\services.ini` (пример — `static/services.ini` в репозитории)
|
| кладётся в папку `config` внутри **папки установки терминала** (не
|
| `Common`, не папку данных профиля). Если на момент запуска терминала по
|
| пути, указанному в файле, уже лежит скомпилированный `.ex5`, сервис
|
| стартует сам, без обращения к Навигатору вручную:
|
|
|
| ```
|
| <service>
|
| name=ServiceStateWriter
|
| path=Services\Shared Projects\mt5-manager\ServiceStateWriter.ex5
|
| expertmode=0
|
| enabled=1
|
| <inputs>
|
| InpDatabase=mt5-manager.sqlite
|
| InpIntervalSec=2
|
| InpJitterMs=1000
|
| InpBusyTimeout=3000
|
| </inputs>
|
| </service>
|
| ```
|
|
|
| **В официальной документации этого файла нет.** Проверено: страница
|
| «Файлы и папки» (metatrader5.com/ru/terminal/help/start_advanced/structure)
|
| перечисляет содержимое `config` явно — `accounts.dat`, `common.ini`,
|
| `metaeditor.ini`, `terminal.ini`, `servers.dat`, сертификаты — `services.ini`
|
| среди них нет. Страница про `[StartUp]` — тоже без него. Учебник, статья
|
| про сервисы и форум ничего не знают об этом файле.
|
|
|
| Косвенное подтверждение гипотезы о происхождении файла: `services.ini`
|
| закодирован в UTF-16LE (BOM `FF FE`), в отличие от читаемых текстовых
|
| конфигов вроде `common.ini`. Это типично для внутренних служебных файлов,
|
| которые терминал пишет и читает сам, а не для документированного
|
| пользовательского API. Похоже, это ровно то представление, в которое
|
| терминал сериализует состояние сервисов, добавленных через Навигатор
|
| командой «Добавить сервис» — то есть подложенный файл имитирует состояние
|
| «сервис уже был добавлен один раз вручную».
|
|
|
| Практический вывод: рабочий приём для развёртывания, но не задокументированное
|
| поведение платформы. В статье стоит подать с осторожной формулировкой —
|
| экспериментально найдено, воспроизводится, но может измениться в будущих
|
| билдах без предупреждения, раз MetaQuotes нигде не фиксирует этот формат
|
| как публичный контракт.
|
|
|
| ### Найдено: рассинхронизация схемы (`CREATE TABLE IF NOT EXISTS`)
|
|
|
| Таблица `states` была создана старой сборкой `.ex5` без колонок `company` и
|
| `holder_name`. `STATE_QUERY` падал с `no such column: s.company`, а
|
| `read_state()` молча возвращал `None` — статус навсегда `starting`. Исправлено:
|
| перекомпилирован и разложен `.ex5`, база удалена (только текущее состояние,
|
| истории нет), в `read_state()` добавлен вывод причины. Когда появится история —
|
| нужен `PRAGMA user_version` + `ALTER TABLE`. Описано в статье.
|
|
|
| ### Сделано: `deploy_service.py`
|
|
|
| git-синхронизация `MQL5/Shared Projects/mt5-manager` по терминалам, одна
|
| компиляция + копирование `.ex5`, раскладка `config/services.ini`. Компиляция ME
|
| из CLI ненадёжна (код 0 без лога и без нового `.ex5`), успех проверяется по
|
| mtime `.ex5`. Описано в статье (подраздел «Скрипт развёртывания»).
|