gendesign/tradein-mvp/backend/tests/test_1970_estimator_constants.py
bot-backend 66dfb030e7
Some checks failed
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 18s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m57s
CI Trade-In / backend-tests (pull_request) Failing after 6m18s
МЕРА/оценщик: диапазон якоря закреплён тестом на FSD_K, мёртвый гейт gate_thin удалён (#3590)
FSD_K задаёт видимый клиенту диапазон range_low_rub/range_high_rub, но гейт
регрессии его не видит: FSD_K=1.5 меняла 857 сделок из 1600 при зелёном гейте.
Новый тест подменяет _compute_same_building_anchor якорем с известным fsd и
нулевым спредом комплов и проверяет полуширину 1.65 × fsd на результате
estimate_quality. Ожидание задано литералом, а не импортом FSD_K: импорт
поехал бы вместе с мутацией.

gate_thin («n < 3 И FSD > 0.20») недостижим дважды: якорь возвращается только
при n ≥ SB_MIN_COMPS = 4, а FSD > 0.20 и так даёт confidence="low", то есть
срабатывает gate_low. В ответ API флаг не уходил (только в лог), поэтому ветка
удалена вместе с SB_GATE_MIN_N и SB_GATE_MAX_FSD. Поля Settings остаются в
проверке «не вернулись».

SB_GUARDRAIL_TOL оставлен и помечен как недостижимый по построению: цена якоря
есть взвешенное среднее, медиана или p70 того же пула, так что
est_ppm2 ≥ comp_min.

Тавтологичный test_fix1_thin_n_high_fsd_triggers_gate (min_comps=1 и копия
формулы) заменён тестом инвариантов на шести пулах: n ≥ SB_MIN_COMPS,
FSD > 0.20 ⇒ low, anchor_ppm2 ≥ comp_min_ppm2. Покрытие ветвей uplift, клипа и
FSD > 0.20 проверяется явно.

В комментарии content.ts старое имя estimate_imv_blend_weight → IMV_BLEND_WEIGHT.

Поведение на проде не меняется: гейт регрессии зелёный без перегенерации
baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:40:47 +05:00

78 lines
4.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""#1970: калибровочные константы движка, вынесенные из Settings, — значения.
Поля estimate_* жили в Settings и формально переопределялись через ENV. При
переносе в константы estimator.py значения обязаны остаться боевыми. Числа ниже
сняты с прода 17.09.2026 (`settings.<поле>` в tradein-backend, tradein-scraper и
tradein-tgbot — совпали, ENV-оверрайдов нет), а не скопированы из кода.
Зачем отдельный тест: замороженная фикстура бэктеста часть механизмов не
проходит вовсе (квартальный индекс в реплее применяется 0 раз из 1600 сделок),
а поведенческие тесты ловят не все пороги. Поменять число — осознанно: здесь,
в estimator.py и с прогоном регрессионного гейта.
"""
from __future__ import annotations
import os
import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
from app.core.config import Settings
from app.services import estimator
# имя константы в estimator.py → (бывшее поле Settings, боевое значение 17.09.2026)
_PROD_VALUES: dict[str, tuple[str, float]] = {
# #2379, #764 квартальный индекс
"QUARTER_INDEX_MIN_N_DEALS": ("estimate_quarter_index_min_n_deals", 10),
"QUARTER_MATCH_SKIP_RATIO": ("estimate_quarter_match_skip_ratio", 0.6),
"QUARTER_INDEX_MAX_FOR_SMALL_N": ("estimate_quarter_index_max_for_small_n", 2.0),
"QUARTER_INDEX_SMALL_N_THRESHOLD": ("estimate_quarter_index_small_n_threshold", 50),
"QUARTER_INDEX_FACTOR_MIN": ("estimate_quarter_index_factor_min", 0.6),
"QUARTER_INDEX_FACTOR_MAX": ("estimate_quarter_index_factor_max", 1.8),
# #2380, #1795 anti-inflation (CORRIDOR_CLAMP_MIN_N определён в app.core.config)
"CORRIDOR_CLAMP_MIN_N": ("estimate_corridor_clamp_min_n", 10),
"CORRIDOR_CLAMP_SLACK": ("estimate_corridor_clamp_slack", 0.40),
"RADIUS_FLOOR_FACTOR": ("estimate_radius_floor_factor", 0.8),
"OUTLIER_SMALL_N_THRESHOLD": ("estimate_outlier_small_n_threshold", 15),
"OUTLIER_TUKEY_K_SMALL": ("estimate_outlier_tukey_k_small", 1.0),
# #2381, #651/#652 same-building anchor + IMV-blend
"IMV_BLEND_WEIGHT": ("estimate_imv_blend_weight", 0.5),
"IMV_BLEND_THRESHOLD": ("estimate_imv_blend_threshold", 1.15),
"SB_MIN_COMPS": ("estimate_sb_min_comps", 4),
"SB_AREA_SIGMA": ("estimate_sb_area_sigma", 0.18),
"SB_ROOMS_MATCH_BOOST": ("estimate_sb_rooms_match_boost", 1.6),
"SB_FLOOR_SIGMA": ("estimate_sb_floor_sigma", 0.25),
"SB_GUARDRAIL_TOL": ("estimate_sb_guardrail_tol", 0.05),
"SB_MAD_K": ("estimate_sb_mad_k", 3.5),
"SB_MAD_K_SMALL_N": ("estimate_sb_mad_k_small_n", 2.5),
"SB_SMALL_N_THRESHOLD": ("estimate_sb_small_n_threshold", 10),
"ANCHOR_TIER_C_CORRIDOR_MULT": ("estimate_anchor_tier_c_corridor_mult", 1.5),
"FSD_K": ("estimate_fsd_k", 1.65),
}
# Поля, чьи ветки из кода удалены: выключатели, схлопнутые к боевому True, и мёртвые гейты.
_REMOVED_SWITCHES = (
"estimate_dedup_analogs_enabled", # #2378
"estimate_expected_sold_le_asking", # #2380
# #3590: мёртвый гейт якоря «n < 3 И FSD > 0.20» удалён вместе с константами.
"estimate_sb_gate_min_n",
"estimate_sb_gate_max_fsd",
)
@pytest.mark.parametrize("const", sorted(_PROD_VALUES))
def test_constant_keeps_prod_value(const: str) -> None:
assert getattr(estimator, const) == _PROD_VALUES[const][1]
@pytest.mark.parametrize("const", sorted(_PROD_VALUES))
def test_settings_field_is_gone(const: str) -> None:
# Поле вернулось в Settings → снова два источника одного числа (ENV и код).
assert _PROD_VALUES[const][0] not in Settings.model_fields
@pytest.mark.parametrize("field", _REMOVED_SWITCHES)
def test_switch_is_gone(field: str) -> None:
assert field not in Settings.model_fields