Тема
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 принадлежит только optionald_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», а:
- Валидация инструмента: ITT decision trace, гейт, counters работают на живом пайплайне (executable best_ask вместо Gamma-mid — единственная информация, которой нет в OOS).
- Монитор режима: непрерывное измерение per-signal edge; PASS-сигнал = режим вернулся (осень/зима?), и гейт снова несёт кромку.
- Честный вердикт по 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 дашборда тоже):
discovery.rediscovery_interval_secs=Some— снапшоты строятся только discovery-координатором.entry_flow=Direct— гейт врезан только в Direct-путь (WatchArmFak обошёл бы предикат).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/(клоны layoutinstances/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 sha256 | TBD-at-launch |
| pm-engine bin sha256 | TBD-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) и есть специально построенный для погодных рынков гейт «до конца локального дня».
- Новый торговый предикат:
NO best_ask ∈ [0.75, 0.95] × каузальная d_fav ≥ 2 (все сиблинг-котировки ≤ 10800 с) × внутри таймзонного окна входа (GstLocalMidnight−12ч…−2ч). Изменения против §2: бенд расширен оператором до 0.95; cutofft ≤ market.endDateудалён как артефакт мусорных метаданных (код: ветка past_cutoff и counterblocked_selection_past_cutoffудалены полностью; startup-digest несёт маркерselection_cutoff=REMOVED 2026-07-15). - Date-cut: сигналы до 2026-07-15 ~02:00 UTC — по старому предикату (§2), после — по новому (п.1). Смешивание до/после-сигналов в одном ITT-агрегате ЗАПРЕЩЕНО. Технический маркер страты:
config_digestстрокиselection_signals(новый digest несётselection_cutoff=REMOVED 2026-07-15). - Оригинальная 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. - Америки = новая популяция: в старом предикате американских сигналов не существовало (структурно выключены cutoff'ом). Отдельная страта
region=americas, descriptive-only — в сравнение с до-2026-07-15 данными не включается.