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 задаёт видимый клиенту диапазон 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>
78 lines
4.2 KiB
Python
78 lines
4.2 KiB
Python
"""#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
|