Один объект на avito_imv + yandex_valuation с разными ext_item_id создаёт
дубликаты в house_placement_history и double-count'ится в помесячной медиане.
Решение: CTE с DISTINCT ON (round(area_m2), floor, price, price_date) с
приоритетом avito_imv через CASE sort. Легитимные разные квартиры (разные
площадь/этаж/цена) не затрагиваются.
За флагом estimate_price_trend_dedup_enabled (дефолт True в settings).
False → точно старое поведение (backward-compat). Graceful: без изменений
в логике min_points и возврата None при недостатке данных.
Тесты: 6 новых тест-кейсов в test_estimator_price_trend_dedup.py (SQL содержит
DISTINCT ON, flag OFF не содержит, Source1 preferred, данные возвращаются корректно).
Ужесточает валидацию ДКП-сделок в _is_plausible_deal:
- floor < 1 или floor=0 → drop (floor=-5/0/999 из битого парсера)
- area_m2 задана и <= 0 → drop (нулевая/отрицательная площадь из битого парсера)
- price_rub задана и <= 0 → drop (нерыночная/техническая сделка)
- floor=None → допустимо (graceful, нечем судить)
Все новые параметры area_m2/price_rub keyword-only с дефолтом None →
backward-compatible для существующих вызовов. Caller _fetch_deals обновлён
для передачи area_m2/price_rub из данных сделки.
Тесты: 16 новых тест-кейсов в test_deals_sanitize.py (floor=-5, floor=999,
floor=None ok, area_m2=0 drop, price_rub=-1 drop и пр.).
Добавляет _is_price_sane() в cian_valuation.py: если sale_price_rub вне
[cian_valuation_min_rub, cian_valuation_max_rub] (дефолты 500k/500M),
или low_price > high_price, или любой bound отрицательный — результат
не кэшируется и возвращается None (graceful). Защита от garbage-ответов
API (999_999 < min, 9_999_999_999 > max).
Новые settings: cian_valuation_min_rub=500_000, cian_valuation_max_rub=500_000_000.
Тесты: 11 новых тест-кейсов в test_cian_valuation.py. Пофиксен pre-existing
KeyError в test_cache_hit_returns_cached (missing low_price/high_price в mock).