fix(tradein/estimate): залипший anchor_tier больше не глушит четыре механизма разом #2920
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2920
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2656-anchor-tier-reset"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Безопасная половина PR #2661, выделенная по решению владельца. Спорная половина — фильтры свежести, двигающие цену клиенту — остаётся в исходном PR и решается отдельно.
Что чинится
anchor_tierвыставлялся при попытке построить якорь дома и не сбрасывался, когда якорь в итоге не построился. Причин «якоря нет» три (коридорный гейт Tier C, гейт низкой уверенности и недобор аналогов в самом расчёте), и после каждой флаг оставался висеть.Флаг читается как «якорь есть», поэтому залипание молча выключало сразу четыре механизма: подмешивание IMV, квартальную поправку, пол по сделкам ДКП и коридорный зажим. Ни один из них не сообщал о себе — они просто не срабатывали.
Теперь сброс делается единой веткой
if anchor is None:у источника, а не в каждом гейте по отдельности. Заодно снят гейтanchor_tier is not Noneу карточки IMV: она показывается пользователю независимо от якоря, и прятать её из-за чужого флага незачем.Почему это безопасно — замер, а не рассуждение
Перезамер на текущем коде, 996 парных оценок: 154 сделки теряют флаг
anchor_tier, и у всех 154 изменение цены ровно 0,000%. То есть чинится именно флаг, а не ценообразование — залипшее значение и раньше не строило итоговую цену, оно лишь глушило соседние механизмы.Второе подтверждение пришло от эталона бэктеста. После штатной перегенерации (
--from-fixture --update-baseline, не правкой JSON руками) все метрики совпали побитово — калибровка, ожидаемая цена сделки, покрытие диапазона, острота, заголовочное число. Единственная дельта — появился ключunrecorded_lookup_calls: 0.Про красный гейт, который был у #2661
Там гейт падал на
unrecorded_lookup_calls: 0 != 19, и это приписывали фильтру свежести. Оказалось иначе: в комбинированном PR фильтр дополнительно сокращал пул аналогов якоря, из-за чего лишние сделки теряли якорь и уходили на ветки обращения к квартальному индексу, не захваченные в фикстуре. Здесь фильтра нет — поток управления совпадает с тем, что записан в фикстуре, и метрика честно равна нулю.Попутно починена сама регенерация эталона:
replay_fixtureтеперь пиннитestimate_dedup_analogs_enabled=False(раньше это стояло только в гейте, из-за чего после #2173 штатная регенерация не воспроизводила baseline), а заглушка обращений к индексу отвечает «промах» вместо исключения, если сброс флага открыл путь, которого в фикстуре не было.Проверка
pytest tests/test_backtest_regression_gate.py— 2 passedruff check app tests— чистоДобавлены три теста: сброс флага при непостроенном якоре; залипший флаг больше не глушит подмешивание IMV; карточка IMV выживает, когда заголовочное число подавлено, а якоря нет. Плюс обновлён существующий тест коридорного гейта — он проверял
anchor_tier == "C"там, где теперь корректноNone.Что осталось в #2661
Фильтр свежести в SQL якоря дома (оба тира), фильтр свежести в двух CTE знаменателя коэффициента выкупа и вынос окна в конфиг. По перезамеру эта половина двигает деньги: знаменатель просаживается на 1,3% по всем бакетам, то есть предложение клиенту системно уменьшается. Решение по ней — отдельное.
Refs #2656
Флаг anchor_tier оставался равным anchor_tier_fetched ("A"/"C"), когда якорь фактически НЕ строился — сброс делал только low-conf гейт (#audit-1), но не Tier C corridor-гейт (#1795) и не сама _compute_same_building_anchor, когда она отклоняет кандидата (комплов меньше estimate_sb_min_comps). Дальше по коду залипший флаг читается как «headline построил якорь» и молча глушит IMV/Yandex blend (#651, гейт `anchor_tier is None`) и quarter-index correction (#764 Guard-1a) — притом что радиусный headline их не получал. Замер: 154 из 996 сделок теряют tier-флаг этой правкой, и у всех 154 изменение цены ровно 0.000% — чинится именно залипший ФЛАГ, не ценообразование (баланс метрик бэктеста подтверждает: единственная дельта в baseline — новая канарейка unrecorded_lookup_calls, все остальные метрики побитово те же). - estimator.py: сброс `anchor_tier = None` единой веткой `if anchor is None` после всех трёх гейтов (Tier C / low-conf / _compute_same_building_anchor); display-only IMV-карточка больше не гейтится по `anchor_tier is not None` (иначе терялась в щели «тир добыт, якорь не построен, headline подавлен»). - backtest_estimator.py: quarter_index_lookup/quarter_indexes_lookup в реплее отвечают «промах» (None/{}), если сброс флага открыл путь, которого не было в замороженной фикстуре, вместо падения с RuntimeError; счётчик таких промахов уходит в baseline как unrecorded_lookup_calls (точное целое, канарейка на расхождение реплея с захватом). Заодно пиннится estimate_dedup_analogs_enabled = False внутри replay_fixture (было только в самом гейте) — иначе штатная регенерация baseline (--from-fixture --update-baseline) писала baseline, который тест не совпадал бы никогда. - backtest_baseline.json: перегенерирован штатным путём, unrecorded_lookup_calls=0. Выделено из #2656/PR #2661 — фильтры свежести (scraped_at) в якоре дома и в знаменателе коэффициента выкупа остаются в исходном PR как отдельная, более спорная правка (двигает деньги: знаменатель просаживается на ~1.3% по бакетам).