Skip to content

Weather-selection форвард-тест — pre-registered протокол (Ф2) ​

Исторический протокол, не инструкция запуска. Контракт neg_risk_favorite_distance был superseded 2026-08-01 probability-rank selection. Новый вход использует neg_risk_probability_rank с явным min_probability_rank и требует entry_outcome_filter = "No"; price-gap принадлежит только optional d_fav_deterioration_exit. Перед будущим запуском конфиг мигрируется backup-first через scripts/migrate-selection-probability-rank.sh. Ни миграция production, ни запуск нового форвард-теста этим изменением не выполнялись. Дополнительно весь описанный ниже двухинстансный dry-run протокол снят 2026-08-22 вместе с runtime-симулятором. Будущая проверка selection выполняется только по factual live/probe telemetry отдельным новым планом.

Статус: протокол зафиксирован ДО старта; менять после старта ЗАПРЕЩЕНО (anti-peeking / pre-registration). Основания: openspec/changes/weather-selection-layer/design.md (D8), .tmp/weather-calib/DISTANCE_RESULTS.md (зимняя калибровка), .tmp/weather-calib-summer/SUMMER_OOS_RESULTS.md (летний out-of-sample).

1. Цель теста (перекалибрована по летнему OOS 2026-07-12) ​

Летний out-of-sample (36 906 рынков, 3 345 кластеров, мощность ×14 зимней) показал: раскол d_fav≥2 > d=1 воспроизводится по знаку, но кромка сжалась ~4×; предикат-ячейка на июнь-июль = ноль (may +0.0273 SEP+ → junjul −0.0030 n.s.). Рыночный режим сменился ~в начале июня.

Поэтому цель Ф2 — НЕ «подтвердить ожидаемый PASS», а:

  1. Валидация инструмента: ITT decision trace, гейт, counters работают на живом пайплайне (executable best_ask вместо Gamma-mid — единственная информация, которой нет в OOS).
  2. Монитор режима: непрерывное измерение per-signal edge; PASS-сигнал = режим вернулся (осень/зима?), и гейт снова несёт кромку.
  3. Честный вердикт по pre-registered правилу — включая FAIL/INCONCLUSIVE как полноценные исходы (fast-verdict-first: NO-GO — легитимный результат).

2. Pre-registered предикат (неизменяем) ​

NO best_ask ∈ [0.75, 0.85) × каузальная d_fav ≥ 2 (все сиблинг-котировки ≤ 10800 с) × t ≤ market.endDate

Реализация: capability weather-event-selection (openspec), гейт в pm-engine directional, конфиг directional.selection.* (mode=neg_risk_favorite_distance, min_favorite_distance=2, sibling_quote_max_age_secs=10800), band существующими ключами 750/850 milli.

3. Сигнал и primary-правило (ITT) ​

  • Сигнал = первый момент, когда condition_id проходит полный предикат и все non-selection гейты, КРОМЕ бюджета и факта филла. Исчерпание бюджета / no-fill / ошибка размещения сигнал НЕ отменяют. Повторные пересечения бенда новых сигналов не создают. Источник истины = таблица selection_signals в engine.db treatment-инстанса.
  • Primary: по резолюции рынков каждого сигнала: edge_i = 1{NO выиграл} − w*_i, где w*_i = p_entry_i / 0.99 (per-signal, через эталонный harvester_yield; p_entry_i = own_best_ask на момент сигнала из trace). PASS ⟺ нижняя граница двустороннего 95% cluster-bootstrap CI (кластер = событие, B=2000) для mean(edge_i) > 0. Включаются ВСЕ resolved eligible-сигналы (intention-to-treat), включая budget_skipped и no-fill.
  • Dual-stop: раньше из {150 resolved treatment-событий; 8 календарных недель от старта}.
  • Validity-минимум: ≥100 resolved событий И ≥250 resolved сигналов; иначе вердикт INCONCLUSIVE (не FAIL).
  • Страты: city, назначены до старта, descriptive-only; глобальный вердикт единственный.
  • Anti-peeking: конфиги обоих инстансов заморожены (hash ниже); изменения во время теста запрещены; остановка «по красивому CI» запрещена; мониторинг без вынесения вердикта разрешён.

4. Secondary (fill-audit; права ограничены) ​

Fill-rate, filled edge, markout mid +5м/+30м/+2ч после сигнала, распределения filled-vs-all по d_fav/цене/городу/времени. Secondary может заблокировать scale-up/live-deployment («selection validated, execution not validated»), но НЕ может отменить primary-вердикт по селекции. Live $5 probe-контур — отдельная программа (mandate оператора), его данные в primary НЕ входят.

5. Baseline-инстанс ​

Текущее поведение (band live-значений, selection-секция ОТСУТСТВУЕТ). Роль = continuity/regime control: ловит сезонный сдвиг, показывает, что band-only политика без гейта не лучше. НЕ формальный компаратор — superiority-claim без отдельной пре-регистрации не делается.

5a. Deployment-инварианты selection (fail-fast + операторские) ​

Три cross-field инварианта fail-fast при mode≠off (иначе движок не стартует, save-time дашборда тоже):

  1. discovery.rediscovery_interval_secs=Some — снапшоты строятся только discovery-координатором.
  2. entry_flow=Direct — гейт врезан только в Direct-путь (WatchArmFak обошёл бы предикат).
  3. rediscovery_interval_secs < sibling_quote_max_age_secs — иначе котировки протухают раньше следующего re-discovery → тихая starvation входов (treatment: 60 < 10800 — с огромным запасом).

Операторский инвариант (НЕ гейтится кодом — документируется): selection on ⇒ discovery-универсум только weather neg-risk (tag 104596). Enrichment строит ось лишь для событий с валидным groupItemThreshold; не-weather neg-risk событие (sports/esports) получит no_context veto на каждом тике (fail-closed, безопасно по деньгам, но рынок никогда не торгуется). Калибровка предиката — под weather; расширять универсум без новой калибровки нельзя. Treatment-инстанс использует tag 104596, поэтому инвариант соблюдён по построению.

6. Операционка ​

  • Инстансы VPS: instances/weather-treatment/, instances/weather-baseline/ (клоны layout instances/harvester-engine/, свои BOT_STRATEGY_LABEL, свои data/engine.db, оба СТРОГО --dry-run).
  • Живые контуры harvester-engine (probe-manage) и rotcap capture НЕ затрагиваются.
  • Мониторинг: counters pm_engine_selection_*, алярм skip-rate >2%/неделя = СТОП и расследование имплементации (баг-сигнал, не рыночный).
  • Вердикт-аналитика: per-signal edge из selection_signals + резолюции (Gamma с VPS) + cluster-bootstrap; прогон через pm-analytics harvester-ev / scripts/analysis/forensics.sh ev (расширение — task 7.3).

7. Freeze (заполняется при старте, ДО первого сигнала) ​

полезначение
дата/время старта (UTC)TBD-at-launch
treatment config sha256 (строка strategies.definition)TBD-at-launch
baseline config sha256TBD-at-launch
pm-engine bin sha256TBD-at-launch
selection_startup_digest (из лога старта)TBD-at-launch

7a. Запуск (операторское go/no-go — НЕ автоматизировано) ​

Механизм реализован, протестирован, закоммичен (main), release-бинарь собран и деплоепригоден. Запуск форвард-теста — сознательное решение оператора, потому что летний OOS (§1) показал кромку ≈0 в текущем июльском режиме: 8-недельный тест сейчас измерит ≈ноль. Ниже — точные шаги, когда/если решено запускать.

Шаг 1 — деплой бинаря (сам по себе безопасен, selection-OFF = bit-parity):

scripts/deploy-vps.sh engine-bot   # рестартит harvester-engine новым бинарём (selection OFF по умолчанию)

Прим.: НЕ выполнено ночью автономно — рестарт живого 12ч-инстанса без behavioral-выгоды (selection OFF идентичен текущему) при активной позиции = лишний риск. Bit-parity доказана тестом mode_off_is_bit_parity_no_veto + selection_absent_is_off_and_round_trips; деплоепригодность — release-сборкой.

Шаг 2 — treatment-инстанс (config.db strategy-row, добавить в definition JSON):

json
"entry_price_band_lower_milli": 750,
"max_entry_price_milli": 850,
"min_probability_milli": 750,
"selection": { "mode": "neg_risk_favorite_distance", "min_favorite_distance": 2,
               "sibling_quote_max_age_secs": 10800 },
"discovery": { ..., "rediscovery_interval_secs": <существующий, должен быть < 10800> }

entry_flow=Direct (уже), tag 104596 (weather). Отдельный BOT_STRATEGY_LABEL + свой data/engine.db.

Шаг 3 — baseline-инстанс: текущий live-конфиг БЕЗ секции selection (continuity control), свой engine.db. Оба СТРОГО --dry-run.

Шаг 4 — freeze: заполнить таблицу §7 (config sha256 обоих, bin sha256, selection_startup_digest из лога старта) ДО первого сигнала; далее конфиги не менять (anti-peeking).

8. Исходы ​

  • PASS → режим вернулся; решение о live-масштабировании — отдельное (гейтится secondary + оператор).
  • FAIL → предикат в текущем режиме без кромки; согласуется с летним OOS; NO-GO-ветка Harvester-v1 для погодной вселенной в летнем режиме — на решение оператора.
  • INCONCLUSIVE → продлить период (НЕ расширять бенд — post-selection запрещён) либо остановить по решению оператора.

9. Аддендум 2026-07-15 — схема selection_signals (протокол §1–8 НЕ изменён) ​

С 2026-07-15 (волна 2 постмортема 2026-07-14, commit 49fc6d72) таблица selection_signals дополнительно содержит диагностические строки outcome='veto_distance' — трассу veto по d_fav<2 (популяция «сосед фаворита», НЕ сигналы). Любая ITT-выборка по §3 ОБЯЗАНА фильтровать outcome <> 'veto_distance' (контракт: SELECTION_VETO_DISTANCE_OUTCOME, crates/pm-engine/src/ports/persistence.rs). При veto→PASS строка заменяется трассой PASS-момента (upsert-over-veto) — ITT-значения всегда соответствуют моменту прохождения предиката. Определение сигнала, предикат и primary-правило не тронуты.

10. Пере-регистрация 2026-07-15 (решение оператора) ​

Основание: Gamma endDate у погодных daily = 12:00 UTC у ВСЕХ событий — метаданные-мусор, никак не связанный с реализацией исхода. Cutoff t ≤ market.endDate структурно выключал все американские рынки (их таймзонные окна входа открываются после 12:00 UTC) и резал европейские до 1–3 часов. Таймзонный алгоритм окна входа (GstLocalMidnight−12ч…−2ч, класс логики entry_window) и есть специально построенный для погодных рынков гейт «до конца локального дня».

  1. Новый торговый предикат: NO best_ask ∈ [0.75, 0.95] × каузальная d_fav ≥ 2 (все сиблинг-котировки ≤ 10800 с) × внутри таймзонного окна входа (GstLocalMidnight−12ч…−2ч). Изменения против §2: бенд расширен оператором до 0.95; cutoff t ≤ market.endDate удалён как артефакт мусорных метаданных (код: ветка past_cutoff и counter blocked_selection_past_cutoff удалены полностью; startup-digest несёт маркер selection_cutoff=REMOVED 2026-07-15).
  2. Date-cut: сигналы до 2026-07-15 ~02:00 UTC — по старому предикату (§2), после — по новому (п.1). Смешивание до/после-сигналов в одном ITT-агрегате ЗАПРЕЩЕНО. Технический маркер страты: config_digest строки selection_signals (новый digest несёт selection_cutoff=REMOVED 2026-07-15).
  3. Оригинальная pre-registered ячейка [0.75,0.85) × d_fav≥2 остаётся вычислимой как подвыборка: selection_signals несёт band_measured_price_pip (фильтр бенда; до 2026-08-13 колонка называлась own_best_ask_pip и хранила ту же величину — тогда коридор мерился именно best_ask, поэтому перенос тождественен, а не приблизителен; с 2026-08-13 коридор меряет прогнозную цену заявки, и БАЗА измерения берётся из directional.entry_price_base эпохи в config.db, а не из строки) и cutoff_ts_ms (сырые endDate-метаданные; фильтр decision_ts_ms <= cutoff_ts_ms восстанавливает старый cutoff-срез). Primary-анализ §3 продолжает считаться на этой ячейке — с раздельными до/после стратами по п.2.
  4. Америки = новая популяция: в старом предикате американских сигналов не существовало (структурно выключены cutoff'ом). Отдельная страта region=americas, descriptive-only — в сравнение с до-2026-07-15 данными не включается.