"""#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), "SB_GATE_MIN_N": ("estimate_sb_gate_min_n", 3), "SB_GATE_MAX_FSD": ("estimate_sb_gate_max_fsd", 0.20), } # Булевы выключатели, схлопнутые к боевому True: их OFF-ветки из кода удалены. _REMOVED_SWITCHES = ( "estimate_dedup_analogs_enabled", # #2378 "estimate_expected_sold_le_asking", # #2380 ) @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