Skip to content

feat(search): перебор конфигураций политики и эталонное распределение очереди - #5

Merged
retrobich921 merged 7 commits into
mainfrom
feat/policy-search
Sep 5, 2026
Merged

retrobich921 merged 7 commits into
mainfrom
feat/policy-search

Conversation

@retrobich921

@retrobich921 retrobich921 commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Что это

Отвечаем на вопрос «а можно ли настроить роутер лучше?» не интуицией, а перебором.
Добавлена команда search, которая гоняет конфигурации политики через настоящий роутер
(не модель поверх модели) на общей батарее очередей, и AssignmentBound, который считает
эталонное распределение той же очереди точным min-cost flow.

Прогнали 113 000 конфигураций в два раунда. Разбор — в docs/search.md.

Главные выводы

  1. Дефолтная политика balanced уже на границе Парето. Из 113 000 конфигураций базу
    доминируют единицы, и все на уровне шума: 67.49 → 67.07 п.п. отклонения, конверсия +0.05%.
    Подбирать веса дальше бессмысленно.
  2. Кривая размена измерена целиком: +21 п.п. отклонения от целевых долей покупают
    +6.2% конверсии и +8% маржи. Это выбор бизнеса одной строкой в YAML, а не программиста в коде.
  3. Жадный онлайновый роутер берёт 92–96% того, что берёт всезнающий пакетный оптимум
    (команда bound). Переписывать выбор на batch-оптимизацию ради этих процентов незачем.
  4. Узкое место не в политике, а во входных данных. На реалистичном потоке payflow
    допустим примерно в 2% заявок при цели 35%: дневной лимит выбран на 96.7% (2.9 из 3 млн),
    потолок чека 50 тыс. Снижение traffic_percentage с 35% до 10% даёт −25 п.п. отклонения
    без потери конверсии — в шестьдесят раз больше, чем весь перебор весов.
    Ровно это наш отчёт и рекомендовал сам; перебор рекомендацию не опроверг, а измерил.
  5. Аблации: turnover_min не меняет вообще ничего (подтверждает goal_activity и правило
    dead_goal), а cascade_priority и amount_band стоят 4.1 п.п. соблюдения долей ради
    0.7% конверсии — осознанный размен, обе цели из ТЗ.

Что добавлено

lib/payout_router/search/ Battery, Space, Front, Engine, Metrics, Outcome, Candidate
analytics/assignment_bound.rb + min_cost_flow.rb эталонное распределение очереди как транспортная задача
strategies/share_deficit.rb доли в заявках (deficit round-robin) вместо процентов
selection.normalization: damped третий режим нормировки: лечит вырождение pool при двух кандидатах
config/policies/bank_affinity_first.yml пресет по итогам перебора: +5.2% бэктеста против +4.7% у conversion_first
docs/search.md разбор перебора: как устроен, что нашёл, что не сработало
docs/criteria.md сверка с каждым подкритерием трека (100 + 140 + 20): артефакт, команда проверки, честно названные слабые места

Ревизия документации

Сверены с фактическим прогоном все числа во всех .md: 236 спеков, покрытие строк 96.0%,
14 hard-правил, 13 целей, 13 правил рекомендаций, 10 пресетов, 11 политик в compare,
бэктест 72.2 → 75.6, соотношение Ruby/Python 91/9. Раньше в разных файлах стояли 211/213/181 спек,
11/12/13 правил рекомендаций и бэктест «+6.9%», которого давно нет.

Исправлена производительность (в доках стояло ≈2 700 заявок/с с ноутбука разработки, на Ryzen 7 9700X
полный конвейер даёт 10 642 на 50 000 и 10 323 на 200 000) и curl localhost:8080 → 127.0.0.1
(на Windows localhost уходит в IPv6 ::1, и соединение не устанавливается).

Удалён docs/checkpoint2.md — он повторял docs/checkpoint.md и уже разошёлся с ним в цифрах.
Уникальное перенесено туда, где ему место: «задача простыми словами» и таблица «если что-то
не так» — в README (их читает жюри), новые вопросы к экспертам — в docs/checkpoint.md §5.
Дублирующая таблица критериев из docs/plan.md заменена указателем на docs/criteria.md.

Исправлена ошибка в модели эталона

Первая версия AssignmentBound давала оптимум с квотами выше оптимума без них — невозможный
результат. Причина: self-provider был доступен эталону без ограничений, а по модели одобрения он
выглядит лучше всех внешних (нет истории, заявленные 0.95 против откалиброванных 0.62–0.80,
нет лимитов). Квоты выталкивали заявки на него и «улучшали» результат. Теперь эталону разрешено
ровно столько заявок на self-provider, сколько использовал наш роутер — наше распределение заведомо
допустимо в его задаче, и оптимум становится настоящей верхней границей. Инвариант закреплён спеком.

Что честно не сработало

  • share_deficit — механика строго сильнее процентной, но в одиночку даёт ровно то же
    распределение
    , что traffic_share: выбор упирается в допустимость, а не в закон управления.
    В смеси — 1 п.п. Оставлен с нулевым весом, за улучшение не выдаём.
  • damped — в финал попадает (208 конфигураций из 473), границу Парето не сдвинула.

Риск для сдачи: нулевой

Дефолтная политика, режим simulation.mode: optimistic и формат attempts не тронуты.
bundle exec rake public перегенерирует routing_decisions.json и routing_report.json
байт в байт такими же, как в main.

Как проверить

$env:PATH = "C:\Ruby40-x64\bin;" + $env:PATH

bundle exec rake            # 236 спеков, 0 падений; RuboCop 164 файла, 0 замечаний
bundle exec rake validate   # скрипт организаторов 29/0/0, наш валидатор 30/0/0
bundle exec rake public     # git status после него должен быть пустым — сдаваемые файлы не изменились

ruby -Ilib bin/payout_router search --samples 60   # ~1 минута: граница Парето и лидеры по метрикам
ruby -Ilib bin/payout_router bound                # эталонное распределение и разрыв с ним
ruby -Ilib bin/payout_router backtest --policy config/policies/bank_affinity_first.yml
ruby -Ilib bin/payout_router strategies            # 14 hard-правил, 13 целей, 10 пресетов

🤖 Generated with Claude Code

https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk

retrobich921 and others added 7 commits September 5, 2026 13:05
data/derived/ (в docs/data.md прямо написано «в git не коммитим») и __pycache__/
висели неотслеживаемыми — один git add -A утащил бы их в историю.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk
Документы разошлись с репозиторием: в шпаргалке стояло 211 спеков и 97.8% покрытия,
в manual_check — 181 спек, 13 hard-правил и 8 пресетов. Эксперт может открыть репозиторий
и сверить, поэтому везде проставлены измеренные значения.

Производительность была занижена вчетверо: «≈2 700 заявок/с» — цифра с ноутбука разработки,
на Ryzen 7 9700X полный конвейер даёт 10 642 заявок/с на 50 000 и 10 323 на 200 000.
Обе цифры теперь с указанием, где мерили.

curl localhost:8080 из шпаргалки на Windows не работает: сервис слушает 127.0.0.1,
а localhost сначала резолвится в IPv6 ::1 — соединение не устанавливается.

Плюс бриф на чекпоинт 2: аудит, разбор задачи и решения, сценарий демо и вопросы к жюри.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk
share_deficit — целевая доля по числу заявок, посчитанная в заявках, а не в процентах:
недобор = цель × обработано − выдано. Процентная разница одинаково оценивает 10 п.п.
на пятой заявке (полоперации) и на пятисотой (полсотни операций), счётная — нет.
Это deficit round-robin на допустимом пуле.

selection.normalization: damped — третий режим, лечит вырождение pool при двух кандидатах,
где любая разница растягивается до 0/1. Разброс в знаменателе увеличен на константу,
поэтому цель, почти не различающая кандидатов, и вклад даёт почти нулевой.

Обе механики по умолчанию выключены: перебор (docs/search.md) показал, что на этих данных
они границу Парето не двигают — выбор упирается в допустимость, а не в закон управления.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk
Роутер работает онлайн — по одной заявке, не зная следующих. AssignmentBound решает
ту же очередь целиком как транспортную задачу «банк × провайдер» точным потоком
минимальной стоимости. У эталона две форы: знает будущее и не считает ёмкости
по деньгам, реквизитам и интенсивности.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk
Команда search отвечает на «а нельзя ли настроить лучше» не интуицией, а перебором.
Каждая точка — не оценка по модели, а полный прогон роутера на батарее очередей
(публичная очередь + синтетические из истории + бэктест), 62 мс на конфигурацию.

Пространство: структурные варианты (нормировка pool/absolute/damped × доли
absolute/attainable × дневной лимит обычный/с резервированием) × веса целей —
база, одиночные цели, аблации, исчерпывающая сетка и случайные точки симплекса,
затем покоординатная доводка лучшей.

На выходе граница Парето «доли против одобрений» и лидеры по каждой метрике.
Фронт достраивается инкрементально и схлопывает совпадающие точки: полный пересчёт
после каждого кандидата стоит дороже самого прогона роутера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk
Лучший рычаг для одобрений — не заявленная провайдером conversion_24h, а наблюдаемая
проходимость пары «провайдер × банк». Заявленная врёт: payflow объявляет 0.91,
по 19 историческим операциям факт 0.47.

Против conversion_first на отложенных очередях: конверсия 0.7635 против 0.7622,
маржа 172.1 ₽ против 169.9 ₽ на заявку, бэктест +5.25% против +4.71%.
Цена та же — доли партнёров не держатся, это политика «максимум денег».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk
Главный вывод: дефолтная политика balanced уже на границе Парето — из 113 000
проверенных конфигураций базу доминируют единицы, и все на уровне шума
(67.49 → 67.07 п.п. отклонения, конверсия +0.05%). Крутить веса дальше незачем.

Узкое место не в политике, а во входных данных: на реалистичном потоке payflow
допустим примерно в 2% заявок при цели 35% — дневной лимит выбран на 96.7%,
потолок чека 50 тыс. Снижение traffic_percentage с 35% до 10% выигрывает 25 п.п.
отклонения без потери конверсии, то есть в шестьдесят раз больше всего перебора весов.

Плюс аблации (turnover_min не меняет вообще ничего, cascade_priority и amount_band
стоят 4.1 п.п. соблюдения долей ради 0.7% конверсии) и честный разбор двух механик,
которые не сработали.

Дефолтную политику и режим optimistic перебор не тронул: routing_decisions.json
и routing_report.json в корне остались байт в байт прежними.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7LBzBD5qBUzAAPsGTB9Nk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant