Commit graph

133 commits

Author SHA1 Message Date
bot-backend
e250869c9d fix(tradein/dadata): suggest-fallback для house_fias_id когда CLEAN выключен
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 56s
На проде у DaData-аккаунта выключена услуга CLEAN (Стандартизация) → /clean/address
отдаёт HTTP 403 «Feature CLEAN disabled», из-за чего house_fias_id (ключ join с
houses/ДОМ.РФ) и гео терялись, хотя /suggest их отдаёт бесплатно (token-only).

- enrich_address(): /clean → graceful fallback на /suggest (house-level кандидат);
  canonical_address/house_cadnum честно None (их даёт только CLEAN — не выдумываем).
- clean_address(): 403 «feature disabled» логируется отдельно от auth-rejection
  (токен валиден — refresh не поможет; чинить = включить услугу либо fallback).
- DadataSuggestion + _parse_suggestion: тянут house_fias_id/kladr_id/qc_geo.
- estimator: enrichment-вызов → enrich_address (алиас dadata_clean_address сохранён,
  это точка патча в 11 тестах — без churn).
- Тесты: feature-disabled лог, enrich clean-path/suggest-fallback/both-fail, новые поля.
  Проверено вживую на прод-кредах: clean→403, enrich→house_fias_id восстановлен.
2026-07-12 19:55:37 +03:00
bot-backend
e2502a0134 fix(tradein/estimator): per-city ценовой band ДКП-сделок для всей обл. 66
All checks were successful
CI Trade-In / frontend-checks (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 / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 50s
Глобальный DEAL_MIN_PPM2=50k резал 46.6% не-ЕКБ сделок (легит дешёвые города: Североуральск p50=21.7k, Новоуральск 36k, Асбест 41k). Band стал per-city.

- migration 178: deal_city_price_bands (city PK, ppm2_min/max, n_deals), populate из deals (source=rosreestr, city<>Екатеринбург, count>=30), min=GREATEST(p1,8000) max=LEAST(p99,800000), idempotent upsert; HAVING-guard p99>=8000 (airtight против inverted band).
- estimator.py оба enforcement-пойнта city-aware с fallback на глобаль:
  - _fetch_dkp_corridor SQL: LEFT JOIN deal_city_price_bands + COALESCE(band, :global).
  - _is_plausible_deal(city, bands) + _fetch_deals: _load_city_price_bands (TTL-cache), SELECT city.
- ЕКБ исключён из таблицы → fallback на 50k/800k → EKB-сделки byte-identical (deals спеллит EKB только 'Екатеринбург', 49791).

EKB deal-level инвариант airtight; interior-EKB оценки идентичны. Регресс-гейт байт-зелёный. Follow-ups: periodic refresh band-таблицы; city-scoping _fetch_dkp_corridor (→ C2).
2026-07-12 19:07:05 +03:00
d22e0e00ae tech-debt(tradein/estimator): collapse won estimate_* flags into defaults (#1970) (#2475)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 45s
Deploy Trade-In / build-backend (push) Successful in 58s
Deploy Trade-In / deploy (push) Successful in 47s
2026-07-12 12:46:18 +00:00
acc1e05357 fix(tradein/estimator): восстановить и канонизировать корпус дома в дедупе (регрессия #2429) (#2432)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 47s
Deploy Trade-In / test (push) Successful in 43s
Deploy Trade-In / build-backend (push) Successful in 56s
2026-07-04 17:08:40 +00:00
752c3656a3 fix(tradein/estimator): дом «2» вместо «15» при глухом названии станции метро в avito-адресе (#2429)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 45s
Deploy Trade-In / build-backend (push) Successful in 55s
Deploy Trade-In / deploy (push) Successful in 46s
2026-07-04 15:50:49 +00:00
a0c609caf4 fix(tradein/estimator): устранить остаточные кросс-сорс дубли (#2291) (#2417)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 57s
Deploy Trade-In / build-backend (push) Successful in 58s
Deploy Trade-In / deploy (push) Successful in 48s
2026-07-04 12:58:15 +00:00
480d4c2128 fix(tradein/estimator): kitchen_area_m2/ceiling_height_m/is_apartments comp-scoring (#2012) (#2396)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m33s
Deploy Trade-In / build-backend (push) Successful in 55s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 46s
2026-07-04 08:33:17 +00:00
a5097c3a97 fix(tradein/estimator): rebase ground-floor hedonic term onto current main (#2002, #2382) (#2391)
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 57s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m32s
Deploy Trade-In / deploy (push) Successful in 51s
2026-07-04 07:33:37 +00:00
2580158d8c fix(tradein/estimator): lock honest asking_to_sold_ratio invariant + disambiguate torg bases (#2087 M2/M3) (#2387)
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
2026-07-04 06:48:51 +00:00
758d7c6bf8 fix(tradein/estimator): честная маркировка quarter-precision дат ДКП + insufficient_data флаг для sell-time-sensitivity (#1995) (#2355)
Some checks failed
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m36s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Has been cancelled
2026-07-04 00:14:02 +00:00
bot-backend
4a6820b8f7 feat(tradein/estimator): switch core valuation call sites to scraper_kit (#2337)
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
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) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m48s
Group E4 (final, highest-risk step of scraper_kit migration epic #2277):
estimator.py and house_imv_backfill.py's avito_imv/cian_valuation/
yandex_valuation call sites now import from scraper_kit.providers.* instead
of app.services.scrapers.*, following the exact wiring proven safe by E1/E2/E3
(#2334/#2335/#2336):

- estimator.py's avito IMV (_get_or_fetch_imv_cached, both call sites) and
  house_imv_backfill.py's _process_one_house: add config=RealScraperConfig()
  — kit's evaluate_via_imv silently drops the configured proxy without it.
- estimator.py's cian valuation (Stage 9): add config=RealScraperConfig() —
  mandatory kwarg on the kit function (TypeError if omitted).
- estimator.py's yandex valuation: add config=RealScraperConfig() (mandatory)
  and delay_provider=get_scraper_delay — without it kit silently falls back
  to a hardcoded 5.0s throttle instead of the DB-configured anti-ban delay.
- All exception classes imported consistently from the same kit module as
  evaluate_via_imv (not just the function) — mixing legacy/kit exception
  classes would break `except IMVAddressNotFoundError` etc. via identity
  mismatch (caught by an existing test that assumed the legacy class,
  fixed alongside).

Observability: both cian_valuation and yandex_valuation graceful-degradation
except-blocks upgraded from logger.warning to logger.exception. GlitchTip's
LoggingIntegration listens at event_level=ERROR (main.py/scheduler_main.py) —
a WARNING never reaches GlitchTip as an event regardless of exc_info, so a
future config-wiring mistake at these call sites needs ERROR level to be
visible in monitoring.

house_imv_backfill.py: RealScraperConfig is imported lazily inside
_process_one_house (not at module level) to avoid a circular import —
app.services.scraper_adapters imports backfill_house_imv/
process_houses_imv_batch from this module at module level. Verified via
direct import in both orders plus a full `app.main` import.

Also fixes a stale docstring claiming process_houses_imv_batch is "not wired
into scheduler" — it is, via scrape_pipeline.py's run_avito_city_sweep.

Test updates: 2 pre-existing tests (test_backfill_wave2.py) mocked the legacy
IMVAddressNotFoundError/IMVEvaluation/IMVGeo classes, now updated to import
from scraper_kit to match the production exception identity. Added
config=/delay_provider= regression-guard asserts to the relevant estimator
and backfill tests, mirroring the existing #2306 cian_price_history pattern.

Legacy app/services/scrapers/{avito_imv,cian_valuation,yandex_valuation}.py
are untouched and still imported by cian_history_backfill.py's valuation
block (separately scoped, not touched here) — revert is a clean single-commit
revert, no schema/data migration involved.
2026-07-04 02:40:34 +03:00
4fedfc5970 fix(tradein/estimator): дедуп кросс-source дублей в anchor-пути (#2265 follow-up) (#2311)
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled
2026-07-03 20:35:37 +00:00
bot-backend
6f170504ba fix(tradein/pii): удалить телефоны продавцов (NULL) + PII клиента (DROP) (#2217, #1969)
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m21s
CI Trade-In / backend-tests (pull_request) Successful in 1m46s
- listings.phones: миграция 166 обнуляет данные; cian.py + scraper-kit
  serp.py (golden-parity зеркало) перестают читать offer.get("phones") на
  входе — write-path выключен у источника, колонка/поле в модели остаются.
  Мёртвая запись "phones" в LISTING_FIELD_PRIORITY убрана.
- trade_in_estimates.client_name/client_phone: миграция 167 дропает колонки;
  синхронно убраны из TradeInEstimateInput, обоих INSERT в estimator.py
  (основной путь + _empty_estimate), frontend TradeInEstimateInput и
  CRM-блока EstimateForm (поля "Имя клиента"/"Телефон").
- sentry_scrub.py и management_companies.phones не тронуты — вне scope.
2026-07-03 23:00:42 +03:00
d15ff3d99d feat(tradein/estimator): сегментная поправка headline+выкуп по ценовому бэнду за флагом (#2255) (#2293)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m29s
Deploy Trade-In / deploy (push) Successful in 44s
Deploy Trade-In / build-backend (push) Successful in 56s
2026-07-03 18:43:38 +00:00
666962185c fix(tradein/estimator): парсер адресов — имя-перед-типом, glued-суффикс, микрорайоны (#2269) (#2290)
All checks were successful
Deploy Trade-In / test (push) Successful in 1m30s
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 56s
Deploy Trade-In / deploy (push) Successful in 45s
2026-07-03 13:03:25 +00:00
bot-backend
6f81ee73d7 fix(tradein/estimator): полный сьют зелёный после дедуп-ключа (#2265)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 2m20s
2026-07-03 10:48:15 +03:00
bot-backend
9cdc7cef21 fix(tradein/estimator): дедуп — нумерованные улицы + house-no guard на street-union (#2265, review follow-up)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Failing after 1m51s
2026-07-03 10:37:26 +03:00
bot-backend
9a9f13d513 fix(tradein/estimator): savepoint+retry вокруг ratio-lookup — transient не гасит expected_sold (#2265)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Failing after 1m36s
2026-07-03 10:19:57 +03:00
bot-backend
a08e263c60 fix(tradein/estimator): кросс-source дедуп — устойчивый адресный ключ street+house (#2265) 2026-07-03 10:19:19 +03:00
bot-backend
291717fa2f fix(tradein/data): external_valuations.house_id — резолв в write-path + идемпотентный бэкфилл (#2236) 2026-07-03 09:22:44 +03:00
9f28092356 fix(tradein/estimator): min-width floor ±12% на ценовой диапазон (#2209) (#2230)
All checks were successful
Deploy Trade-In / test (push) Successful in 1m31s
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m18s
Deploy Trade-In / deploy (push) Successful in 46s
2026-07-02 21:21:28 +00:00
4ce0f51bb8 perf(tradein): снять блокировку event loop на POST /estimate (#2207) (#2225)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m35s
Deploy Trade-In / build-backend (push) Successful in 1m27s
Deploy Trade-In / deploy (push) Successful in 1m5s
2026-07-02 20:39:48 +00:00
b2d794587f Merge pull request 'feat(tradein/estimator): кросс-source физдедуп аналогов за флагом (#2087 H4)' (#2173) from fix/tradein-analog-dedup-cv into main
All checks were successful
Deploy Trade-In / test (push) Successful in 1m29s
Deploy Trade-In / build-backend (push) Successful in 1m0s
Deploy Trade-In / changes (push) Successful in 7s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 51s
2026-07-02 18:04:39 +00:00
bot-backend
67a852caf1 feat(tradein/matching): fias_exact tier + estimate fias persist + suggest fias_id contract
All checks were successful
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 / changes (pull_request) Successful in 10s
2026-07-02 20:49:04 +03:00
bot-backend
3909db745b fix(tradein/estimator): не отравлять asking→sold ratio-кэш транзиентной ошибкой
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
Блок «ОЖИДАЕМАЯ ЦЕНА СДЕЛКИ» (expected_sold, «с учётом торга») молча гас в «—» у
части оценок с полноценным headline (прод: high-confidence, 17 аналогов, цена
есть — expected_sold NULL). Причина: `_get_asking_sold_ratio` gracefully ловит
ЛЮБУЮ ошибку БД → (None, None), но запись в `_asking_sold_ratio_cache` шла
БЕЗУСЛОВНО (после try/except) → один транзиентный сбой (poisoned tx от
вышестоящего graceful-except, коннект-хиккап) отравлял кэш бакета `(None, None)`
на весь TTL (300с) и гасил expected_sold для ВСЕХ оценок этого rooms-бакета на
воркере до истечения TTL.

Фикс: ранний `return None, None` из except БЕЗ записи в кэш → следующая оценка
ретраит. Кэшируем только успешный lookup (ratio может быть None, если строки
реально нет — стабильный факт БД, безопасно кэшировать).

Прод-диагностика (30 дней, 304 оценки): 39 пустых expected_sold, из них 37 —
легитимно n_analogs=0 (нет аналогов), а 2 — этот баг (headline есть, ratio-кэш
отравлён). Таблица asking_to_sold_ratios полна по всем бакетам + global fallback
0.843 → при исправном lookup ratio всегда резолвится.

Юнит-тесты: транзиентная ошибка → None БЕЗ кэша; error→retry не залипает;
успех кэшируется.
2026-07-02 20:34:10 +03:00
bot-backend
3ad1550363 feat(tradein/estimator): кросс-source физдедуп аналогов за флагом (#2087 H4)
All checks were successful
CI / changes (pull_request) Successful in 7s
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
Один физический лот кросс-постится на avito+cian+domklik (разные source и
source_id) → existing radius-дедуп по (source, source_id) его НЕ ловит →
n_analogs И source_counts раздуты кросс-постами (аудит #2087: лот
80м²/265000₽/м² = N1+Домклик+Циан ×3; «14 аналогов» → ~6-7 уникальных).

_dedup_cross_source схлопывает дубли по физическому ключу (building_cadastral
| нормализованный address + floor + area-bucket round(m²) + price-bucket
round(₽/100k)) ДО подсчёта n_analogs/median/cv, оставляя свежайшего (scraped_at)
представителя. За флагом estimate_dedup_analogs_enabled (default OFF =
байт-идентично; регресс-гейт зелёный).

Бэктест #1966 (400 ДКП, full spine, OFF vs ON, тот же сэмпл): MAPE 13.89%→13.89%,
coverage 83.33%→83.33%, bias −3.83%→−3.83%, median width 0.743→0.743, median cv
0.0988→0.0988, avg n_analogs 27.64→27.57 (дедуп отработал 107× на 335 оценках).
Кросс-посты имеют identical price → нулевой вклад в дисперсию → cv/коридор НЕ
сужаются: это фикс ЧЕСТНОСТИ СЧЁТА (n_analogs не раздут, source_counts по
физлотам), accuracy-нейтральный, а НЕ рычаг cv-сужения (рычаг cv→коридор —
estimate_sb_clip_after_weight, уже default ON). Default OFF — narrowing не
продемонстрирован; флаг готов к canary/монкипатч-бэктесту.

Refs #2087
2026-07-02 20:22:46 +03:00
ecc3ab5aab fix(tradein/estimator): честный asking_to_sold_ratio — бейдж «−N% к рынку» больше не врёт
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Оценщик клиента жаловался на «большой интервал между рекомендованной ценой
и оценкой». Разбор: бейдж «−23% к рынку» (web HeroSummary + PDF, формула
round((1−ratio)×100)) систематически завышал скидку.

Root cause: сохранённый asking_to_sold_ratio — это СЫРОЙ per-rooms/tier дисконт
из ratio_resolver, но фактический expected_sold сдвинут относительно median×ratio
последующими корректировками: hedonic year+area (#2002, factor ∈ [0.75, 1.30], ON
by default), le_asking-clamp и corridor-clamp. Пример с прода (451de30b): median
7.75M × raw 0.771 = 5.97M, hedonic ×1.226 → expected_sold 7.32M — но stored ratio
остался 0.771, тогда как фактическое expected_sold/median = 0.945. Бейдж показывал
«−23%» вместо честных «−5%».

Fix: после финализации expected_sold пересчитываем сохранённый asking_to_sold_ratio
как реальное expected_sold_price/median_price (честный дескриптор). Сам expected_sold
(выкуп) НЕ трогаем — hedonic-uplift остаётся прибит к sale-модели, buyout не падает
до наивного median×raw. Порог _RATIO_DESCRIPTOR_EPS=1e-4 отсекает шум округления:
без сдвига (hedonic OFF, нет клампа) табличный ratio сохраняется байт-в-байт →
регрессия на не-зажатых оценках отсутствует.

Стор asking_to_sold_ratio — чисто ДЕСКРИПТОР (web/PDF/history badge), НЕ калибровочный
вход: калибровочный ratio живёт в таблице asking_to_sold_ratios (refresh-task, читает
resolver) — не тронута. Backtest #1966 скорит expected_sold_per_m2 (не stored ratio) —
не затронут (expected_sold без изменений).

Tests: 3 новых в test_estimator_price_spine.py (инвариант при hedonic-uplift +
corridor-clamp; byte-identical регрессия без сдвига); поправлен
test_global_fallback_basis_carried_through (hedonic OFF для сырого ratio).
Full suite: 2749 passed (кроме pre-existing test_search_cache_hit).

Refs #2141
2026-07-02 18:26:19 +03:00
18b22c9df8 fix(tradein/v2): canonical sources_used — kill «ИСТОЧНИКОВ X/7» POST↔GET desync
All checks were successful
CI / changes (pull_request) Successful in 7s
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
Единый helper estimator._canonical_sources(analogs, valuation_flags) — источник
правды для sources_used, зовётся идентично на POST (estimate_quality) и
GET-rehydrate (get_estimate) из ТЕХ ЖЕ persisted analogs + оценочных флагов.

Root cause: три расходящиеся деривации — POST брал sources_used из top-N
analogs_lots, GET возвращал persisted-колонку (радиусный набор + quarter_index),
source_counts на GET считался из persisted analogs. Источник (напр. cian) мог
попасть в source_counts, но не в sources_used → счётчик «X/7» прыгал 4→5 между
POST-ответом и reload(GET).

- sources_used = {листинговые из persisted analogs} ∪ {avito_imv/yandex_valuation/
  cian_valuation}. Детерминированно отсортирован.
- source_counts на POST теперь тоже из analogs_lots (не полной metadata-выборки)
  → инвариант source_counts.keys() ⊆ sources_used на POST и GET.
- POST персистит канонический sources_used в колонку (history/PDF консистентны для
  новых строк); GET рехайдрейтит его же helper'ом — чинит и СТАРЫЕ строки
  (листинговая часть пересобирается из analogs, quarter_index/радиусный шум
  отбрасывается фильтром оценочных флагов).

Оценочные флаги персистятся в колонке sources_used и читаются оттуда на GET —
реконструкция не требуется.

Repro 451de30b: до — sources_used=[avito,avito_imv,domklik,yandex], source_counts
имеет cian (не в sources_used); после — sources_used=[avito,avito_imv,cian,
domklik,yandex] (5/7), counts.keys() ⊆ sources_used.

Part of #2087 (M1).
2026-07-02 17:22:20 +03:00
24b107ddb5 feat(tradein): #2043 BE-1 — surface cv/source_counts/created_at + /history.sources_used + cache-KPI
Данные уже считаются в estimator — отдаём наружу для /trade-in/v2 (снимает
approximate-флаги CV / счётчиков источников / даты отчёта / «ИСТОЧНИКОВ N/7»).

- AggregatedEstimate += cv (float|None), source_counts (dict[str,int]), created_at.
- estimator: _cv_from_ppm2 / _source_counts helpers. cv прокинут через
  PricingResult — anchor-путь берёт CV комплов (anchor["cv"]), radius-путь — CV
  радиусной ₽/м²-выборки. source_counts считается по ПОЛНОЙ выборке (metadata_lots)
  до top-N отсечки. created_at = момент создания.
- POST /estimate возвращает все три; GET /estimate/{id} пересчитывает cv/
  source_counts из сохранённых analogs (best-effort) + created_at из колонки.
- /history: += sources_used (jsonb) в проекции → колонка «ИСТОЧНИКОВ N/7».
- /cache-stats: += avg_median_price (по median_price>0) + repeat_address_pct
  (доля строк с неуникальным address). Честный best-effort по persisted-оценкам.

Refs #2043
2026-07-02 16:50:18 +03:00
e23dabe4f5 fix(tradein/v2): wire РАДИУС (backend radius_m) + map +/- zoom + building image + deglue word+word
All checks were successful
CI / changes (pull_request) Successful in 7s
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
Live-browser audit (round 2): РАДИУС/+- dead, building photo missing.
- РАДИУС: backend BE-2 — optional radius_m (int 100–5000) on TradeInEstimateInput,
  threaded into estimate_quality comp search (base + fallback). None preserves the
  exact two-tier default (1000m primary / 2000m fallback) byte-identically. FE: РАДИУС
  dropdown with **Авто** default (sends no radius_m → legacy behaviour, NOT a silent
  500m narrowing) + 300/500/1000/2000 overrides; included as radius_m on submit; outer
  map ring tracks the value.
- Map +/− buttons: now a real zoom (transform: scale() on the map content layer, step
  .25, clamp .6–2.5); controls/bracket stay unscaled.
- building.png: next/image didn't apply basePath → 400; switched to plain <img> with
  NEXT_PUBLIC_BASE_PATH-prefixed src (→ 200) + onError fallback. HERO АДРЕС card шоу.
- deglueAddr: also splits word+word glue ('БольшаковаГеологическая'→'Большакова
  Геологическая'), still preserves house letters (14А, д.5К).

next build green (/v2 34.3 kB); estimator None-default byte-identical (ge=100 rules out
falsy-0); code-reviewer  after the Авто-default fix (avoided a default-radius regression).
2026-06-28 18:03:07 +03:00
44b9305d89 feat(tradein/estimator): elite-segment manual_review trigger by asking ppm² (#2002)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m25s
Deploy Trade-In / build-backend (push) Successful in 51s
Deploy Trade-In / deploy (push) Successful in 48s
2026-06-27 20:15:38 +00:00
e39fb86a9b feat(tradein): AI-curated premium-building overlay — class + false-positive demotion (#2002)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m29s
Deploy Trade-In / deploy (push) Successful in 52s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 56s
2026-06-27 18:43:05 +00:00
275e7e9618 feat(tradein/estimator): manual_review recommendation flag (#2002)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m30s
Deploy Trade-In / build-backend (push) Successful in 1m2s
Deploy Trade-In / deploy (push) Successful in 49s
2026-06-27 18:17:24 +00:00
50684ce87e fix(tradein/estimator): remove dead tier-aware-ratio path — truncation artifact + footgun (#2002)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m25s
Deploy Trade-In / deploy (push) Successful in 49s
Deploy Trade-In / build-backend (push) Successful in 53s
2026-06-27 17:34:54 +00:00
840d64a4a0 feat(tradein): premium_houses MV + premium_building flag (#2002)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m33s
Deploy Trade-In / build-backend (push) Successful in 56s
Deploy Trade-In / deploy (push) Successful in 51s
2026-06-27 16:40:39 +00:00
c45445b726 feat(tradein/estimator): hedonic year+area correction on expected_sold (#2002)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 54s
Deploy Trade-In / deploy (push) Successful in 47s
Deploy Trade-In / test (push) Successful in 1m25s
2026-06-27 16:06:08 +00:00
9448a945d4 feat(tradein/estimator): calibrated 80% prediction interval for expected_sold range (#1966)
All checks were successful
Deploy Trade-In / test (push) Successful in 1m26s
Deploy Trade-In / deploy (push) Successful in 47s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 51s
2026-06-27 14:01:24 +00:00
24a80ee0cf wip: extract pricing spine (pre-test) 2026-06-27 12:51:14 +03:00
bcdc7ccb04 fix(estimator): deterministic comp tie-breaker + n_analogs counts priced analogs
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Found by adversarial valuation audit (2 confirmed, bot-safe).

FIX A (#5): both radius comp queries (Tier H ~3990, Tier W ~4135) ended with
a bare ORDER BY relevance_score; on score ties Postgres returned rows in
undefined order, so the same /analyze could pick different comps across runs.
Append deterministic tiebreaker: relevance_score ASC, distance_m ASC,
scraped_at DESC NULLS LAST, id ASC (id = listings PK → total order). Added id
to each base CTE; outer projection unchanged (no leak downstream).

FIX B (#2): _filter_outliers keeps rows with price_per_m2 IS NULL, but the
median is built from prices_ppm2 (drops them) while n_analogs counted all of
listings_clean — overstating contributing comps ("Найдено N аналогов"
misleading; all-price-less -> median=0 but n_analogs>0). Count n_analogs from
prices_ppm2 in the radius path. #698 anchor overwrite + #691 zero-analog->low
guard unaffected; listings_clean itself not filtered.

Adds tests/test_estimator_n_analogs_priced.py (verified to fail on old code).
Audit also flagged velocity fan-out (false-positive: 0 duplicate domrf_obj_id
on prod) and >/>= disclosure tweaks (cosmetic) — deliberately not changed.

Refs #1871
2026-06-27 05:57:04 +05:00
5d5b2981fc Merge pull request 'fix(estimator): флаг + тесты для ghost-anchor guard и radius-dedup (#1871)' (#1919) from fix/estimator-trust-ghost-dedup into main
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled
Reviewed-on: #1919
2026-06-26 16:18:58 +00:00
2fe34ac785 fix(estimator): ghost-anchor — confidence='low' when radius analogs empty (#1871) (#1920)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m26s
Deploy Trade-In / deploy (push) Successful in 48s
Deploy Trade-In / build-backend (push) Successful in 58s
2026-06-26 16:07:12 +00:00
68f7c0e015 fix(estimator): запрет confidence=high при n_analogs=0 (#1871 ghost-anchor)
Добавляет settings-флаг estimate_confidence_floor_no_analogs (дефолт True),
гейтящий вызов _enforce_zero_analog_low перед сборкой AggregatedEstimate.
При n_analogs==0 и confidence!='low' форсит 'low' + добавляет caveat в
explanation («без сопоставимых аналогов рядом»), предотвращая показ выдуманного
«высокого» доверия когда оценка построена только на внешних оценщиках
(yandex_valuation/cian_valuation) без реальных рыночных аналогов.
2026-06-26 18:50:19 +03:00
77a3d3f75a fix(estimator): enable split-home disclosure at validated threshold 1.2 (audit #1871)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / openapi-codegen-check (pull_request) Has been skipped
Включает wide-corridor disclosure (PR #1880, был default OFF) — но с
ИСПРАВЛЕННЫМ порогом по prod-данным.

Проверка на trade_in_estimates (60д): corridor_pct = (range_high-range_low)/
median_price имеет median≈0.48, p90≈0.93, p99≈1.38; **30.7% оценок > 0.6**.
Старый порог 0.6 прилепил бы caveat «дом разбит на секции» к ~трети оценок —
большинство НЕ split-дома (широкий коридор бывает от разных причин). Это была
бы ложная атрибуция (дезинформация).

- estimate_wide_corridor_threshold 0.6 → 1.2: genuine split-дома из аудита =
  148-170% (1.48-1.70) → 1.2 ловит только экстремальный хвост (>p99), не трогая
  нормальную оценочную неопределённость.
- estimate_wide_corridor_disclosure_enabled False → True (включаем после
  валидации). ENV-override сохранён (kill-switch).
- Формулировка смягчена: «дом разбит на секции» → «вероятно, дом разбит на
  секции разной этажности ИЛИ разнородный фонд» — мы ИНФЕРИМ split по ширине
  коридора, не доказываем структурно.
- Логика не тронута: фаерит только Tier A + corridor>threshold, только понижает
  confidence + дописывает explanation; point/median/range не трогает.

Тесты: обновлены default-ассерты (флаг ON, порог 1.2) + 2 behavioral boundary-
теста (фаерит при pct>threshold, НЕ фаерит при умеренном 1.045<1.2 — доказывает
что raise убрал false-positive flood). 13 passed.

Refs #1871
2026-06-24 01:54:58 +05:00
1b23fc5d95 fix(estimator): radius (source,source_id) dedup + split-house corridor disclosure (audit #1871 P2)
All checks were successful
CI / changes (pull_request) Successful in 6s
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
Два P2-фикса в чувствительном estimator.py, оба за флагами.

DEDUP (estimate_radius_dedup_enabled, default ON):
radius-путь _fetch_analogs (Tier S-canonical/S-fallback/H/W) кэпил только
per-address (rn_addr ≤ MAX_ANALOGS_PER_ADDRESS=5), но (source,source_id)-дубли
делят один address и выживали на разных rn_addr рангах → раздували n_analogs
(prod 2026-06-23: yandex 48, cian 9, n1 5 excess). Добавлен rn_dup-window
PARTITION BY source, CASE(id:source_id|url:source_url|ctid) ORDER BY scraped_at
DESC + AND rn_dup=1 во все 4 тира — зеркалит anchor-dedup (:1556). CASE/ctid-guard
не схлопывает legacy NULL-source_id. ctid доступен (все тиры FROM listings
single-table). psycopg3: column::type (легально), bind-param :: отсутствует.

SPLIT DISCLOSURE (estimate_wide_corridor_disclosure_enabled, default OFF):
Tier A матчит по address-regex (намеренно — дом дробится на house_id), на
split-доме разной этажности comp_min..max растягивает коридор до 148-170%.
При corridor_pct > threshold (0.6) → понизить confidence на ступень + caveat
«дом разбит на секции разной этажности — оценка ориентировочная». НЕ трогает
point/median/range — только confidence+explanation. Helper _downgrade_confidence.
Default OFF (no-regress, включим осознанно). Merge house_id отклонён (recon:
вреден — Tier A намеренно обходит house_id-группировку).

Тесты: +22 (dedup 4 тира + семантика + psycopg3-regex guard; split disclosure
через реальный _run_estimate harness + does-not-touch-median/range + helper).
399 estimator passed, ruff clean, mypy pre-existing-only.

Refs #1871
2026-06-23 14:48:02 +05:00
41e9c820c9 fix(tradein/estimator): ghost-anchor invariant guard — n_analogs=0 forces low (audit #1871 P1.2)
All checks were successful
CI / changes (pull_request) Successful in 7s
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
Audit #1871 P1.2: estimate 5fcc1e99 показывал confidence='high' при
n_analogs=0 (median 38.45M ₽, sources=['yandex_valuation']) при реальной
истории листингов дома 4.2-6.5M. По prod-DB это ЕДИНСТВЕННАЯ legacy-строка
от 2026-05-30, давно expired (TTL оценок = 1 день, все 66 high/medium-c-n<5
строк expired). Текущий mainline уже честен. Фикс — defensive invariant,
чтобы ghost не мог родиться снова из будущих external-valuation-blend /
rehydrate / migration путей.

Backend (estimator.py):
- helper _enforce_zero_analog_low(confidence, n_analogs, explanation, ...)
  форсит 'low' + caveat когда n_analogs==0 and confidence!='low'.
  Идемпотентен (low+0 → low без дубля caveat), не трогает high+N>0, реальный
  median внешнего сервиса сохраняется (fake-коэффициенты не вводятся),
  logger.warning для observability.
- Вызов перед сборкой AggregatedEstimate в estimate_quality (единственный
  уязвимый return-путь; _empty_estimate хардкодит low — структурно безопасен).
- +5 unit-тестов (high+0→low+caveat, medium+0→low, low+0 идемпотент,
  high+N unchanged, None-explanation).

Frontend (HeroSummary, HeroTransparency, history):
- effectiveConfidence = n_analogs===0 ? 'low' : confidence — defensive depth
  на случай legacy-rehydrate / контракт-drift. Caveat-текст из backend
  confidence_explanation (не хардкод). Type-safe (ConfidenceLevel).

Refs #1871
2026-06-23 11:05:34 +05:00
92725c6122 fix(tradein): Mera-audit fix-4 — верифицировать DEAL_MAX_PPM2 vs prod данных
All checks were successful
CI / changes (pull_request) Successful in 7s
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
Данные на 2026-06-21 (prod deals table, 49 791 записей):
  p90=172 419 ₽/м², p95=199 583 ₽/м², p99=278 681 ₽/м², p99.9=500 000 ₽/м²
  deals >800k = 13 штук (0.026%) — нерыночные outlier'ы, не легитимный премиум ЕКБ.
  listings p99 (is_active, <800k фильтр) = 392 795 ₽/м².

Вывод: DEAL_MAX_PPM2=800_000 НЕ срезает легитимный ЕКБ-премиум (p99.9 сделок=500k).
Значение NOT изменено — задокументировано в комментарии рядом с константой
для обоснования порога и последующей ревалидации при росте рынка.
2026-06-21 09:06:36 +03:00
af1d882745 fix(tradein): Mera-audit fix-3 — cross-source dedup в _fetch_price_trend
Один объект на 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, данные возвращаются корректно).
2026-06-21 09:05:57 +03:00
81ee864d80 fix(tradein): Mera-audit fix-2 — _is_plausible_deal floor edge + area/price guards
Ужесточает валидацию ДКП-сделок в _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 и пр.).
2026-06-21 09:04:54 +03:00
b2cd2925fd fix(estimator): clamp expected_sold <= asking (ratio cap 1.0) -- smoke 3к exp_sold>asking
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
2026-06-21 00:35:23 +03:00
f2cfe66b02 fix(scrapers): yandex history skip area<=0 items
_save_yandex_history_items теперь пропускает items с area_m2 is None или
area_m2 <= 0 (битый парс «0,5 м²» и пр.) перед сохранением в
house_placement_history. Грязь копилась в БД и искажала price_trend.

Estimator защищён NULLIF на уровне SQL, но фильтрация на входе надёжнее.
Лог кол-ва отброшенных (info). Батч не падает из-за одного битого item.
2026-06-21 00:25:43 +03:00