Перейти к содержимому
VC
Кейс 21 из 28 · Python · Данные

Автономные боты и проверка на истории: инженерия надёжности

Собственная внутренняя разработка в NDA-домене «финтех»: десяток автономных сервисов, которые принимают решения в реальном времени по потоку внешних событий. Кейс не про доходность — про инженерию: сбор данных с прослеживаемым источником, фоновые службы, которые не умирают тихо, проверка на реальной истории вместо веры в красивую кривую, и риск-контур, который выключает систему раньше человека.

Отрасль
Финтех (NDA) · собственная разработка
Стек
Python · asyncio · WebSocket · DuckDB / SQLite
Формат
Собственный инструмент, не клиентский проект
Итог
Тихие отказы стали видимыми
01 · Боль

Автономная система ломается тихо

У фонового процесса нет пользователя, который пожалуется. Он продолжает крутиться, писать логи и честно рапортовать «жив» — при том что данных он уже не получает, а решения принимает по устаревшему состоянию. Между поломкой и её обнаружением проходят часы, и всё это время система выглядит здоровой.

Три реальных сценария из этого проекта. Первый: внешний API поменял одно слово в названии инструмента — парсер перестал распознавать часть потока, поломку заметили спустя 10 часов. Второй: сокет открыт, исключений нет, события идут — но события одного конкретного типа не приходят уже 20+ минут; общий сторожевой процесс, следивший за «последним событием вообще», молчал, потому что остальные типы продолжали идти. Третий: сторожевой процесс часами писал restarts=0 failures=0, пока два процесса были мертвы — pgrep -f без якоря матчил командную строку соседних шеллов, включая собственную.

И четвёртый класс, самый дорогой: система не падает, а тихо врёт. Офлайн-симулятор определял исход сделки по неверному моменту фиксации — совпадение с реальным правилом расчёта оказалось 51%, то есть подбрасывание монеты. Всё, что было построено поверх этого симулятора, месяцами выглядело как аналитика, а было шумом. Пока никто не проверил само правило расчёта, отличить одно от другого было нечем.

02 · Решение

Пять контуров, каждый проверяется отдельно

Система принятия решений в реальном времени — это не «алгоритм». Это пять независимых контуров: сбор данных, живучесть демонов, наблюдаемость, офлайн-проверка гипотез и риск-контур. Ломается любой из них по отдельности, поэтому каждый должен быть проверяем без остальных.

01
Сбор

Основной поток событий плюс догоняющий опрос раз в 5 с. Каждая запись в базе помечена источником, временем и тем, кто её загрузил

02
Фоновые службы

Свой запускатор на Python с двойным ветвлением вместо запуска из оболочки, systemd с автоперезапуском и общим журналом

03
Наблюдаемость

Сторожевой процесс отдельно на каждый тип события, проверка на изменение формата данных у внешнего сервиса, сводка в мессенджер

04
Проверка на истории

Прогон по историческим записям: реальные движения цены, отсев повторов, разбивка по режимам рынка

05
Риск

Многослойная проверка по расписанию, файл-метка полной остановки как аварийный выключатель, возврат в работу только через скрипт

Bash не отцепляет демон на macOS — это проверено, а не мнение

Перезапуск процесса из скрипта выглядит тривиально ровно до первого зависания. Пять вариантов фонового запуска были проверены эмпирически, и все пять оставляли родительский шелл висеть в __wait4 на якобы отцепленном потомке — один процесс провисел так 1 ч 32 мин:

  • nohup … & disown — классика, которая не работает
  • set -m + disown, запуск в подоболочке, вложенные варианты форка — тот же результат
  • в macOS нет setsid(1), поэтому «правильного» шелл-способа просто не существует

Решение — один общий запускатор на Python: fork → setsid → fork → execvp, внук гарантированно получает PPID=1. Все перезапуски в проекте идут только через него; для задач под macOS дополнительно выставлен AbandonProcessGroup, иначе система прибирает потомков вместе с завершившимся скриптом.

Живучесть меряется по каждому типу события отдельно

Базовый сторожевой процесс следит за временем последнего события любого типа и останавливает сервис, если тишина длится дольше 360 секунд — порог подобран эмпирически, на 180 с ловились ложные срабатывания на штатных паузах цикла. Служба управления поднимает сервис обратно, поэтому сторожевой процесс может позволить себе просто выйти с особым кодом возврата.

Но это лечит только полную смерть соединения. Реальный инцидент был тоньше: перестал приходить один тип события, остальные текли как обычно — и общий счётчик тишины не сработал ни разу. Отсюда правило, которое теперь применяется ко всем сервисам: liveness — это не «процесс жив», а «когда я последний раз получил то, ради чего я живу». У каждого критичного типа события свой счётчик, в отчёт о состоянии выводится «сколько минут назад» по каждому потоку, плюс превентивная переподписка по таймеру.

Проверка «процесс жив» должна быть честной

Шаблоны pgrep -f обязаны быть заякорены на конец аргументов (…/book_recorder\.py$). Без якоря проверка матчит любой соседний шелл, у которого искомая строка оказалась в командной строке — включая сам вызывающий скрипт. Мониторинг, который сам себя считает живым процессом, хуже отсутствия мониторинга: он выдаёт зелёный статус на мёртвую систему.

Сначала SQL и происхождение каждой строки

Правило для всего нового кода: данные пишутся в базу, а файлы JSONL допустимы только как скользящий буфер с ретеншеном ≤48 ч и ежедневной ротацией. У каждого датасета — строка в data_provenance (источник, статус данных, заметки аудита, можно ли удалять), у каждой записи — ingested_at и ссылка на исходный датасет. Повод был предельно бытовой: аудит нашёл десятки гигабайт дубликатов, которые никто не мог удалить, потому что никто не мог доказать их происхождение.

  • живые писатели открывают соединение на flush и сразу закрывают — блокировка держится миллисекунды, а не всё время жизни процесса
  • база с непрерывной записью — single-writer; аналитические подключения только read_only=True
  • новый живой писатель получает отдельный файл базы, а не делит его с существующим

Записи о решениях не удаляются никогда

Постмортем, из которого выросло отдельное правило: около пяти недель истории решений исчезли под давлением на диск — штатная ротация архивов снесла файлы, которые невозможно восстановить. Ответ — хранилище, куда можно только дописывать: все записи о входах и закрытиях собираются из живых прогонов и из архивов, дедуп по хешу строки (операция идемпотентна, можно гонять хоть каждые 15 минут), синхронизация на офсайт-копию. Главное правило вшито в код чистки: харвест выполняется до любого удаления, и если он не удался — удаление отменяется целиком. Диск дешевле данных, всегда.

Событийная реакция вместо опроса по таймеру

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

  • подключение и подписка — ~40 мс; запрос детали — ~26 мс; чтение внешнего состояния — ~35 мс; отправка — ~41 мс
  • криптографическая подпись — 0,24 мс после перехода с реализации на чистом Python на встроенную в систему; до этого она была на три порядка дороже и съедала весь выигрыш
  • фоновые кэши и параллельные запросы вместо последовательных ожиданий — ещё десятки миллисекунд
  • быстрый путь целиком — 250–350 мс; ниже без смены архитектуры не опуститься, дальше упор в сеть
03 · Стек

Ничего экзотического — всё держится на дисциплине

Python 3 · asyncio

Приём потока событий, циклы принятия решений, сторожевые задачи рядом с рабочим кодом

WebSocket, запасной путь через REST

Реакция на событие — основной путь, опрос раз в 5 с — только чтобы догнать пропущенное

DuckDB / SQLite

Основное хранилище — база; у каждой записи есть источник; пишет один процесс, аналитика читает только на чтение

systemd · Restart=always · journald

10 сервисов на одной машине, перезапуск без ручных действий, журналы в одном месте

launchd и запускатор с двойным ветвлением

Фоновые задачи на macOS, которые гарантированно не умирают вместе с терминалом

Cron и таймеры

Синхронизация хранилища записей, определение исходов сделок, регулярные сверки

Скользящий буфер JSONL (≤48 ч)

Только там, где нужен сырой поток; всё остальное — в базе

Регрессионные тесты на каждое исправление

Ошибка закрывается тестом, который бы её поймал, — иначе она вернётся

Сводка в мессенджер

Состояние всех сервисов и «сколько минут назад» по каждому потоку событий

Журнал инвариантов (I-NNN)

Правила с датой доказательства, инцидентом и условиями отмены

PythonasyncioWebSocketDuckDBSQLitesystemdjournaldlaunchdcrondouble-forkwatchdogdata_provenance
04 · Результат

Система не стала непадающей — она стала честной

Порог обнаружения тихого отказа
10 ч 6 мин

порог по каждому типу событий и проверка на изменение формата данных

Сервисов под супервизором
10

на одном хосте, Restart=always + journald, перезапуск без человека

Точность офлайн-симулятора
51% 100%

совпадение с реальным правилом расчёта после исправления — на всей проверенной выборке, она пока небольшая

Журнал инвариантов вместо устной памяти

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

Проверка на истории, которая не льстит

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

  • сначала метрики без допущений — распределения и доли достижения уровней; если движения нет вообще, никакая настройка выходов не поможет
  • дедуп реплик: один и тот же сигнал размножается по конфигурациям примерно в 12 раз — без дедупа выборка фиктивно раздувается
  • сегментация по режиму рынка и по типу сигнала; смешанное число прячет, что система выигрывает в одном режиме и проигрывает в другом
  • границы для неоднозначных случаев: если порядок событий внутри бара неизвестен, считаются оптимистичная и пессимистичная оценки; вывод принимается, только если он одинаков в обеих
  • контроль случайным входом: та же логика выхода на случайных входах должна проигрывать — иначе «эффект» создан не сигналом, а механикой выхода
  • n ≥ 30 в каждой ячейке, out-of-sample и walk-forward, поправка на множественную проверку гипотез

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

Риск-контур выключает раньше человека

Многослойный монитор ходит по расписанию и проверяет несколько независимых условий остановки. Останов оформляется файлом-маркером, который видят все процессы, — это простой и надёжный kill-switch без сетевой зависимости. Дальше действует дисциплина: маркеры остановки не редактируются руками, возврат в работу — только через штатный скрипт, который проверяет причину останова. И отдельное правило для новых фильтров: сначала audit-режим, где фильтр только логирует своё решение, и лишь после накопления данных он получает право блокировать.

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

05 · Применимость

Где ещё ложится та же методология

Это кейс не про трейдинг. Это типовая задача «автономный процесс принимает решения по потоку внешних событий, и никто не жалуется, когда он ломается». Контур переносится почти без изменений:

  • Интеграции с внешними API, где формат меняется без предупреждения — canary на дрейф схемы вместо разбора инцидента постфактум
  • Фоновые обработчики и очереди, где «процесс жив» ≠ «работа идёт»: liveness по типам событий, а не по PID
  • Машинное обучение и аналитика, где расчёты на исторических данных расходятся с рабочей системой — сначала проверяется правило расчёта целевого показателя, потом сама модель
  • Автономные AI-агенты, которые тратят деньги или совершают действия — им нужны тот же аварийный выключатель, режим наблюдения и лимиты
  • Наборы данных под аудит — записанное происхождение каждой строки единственный способ через полгода ответить, откуда взялась цифра в отчёте
  • Телеметрия, IoT, логистика — тот же класс: поток событий, узкие окна реакции, отказы без единого сообщения об ошибке
Что переиспользуется на следующих проектах
  • Запускатор с двойным ветвлением и готовые описания служб для systemd и launchd — фоновая задача действительно отцепляется от терминала
  • Сторожевой процесс на каждый тип событий, проверка на изменение формата данных у внешнего сервиса и отчёт о состоянии со временем последнего события
  • Слой хранения на базе SQL: у каждой записи есть источник, соединение открывается только на запись, чтение отделено от записи
  • Хранилище только на дозапись: данные собираются до любой чистки, при сбое сбора чистка отменяется, копия хранится отдельно
  • Методика офлайн-проверки: дедуп, сегментация, границы для неоднозначных случаев, контроль случайным входом
  • Формат журнала инвариантов: дата доказательства, инцидент, как применять, когда правило умрёт
Похожая задача?

Если процесс работает без надзора, вопрос не «упадёт ли», а узнаете ли вы об этом

Начинать имеет смысл с двух измерений: через сколько минут вы узнаете о тихом отказе и считает ли офлайн-оценка то же самое, что прод. Обычно этих двух проверок хватает, чтобы найти первые две-три дыры — до того, как они найдутся сами.

Готовы начать?

Аудит за 5 000 ₽ — с конкретным отчётом и сметой

Расскажу что внедрить в вашем бизнесе в первую очередь, какая будет окупаемость, и нужен ли вообще AI для вашей задачи (иногда — нет).

Или просто напишите свой вопрос — отвечу в течение 2 часов