На главную

Как обновление pandas молча обнулило бэктесты TrendX

pandas 3.0 незаметно поменял поведение to_datetime — и бэктесты TrendX стали тихо возвращать ноль сделок без единой ошибки или warning'а.

Самые опасные баги — не те, что падают с трейсбеком. Трейсбек хотя бы показывает, куда смотреть. По-настоящему неприятны баги, которые ничего не ломают на виду: скрипт отрабатывает, выходит с кодом 0, просто результат оказывается тихо неправильным. Такие баги особенно любят прятаться в апгрейдах зависимостей — библиотека выходит с новой мажорной версией, changelog обещает “улучшения по умолчанию”, а что именно изменилось в поведении под капотом, узнаёшь только когда что-то у тебя перестаёт находить то, что раньше находило. Именно такой мы поймали в TrendX — нашей системе трендовой торговли на крипто-фьючерсах, одном из пяти ботов Challenge.

Симптом без единой ошибки

Понадобилось перезапустить на сервере одну из бэктест-методик (v9cz) — рутинная операция, делали её десятки раз. Скрипт отработал штатно. Результат: 0 сделок. Ноль — на любом окне train/OOS/walk-forward, без единого исключения, предупреждения или строчки в логе, которая намекала бы, что что-то не так. С точки зрения самого скрипта всё было в порядке — просто ни одна сделка не прошла фильтр по времени.

Откуда взялась рассинхронизация

Причина оказалась не в торговой логике, а в библиотеке под ней. В venv TrendX стоял pandas 3.0.3, где pd.to_datetime(..., unit='ms') по умолчанию стал возвращать datetime64[ms, UTC] вместо datetime64[ns, UTC], как раньше. Мелочь на первый взгляд — другая единица измерения времени внутри одного и того же типа данных.

Но вся линия v9-скриптов (trendx_rule_based_funding_carry_v9cz.py и вокруг 90 файлов той же схемы) сравнивает временные окна через .astype(int64), ожидая на выходе наносекунды — именно в этом масштабе работает pd.Timestamp(...).value, с которым идёт сравнение. Когда единицы разошлись, .astype(int64) стал отдавать миллисекунды вместо наносекунд. Мы это воспроизвели отдельно: одна и та же метка в одну секунду до фикса превращалась в 1000, после — в правильные 1000000000. Разница в миллион раз означает, что любое сравнение “эта сделка попадает в тестовое окно?” стабильно давало “нет” — вот и нулевые сделки без единой ошибки.

Отдельно проверили живого демона (trendx_live_daemon.py) — он pandas вообще не импортирует, баг его не касался. А вот trendx_pair_discovery.py, который pandas использует, уже сразу после to_datetime явно приводил результат к .astype("datetime64[ns, UTC]") — и поэтому бага у него не было изначально, апгрейд pandas для него ничего не менял.

Почему не переписали 90 скриптов

Два пути на выбор: явно проставить нужный тип datetime в каждом месте, где это важно (порядка 90 файлов), или закрепить версию pandas ниже 3.0 на уровне зависимостей всего проекта. Задача была низкоприоритетная — багу не блокировал текущую работу, только будущие бэктест-исследования — поэтому переписывать десятки файлов ради несоразмерно маленькой проблемы не стали. Понизили pandas 3.0.3 → 2.3.3 в общем venv TrendX одной командой.

У TrendX до этого вообще не было requirements.txt — зависимости жили неявно, кто как поставил. Теперь файл появился, с пином pandas<3.0 и комментарием, объясняющим причину — чтобы при следующей пересборке окружения кто-нибудь случайно не откатил фикс обратно, просто поставив последнюю версию по умолчанию.

Как проверили, что это действительно чинит

Правка библиотеки — это ровно тот тип фикса, который легко “вроде бы починил” и не заметить, что нет. Поэтому проверили не по ощущениям: тот же однобайтовый тест с меткой в одну секунду после понижения версии стал давать значение, совпадающее с Timestamp.value. Дальше прогнали trendx-pair-discovery.service живьём — вышел с кодом 0 и вернул реальные OOS-метрики по нескольким парам (например ETHFIUSDT: profit factor 0.87, просадка 52%; XMRUSDT: profit factor 0.95, просадка 45%) — не молчаливые нули, а настоящие цифры, в том числе не самые радужные, что тоже хороший знак: система не просто перестала падать в ноль, она честно считает.

Похожая история про тихий сбой мониторинга уже была на другом боте того же челленджа — можно почитать про то, как Snipe потерял 99.78% из-за одного бага в стоп-лоссе. Разные причины, один и тот же урок: в алготрейдинге “скрипт не упал” и “результат правильный” — не одно и то же, и разница между ними стоит реальных денег.

Текущий счёт всех пяти ботов Round 2, включая TrendX, — на странице Challenge, обновляется в реальном времени.