П/ВИН

Мониторинг торгового счета MT5: дашборд с нуля

·9 мин чтения

Брат скинул мне ссылку на свой трейдинг-дашборд с вопросом «почему данные не обновляются» - я думал, что это займёт час. Оказалось, что нормальный мониторинг торгового счета требует куда большего: в итоге получился полноценный продукт с живым интерактивным графиком, выбором периода, портфельной страницей для нескольких стратегий и поддержкой двух платформ - 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 на кадр15100

Причина: выбор сделки перестраивал весь журнал целиком, и на каждый элемент вызывался 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БаровПокрывает
M1560727.07 → 04.08
M16000 (потолок 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 раз. Меняется вместе с выбранным периодом:

ПериодСделокОборот×капиталаРезультат
Всё2631 447 121 $×145+656,92
Неделя2471 121 202 $×111+583,17
Сутки135615 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 в бизнес.