Skip to main content
Glama
zvalkorib

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).

Maintenance

ActivityMaintained
ResponsivenessSyncing