fix(estimator): anchor low-confidence gate (#audit-1)

Якорь с confidence=low ИЛИ (n<gate_min_n И FSD>gate_max_fsd) не заменяет
headline — fallback на radius-median. Дефолты (min_n=3, max_fsd=0.20) не
режут здоровые якоря (n≥4, FSD<0.15 проходят). За флагом
estimate_sb_low_conf_gate_enabled (дефолт True). Логируется с причиной.
This commit is contained in:
bot-backend 2026-06-20 14:59:05 +03:00
parent a425f2f200
commit 55c7bc2c72
2 changed files with 51 additions and 0 deletions

View file

@ -134,6 +134,35 @@ class Settings(BaseSettings):
asking_to_sold_haircut: float = 0.05 # дефолтная asking→sold скидка (banded по ppm²)
estimate_fsd_k: float = 1.65 # множитель FSD → полуширина диапазона
# ── #audit-1: anchor low-confidence gate ─────────────────────────────────
# Якорь с низкой уверенностью (confidence="low" ИЛИ n < min_n И FSD > max_fsd)
# НЕ заменяет headline — fallback на radius-median. Дефолты подобраны так, что
# здоровые якоря (n≥4 с FSD<0.15) проходят без изменений.
# estimate_sb_gate_min_n=3 : при n<3 И FSD>max_fsd гейт срабатывает
# estimate_sb_gate_max_fsd=0.20: FSD>0.20 при малом n → ненадёжный якорь
# Отдельный флаг: False → точно старое поведение (гейта нет).
estimate_sb_low_conf_gate_enabled: bool = True
estimate_sb_gate_min_n: int = 3
estimate_sb_gate_max_fsd: float = 0.20
# ── #audit-3: price_trend freshness filter ────────────────────────────────
# Исключать items старше N месяцев из price_trend (house_placement_history).
# Дефолт 6 (консервативно); аудит предложил 3 — конфигурируемо.
estimate_price_trend_max_age_months: int = 6
# ── #audit-4: MAD-clip after similarity-weighting ─────────────────────────
# True = clip происходит ПОСЛЕ similarity-weighting (на взвешенных ppm²).
# False = clip ДО weighting (старое поведение). Дефолт True.
estimate_sb_clip_after_weight: bool = True
# ── #audit-5: data-age guards ─────────────────────────────────────────────
# sber_index_max_age_days: максимальный допустимый возраст последнего месяца
# СберИндекс-серии (дней). Если latest месяц старее — логируем warning.
sber_index_max_age_days: int = 35
# avito_imv_thin_market_threshold: если market_count < порога — IMV-оценка
# на тонком рынке (thin_market=True в AvitoImvSummary) + warning.
avito_imv_thin_market_threshold: int = 10
# ── #764: per-cadastral-quarter price index correction ───────────────────
# Gap-correction: квартальный индекс применяется ТОЛЬКО в pure-radius пути
# (когда same-building anchor и IMV-blend не сработали). Корректирует РАЗРЫВ

View file

@ -2321,6 +2321,28 @@ async def estimate_quality(
int(gate_threshold),
)
anchor = None
# #audit-1: low-confidence gate — якорь с низкой уверенностью или малым n
# при высоком FSD НЕ заменяет headline; fallback на radius-median.
# Tier A (n≥4, FSD<0.15) проходит без изменений при дефолтных порогах.
if anchor is not None and settings.estimate_sb_low_conf_gate_enabled:
gate_low = anchor["confidence"] == "low"
gate_thin = (
anchor["n"] < settings.estimate_sb_gate_min_n
and anchor["fsd"] > settings.estimate_sb_gate_max_fsd
)
if gate_low or gate_thin:
logger.info(
"sb_anchor low-conf gate #audit-1: tier=%s n=%d fsd=%.3f conf=%s"
" → suppressed (gate_low=%s gate_thin=%s) → radius fallback",
anchor_tier,
anchor["n"],
anchor["fsd"],
anchor["confidence"],
gate_low,
gate_thin,
)
anchor = None
anchor_tier = None
if anchor is not None:
# #694: якорь мутирует headline — UI-аналоги должны отражать ЭТИ комплы.
anchor_comps_used = comps