Мониторинг торгового счета MT5: дашборд с нуля
Брат скинул мне ссылку на свой трейдинг-дашборд с вопросом «почему данные не обновляются» - я думал, что это займёт час. Оказалось, что нормальный мониторинг торгового счета требует куда большего: в итоге получился полноценный продукт с живым интерактивным графиком, выбором периода, портфельной страницей для нескольких стратегий и поддержкой двух платформ - MetaTrader 5 и MetaTrader 4. Что было сломано, как чинили и что получилось - по порядку.
Контекст: что такое mt5-tracker
Проект — это статический дашборд для мониторинга торгового счёта в MetaTrader 5. Архитектура простая: Python-коллектор под Wine подключается к MT5-терминалу через его API, собирает данные о сделках, балансе и открытых позициях, сохраняет дамп в JSON, а скрипт анализа генерирует статическую HTML-страницу. Всё это крутится в Docker-контейнере и публикуется по крону.
Звучит несложно. Но дьявол, как обычно, в деталях.
Проблема первая: данные не обновлялись четыре дня
Самый коварный баг — тот, который не кричит об ошибке, а просто тихо умирает. Именно это и произошло с коллектором.
Windows-питон под Wine пишет в pipe и выбирает кодировку cp1252. В dump.py на строчке логирования был символ →:
# было
log(f"bars {label}: {len(series)} → keeping last {cap}")Этот невинный символ стрелки вызывал UnicodeEncodeError: 'charmap' codec can't encode character '\u2192' — и процесс падал до того, как успевал записать mt5_dump.json. Запись в файл была в самом конце main(), поэтому последний живой дамп датировался 30 июля 09:22, а дальше — четыре дня тишины.
Фикс элементарный:
# стало
log(f"bars {label}: {len(series)} -> keeping last {cap}")Или, если хочется сохранить красоту, принудительно задать кодировку при открытии pipe. Но проще убрать не-ASCII из логов — они там не нужны.
Урок: если коллектор пишет дамп в самом конце — любая ошибка до этой точки убивает весь цикл. Лучше писать промежуточные дампы или хотя бы логировать факт успешной записи отдельно.
Проблема вторая: нет выбора периода
Второй вопрос был про интерфейс: «почему нельзя выбрать период и посмотреть на графике». Оказалось — три отдельные причины, все реальные.
Первая: переключатель М1 М5 М15 М30 Ч1 Ч4 Д1 — это детализация свечи, а не диапазон дат. При переключении код специально удерживал то же временное окно. Вторая: никакого контрола для выбора диапазона дат в дашборде просто не существовало. Третья: вся статистика считалась один раз по всей истории в analyze.py и не пересчитывалась.
Добавил полноценный селектор периода:
| Было | Стало | |
|---|---|---|
| Выбор периода | Только детализация свечи | Всё / Сутки / Неделя / Месяц + произвольный диапазон дат |
| Статистика | Считалась один раз по всей истории | Пересчитывает всё: плитки, кривую счёта, просадку, серии, разбивки, журнал |
| Зум | Отсутствовал | Pinch zoom на мобиле, колесо на десктопе |
Ключевой момент: пересчёт при смене периода должен затрагивать всё — не только график, но и плитки со статистикой, журнал сделок, карточку для шаринга. Иначе получается ситуация, когда на графике видишь неделю, а в плитках — данные за всё время.
Проблема третья: график не показывал, где вход и где выход
На скриншоте была колонка синих стрелок, которая не отвечала ни на вопрос «по какой цене», ни на «какой выход чей». Проблема была в иерархии отрисовки.
Вход рисовался не там, где он был: стрелка парковалась на 13px за экстремум свечи, и на каждую повторную сделку той же свечи уезжала ещё на 15px. Настоящая цена входа оставалась точкой 3px. Связь вход→выход была самым слабым элементом — пунктир 1.2px на alpha 0.7.
После правки:
- Метки входа и выхода рисуются точно на цене исполнения
- Линия связи вход→выход стала читаемой
- Добавлена «выбивка» фоном, чтобы метки не тонули в свечах того же цвета
- Клик по метке подсвечивает сделку латунью, гасит всё остальное до 15%, подсвечивает строку в журнале
Проблема четвёртая: зависание после клика по метке
После добавления интерактивности появился неприятный эффект: после клика по метке на графике всё зависало — канвас переставал получать события, даже колесо прокрутки не работало.
Сначала я не смог воспроизвести на десктопе. Прогнал под троттлингом CPU ×6 (примерно средний телефон) — и вот оно. Профилировщик показал:
| Было | Стало | |
|---|---|---|
| Один тап по метке | 219 мс заблокированного потока | 13,9 мс |
| Один кадр отрисовки | 41,2 мс | 10,8 мс |
Вызовов getComputedStyle на кадр | 1510 | 0 |
Причина: выбор сделки перестраивал весь журнал целиком, и на каждый элемент вызывался getComputedStyle для получения цветов из CSS-переменных. 1510 принудительных layout reflow за один клик — вот откуда 219 мс.
Решение: закешировать палитру при инициализации и больше не лезть в computed styles на горячем пути. Результат — 16-кратное ускорение.
// было: на каждый кадр
const color = getComputedStyle(canvas).getPropertyValue('--accent-color');
// стало: один раз при инициализации
const palette = {
accent: getComputedStyle(canvas).getPropertyValue('--accent-color'),
// ...
};Проблема пятая: график сбрасывался на 29 июля при переключении таймфрейма
При переключении таймфрейма и масштабировании всё сбрасывалось на конкретную дату — 29 июля — и выбраться оттуда было невозможно.
Причина: setTimeframe подменял массив баров, но не пересчитывал границы периода. А границы — это индексы, и у каждого таймфрейма своя система индексов:
| TF | Баров | Покрывает |
|---|---|---|
| M15 | 607 | 27.07 → 04.08 |
| M1 | 6000 (потолок MAX_BARS) | 29.07 06:24 → 04.08 |
При переключении на M1 границы оставались 0..607 — то есть первые 607 баров минутного ряда. А минутный ряд начинается 29 июля. Отсюда ровно та дата.
И выбраться было нельзя не случайно: клампинг считался по фантомному диапазону, поэтому любая попытка прокрутить или зумить упиралась в невидимую стену.
Фикс: при каждом переключении таймфрейма пересчитывать границы в системе индексов нового массива. Плюс добавить логику: если пользователь не зумил, переключение TF не должно сужать окно.
Сессионные куки: 40 дней вместо одной вкладки
Basic auth не использует куки — браузер держит креды в памяти вкладки и теряет их при закрытии. Для трейдингового дашборда, который открываешь каждый день, это неудобно.
Решение: поверх Basic auth поставить долгоживущую сессионную куку через Caddy. Важный нюанс: нужно убедиться, что кука не утекает в 401-ответе. Проверил — свойство держится: 401 куку не отдаёт. Срок жизни куки — 40 дней.
Плитки со статистикой: что реально важно трейдеру
В процессе работы выяснилось несколько интересных вещей про отображение данных.
Средний вход показывал 5 794,51 — и это легко прочитать как «5000 долларов заходит в сделку». На самом деле это ноционал: стоимость золота, которым управляет позиция:
0,014 лота × 100 унций × 4060 $/унц ≈ 5 794 $
А плечо 1:1000 означает, что реально из баланса блокируется:
5794 ÷ 1000 = 5,79 $
Это 0,056% счёта на одну сделку. Переделал плитку: теперь показывает реальные деньги из баланса, а не ноционал.
Оборот за период — показатель, который добавил по запросу. $1,45 млн оборота на капитале $10k означает, что капитал прокручен 145 раз. Меняется вместе с выбранным периодом:
| Период | Сделок | Оборот | ×капитала | Результат |
|---|---|---|---|---|
| Всё | 263 | 1 447 121 $ | ×145 | +656,92 |
| Неделя | 247 | 1 121 202 $ | ×111 | +583,17 |
| Сутки | 135 | 615 185 $ | ×59 | +310,86 |
Портфельная страница и поддержка MT4
Самая интересная часть — добавление портфельной страницы для нескольких стратегий. Задача: сделать публичную страницу /GoldCor с пятью торговыми счетами, где можно видеть общую статистику портфеля и проваливаться в каждую стратегию.
Первым делом снял авторизацию с сайта — данные публичные, незачем городить логин. Заголовок noindex оставил: быть открытым по ссылке и лежать в поиске — разные вещи.
Портфельная страница строится из тех же дампов: общие плитки сверху (итог портфеля, суммарный оборот), карточки стратегий с мини-статистикой, клик по карточке ведёт на детальную страницу стратегии.
И тут выяснилось главное: счета GoldCor — это MetaTrader 4, а не MT5. MT5-терминал физически не может войти в MT4-счёт — вот почему падали все попытки подключения, дело было не в пароле и не в сервере.
Пришлось добавить отдельный MT4-коллектор. Архитектурно это отдельный контейнер с MT4-терминалом под Wine, который работает параллельно с MT5-контейнером. Флаг платформы пробрасывается на страницу, чтобы корректно отображать специфику каждой платформы.
# collector.py — определение платформы
platform = os.environ.get('MT_PLATFORM', 'mt5') # 'mt4' или 'mt5'
if platform == 'mt4':
import MetaTrader4 as mt
else:
import MetaTrader5 as mtВ итоге пайплайн выглядит так:
- MT5-контейнер собирает данные основного счёта → публикует на
/ - MT4-контейнер собирает данные пяти счетов GoldCor → публикует на
/GoldCor/и подстраницы - Оба контейнера пишут дампы в общий volume
- Скрипт публикации читает все дампы и генерирует статику
Деплой в GitHub
Проект изначально не был в Git — второй вопрос из самого начала. Собрал .gitignore с учётом специфики: исключил дампы с торговыми данными (они генерируются на сервере), Wine-префикс, логи коллектора. В репозиторий вошли: шаблоны страниц, скрипты коллектора и анализа, docker-compose.yml, документация в hosting.md.
В hosting.md описал всю инфраструктуру: как поднять контейнеры, как настроить Caddy, ловушка про 401 с куками (если решат вернуть авторизацию). Документация рядом с кодом — это не бюрократия, это страховка.
Результат
За несколько итераций проект прошёл путь от «данные не обновляются четыре дня» до полноценного продукта:
- Коллектор работает стабильно, Unicode-баги устранены
- График показывает точные цены входа и выхода, клик по метке выделяет сделку
- Выбор периода пересчитывает всю статистику, а не только график
- Переключение таймфрейма не сбрасывает позицию на графике
- Клик по метке работает за 14 мс вместо 219 мс
- Портфельная страница
/GoldCorпоказывает пять стратегий с агрегированной статистикой - Поддержка MT4 добавлена как отдельный коллектор
- Авторизация убрана, данные публичны по ссылке
- Проект в GitHub с документацией
Выводы
Главный урок этого проекта — молчащие баги опаснее громких. UnicodeEncodeError в логе убивал процесс до записи файла, и снаружи это выглядело как «данные просто не обновляются». Никакого алерта, никакого очевидного симптома. Если ваш коллектор пишет результат в самом конце — добавьте промежуточные чекпоинты или хотя бы heartbeat-файл с временем последнего успешного запуска.
Второй урок: производительность на мобиле — это отдельная задача. 219 мс заблокированного потока на десктопе незаметны, на среднем телефоне это полный фриз интерфейса. Троттлинг CPU в DevTools — обязательный шаг при отладке интерактивных канвас-компонентов. И getComputedStyle в горячем пути — это всегда проблема, кешируйте палитру при инициализации.
Третий урок: индексы — это не универсальный язык. Когда у каждого таймфрейма своя система индексов, любое состояние, которое хранит «позицию» в массиве, нужно пересчитывать при смене массива. Звучит очевидно, но именно это стало причиной бага с застреванием на 29 июля.
Четвёртый урок, самый дорогой по времени: прежде чем отлаживать пароли и серверы — проверь платформу. MT5-терминал не войдёт в MT4-счёт ни при каких условиях. Восемь попыток с разными серверами RoboForex, прежде чем выяснилось, что проблема не в учётных данных, а в том, что это принципиально разные протоколы. Один вопрос в начале — «это MT4 или MT5?» — сэкономил бы несколько часов.
Ссылки:

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.