2026-07-25 19:24:56 +03:00
|
|
|
# Часть 4 «Создание сервиса» — конспект проработки
|
|
|
|
|
|
|
|
|
|
Рабочие заметки к статье и коду. Составлены до первой компиляции сервиса,
|
|
|
|
|
поэтому часть утверждений помечена как требующая проверки.
|
|
|
|
|
|
|
|
|
|
Ветка: `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» (частично: `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, кладётся
|
2026-07-25 20:52:51 +03:00
|
|
|
напрямую из `TimeGMT()`. **Не `TimeCurrent()`** — последняя возвращает
|
|
|
|
|
время сервера брокера, и терминалы у разных брокеров дадут расхождение в
|
|
|
|
|
часы при вычислении свежести данных.
|
|
|
|
|
**Не `TimeLocal()` тоже** — изначально был выбран именно он, но при первом
|
|
|
|
|
прогоне обнаружилось: `TimeLocal()` возвращает цифры настенных часов
|
|
|
|
|
машины, выданные за UTC-эпоху, а не настоящий UTC (проверено эмпирически,
|
|
|
|
|
расхождение с `time.time()` в Python совпало с часовым поясом машины день
|
|
|
|
|
в день). Сравнение с `time.time()` на стороне Python было бы systematически
|
|
|
|
|
неверным на величину смещения пояса. `TimeGMT()` даёт настоящий UTC и
|
|
|
|
|
сохраняет исходное преимущество перед `TimeCurrent()` — не зависит от
|
|
|
|
|
брокера. Заменено в обеих функциях сервиса (`RegisterTerminal()`,
|
|
|
|
|
`StoreState()`).
|
2026-07-25 19:24:56 +03:00
|
|
|
- `margin_level` отдельным полем, хотя выводится из equity и margin: при
|
|
|
|
|
нулевой марже не определено, пусть эту ветку обработает MQL5 один раз.
|
|
|
|
|
- `pid` и `status` в схеме сознательно отсутствуют — сервис физически не
|
|
|
|
|
может записать «я остановлен», это остаётся за `psutil`.
|
|
|
|
|
- Открытые позиции построчно (символ, объём, прибыль) — **следующая часть**,
|
|
|
|
|
сейчас только агрегат `positions`.
|
|
|
|
|
|
|
|
|
|
### Определение имени экземпляра
|
|
|
|
|
|
|
|
|
|
Сервис берёт `TerminalInfoString(TERMINAL_PATH)` и отрезает последний
|
|
|
|
|
компонент пути.
|
|
|
|
|
|
|
|
|
|
Почему `TERMINAL_PATH`, а не `TERMINAL_DATA_PATH`: Python-сторона в
|
|
|
|
|
`MT5_Control.instance_path()` собирает путь как `mt5_folder` + имя папки +
|
|
|
|
|
`mt5_exe`, то есть оперирует местом установки. В portable-режиме обе
|
|
|
|
|
константы совпадают, но без `/portable` `TERMINAL_DATA_PATH` даст
|
|
|
|
|
`Terminal\<хеш>` вместо ожидаемого имени папки.
|
|
|
|
|
|
|
|
|
|
Функции `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 его переваривает, спецификация не
|
2026-07-27 12:34:18 +03:00
|
|
|
гарантирует.
|
|
|
|
|
|
|
|
|
|
**Решено:** миллисекундная точность не нужна, наружу отдаётся то же целое
|
|
|
|
|
число секунд Unix, что лежит в `updated_at` (`mt5_control.py`), без
|
|
|
|
|
умножения на 1000. `formatDateTime()` в `static/script.js` сам умножает
|
|
|
|
|
полученное значение на 1000 перед `new Date()`. Сделано.
|
2026-07-25 19:24:56 +03:00
|
|
|
|
|
|
|
|
### Недостающие поля
|
|
|
|
|
|
|
|
|
|
Фронтенд обращается к `data.account.company`. Добавлены `company` и
|
|
|
|
|
`holder_name` (`ACCOUNT_COMPANY`, `ACCOUNT_NAME`) в `states`, чтобы словарь
|
|
|
|
|
`account` собирался из базы без потерь. Учесть, что `company` у счёта и
|
|
|
|
|
`company` у терминала — разные вещи, хотя обычно совпадают.
|
|
|
|
|
|
|
|
|
|
### Мелочи
|
|
|
|
|
|
|
|
|
|
- `config.json`: у `MetaTrader5.4` логин совпадает с `MetaTrader5.1`.
|
2026-07-27 12:34:18 +03:00
|
|
|
**Не баг** — так задумано намеренно (терминал 4 подключается к тому же
|
|
|
|
|
счёту, что и терминал 1). Логины из конфигурации пока нигде не
|
|
|
|
|
используются, начнём использовать в следующих частях — тогда и
|
|
|
|
|
проверится, что дублирование обрабатывается корректно.
|
2026-07-25 19:24:56 +03:00
|
|
|
- `script.js`: в расчёте `profitPct` нет защиты от нулевого баланса.
|
|
|
|
|
- `mt5_control.py`: `self.instances = terminals.copy()` — поверхностная
|
|
|
|
|
копия, запись `pid` и `status` меняет вложенные словари самой
|
|
|
|
|
конфигурации. Сейчас безобидно, аукнется при появлении перечитывания
|
|
|
|
|
конфига.
|
2026-07-27 12:34:18 +03:00
|
|
|
**Исправлено:** заменено на `copy.deepcopy(terminals)`.
|
2026-07-25 19:24:56 +03:00
|
|
|
- `mt5_control.py`: в именах `instanсes_paths` и `paths_instanсes` буква «с»
|
|
|
|
|
в слове `instanсes` кириллическая. Код рабочий (используется
|
|
|
|
|
последовательно), но поиск по латинскому написанию ничего не найдёт.
|
2026-07-27 12:34:18 +03:00
|
|
|
**Исправлено:** переименовано на латиницу (`instances_paths`,
|
|
|
|
|
`paths_instances`).
|
2026-07-25 19:24:56 +03:00
|
|
|
- `create_mt5()` и `POST /create/{name}` выглядят заготовкой: внутри
|
|
|
|
|
`mt5.initialize()` без пути, возвращается `{folder: folder}`. В статьях
|
|
|
|
|
цикла этого нет.
|
2026-07-27 12:34:18 +03:00
|
|
|
**Решено:** оставляем как есть, не трогаем и не упоминаем в 4-й части —
|
|
|
|
|
дойдём до этого функционала в 5-й части.
|
|
|
|
|
- `main.py`: в обработчике `index()` был закомментированный черновой код
|
|
|
|
|
(`for name in instances: ... instances[name]['info'] = ...`).
|
|
|
|
|
**Исправлено:** убран — эта информация приходит на главную страницу
|
|
|
|
|
через ajax-запросы (`POST /instances/{name}`), а не при первом рендере.
|
2026-07-25 19:24:56 +03:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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()` на таких
|
|
|
|
|
запросах может вернуть ошибку. Запасной путь — `DatabasePrepare()` +
|
|
|
|
|
`DatabaseRead()` + `DatabaseFinalize()`, он и использован в наброске.
|
|
|
|
|
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-й части
|
2026-07-27 13:07:03 +03:00
|
|
|
- автоматизация установки сервиса — **закрыта частично** находкой
|
|
|
|
|
`config\services.ini` (раздел 10); программная раскладка самого `.ex5`
|
|
|
|
|
по папкам новых терминалов и генерация `services.ini` под конкретную
|
|
|
|
|
установку остаются на будущее
|
2026-07-25 20:52:51 +03:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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` через
|
|
|
|
|
`DatabasePrepare`/`DatabaseRead` работает, `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. Честно назвать в статье шагом разворачивания, а не ограничением.
|
2026-07-27 13:07:03 +03:00
|
|
|
|
|
|
|
|
**Пересмотрено ниже** — найден рабочий обходной путь, см. следующий раздел.
|
|
|
|
|
|
|
|
|
|
### Найдено: `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 нигде не фиксирует этот формат
|
|
|
|
|
как публичный контракт.
|