forké depuis antekov/mt5-manager
- оформить комментарии и заголовки функций по замечаниям модератора - обновить листинги и версию сервиса до 1.01 - подготовить MQL5.zip без EX5 и личных реквизитов - сохранить исправленную редакцию статьи и правила оформления
# Часть 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`. Описано в статье (подраздел «Скрипт развёртывания»).
|