diff --git a/backend/app/services/forecasting/confidence_engine.py b/backend/app/services/forecasting/confidence_engine.py index 16fd9399..64192458 100644 --- a/backend/app/services/forecasting/confidence_engine.py +++ b/backend/app/services/forecasting/confidence_engine.py @@ -375,6 +375,7 @@ def compute_report_confidence( *, component_confidences: list[Confidence] | None = None, deal_count: int | None = None, + deal_count_months: int | None = None, analog_count: int | None = None, domrf_coverage: float | None = None, history_months: int | None = None, @@ -403,6 +404,8 @@ def compute_report_confidence( component_confidences: per-service confidence (#950/#952/#985/#986…), None/[]→ нет вкладывающих компонентов. deal_count: число сделок за окно (None → нет данных, тянет в low). + deal_count_months: окно наблюдения для deal_count (мес) — добавляет «за N мес» + в ноту фактора («7 сделок за 6 мес — мало»). None → нота без периода. analog_count: число ЖК-аналогов в выборке (= market_metrics.obj_count). domrf_coverage: доля domrf↔objective ∈ [0,1] (главный sparse-риск проекта). history_months: глубина ряда (мес). @@ -417,6 +420,7 @@ def compute_report_confidence( # ── 1. Сырые счётчики качества данных → факторы (только заданные) ────────── if deal_count is not None: + _deal_suffix = f"за {deal_count_months} мес" if deal_count_months is not None else "" factors.append( _factor_from_count( _F_DEAL_COUNT, @@ -424,6 +428,7 @@ def compute_report_confidence( high_at=_DEAL_COUNT_HIGH, low_below=_DEAL_COUNT_LOW, unit="сделок", + suffix=_deal_suffix, ) ) if analog_count is not None: diff --git a/backend/tests/services/forecasting/test_confidence_engine.py b/backend/tests/services/forecasting/test_confidence_engine.py index f14c5991..df6718b5 100644 --- a/backend/tests/services/forecasting/test_confidence_engine.py +++ b/backend/tests/services/forecasting/test_confidence_engine.py @@ -342,6 +342,26 @@ class TestComputeReportConfidence: # Полностью JSON-сериализуем (контракт для экспортёров/чата). assert json.loads(json.dumps(d, ensure_ascii=False)) == d + def test_deal_count_note_carries_window_months(self) -> None: + # #1637: deal_count_months → нота «за N мес» в факторе (и в rationale). + res = compute_report_confidence( + deal_count=7, + deal_count_months=6, + advisory=False, + ) + dc_factor = next(f for f in res.factors if f.name == "deal_count") + assert "за 6 мес" in dc_factor.note + assert "7 сделок" in dc_factor.note + # Структурная причина тоже содержит период (через ноту фактора-виновника). + assert "за 6 мес" in res.rationale + + def test_deal_count_note_without_window_has_no_period(self) -> None: + # deal_count_months=None (по умолчанию) → нота без «за N мес» (backward compat). + res = compute_report_confidence(deal_count=7, advisory=False) + dc_factor = next(f for f in res.factors if f.name == "deal_count") + assert "7 сделок" in dc_factor.note + assert "за" not in dc_factor.note + def test_ignores_garbage_component_confidence(self) -> None: # Мусорный component-уровень не учитывается (whitelist), не роняет искусственно. res = compute_report_confidence(