fix(site-finder): non-overlapping L2/L3 ready_dt windows, stop double-count of 13-24mo supply (#1355)
Some checks failed
CI / changes (push) Successful in 10s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m52s
CI / backend-tests (push) Failing after 8m55s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m47s
CI / backend-tests (pull_request) Failing after 9m0s

This commit is contained in:
bot-backend 2026-06-17 20:29:09 +03:00
parent 18da92ccc7
commit be68ba8f54

View file

@ -81,9 +81,14 @@ _EKB_REGION_CODE: int = 66
# Ниже — слишком много исключённых (NULL free_flats) объектов → 'low' (см. limitations). # Ниже — слишком много исключённых (NULL free_flats) объектов → 'low' (см. limitations).
_L2_MEDIUM_MIN_COVERAGE: float = 0.6 _L2_MEDIUM_MIN_COVERAGE: float = 0.6
# L2/L3 граница (включительно в L2, исключительно в L3) — половинно-открытые интервалы:
# L2 (hidden) : ready_dt IS NULL OR ready_dt <= now + L2_NEAR_HORIZON [now, now+12mo]
# L3 (future) : ready_dt IS NOT NULL AND ready_dt > now + L3_FUTURE_HORIZON (now+12mo, ∞)
# Оба используют одно и то же значение 12 мес., но по разные стороны границы — нет overlap.
_L2_NEAR_HORIZON_MONTHS: int = 12 # L2 берёт объекты с ready_dt ≤ сейчас+12мес (или NULL)
# L3: «будущее» = ready_dt дальше этого горизонта от сегодня. Раньше — это уже # L3: «будущее» = ready_dt дальше этого горизонта от сегодня. Раньше — это уже
# почти-на-рынке предложение (ловится L1/L2), не «future». # почти-на-рынке предложение (ловится L1/L2), не «future».
_L3_FUTURE_HORIZON_MONTHS: int = 12 _L3_FUTURE_HORIZON_MONTHS: int = 12 # L3 берёт объекты с ready_dt > сейчас+12мес
# L3: «ещё не продаётся» = выставлено не больше этого числа квартир. >0 порог (не строго # L3: «ещё не продаётся» = выставлено не больше этого числа квартир. >0 порог (не строго
# 0) терпит единичные тестовые/служебные листинги в проекте, который по сути не открыт. # 0) терпит единичные тестовые/служебные листинги в проекте, который по сути не открыт.
@ -215,6 +220,10 @@ _L1_OPEN_SQL = text(
# free_flats) к району. ИСКЛЮЧАЕМ free_flats IS NULL (нет sold-колонки → не вычислим; # free_flats) к району. ИСКЛЮЧАЕМ free_flats IS NULL (нет sold-колонки → не вычислим;
# documented). clamp ≥ 0 через GREATEST. Считаем coverage (всего объектов района vs с # documented). clamp ≥ 0 через GREATEST. Считаем coverage (всего объектов района vs с
# free_flats) для confidence. NO join на objective_lots / complexes — намеренно. # free_flats) для confidence. NO join на objective_lots / complexes — намеренно.
#
# Граница L2/L3 (#1355): L2 захватывает ТОЛЬКО объекты с ready_dt ≤ горизонту (или NULL —
# объект уже должен был выйти). ready_dt > горизонту → это L3 (future), НЕ L2.
# Полу-открытые интервалы (half-open): L2 = [now, now+12mo], L3 = (now+12mo, ∞).
_L2_HIDDEN_SQL = text( _L2_HIDDEN_SQL = text(
""" """
WITH latest AS ( WITH latest AS (
@ -222,7 +231,8 @@ _L2_HIDDEN_SQL = text(
o.obj_id, o.obj_id,
o.district_name, o.district_name,
o.flat_count, o.flat_count,
o.free_flats o.free_flats,
o.ready_dt
FROM domrf_kn_objects o FROM domrf_kn_objects o
WHERE o.region_cd = :region_cd WHERE o.region_cd = :region_cd
AND o.district_name IS NOT NULL AND o.district_name IS NOT NULL
@ -240,6 +250,8 @@ _L2_HIDDEN_SQL = text(
GREATEST(0, COALESCE(flat_count, 0) - free_flats) GREATEST(0, COALESCE(flat_count, 0) - free_flats)
) FILTER (WHERE free_flats IS NOT NULL), 0) AS hidden_units ) FILTER (WHERE free_flats IS NOT NULL), 0) AS hidden_units
FROM latest FROM latest
WHERE ready_dt IS NULL
OR ready_dt <= CURRENT_DATE + CAST(:near_horizon AS interval)
GROUP BY district_name GROUP BY district_name
""" """
) )
@ -368,7 +380,11 @@ def compute_layer2_hidden(
source='domrf_declared'. obj_class=None (district-агрегат класс-agnostic). source='domrf_declared'. obj_class=None (district-агрегат класс-agnostic).
units_estimate всегда 0 (clamp). Пустые данные []. Не бросает. units_estimate всегда 0 (clamp). Пустые данные []. Не бросает.
""" """
params: dict[str, Any] = {"region_cd": _EKB_REGION_CODE, "district": district} params: dict[str, Any] = {
"region_cd": _EKB_REGION_CODE,
"district": district,
"near_horizon": f"{_L2_NEAR_HORIZON_MONTHS} months",
}
rows = _safe_rows(db, _L2_HIDDEN_SQL, params, layer=2) rows = _safe_rows(db, _L2_HIDDEN_SQL, params, layer=2)
out: list[SupplyLayerRow] = [] out: list[SupplyLayerRow] = []