mt5-mcp-observer
by zvalkorib
README.md
# mt5-mcp-observer
MCP-сервер для MetaTrader 5: наблюдение за торговым счётом и управление
Algo Trading через AI-агента. **Торговые операции отсутствуют намеренно** —
инструментов отправки, изменения и закрытия ордеров в сервере нет.
Решает проблему изоляции сессий Windows: агент, подключённый по SSH,
работает в сессии 0 и не видит окон терминала, открытого через RDP.
---
## Зачем
Существующие MCP-серверы для MetaTrader 5 дают агенту торговать. Этот —
наоборот: агент видит счёт, считает риски и предупреждает, но открыть или
закрыть позицию не может. Полезно, когда торгует советник, а от агента
нужен мониторинг и один-единственный тумблер.
Что умеет:
- состояние счёта, позиции, отложенные ордера, история сделок;
- экспозиция по сторонам, признак полного хеджа, стоимость свопов за ночь;
- спецификация символа: лимиты объёма, маржа, свопы, уровни Stop Out;
- проверка исполнимости уровней сеточного советника по лимитам и марже;
- прогноз волатильности дня по календарю событий (ForexFactory,
MetalsMine), времена в МСК;
- статистика реализованной волатильности в SQLite: дневные диапазоны,
ATR14, фактическая реакция цены на события — сырьё для калибровки
прогноза;
- детектор резких движений: тик раз в секунду, пороги от дневного ATR
и класса дня, разлёт спреда, предупреждение о красных окнах;
- включение и выключение Algo Trading с защитой от брошенной серии.
---
## Архитектура
```
┌─────────────────┐ ┌──────────────────────────────────────┐
│ Linux / хост │ │ Windows │
│ агента │ │ │
│ │ │ сессия 0 (SSH) │
│ MCP client │ SSH │ mt5_mcp_server.py ──┐ │
│ (stdio) ├────────►│ stdio, forced cmd │ │
│ │ │ │ файлы │
└─────────────────┘ │ интерактивная сессия │ state/ │
│ algo_worker.py ◄────┘ │
│ │ Ctrl+E │
│ ▼ │
│ terminal64.exe ◄─── MT5 Python API│
└──────────────────────────────────────┘
```
### Почему два процесса
SSH запускает процессы в сессии 0. Окна приложений из интерактивной сессии
оттуда не видны — `EnumWindows` их не перечислит никогда. При этом MT5
Python API работает через IPC и границы сессий игнорирует.
| Компонент | Сессия | Роль |
|---|---|---|
| `mt5_mcp_server.py` | 0 (SSH) | Чтение через API, приём команд |
| `algo_worker.py` | интерактивная | Эмуляция Ctrl+E в окне терминала |
| обмен | файлы в `state/` | `command.json` → `result.json`, `heartbeat.json` |
Инструменты чтения работают всегда. Переключение требует живого воркера.
---
## Состав
```
config.example.json шаблон конфигурации
requirements.txt
src/
config.py загрузка config.json
mt5_algo.py поиск окна, Ctrl+E, чтение состояния, CLI
vol_forecast.py прогноз волатильности дня по календарю, CLI
vol_stats.py реализованная волатильность в SQLite, CLI
price_watcher.py детектор резких движений, события в state/
mt5_mcp_server.py MCP-сервер, 18 инструментов
integrations/hermes/ супервизор детектора (systemd) и доставка
алертов в Telegram с хоста агента
algo_worker.py исполнитель в интерактивной сессии
diag.py диагностика готовности сессии
smoke_test.py проверка сервера без Node
skills/
mt5-observer/SKILL.md шаблон скилла для агента
```
---
## Требования
**Windows** — MetaTrader 5, Python 3.11–3.14 64-бит, OpenSSH Server,
постоянно залогиненная интерактивная сессия.
**Хост агента** — MCP-клиент с поддержкой stdio, SSH-клиент.
```powershell
py -m pip install -r requirements.txt
```
---
## Установка
### 1. Конфигурация
```powershell
copy config.example.json config.json
(Get-Process terminal64).Path # путь для terminal_path
```
Заполнить `config.json`: `terminal_path`, `state_dir`, `default_symbol`,
при необходимости `window_title_hint` (если терминалов несколько) и
параметры `grid` под своего советника.
### 2. Проверка связи
```powershell
cd src
py mt5_algo.py find # ровно одно окно
py mt5_algo.py state # баланс и позиции
py smoke_test.py # 18 инструментов, чтение отвечает
```
### 3. Диагностика сессии
```powershell
py diag.py
```
Ключевой пункт — тест захвата фокуса. Берётся фокус — переключение
заработает. Остальное объясняет причину, если не берётся.
### 4. Воркер
```powershell
py algo_worker.py
```
Автозапуск при входе в систему:
```powershell
schtasks /create /tn "MT5Observer" /tr "pythonw C:\mt5\src\algo_worker.py" `
/sc onlogon /ru <user> /it /f
```
`/it` обязателен: без него задача уйдёт в сессию 0 и окон не увидит.
### 5. SSH
```powershell
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
```
Правило файрвола должно иметь профиль `Any`. Установщик иногда создаёт его
с профилем `Private`, и на публичном интерфейсе оно не применяется — внешне
это выглядит как `Connection timed out`:
```powershell
Get-NetFirewallRule -DisplayName "*SSH*" | Select DisplayName, Enabled, Profile
Set-NetFirewallRule -DisplayName "OpenSSH SSH Server (sshd)" -Profile Any
```
Ключ на стороне агента:
```bash
ssh-keygen -t ed25519 -f ~/.ssh/id_mt5 -N "" -C "mcp-observer"
```
Публичная часть — в `C:\ProgramData\ssh\administrators_authorized_keys`
(для учётных записей администраторов `~/.ssh/authorized_keys` игнорируется):
```powershell
$path = 'C:\ProgramData\ssh\administrators_authorized_keys'
Set-Content -Path $path -Value $key -Encoding ascii
icacls $path /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
```
`-Encoding ascii` принципиально: UTF-8 в Windows PowerShell 5.1 добавляет
BOM, sshd такой файл молча не принимает, а диагностика ограничивается
`Permission denied (publickey)`.
### 6. Конфигурация агента
`~/.ssh/config`:
```
Host mt5host
HostName <ip>
Port <port>
User <user>
IdentityFile ~/.ssh/id_mt5
ServerAliveInterval 30
ServerAliveCountMax 3
```
Подключение MCP-сервера (пример для клиента с stdio-транспортом):
```yaml
mcp_servers:
mt5:
command: "ssh"
args: ["-T", "-q", "-o", "BatchMode=yes", "mt5host",
"py C:/mt5/src/mt5_mcp_server.py"]
timeout: 120
connect_timeout: 60
```
`-T` и `-q` обязательны: иначе SSH подмешает в stdout псевдотерминал или
баннер, и JSON-RPC развалится. Проверка до подключения агента:
```bash
ssh -T -q mt5host "py C:/mt5/src/mt5_mcp_server.py" # молча ждёт ввода
```
Скилл — скопировать `skills/mt5-observer/` в каталог скиллов агента и
заполнить блок контекста счёта.
---
## Безопасность
**Читать до продуктивного использования.**
### Модель угроз
Граница безопасности проходит **не по списку инструментов MCP-сервера**, а
по SSH-ключу. Отсутствие торговых функций не мешает агенту торговать, если
у него есть shell-доступ и неограниченный ключ:
```bash
ssh mt5host "py -c \"import MetaTrader5 as mt5; mt5.initialize(); mt5.order_send(...)\""
```
Аллоулист инструментов фильтрует только вызовы через MCP. SSH идёт мимо.
### Обязательный харденинг
**1. Forced command.** Ключ агента должен запускать ровно одну команду:
```
restrict,command="py C:/mt5/src/mt5_mcp_server.py" ssh-ed25519 AAAA... mcp-observer
```
После этого любая команда в `ssh mt5host "..."` игнорируется. Для
администрирования завести отдельный ключ и хранить его не на хосте агента.
**2. Ограничить источник и отключить пароли**
```powershell
Set-NetFirewallRule -DisplayName "OpenSSH SSH Server (sshd)" -RemoteAddress <agent_ip>
# sshd_config: PasswordAuthentication no
Restart-Service sshd
```
Сужать `RemoteAddress` только после проверки работы ключа.
**3. RDP.** На машине с публичным адресом 3389 доступен всему интернету.
Ограничить по источнику или закрыть.
**4. Канал управления агентом.** Если агент принимает команды из
мессенджера, ограничить список отправителей.
**5. Prompt injection.** Инструкция во внешнем контенте для агента
неотличима от команды оператора. При forced command худшее, что он может
сделать — переключить Algo Trading.
### Что учтено в проекте
- торговых инструментов нет;
- учётные данные MT5 нигде не хранятся — используется залогиненный терминал;
- секретов в конфигурации нет, аутентификация целиком в SSH-ключах;
- выключение при активной серии требует явного `force=true`;
- `config.json` и `state/` в `.gitignore`.
### Остаточные риски
- номер счёта возвращается `get_account`;
- любой процесс с правом записи в `state_dir` может переключить Algo Trading;
- воркер работает от учётной записи с доступом к терминалу.
---
## Эксплуатация
### Инструменты
Чтение: `get_status`, `get_account`, `get_symbol_spec`, `get_positions`,
`get_orders`, `get_exposure`, `get_history_deals`, `check_grid_volumes`,
`calc_margin`, `get_vol_forecast`, `get_vol_stats`, `get_watcher_status`,
`get_price_events`, `get_algo_trading`, `get_worker_status`,
`get_worker_log`, `get_last_command_result`.
Запись: `set_algo_trading`.
### Прогноз волатильности
`get_vol_forecast` классифицирует день (low / medium / high) по календарю
экономических событий и размечает внутри дня окна повышенного риска.
Источник — официальные JSON-фиды Fair Economy (ForexFactory и MetalsMine),
скрейпинга нет. Все времена — МСК.
Скоринг детерминированный: вес события = важность из фида x близость
валюты к золоту (USD максимальна), поверх — спецсписок минимумов для
событий, которые фид недооценивает (FOMC, NFP, CPI, выступления Пауэлла
и Трампа). События кластеризуются в окна по перекрытию временных гало
(45 минут, для выступлений 90 — их время в календаре примерное). Скор дня
= сильнейшее окно + 0.3 от остальных. Пороги и веса — в секции `forecast`
файла `config.json`.
Проверка без агента:
```powershell
py vol_forecast.py today # прогноз на сегодня по окнам
py vol_forecast.py week # остаток недели сводкой
py vol_forecast.py refresh # обновить state/forecast.json
```
Инструмент сам обновляет прогноз при устаревании (`max_age_hours`).
Ежедневный `refresh` по расписанию нужен только для накопления
`state/forecast_history.jsonl` — истории скоров под будущую калибровку
весов по реализованной волатильности:
```powershell
schtasks /create /tn "MT5VolForecast" /tr "py C:\mt5\src\vol_forecast.py refresh" `
/sc daily /st 07:00 /f
```
**Ограничение принципиальное:** прогноз покрывает только запланированную
волатильность. Пустой календарь означает «нет запланированных причин»,
а не «будет тихо» — внеплановые новости в календаре не появляются.
Требуется исходящий доступ к `nfs.faireconomy.media` с Windows-хоста.
### Детектор резких движений
`price_watcher.py` — третий процесс. Окон не трогает, интерактивная
сессия не нужна: можно держать под супервизором с хоста агента
(`ssh <host> "py C:/mt5/price_watcher.py"` под systemd с
`Restart=always`). Терминал сам не запускает — если terminal64 не
работает, ждёт.
Раз в секунду читает тик и порождает события в `state/events.jsonl`:
| Тип | Условие |
|---|---|
| `sharp_move` | Диапазон цены в окне 1/5/15 мин превысил порог |
| `spread_blowout` | Спред ≥ max(4 × медианы часа, медиана + 20 пунктов) |
| `red_window_ahead` | До красного окна календаря осталось ≤ 15 мин |
Пороги адаптивные: `ATR14 (из vol_stats.sqlite) × atr_frac(окно) ×
class_factor(класс дня из forecast.json)`. В high-день пороги выше
(меньше шума при ожидаемой волатильности), в low-день ниже — то же
движение в тихий день аномальнее. Повторные срабатывания глушатся
cooldown'ом. Живость — `state/watch_status.json` (свежее 30 с = жив),
агенту — `get_watcher_status` и `get_price_events`.
Детектор только наблюдает: никаких действий по событиям он не выполняет.
Готовый супервизор (systemd) и доставка событий в Telegram через
no_agent-джоб — в [integrations/hermes/](integrations/hermes/).
### Статистика реализованной волатильности
`vol_stats.py` копит в `state/vol_stats.sqlite` два ряда: дневные бары
символа (диапазон дня, ATR14, скор и класс прогноза на этот день) и
фактическую реакцию цены на события календаря — диапазон и нетто-движение
по M1 в окне от `event_pre_minutes` до `event_post_minutes` вокруг события.
```powershell
py vol_stats.py update --backfill 180 # первый прогон: полгода дневок
py vol_stats.py update # ежедневно: дозаполнить до вчера
py vol_stats.py report --days 30 # прогноз против факта
```
`update` требует запущенного терминала. Дневные бары заполняются задним
числом, реакция на события копится только вперёд — исторического
календаря у фида нет, события берутся из `forecast_history.jsonl`.
Скор дня фиксируется по **самому раннему** снапшоту прогноза (честный
прогноз «до открытия», а не уточнённый по ходу дня).
Смещение времени сервера брокера определяется автоматически по свежему
тику; при закрытом рынке берётся `stats.server_utc_offset` из конфига.
Инструмент `get_vol_stats` отдаёт агенту дневной ряд, сводку
«класс прогноза → фактический диапазон», корреляцию скора с фактом и
топ движений вокруг событий. Корреляция осмысленна от ~20 размеченных
дней — до этого веса скоринга считаются некалиброванными.
Ежедневное расписание (`refresh` до `update`, чтобы у дня был утренний
снапшот прогноза):
```powershell
schtasks /create /tn "MT5VolDaily" /sc daily /st 07:00 /f `
/tr "cmd /c py C:\mt5\vol_forecast.py refresh && py C:\mt5\vol_stats.py update"
```
### Коды отказа set_algo_trading
| `reason` | Значение | Действие |
|---|---|---|
| `worker_down` | heartbeat старше порога | Запустить воркер в интерактивной сессии |
| `grid_active` | есть позиции или ордера | Осознанно передать `force=true` |
| `timeout` | команда ушла, состояние не изменилось | `get_last_command_result` |
| нет, `ok: true` | выполнено | — |
### Переживание разрыва RDP
Сессия в состоянии `Disconnected` не принимает эмулированный ввод. Перед
отключением перевести её на консоль:
```powershell
query session
tscon <ID> /dest:console
```
RDP-клиент отвалится — это признак успеха. Процессы продолжают работать,
сессия остаётся интерактивной. **Повторять после каждого подключения по
RDP.**
Проверка: `get_worker_status` возвращает `connect_state: Active` и
`is_console_session: true` при отключённом RDP.
### После обновления кода
Клиенты MCP переиспользуют поднятый подпроцесс — заменённый файл сам собой
не подхватится. Перезапустить соединение или шлюз агента.
---
## Известные ограничения
**Global Variables советника недоступны.** Python API их не отдаёт.
Внутреннее состояние советника сверить нельзя, доступна только фактическая
картина позиций и ордеров. Полная сверка требует MQL5-сервиса, выгружающего
переменные в файл.
**Переключение Algo Trading — не аварийный выключатель.** Цепочка зависит
от воркера, интерактивной сессии и эмуляции клавиши. Кроме того, отложенные
ордера живут на сервере брокера и исполняются независимо от состояния Algo
Trading: выключение при активной серии создаёт позиции без обслуживания
советником. Для гарантированной остановки используйте прямой доступ к
терминалу.
**Диагностика воркера справочная.** `toggle_ready` может дать ложный
негатив. Команда блокируется только при отсутствии окна; в остальных
случаях выполняется попытка, а результат проверяется через MT5 API
независимо от диагностики.
**Только Windows.** Пакет `MetaTrader5` работает исключительно там, где
запущен терминал.
---
## Отладка
| Симптом | Причина | Проверка |
|---|---|---|
| `Connection timed out` | Профиль правила файрвола не `Any` | `Get-NetFirewallRule -DisplayName "*SSH*"` |
| `Permission denied (publickey)` | BOM в authorized_keys | `Get-Content $path -Encoding Byte -TotalCount 3` |
| JSON-RPC не парсится | Баннер или pty в stdout | `ssh -T -q ...` вручную |
| «Окно не найдено» | Воркер не запущен или сессия 0 | `get_worker_status` |
| Инструмент не виден агенту | Старый подпроцесс MCP | Перезапуск соединения |
| Тумблер не срабатывает | Сессия `Disconnected` | `diag.py`, пункт 3 |
Логи воркера — `<state_dir>\worker.log`, последний результат —
`<state_dir>\result.json`.
---
## Планы
- MQL5-сервис для выгрузки Global Variables и полной сверки состояния;
- периодические снапшоты экспозиции для восстановления истории;
- калибровка весов прогноза по накопленной `vol_stats.sqlite` (данные
уже копятся, пересмотр весов — от ~20 размеченных дней);
- поддержка нескольких терминалов в одной установке.
---
## Дисклеймер
Проект предназначен для наблюдения за торговым счётом. Он не даёт торговых
рекомендаций и не выполняет сделок. Торговля на финансовых рынках связана с
риском потери средств. Программное обеспечение предоставляется «как есть»,
без каких-либо гарантий.
## Лицензия
MIT — см. [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing