Журнал, который нельзя подделать
Каждое движение капитала, каждый апрув и каждая смена режима пишутся в журнал с хеш-цепочкой. Зачем это нужно в трейдинг-софте и как проверить целостность за полминуты.
В финансовом софте есть один сорт багов страшнее остальных: не падение, а тихая поломка. Алгоритм продолжает торговать, дашборд по-прежнему рисует кривую, а в фоне кто-то — баг, гонка, ошибка сериализации — переписал прошлую запись задним числом. Поймать такую поломку через неделю нельзя: вы не знаете, какие цифры были настоящими и какие подделанными.
Аудит-журнал в inite.fund устроен так, чтобы исключить ровно этот класс сбоев. Не нашуметь после факта, а в принципе не позволить переписать прошлое.
Append-only по конструкции
Все таблицы аудита — audit_log, capital_flows, mode_history,
hil_decisions — работают по принципу «только дописывать». UPDATE по
этим таблицам запрещён на уровне триггеров БД. DELETE разрешён только
для самой свежей записи и только если она помечена как pending.
Старше суток ни одна строка не может быть изменена даже админом.
Звучит как ограничение, но на практике это освобождение. Если строка в журнале однажды записалась — она там навсегда. Никаких «а тут мы поправили вчерашнее значение, потому что увидели опечатку». Опечатка живёт рядом с исправлением, и обе видны.
Хеш-цепочка
Каждая строка журнала хранит, помимо данных, два дополнительных
поля: prev_hash и row_hash. row_hash — это SHA-256 от
конкатенации всех значимых полей текущей строки плюс prev_hash
предыдущей. То есть каждая запись подписывает не только себя, но и
всё, что было до неё в этом журнале.
Дальше арифметика простая. Если кто-то меняет одну строку в
середине, у неё ломается row_hash. Чтобы это спрятать, нужно
пересчитать все row_hash после неё. Чтобы пересчитать — нужно
поднять флаги триггеров на уровне БД. Чтобы их поднять — нужен
суперпользователь postgres. И каждое такое поднятие пишется в
системный аудит postgres, который мы тоже храним.
Цепочку можно проверить любым внешним инструментом за один запрос.
Берём первую строку журнала, считаем её row_hash руками, сравниваем
с записанной. Берём следующую, считаем уже с учётом prev_hash,
сравниваем. До конца. Если хоть одна не сошлась — журнал тронули.
Скрипт укладывается в 20 строк, мы его запускаем в каждом ночном
бэкапе.
Идемпотентность через audit_ref
У всех капитальных операций — move_to_long, move_to_trading,
approve_trade, set_mode — обязателен audit_ref. Это уникальный
строковый ключ, который генерирует вызывающая сторона ещё до запроса.
БД проверяет, что записи с таким ref ещё нет, и только тогда
применяет операцию.
Зачем это нужно. Сетевой сбой посередине вызова — обычное дело.
Клиент отправил запрос, БД его применила, ответ не дошёл, клиент
ретраит. Без audit_ref второй вызов проходит, и одна и та же
ребалансировка случается дважды. С audit_ref второй вызов получает
ошибку «уже применено» с той же row_hash, что и первый, и клиент
видит, что ничего нового делать не нужно.
В терминах последствий это значит, что капитальные операции в inite.fund безопасны для ретраев на любом уровне. Можно ретраить из сетевого слоя, из MCP-клиента, из ручного скрипта оператора — журнал останется консистентным.
Что это даёт оператору
Три вещи. Первое — полная история движения средств с разбиением по причинам и инициаторам. На любую цифру в дашборде вы можете перейти к конкретной строке аудита и увидеть, какой вызов её породил, когда, от чьего имени и с каким обоснованием.
Второе — внешняя проверяемость. Если когда-то понадобится показать регулятору, аудитору или партнёру, что данные не правились задним числом, у нас на это есть скрипт и формат экспорта. Это не «доверьтесь нам, мы хорошие». Это математическое свойство журнала, которое можно проверить независимо.
Третье, и для нас самое важное, — психологическая разгрузка. Когда вы знаете, что любая операция оставила след и след не подделывается, кончается отдельный класс беспокойства. Не нужно держать в голове, что система могла по-тихому решить за вас и спрятать решение. Если в журнале пусто — действия не было. Если в журнале есть запись — она точно такая, какая была в момент совершения.
Какой ценой
Каждая запись в журнал — это плюс один INSERT, плюс один SHA-256,
плюс одна проверка уникальности audit_ref. По нашим замерам это
40-60 микросекунд на операцию. Для торгового движка с тактом 200
миллисекунд между тиками это шум. Для нагруженного брокерского API
было бы критично, но мы не брокер.
Хранение растёт примерно на 4-6 КБ на сделку. За год живой работы торговой стратегии — порядка 20-40 МБ журнала. Для портфельной части на порядок меньше, потому что ребаланс раз в неделю. Это бесплатно по любой шкале современных дисков.
В сумме: операционная стоимость близка к нулю, выигрыш — невозможность тихо переписать прошлое. Редкий случай в архитектуре, когда не приходится выбирать.
— inite team
- 2026-04-19Своя стратегия — свой ключ
Мы выкинули модель глобального торгового юзера. Каждая стратегия теперь читает ключи бирж из vault'а своего владельца.
- 2026-04-22Нелинейный торговый баланс
Почему торговому NAV разрешено быть больше аллокации — и почему мы держим их в разных БД.
- 2026-05-09Когда движок останавливается
Алгоритм работает сам. Но в трёх случаях он спрашивает оператора, и ещё в одном просто ложится. Разбираем, как устроены HIL-очередь, режимы стратегии и kill switch и где между ними проходит граница.