ГлавнаяБлогКэш-баг: почему сравнение с базовой линией ломается
Python

Кэш-баг: почему сравнение с базовой линией ломается

Узнайте, как кэширование базовой линии ломает сравнение при внешних изменениях. Практический пример на Python и фикс.

Al
Редакция Algolitalgolit.ru
7 мин чтения30 августа 2026 г.

Кэш-баг: как сравнение с базовой линией ломается при внешних изменениях

Представьте: ваша программа внезапно закрывает все позиции и прекращает работу, хотя ничего не сломалось. Причина — кэшированная базовая линия, которая перестала соответствовать реальности. Разберём этот баг на примере торгового робота и покажем, как его исправить.

Код, который выглядит правильным

Программа защищает от дневных убытков: сохраняет базовую линию на старте дня и сравнивает с текущим значением. Вот типичный код:

day_pnl = current_equity - day_start_equity
if day_pnl <= -daily_loss_limit:
    close_everything()
    stop_for_the_day()

Этот код корректен для измерения торговых результатов, но полностью ошибочен, если стоимость актива меняется по другим причинам — например, при выводе средств. Equity падает на 500, day_pnl показывает -500, и программа решает, что день катастрофический, хотя позиции ни в чём не виноваты. Депозит даёт зеркальную проблему: цель по прибыли достигается деньгами, которые просто добавили.

Суть бага

Базовая линия и измеряемое значение молча перестали означать одно и то же. day_start_equity — это стоимость счёта до сегодняшней торговли, а current_equity — текущая стоимость. Их разница равна торговому результату, только если торговля — единственное, что меняет число. Никто не записал это допущение. Оно было верным в момент написания кода и оставалось верным, пока не перестало.

Это не торговый баг. Это баг, который возникает, когда вы кэшируете опорную точку и сравниваете с ней живое состояние, а другой актор может менять базовое значение вне полосы. Примеры: лимитеры скорости, хранящие стартовый счётчик и сбрасываемые административно; сигнализации дискового пространства, базирующиеся на загрузке, после чего кто-то монтирует том; прогресс-бары, вычисляющие процент от изменяющегося общего значения.

Исправление

Нельзя остановить изменение значения. Можно обнаружить внешнее изменение и сдвинуть базовую линию на ту же величину, чтобы разница продолжала измерять то, что вы имели в виду.

shift = detect_out_of_band_change()  # +500 депозит, -500 вывод

day_start_equity += shift
peak_equity += shift
initial_balance += shift  # все базовые линии, не только очевидные

В нашем случае платформа фиксирует переводы как отдельный тип записей, так что обнаружение — это сканирование истории по этому типу. Конкретно:

# MQL5 код для детекции
long type = HistoryDealGetInteger(ticket, DEAL_TYPE);
if (type == DEAL_TYPE_BALANCE || type == DEAL_TYPE_CREDIT)
    sum += HistoryDealGetDouble(ticket, DEAL_PROFIT);

Сдвигаем базовые линии — и измеряемый результат не меняется от перевода, что и было нужно.

Три вещи, которые сделали это production-ready

  • Сканируем только при изменении. Сканирование истории на каждом тике расточительно. Баланс меняется только при закрытии сделки или переводе, так что это изменение — триггер; в остальных случаях пропускаем сканирование.
  • Ограничиваем сканирование. Запрос всей истории аккаунта замедляется на старых счетах. Всё, что было до сегодняшнего дня, уже учтено в базовой линии по определению, так что сканирование покрывает только текущий день.
  • Отключаем в симуляции. В бэктестах нет денежных переводов, а посимвольное сканирование истории тысяч записей превращает оптимизацию в перерыв на кофе.

Часть, которую я чуть не испортил

Моей первой мыслью было исправить всё, что читает баланс. Это была бы вторая ошибка. Размер позиции — процент от баланса. Если половину счёта вывели, сумма, которую вы можете рисковать, действительно должна уменьшиться вдвое. Это не искажение, а правильная реакция на уменьшение денег. Только рисковые базовые линии нужно было сдвигать — те, что измеряют изменение во времени. Всё, что измеряет текущую способность, уже было верно.

Стоит спросить при любом таком фиксе: какие из этих значений измеряют изменение, а какие — состояние? Только первый тип нуждается в корректировке.

То, что вас поймает

Программа сохраняет базовые линии, чтобы перезапуск в середине дня не сбрасывал их. Итак: переводите деньги, пока она остановлена, запускаете снова — и она верно восстанавливает базовую линию до перевода, а затем мгновенно срабатывает на фикс, который в остальном работает идеально.

Сверка должна происходить и при запуске, и во время работы. Я нашёл это только логически пройдя путь перезапуска, а не тестированием, что само по себе маленький урок.

Проверьте это

Направьте программу на демо-счёт, выведите небольшую сумму и убедитесь, что измеренный результат дня не изменился. Две минуты. Я никогда не запускал этот тест, и, подозреваю, почти никто из тех, кто выпускает подобный софт, тоже.

Реализация на MQL5 доступна на GitHub под лицензией MIT: mql5-balance-operations-guard. Я пишу о создании автоматических торговых систем на goldscalpers.com.

#кэширование#базовая линия#торговые роботы#отладка#Python
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →