После запуска live-торговли: как мы стабилизировали профиль опционной позиции
Прошло всего два дня с предыдущего поста, но цикл итераций оказался плотным. На живой торговле опционов бот вёл портфель, а параллельно уходили в продакшн четыре изменения, которые подписчик увидит и почувствует — даже если бот никогда о них не «расскажет» в логах ордеров.
Белая линия payoff больше не прыгает
Если открыть вкладку Portfolio или Payoff Profile и обновлять её каждые пять минут, можно было увидеть, как линия «At expiry» (та, что фиксирует премию + страйки) дёргается на ±100 рублей. Источник дрожания был неожиданный: дашборд при каждом обновлении пересчитывал среднюю цену входа из плавающего окна последних 200 трейдов через Trades API. Окно сдвигалось — двигалась и линия, хотя позиции не менялись.
Решение получило шифр BUG-121. В базу данных добавлена отдельная таблица options_entry_prices, в которой хранится стабильная entry-цена для каждой открытой позиции. Логика лукапа теперь трёхуровневая:
- Сначала ищем в кэше с проверкой свежести (для строки из option_fills — нет ли более новых фактов, для fallback — TTL один час).
- Если кэш промахнулся, реконструируем цену из таблицы
option_fillsчерез FIFO open-lot: чем дольше лот живёт, тем раньше его закрывают противоположные фиксы. Buyback и принудительные ликвидации в реконструкцию не идут — они пишутся до подтверждения и не считаются авторитетным экономическим фактом. - Только если первые два пути не сработали, мы спускаемся к менее точному fallback из Trades API. Он отдельно помечается как
lower-confidenceв логах, и никогда не перезаписывает строку, рождённую из option_fills.
После переключения линия «At expiry» перестала прыгать между обновлениями при неизменной позиции. На пяти позициях по CRM6 цена входа стабильна между обновлениями, мы просто видим её три цикла подряд одинаковой: 0.4035, 0.0750, 0.1600, 0.2010, 0.3182. План прошёл семь итераций ревью у Codex (R1-R7) — это исключительно вспомогательная штука для дашборда, торговая логика бота к этой таблице не обращается.
Карусель безопасности: BUG-118, 119, 120
Кроме BUG-121 закрылись ещё три тонких бага, каждый из которых раньше мог тихо выпустить лишний ордер.
BUG-118 касался reconcile-цикла: бот сверял состояние с брокером и при этом терял entry_price ранее открытых позиций. После фикса entry_price переносится через сверку без потерь — это особенно важно для дашбордного P&L и для решений на следующей итерации.
BUG-119 ловил режим managed residual — ситуацию, когда у нас остаётся непарная нога после частичного закрытия. Раньше бот мог в этом состоянии открыть свежий лонг и ухудшить картину. Теперь свежие покупки в residual режиме блокируются явно: сначала выровняем то, что есть.
BUG-120 связан с другой ловушкой: позиция формально считалась covered orphan (например, осталась нога с противоположным значением дельты), и это блокировало любой новый вход. Фикс отделяет реальные orphans от «прикрытых» и возвращает возможность входа, когда риска нет.
Эти три бага по отдельности не катастрофические, но в сумме они закрывают типичный класс «бот делает что-то, чего не должен» в режимах, которые раньше встречались редко, а теперь, на живой торговле с несколькими позициями, стали появляться регулярно.
Логика «шапки прибыли над spot» сохраняется
Параллельно с фиксами мы явно подтвердили основную стратегию бота: он никогда не разрушает уже выстроенный профиль ради реакции на рынок. Бот не роллит позиции принудительно, не режет старые ноги, чтобы освободить маржу, и не закрывает безубыток ради нового сигнала. Новый спред добавляется только при одновременном выполнении трёх условий: в стакане есть свободные страйки, риск-бюджет не исчерпан, и все safety-gates (quality, post-trade admissibility, naked-short guard) дают зелёный свет.
Цель — постепенно «натягивать шапку» суммарного payoff’а над текущим spot, не разрушая то, что уже стоит. Все фиксы 16-17 апреля проверялись именно через эту инвариантность: бот не должен менять стратегию из-за улучшений инфраструктуры. Тестовая ночь подтвердила — за восемь часов ни одного forced-action, ни одного принудительного roll, портфель в лимитах риска.
Portfolio tab: отдельный портфельный вид
Раньше payoff-профиль рисовался единым полотном для всех CR-опционов вперемешку. С расширением до 4-5 одновременных позиций это перестало быть читаемым. На дашборде появилась отдельная вкладка Portfolio, где payoff строится для текущего срока экспирации с учётом всех позиций по этому underlying. Идея простая, но эффект существенный: владелец видит реальный портфельный риск-профиль, а не размазанную карту.
Связка с BUG-121 здесь буквальная: чтобы вкладка имела смысл, белая линия должна быть стабильной. Иначе каждое обновление выглядело бы как «портфель изменился», хотя по факту никаких сделок не было.
За кулисами: token service не падает
Это исправление подписчик не видит напрямую, но без него вся остальная история была бы невозможной. Token service на нашем production-сервере писал в один файл лог настолько подробно, что тот распух до 4.8 GB. В какой-то момент диск-операции начали блокировать асинхронный рантайм, процесс уходил в зомби, порт 8765 переставал отвечать — и тогда дашборд просто не мог получить свежие котировки и payoff-кривые.
Лечилось параллельно с двух сторон. В коде — убрали лишнюю детализацию tracing у файлового слоя. Снаружи — добавили logrotate с copytruncate на стандартные потоки и cron-скрипт со сжатием суточных файлов tracing-аппендера. Перед деплоем сразу вскрылся пропущенный нюанс: на сервере logrotate не был установлен. Поставили пакетом, перепроверили конфиг dry-run’ом, развернули.
После рестарта оркестратора ночь прошла спокойно: старые суточные файлы сжались (некоторые с 400 MB до 21 MB), новые растут со скоростью около 100 KB в минуту вместо нескольких сотен. За это время бот оставался active, JWT обновлялся по 15-минутному циклу, forced actions не появлялись.
Внутренняя оснастка для дальнейшей скорости
Параллельно мы укрепили внутренний инструментарий, которым пользуются агенты для разработки. Один из агентов несколько раз случайно убивал свою сессию через OOM, запуская cargo build без изоляции. Теперь это заблокировано на уровне харнесса: глобальный pre-tool hook требует, чтобы любой тяжёлый cargo-вызов был обёрнут в отдельный systemd-cgroup и ограничен по jobs. План прошёл шесть итераций ревью.
Дополнительно поправили транспорт ansible над VPN — он иногда зависал на длинных командах, мешая выкатке. Сейчас управляющие команды идут предсказуемо.
Эти изменения чисто внутренние, но без них фиксы из этого поста делались бы заметно дольше.
Что дальше
Бот сейчас удерживает пять позиций, риск-бюджет заполнен примерно на треть, дельта почти нейтральна (-0.40), форс-действий за последние сутки не было. Cron-сжатие суточных логов отработало автоматически уже за первую ночь после деплоя.
Ближайшие задачи на следующие итерации — продолжение работы по multi-expiry, расширение per-position IV-моделей и подготовка ML-фичей для регимов волатильности. Но это уже история для отдельного поста.
Полный live-дашборд по-прежнему доступен по адресу bot.majotrade.net.