Commit graph

340 commits

Author SHA1 Message Date
2ea49b637f refactor(tradein/estimator): extract deterministic pricing into pure _price_from_inputs (#1966 foundation)
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
Behavior-preserving structural refactor: the ~870-line deterministic pricing
block inside estimate_quality() is extracted into a new pure synchronous function
_price_from_inputs() returning a PricingResult dataclass.

Key design decisions:
- All async DB fetches (imv_eval, yandex_val, cian_val) hoisted to estimate_quality
  BEFORE the call; passed as pre-fetched values / bool flags.
- DB-dependent helpers whose arguments are computed inside the block (_get_asking_sold_ratio,
  _lookup_quarter_index, _lookup_quarter_indexes) injected as Callable parameters
  (ratio_resolver, quarter_index_lookup, quarter_indexes_lookup).
- _fetch_house_imv_anchor called once in estimate_quality (was two separate conditional
  calls inside the block); single result passed as imv_anchor dict.
- _fetch_dkp_corridor and _fetch_anchor_comps hoisted to estimate_quality with
  identical guards.

All 2443 existing tests pass UNMODIFIED. 9 new hermetic unit tests added that
call _price_from_inputs directly with stub callables.
2026-06-27 13:00:21 +03:00
e24d3cf4be fix(scrapers): cian full_load anti-zombie — per-fetch wait_for + heartbeat внутри бакета (#1949)
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-27:

ФИКС C (run_cian_full_load, per-fetch timeout): monkey-patch _browser.fetch →
asyncio.wait_for(_f(url), timeout=cian_full_load_per_fetch_timeout_s=90s).
BrowserFetcher.fetch имеет httpx-timeout 120s, но browser-сервис иногда виснет
так что httpx не получает ответ (нет EOF) → fetch висит → asyncio.gather
блокирует весь bucket → heartbeat_at не обновляется → reap_zombies убивает живой run.
wait_for(90s) отменяет зависший fetch → TimeoutError → _fetch_page_html ловит
как Exception → None → _one_page → [] → gather завершается нормально.
За флагом (cian_full_load_per_fetch_timeout_s=0 = отключить, >0 = включить).

ФИКС D (background heartbeat): asyncio.create_task(_background_heartbeat())
обновляет heartbeat_at каждые 60s независимо от on_bucket/on_progress прогресса.
Без него пустые bucket или зависший gather замораживает heartbeat на несколько часов.
Task отменяется в finally → не течёт при return/raise/cancel.

Новая настройка (ENV):
  CIAN_FULL_LOAD_PER_FETCH_TIMEOUT_S=90.0  — timeout одного browser-fetch
2026-06-27 11:32:42 +03:00
757f6486f5 fix(scrapers): avito changeip-resilience (retry короткими попытками) + честный статус когда SERP отработал (#1950)
Два связанных фикса из диагностики 2026-06-27:

ФИКС A (_rotate_proxy_ip): вместо одношотного GET с timeout=30s — retry-loop
(proxy_rotate_attempts=3 попыток по proxy_rotate_attempt_timeout_s=8s каждая).
Зависший changeip-сервис больше не блокирует весь sweep на 30s.
Первый успешный attempt → True; все провалились → False (логируется как error).

ФИКС B (run_avito_city_sweep): когда SERP-фаза уже сохранила лоты
(lots_inserted + lots_updated > 0) и упала только detail/houses-фаза
(AvitoBlockedError / RateLimitedError), помечаем run как done (не banned)
при avito_serp_ok_not_banned=True (default). Метрика banned зарезервирована
для случаев где SERP сам заблокирован (0 лотов).

Новые настройки (ENV):
  PROXY_ROTATE_ATTEMPT_TIMEOUT_S=8.0   — timeout одной changeip-попытки
  PROXY_ROTATE_ATTEMPTS=3              — число попыток перед отказом
  AVITO_SERP_OK_NOT_BANNED=true        — флаг нового поведения (default on)
2026-06-27 11:24:54 +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
8bb302aadf test(tradein): cover avito/cian + mark_failed total_seen/new_count persistence (#1926)
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
#1928 fixed scrape_runs.total_seen/new_count for sweeps via _column_counts
but only regression-tested the yandex mark_done path. Add equivalents for
the CitySweepCounters (avito/cian) sweep and the mark_failed path — both
explicitly named in the #1926 audit — so the all-sweeps + failure-path
guarantee is locked in.

Refs #1926
2026-06-27 05:16:14 +05:00
63ac12dc23 feat(tradein): recurring house-dedup merge (schedule dormant) (#1772) (#1933)
All checks were successful
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 51s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m23s
Deploy Trade-In / build-backend (push) Successful in 52s
2026-06-26 21:39:30 +00:00
89d05bbfec fix(tradein): persist scrape_runs.total_seen/new_count from sweep counters (#1926) (#1928)
All checks were successful
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / test (push) Successful in 2m24s
Deploy Trade-In / build-backend (push) Successful in 54s
Deploy Trade-In / deploy (push) Successful in 49s
2026-06-26 18:09:17 +00:00
87364eebca fix(test): test_849 expects 3 begin_nested/lot after #1921 upsert-reconcile SAVEPOINT — unblock red main gate (#1925)
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) Successful in 28s
Deploy Trade-In / test (push) Successful in 1m24s
Deploy Trade-In / build-backend (push) Successful in 53s
Deploy Trade-In / deploy (push) Successful in 51s
2026-06-26 17:23:24 +00:00
74ea6659ea Merge pull request 'fix(scrapers): avito_full_load краш (source_id-reconcile) + cian/yandex ban-ротация' (#1921) from fix/scraper-resilience-upsert-rotation into main
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Has been skipped
Reviewed-on: #1921
2026-06-26 16:19:06 +00: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
c1cdf3969c fix(scrapers): ban/stuck-ротация для cian+yandex (provider-aware _rotate_proxy_ip)
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
2026-06-26 19:10:26 +03:00
197068c515 fix(scrapers): save_listings — UPDATE by (source,source_id) при dedup_hash-дрейфе (avito_full_load краш) 2026-06-26 19:10:12 +03: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
9678aface5 fix(estimator): dedup radius-аналогов — не раздувать n_analogs (#1871)
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
Добавляет тест test_n_analogs_reflects_deduped_count: 3 дубля одного объекта
(один source_id) → 1 кандидат (freshest scraped_at); 3 разных объекта → все 3.
Верифицирует ключевое требование #1871 P2: n_analogs отражает дедупнутое число,
а не raw кол-во строк в listings с дублями (prod: 797 yandex + 93 cian дублей
раздували n_analogs и искажали медиану/перцентили).
2026-06-26 18:52:11 +03: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
e03c810306 feat(tradein-imv): route IMV backfill через BrowserFetcher.fetch_json за dormant-флагом (#915 Stage 3) (#1916)
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) Successful in 27s
Deploy Trade-In / test (push) Successful in 1m23s
Deploy Trade-In / build-backend (push) Successful in 51s
Deploy Trade-In / deploy (push) Successful in 46s
2026-06-26 09:54:28 +00:00
d479edeb3f feat(tradein-browser): additive POST /fetch-json + BrowserFetcher.fetch_json (#915 Stage 2) (#1915)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m25s
Deploy Trade-In / build-backend (push) Successful in 52s
Deploy Trade-In / build-browser (push) Successful in 2m53s
Deploy Trade-In / deploy (push) Successful in 1m30s
2026-06-26 09:30:35 +00:00
1d5b081cb4 fix(cian): recover new-build house via cadastral anchor + listings (source,source_id) dedup (#1773) (#1912)
Some checks failed
Deploy Trade-In / changes (push) Successful in 13s
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 1m28s
Deploy Trade-In / deploy (push) Failing after 46s
Deploy Trade-In / build-backend (push) Successful in 50s
cian new-build cadastral anchor + listings (source,source_id) dedup (#1773): migration 133 (deep-reviewed data-loss-safe). Refs #1773
2026-06-26 07:45:22 +00:00
3f1925052b feat(scheduler): wire house_imv_backfill as scheduled source (#854) (#1910)
Some checks failed
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) Successful in 11s
Deploy Trade-In / test (push) Has been cancelled
wire house_imv_backfill as scheduled source (#854): trigger fn + seed migration 132. Refs #854
2026-06-26 07:45:08 +00:00
5ed88c41a4 fix(avito): post-merge polish — unify fetch_mode guard + empty-render guard (#1811) (#1909)
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
avito SERP browser post-merge polish (#1811): unify fetch_mode guard, empty-render guard, debug logging. Refs #1811
2026-06-26 07:44:22 +00:00
4eaf184d0e fix(avito): graceful sidecar HTTP 500/503 in browser SERP fetch (#1790)
All checks were successful
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 58s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / test (push) Successful in 1m25s
Deploy Trade-In / build-backend (push) Successful in 2m0s
Classify tradein-browser sidecar HTTP 500/503 in _fetch_serp_html_browser and route into existing rotation/retry machinery: soft-ban → IP rotation, transient → bounded backoff, both exhausted → AvitoRateLimitedError (graceful mark_banned, partial preserved) instead of mark_failed. Fixes run-#356 class where one late 500 nuked a run that had saved 3186 listings.

Refs #1790
2026-06-25 06:47:41 +00: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
7ae96ad1af Merge pull request 'fix(estimator): radius (source,source_id) dedup + split-house corridor disclosure (audit #1871 P2)' (#1880) from audit/1871-p2-estimator-dedup-split into main
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 46s
Deploy Trade-In / changes (push) Successful in 8s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m22s
Deploy Trade-In / build-backend (push) Successful in 51s
2026-06-23 09:49:11 +00: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
04550bc94f fix(tradein): drop non-EKB avito detail coords on ingest (audit #1871 P2)
All checks were successful
CI / changes (pull_request) Successful in 5s
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_detail.parse_detail_html писал data-map-lat/lon прямо в listings
UPDATE COALESCE без bbox-валидации — в отличие от geocoder.py, который
фильтрует везде. Итог: 406 active avito-листингов с координатами
Питер/Тюмень/Уфа просочились в ЕКБ-датасет (ложные аналоги через Tier A
address-match оценщика). Prod-verified 2026-06-23.

- geocoder: DRY-хелпер is_within_ekb_bbox(lat, lon, bbox) + именованные
  EKB_BBOX_TIGHT (geocoder-фильтр, числа не изменены) и EKB_BBOX_WIDE
  (ingest-guard); 2 inline tight-проверки → хелпер (поведение identical).
- avito_detail: после извлечения lat/lon → reset None + logger.warning
  если вне wide-bbox; листинг падает в geocode-cron путь (COALESCE NULL
  не затирает прежнее хорошее значение).
- tests: bbox-хелпер (ЕКБ True, Питер/Тюмень/Уфа False, inclusive,
  WIDE⊇TIGHT инвариант) + parse_detail_html guard end-to-end.

WIDE строго содержит TIGHT — guard физически не режет ничего, что
geocoder сам бы пропустил. NULL lat (14935, geocode backlog) — отдельная
проблема, не в scope. One-time cleanup существующих 406 — отдельно
(database-expert, prod data mutation).

Refs #1871
2026-06-23 14:33:10 +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
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
50a72f030a fix(tradein): Mera-audit fix-1 — Cian valuation price sanity bounds
Добавляет _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).
2026-06-21 09:02:32 +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
b6ea986134 fix(estimator): radius-path нижний floor от DKP-коридора (анти-undershoot)
Anchor-путь имеет hard floor от comp_min_ppm2×(1-tol). Radius-путь аналогичной
защиты снизу не имел — quarter-index-вниз или corridor-clamp могли занизить
median неоправданно (asymmetry).

Добавлен floor: если итоговый median_ppm2 < dkp_low_ppm2 × factor → поднять
до floor + лог. Только radius-путь (anchor_tier is None); без dkp_raw → no-op.
За флагами estimate_radius_floor_enabled (default True) и
estimate_radius_floor_factor (default 0.8).
2026-06-21 00:25:05 +03:00
352dfc8053 fix(estimator): asking_to_sold ratio tier по финальному headline
_get_asking_sold_ratio теперь вызывается ПОСЛЕ anchor/IMV-blend/quarter-index/
corridor-clamp, получая ФИНАЛЬНЫЙ median_ppm2 для tier-placement (#928 audit).

До фикса: tier резолвился по pre-anchor radius-медиане (~105k), но ratio
применялся к post-anchor headline (~300k) — несовпадение tier'а в premium-
сегменте. После: ratio берётся из того же tier, что и финальный headline.

Graceful: нет ratio → expected_sold_* = None, headline не меняется.
2026-06-21 00:24:02 +03:00
7f036a96af fix(scrapers): avito ban-triggered IP rotation (#1790) — changeip on block, bounded
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
- Добавлен `_rotate_proxy_ip()` в scrape_pipeline.py: вызывает changeip-URL
  (settings.avito_proxy_rotate_url) + settle-sleep
  (settings.avito_proxy_rotate_settle_s, default 9s), логирует old→new IP.
- В `run_avito_pipeline` (Steps 4/5/5b) и `run_avito_city_sweep` (Phase 4/5/5b):
  при достижении consecutive-block порога — сначала ротация IP, сброс счётчика,
  продолжение; только при исчерпании бюджета (enrich_rotations_done >=
  avito_proxy_max_rotations) — abort (bounded). #1820 graceful сохранён: один
  блок ниже порога ротацию не вызывает.
- Добавлено `avito_proxy_rotate_settle_s: float = 9.0` в config.py
  (ENV: AVITO_PROXY_ROTATE_SETTLE_S). avito_proxy_max_rotations был 4 (с #1731).
- Тесты: rotation_called / bounded_abort / single_block_no_rotation.
2026-06-20 21:24:12 +03:00
37f12f6303 fix(tradein/yandex): инкрементальный per-combo save + bounded gate-timeout (анти-зомби city_sweep)
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
2026-06-20 20:57:41 +03:00
06fa5b4910 Merge pull request 'feat(tradein): бэкфилл координат листингов из ekb_geoportal_buildings (street+house match)' (#1841) from feat/tradein-backfill-coords-geoportal into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 7s
Deploy Trade-In / build-browser (push) Successful in 28s
Deploy Trade-In / build-frontend (push) Successful in 28s
Deploy Trade-In / test (push) Successful in 1m23s
Deploy Trade-In / build-backend (push) Successful in 50s
Deploy Trade-In / deploy (push) Successful in 47s
Reviewed-on: #1841
2026-06-20 14:14:12 +00:00
d35e61be16 feat(tradein): бэкфилл координат листингов из ekb_geoportal_buildings (street+house match)
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
2026-06-20 17:08:46 +03:00
f026423715 feat(tradein/avito): per-card ЕКБ гео-фильтр (отсечь padding по России) + сплит комнат 4/5+
All checks were successful
CI / changes (pull_request) Successful in 5s
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
FIX 1: в _parse_html добавлен гео-фильтр per-card source_url. При
avito_serp_ekb_only=True (ENV AVITO_SERP_EKB_ONLY, дефолт True) карточки без
/ekaterinburg/ в source_url отбрасываются — Авито добивает выдачу
«по всей России» при малом числе ЕКБ-результатов (rooms=4 давал 96% не-ЕКБ).
Карточки без распознанного city-slug пропускаются консервативно.
Лог: «avito SERP: dropped N non-EKB cards (padding)».

FIX 2 (анализ): ROOM_SLUGS уже содержит отдельные бакеты «4-комн.» и «5+комн.»
(разбиты ранее). rooms-значение берётся из текста заголовка карточки
(_extract_rooms_from_title: regex «(\d)-к. квартира»), а не из бакета —
4/5/6 различаются по факту парсинга. Сплит slug'ов был сделан ранее, регрессий нет.

Тесты: 4 новых unit-теста в test_avito_ekb_geo_filter.py покрывают:
EKB+non-EKB → только EKB; ekb_only=False → все проходят;
без city-slug → консервативно оставляем; все EKB → ничего не дропается.
2026-06-20 16:59:12 +03:00
cc3ff7c3d2 feat(tradein/scrapers): detail_backfill через curl_cffi+backconnect вместо браузера/auv (развязка прокси)
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
SERP (full_load/city_sweep) и detail_backfill делили один прокси-аккаунт auv (~5 коннектов).
Browser-фетч в backfill открывал десятки CONNECT-туннелей → cap превышался → HTTP 500/краши.

- config.py: avito_detail_backfill_use_curl=True (ENV AVITO_DETAIL_BACKFILL_USE_CURL)
- avito_detail_backfill.py: browser_mode гейтится флагом; use_curl=True → session=None,
  browser_fetcher=None → fetch_detail строит _build_detail_session() через scraper_proxy_url
  (backconnect mproxy, авто-ротация, 1 коннект/запрос)
- tests: 2 новых теста (use_curl=True → BrowserFetcher не создаётся; False → создаётся)
2026-06-20 15:55:24 +03:00
93783602c5 Merge pull request 'feat(scrapers): live pacing-регулятор + data-quality coverage API' (#1829) from feat/scrapers-config-apis into main
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 7s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been cancelled
Deploy Trade-In / test (push) Has been cancelled
Reviewed-on: #1829
2026-06-20 12:31:47 +00:00
daa2edfac3 test(estimator): unit tests для audit fixes #audit-1..5 + empty-series guard
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
18 тестов покрывают все 5 фиксов: low-conf gate, analog_tier enum,
price_trend freshness, sigma=0 guard, sber staleness, thin-market flag.
Попутный fix: _load_sber_index_series — guard 'if not series: continue'
предотвращает max() на пустом dict при MagicMock rows (truthy=True).
2026-06-20 15:06:59 +03:00
b6634ea40c feat(scrapers): live pacing-регулятор (GET/PUT /scraper/pacing) + data-quality coverage endpoint
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
- browser/server.py: GET /pacing и PUT /pacing — read/write _MIN_PAGE_INTERVAL_BY_PROVIDER
  in-memory; валидация source in PROVIDERS, interval_s >= 0; логирует изменения; сбрасывается
  к env-дефолту на рестарте by design
- backend/admin.py: GET /scraper/pacing — прокси к browser /pacing, PacingResponse pydantic;
  PUT /scraper/pacing/{source} — прокси PUT с валидацией ge=0 le=120; 502/503 при недоступности
- backend/admin.py: GET /scraper/data-quality — single-pass FILTER-агрегаты по listings WHERE
  is_active GROUP BY source (description/photo_urls/address/lat/lon/kitchen_area_m2/living_area_m2/
  ceiling_height/ceiling_height_m/metro_stations); houses (total/avito_validated_at%/rating_score%/
  house_type%); house_reviews count
- browser/test_server_pacing.py: 10 новых тестов GET+PUT /pacing (16 total, все зелёные)
- backend/tests/test_scraper_admin_apis.py: тесты pacing-прокси + data-quality shape/pct-range
  (21 total, все зелёные)
2026-06-20 12:47:03 +03:00
533299ea47 feat(tradein/scrapers): full_load пропускает листинги обновлённые сегодня при повторном прогоне (skip_seen_today)
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
2026-06-20 12:13:32 +03:00
8361961a44 fix(scrapers/backend): socks5 health-probe (httpx[socks]) + status Literal whitelist + #1820 ext_id=0 dedup-гард
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
- pyproject.toml: httpx[socks]>=0.27.0 — тянет socksio, socks5:// прокси в _probe_current_ip работает прозрачно
- admin.py list_scrape_runs_unified: status Literal["done","running","banned","zombie","failed","cancelled"] вместо str|None — невалидные значения → 422
- avito_houses.py save_house_catalog_enrichment: гард ext_id=0 + нет адреса → skip without DB touch; два id-less novostroyka URL не схлопываются в bogus avito:0 в house_sources
- тесты: test_unified_runs_invalid_status_422 + test_unified_runs_valid_statuses_200; test_save_house_catalog_enrichment_skips_zero_ext_id_no_address + *_with_address_persists
2026-06-20 10:17:37 +03:00
cd91d5e279 fix(scrapers): avito pipeline — новостройки house graceful (без housePage), AvitoBlockedError per-item не крашит прогон (#1820)
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-20 09:53:46 +03:00
2f88a71915 feat(admin): unified scrape/runs + scraper/health + rotate-ip API для единой scrapers-страницы
All checks were successful
CI / changes (pull_request) Successful in 5s
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-20 09:25:57 +03:00
37fc50ad08 fix(scrapers): avito_detail_backfill — снапшот ЕКБ+active, detail browser-only без curl-fallback на бан (#1814)
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
2026-06-19 21:58:35 +03:00
ebdd70a38d Merge pull request 'feat(scrapers): avito SERP дефолт на BrowserFetcher (camoufox), curl_cffi fallback (#1805)' (#1809) from feat/avito-serp-browser-default into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
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 1m15s
Deploy Trade-In / build-backend (push) Successful in 58s
Deploy Trade-In / deploy (push) Successful in 45s
Reviewed-on: #1809
2026-06-19 17:53:12 +00:00
65cec74cca Merge pull request 'feat(scrapers): cian SERP на BrowserFetcher (camoufox), снят code-side rotate (#1806)' (#1810) from feat/cian-serp-browser into main
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 5s
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
Reviewed-on: #1810
2026-06-19 17:51:59 +00:00
93f7c9ae3f feat(scrapers): avito SERP дефолт на BrowserFetcher (camoufox), curl_cffi fallback (#1805)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
2026-06-19 20:42:04 +03:00