Своя стратегия — свой ключ
Мы выкинули модель глобального торгового юзера. Каждая стратегия теперь читает ключи бирж из vault'а своего владельца.
Старая модель: один словарь settings["exchanges"] в БД, в нём ключи
Binance + OKX, используется всеми. Любой залогиненный админ мог
положить туда свои ключи, и торговый движок ходил по ним из каждой
стратегии — длинных, уровней внутри дня, paper, live.
Это ломается, как только в команде два оператора. Ломается сильнее, когда заходит речь о третьем. Хуже всего ломается на инцидентах: ротация ключей значит остановку каждой стратегии разом.
Новая модель
Две колонки и одна новая таблица:
PortfolioMeta.owner_user_id— каждая длинная стратегия принадлежит ровно одному пользователю.TradingAccount(в торговой БД) — каждая стратегия торгового класса принадлежит ровно одному пользователю и хранит у себя биржу и метку ключа.UserCredential— зашифрованная запись (Fernet), по одной на (пользователь, провайдер, метка).
Когда движок хочет торговать paper_levels_bob, он зовёт
_strategy_creds:
def _strategy_creds(strategy_id, provider):
# 1. Trading account?
ta = TradingAccount.find(strategy_id)
if ta and ta.owner_user_id:
return load_credential(ta.owner_user_id, ta.venue_provider, ta.credential_label)
# 2. Portfolio strategy?
pm = PortfolioMeta.find(strategy_id)
if pm and pm.owner_user_id:
return load_credential(pm.owner_user_id, provider, "default")
# 3. No owner → no creds. Engine logs warning and skips.
return {}
Никаких запасных вариантов. Стратегия без владельца не торгует. Движок
пишет в лог WARNING: no credentials for strategy X и тихо пропускает
тик — никаких ордеров, позиций и сюрпризов.
Что это даёт
Подключение пользователя теперь — это админская форма из четырёх шагов: завести пользователя → привязать стратегию → пользователь кладёт ключи в своё хранилище → админ зачисляет кэш. Каждый шаг обратим.
Ротация ключей — на уровне пользователя. Боб отзывает свой ключ Binance на стороне биржи; заходит в /account, удаляет старый ключ, генерирует новый, сохраняет. На следующем тике стратегия подхватывает новый ключ. Без рестарта, без участия админа, без влияния на чужие стратегии.
Реакция на инцидент, когда оператор скомпрометирован: отзываем его токен (MCP перестаёт работать), ротируем его ключи бирж (удаляем из хранилища) — и его стратегии уходят в деградированный режим. Радиус поражения — один пользователь.
Изоляция торгового баланса
Хелпер compute_user_balance(user_id) джоинит обе БД и возвращает
только дерево этого пользователя. Глобального представления «AUM
фонда» по умолчанию нет — на маркетинговом сайте мы его показываем
(Total AUM = сумма публичных стратегий), но именно ради этого случая
у нас и заведён флаг is_public=True. Внутренние операционные
дашборды показывают только ваш кусок.
Почему без запасных ключей
Мы рассматривали временный запасной путь на settings["exchanges"]
ради обратной совместимости. Убрали ещё до выкатки. Причины:
- Запасной путь — опасное удобство. Операторы кладут «дефолтные» ключи «просто для paper-тестов», забывают про них, и потом реальная стратегия без владельца их подбирает.
- Поведение движка должно быть скучным. «Стратегия без владельца ничего не делает» — скучно. «Стратегия без владельца уходит на глобальные ключи» — требует помнить про запасной путь, чтобы предсказать поведение.
- Миграция — пара строк SQL. Старым стратегиям выставили
is_public=Trueради публичной видимости в режиме только-чтения: у них остались только публичные данные, без владельца и без права торговать. Новые админы назначают владельцев явно.
Что видит маркетинговый сайт
Публичный эндпоинт /api/public/strategies/{id} перед отдачей
вырезает owner_user_id, внутренности control, сигналы и причину
срабатывания kill-switch’а. На inite.fund/strategies вы видите ровно
это: equity + просадка + активы + последние 50 сделок + последние 365
точек equity. Без идентификаторов, без внутреннего состояния, без
секретов.
Astro-страницы рендерятся на сервере, дёргают этот эндпоинт и кешируются на эдже на 30 секунд. Обновите страницу дважды подряд — увидите те же числа; через 30 секунд они обновятся. Это и есть наш «real-time»: достаточно близко для страницы с историей результатов и достаточно далеко от origin’а, чтобы он не лежал под трафиком.
Сколько это стоило
Рефакторинг затронул 12 файлов в продуктовом репо, 2 новые таблицы, разовую миграцию при старте, ~600 строк кода и ~70 новых тестов. Тесты покрывают резолвинг владельца, зашифрованное хранилище, движения капитала с идемпотентностью и MCP-вызовы с ролевыми проверками.
До: 1004 зелёных теста. После: 1180. Ни одной регрессии.
Маркетинговый сайт (этот самый) построен на той же модели. Полоса
метрик наверху? Это compute_user_balance(NULL) — агрегат по
публичным стратегиям, забирается на сервере, рендерится в HTML. Ни
один кусок приватных данных через границу не уходит.
- 2026-05-11Журнал, который нельзя подделать
Каждое движение капитала, каждый апрув и каждая смена режима пишутся в журнал с хеш-цепочкой. Зачем это нужно в трейдинг-софте и как проверить целостность за полминуты.
- 2026-04-22Нелинейный торговый баланс
Почему торговому NAV разрешено быть больше аллокации — и почему мы держим их в разных БД.
- 2026-05-09Когда движок останавливается
Алгоритм работает сам. Но в трёх случаях он спрашивает оператора, и ещё в одном просто ложится. Разбираем, как устроены HIL-очередь, режимы стратегии и kill switch и где между ними проходит граница.