Skip to content

RTDS Toxic-Flow Activation Runbook (pm-engine) ​

Операторский playbook пошаговой активации RTDS toxic-flow детектора в pm-engine. Это процедура (staged rollout), а не модель: контракт, knob-таблица и метрики — в SSOT ../integrations/rtds.md. Здесь только последовательность включения.

В pm-engine RTDS-соединение поднимается по потребителю в config.db (решение оператора 2026-08-31, env-переключателя нет), toxic-flow cancel — за двумя config.db-гейтами (fail-safe):

  • RTDS-соединение включается тем, что активная строка стратегии называет потребителя: stop_loss.confirm_source = "rtds_actor_buffer" либо присутствующая секция directional.toxic_flow. Ни того ни другого — соединение не открывается. env для этого переключателя ЗАПРЕЩЁН (ADR-015); заданная ENGINE_RTDS_ENABLED отклоняется на старте как снятая переменная.
  • Toxic-flow cancel гейтится двумя knob'ами в том же блоке directional.toxic_flow: log_only (Gate 1) → active_cancel_enabled (Gate 2). Оба по умолчанию SAFE (log_only=true, active_cancel_enabled=false). env для этих knob'ов ЗАПРЕЩЁН (ADR-015) — их нет в env, они не читаются.

Строгий порядок: не открывать Gate 2 до завершения baseline-обкатки и чистого прогона Gate 1.

Related:

  • ../integrations/rtds.md — модель, knob-таблица, observability (SSOT).
  • crates/pm-engine/src/adapters/rtds/ — реализация (toxic_flow.rs детектор, toxic_flow_consumer.rs гейтинг).

0. Как менять toxic_flow knob'ы ​

Блок directional.toxic_flow — вложенный объект внутри опубликованной версии definition стратегии в config.db. После schema v3 менять его можно только полной типизированной версией через dashboard Strategies editor или pm-cli strategy versions create; затем версию нужно явно опубликовать. Прямая правка строки и legacy field update запрещены.

Reload: движок читает config.db при старте; hot-reload нет. Любая правка knob'а вступает в силу только после рестарта pm-engine (scripts/deploy-vps.sh engine-bot или dashboard Reset — см. local-dashboard-runbook.md §Reset).

1. Pre-flight — включить ingestion ​

БЛОКИРУЮЩАЯ НЕСОВМЕСТИМОСТЬ — проверить ДО шага 1. Пара «stop_loss.flow_sell_share_min_permille задан в config.db» + «stop_loss.confirm_source = "rtds_actor_buffer"» — это инвариант типизированной секции (ConfigError::StopFlowShareWithRtdsConfirmSource, config/loader.rs), а не предупреждение: доля продаж считается по анонимной ленте store'а, тогда как rtds_actor_buffer берёт подтверждающие принты из actor-буфера RTDS, то есть кандидата судили бы числа ЧУЖОЙ ленты (design.md D5 изменения lossless-stop-confirm-trade-window). Проверку проходят и загрузчик движка на старте, и путь сохранения ПОЛНОГО definition (pm_engine::config::versioning::validate_version_snapshot, тот же каскад, что вызывает и конфигуратор дашборда, и pm-cli strategy add/versions create) — отказ ловится ПРИ СОХРАНЕНИИ, а не только на следующем рестарте/Reset. pm-cli strategy update этот каскад для confirm_source НЕ проходит: структурные флаги команды поле не покрывают (crates/pm-cli/CLAUDE.md §8) — правка существующей строки только через versions create.

Проверить и, если ключ задан, СНЯТЬ его из config.db до переключения источника на rtds_actor_buffer:

bash
scripts/stop-evidence-forward-check.sh --preflight-only        # раздел 0 = ровно эта пара

Известная граница: сам скрипт пока читает снятую ENGINE_RTDS_ENABLED из .env (не поле stop_loss.confirm_source) — его вывод о состоянии RTDS-транспорта после этой карточки не заслуживает доверия; проверка пары «доля потока + источник» в разделе 0 читает config.db напрямую и не зависит от этого расхождения. Обновление скрипта — отдельная задача, не покрытая этой карточкой.

  1. Задать stop_loss.confirm_source = "rtds_actor_buffer" в определении активной стратегии (dashboard Strategies editor или pm-cli strategy versions create + публикация — см. §0 выше), оставить directional.toxic_flow на SAFE-дефолтах (log_only=true, active_cancel_enabled=false). Если цель — только детектор токсичного потока без переключения подтверждения стопа, источник можно оставить anonymous_tape и задать только секцию directional.toxic_flow — она сама по себе потребитель и поднимет транспорт.
  2. Рестарт pm-engine. В логах ожидать rtds: transport enabled — consumer(s) configured со строкой consumers, перечисляющей включённых потребителей (stop_confirm, toxic_flow или оба). Если вместо неё видна rtds: transport disabled, no consumer configured (no connection) — потребитель не сохранился, проверить строку стратегии.
  3. В течение ~2 минут наблюдать, что ряды pm_engine_rtds_tracked_tokens и pm_engine_rtds_own_* движутся (точный перечень и что каждый ряд значит — ../integrations/rtds.md §Observability; toxic-flow счётчики на /metrics НЕ выведены — их наблюдаемость только через tracing-логи, см. §2 ниже). Если соединение не поднимается — стоп, разобрать до продолжения.

Standalone one-shot RTDS wire-probe (legacy pm-bot) в pm-engine отсутствует — валидация wire-контракта = наблюдение rtds_* метрик на живом соединении.

2. Baseline в log-only (≥48h) ​

Оставить движок с SAFE-дефолтами (log_only=true) минимум на 48 часов. Toxic-flow счётчики (RtdsMetrics::toxic_*) на Prometheus /metrics НЕ выведены — ни один describe_counter!/metrics::counter! их не публикует; наблюдаемость baseline идёт только по структурным tracing-логам (подробности — ../integrations/rtds.md §Observability):

  • Логи rtds.toxic_flow.detected (log_only) — оценить false-positive rate по condition_id (повторные срабатывания на тонком рынке ⇒ порог низок; корреляция с последующими CLOB-движениями ⇒ истинные позитивы).

Калибровка (опционально): если много false-positive — поднять threshold_size в directional.toxic_flow, рестарт, перезапустить baseline. Если строк rtds.toxic_flow.detected нет за 48h — порог слишком высок, либо рынки слишком тонкие, либо min_trade_size рубит все события.

3. Gate 1 — log_only=false ​

Выставить directional.toxic_flow.log_only=false, оставив active_cancel_enabled=false. Рестарт.

После рестарта:

  • Логи меняются с rtds.toxic_flow.detected (log_only) на rtds.toxic_flow.would_cancel (active_cancel disabled).
  • Ни один Decision::CancelOrder не доходит до executor'а — проверить по логам executor'а (префикс причины rtds_toxic_flow: не должен появляться).

Обкатать ≥24h. Убедиться, что частота would_cancel по магнитуде совпадает с частотой detected из шага 2 (сравнение только по логам — счётчиков на /metrics для этого нет).

4. Gate 2 — active_cancel_enabled=true ​

БЛОКИРУЮЩАЯ ГРАНИЦА (design.md D5 карточки rtds-transport-remediation, принято 2026-09-06). Пара log_only=false + active_cancel_enabled=true — та самая, что ToxicFlowGates::resolve превращает в Decision::CancelOrder, — ОТКАЗЫВАЕТ на сохранении/старте, пока ToxicFlowDefect::open() не пуст. Сегодня открыты четыре из пяти дефектов детектора (актор, агрегация без исхода Yes/No, скоуп отмены по condition_id+сторона без токена, отсутствие hysteresis у повторной отмены — перечень и статус каждого в ../integrations/rtds.md §«Harvester activation status»). Проверка одна на все три поверхности записи (движок на старте, сохранение конфигуратора, pm-cli strategy add/versions create) — этот шаг сегодня НЕ ВЫПОЛНИМ ни на одном из них, пока оператор не примет оставшиеся дефекты явным решением и код не снимет их из open().

Только после чистого прогона Gate 1 (24h+) И закрытия/принятия оператором оставшихся дефектов из ToxicFlowDefect::open(). Выставить directional.toxic_flow.active_cancel_enabled=true. Рестарт.

После рестарта:

  • Появляются rtds.toxic_flow.actual_cancel лог-строки.
  • Executor начинает получать Decision::CancelOrder с reason = rtds_toxic_flow:<signed_volume>. Сверить счётчики открытых ордеров в SPA.

Первый час — плотный мониторинг. Тревожно, если:

  • actual_cancels_total >> would_cancel_total из шага 3 — active-side фильтр слишком агрессивен, разобрать до продолжения.
  • Просадка P&L совпадает с отменами — проверить condition_id в warn-логах, порог может быть занижен.

5. Rollback ​

Любой из гейтов откатывается правкой directional.toxic_flow в config.db + рестарт. active_cancel_enabled=false мгновенно останавливает эмиссию отмен, сохраняя would-cancel-логирование. Полный откат в log-only — вернуть log_only=true.

Полностью отключить RTDS-ingestion — убрать секцию directional.toxic_flow из определения (или оставить anonymous_tape в stop_loss.confirm_source, если она была единственным потребителем), опубликовать версию, рестарт: ни одного потребителя не осталось, транспорт не поднимается.