fix(tradein/domklik): diff_percent — проценты из соседних price_rub вместо рублей источника, гейт отвергает |x|>100, миграция 285 пересчитывает историю #3370

Merged
bot-backend merged 2 commits from fix/3225-domklik-diff-percent into main 2026-09-05 20:14:18 +00:00
Collaborator

Closes #3225. offer_price_history.diff_percent у domklik содержал рубли: лоадер клал diff источника как есть, clamp_diff_percent зажимал ±999999.99 вместо отказа. Прод: 8900 из 11 075 значений с |diff| > 50; у одного listing соседствовали −200000 (бэкфилл) и −0.82 (триггер).

Что сделано

  1. Лоадер (providers/domclick/detail.py): процент из соседних price_rub (сортировка по change_time), первая запись → NULL.
  2. Гейт (scraper_kit/offer_price_history.py): clamp_diff_percentvalidate_diff_percent(value, listing_id=None): |x| > 100 → NULL + warning с listing_id и сырым значением, не кламп. Гейт общий — переименован вызов и у cian. Осознанный размен: настоящий рост цены >2× тоже уйдёт в NULL (в докстринге).
  3. Миграция 285 (285_domklik_diff_percent_recompute.sql; номер свободен по всем 419 веткам origin): пересчёт всех domklik-строк оконной lag() по (listing_id, change_time, id); бэфилл-строки от триггерных не отличаются намеренно — триггер пишет тот же source='domklik' и ту же формулу, у него пересчёт даёт то же значение и IS DISTINCT FROM его не трогает; фильтр |x|>100 не берётся — пропустил бы рублёвые diff вроде −50. Идемпотентно, lock_timeout, RAISE NOTICE.

Тесты

246 passed, 3 skipped (-k domclick/price_history/diff); гейт lock_timeout/нумерации 281 passed. Фальсификация (откат только вычисления процента): assert [-300000] == [None] — рубли в колонке, по значению.

Прод-замер до мержа

SQL воркера — комментарием ниже.

Closes #3225. `offer_price_history.diff_percent` у domklik содержал рубли: лоадер клал `diff` источника как есть, `clamp_diff_percent` зажимал ±999999.99 вместо отказа. Прод: 8900 из 11 075 значений с |diff| > 50; у одного listing соседствовали −200000 (бэкфилл) и −0.82 (триггер). ## Что сделано 1. **Лоадер** (`providers/domclick/detail.py`): процент из соседних `price_rub` (сортировка по `change_time`), первая запись → NULL. 2. **Гейт** (`scraper_kit/offer_price_history.py`): `clamp_diff_percent` → `validate_diff_percent(value, listing_id=None)`: |x| > 100 → NULL + warning с listing_id и сырым значением, не кламп. Гейт общий — переименован вызов и у cian. Осознанный размен: настоящий рост цены >2× тоже уйдёт в NULL (в докстринге). 3. **Миграция 285** (`285_domklik_diff_percent_recompute.sql`; номер свободен по всем 419 веткам origin): пересчёт всех domklik-строк оконной `lag()` по `(listing_id, change_time, id)`; бэфилл-строки от триггерных **не отличаются намеренно** — триггер пишет тот же `source='domklik'` и ту же формулу, у него пересчёт даёт то же значение и `IS DISTINCT FROM` его не трогает; фильтр `|x|>100` не берётся — пропустил бы рублёвые diff вроде −50. Идемпотентно, lock_timeout, `RAISE NOTICE`. ## Тесты `246 passed, 3 skipped` (-k domclick/price_history/diff); гейт lock_timeout/нумерации `281 passed`. Фальсификация (откат только вычисления процента): `assert [-300000] == [None]` — рубли в колонке, по значению. ## Прод-замер до мержа SQL воркера — комментарием ниже.
bot-backend added 1 commit 2026-09-05 19:50:41 +00:00
fix(tradein): считать domklik diff_percent из цен, отвергать не-проценты (#3225)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
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) Successful in 5m6s
769a36098b
offer_price_history.diff_percent у domklik содержал РУБЛИ: загрузчик карточки
клал поле источника priceHistory.diff как есть, а clamp_diff_percent только
зажимал его в ±999999.99 — заведомо неправдоподобное значение проходило молча.
Прод 29.08.2026: 8900 из 11 075 непустых значений с |diff| > 50, p50 = -50 010.

- парсер Домклика считает процент сам из соседних price_rub (сортировка по
  change_time); у самой ранней записи предыдущей цены нет -> NULL, не 0;
- clamp_diff_percent -> validate_diff_percent: |x| > 100 не зажимается, а
  отвергается (NULL + warning с listing_id и сырым значением). Гейт стоит в
  общем хелпере, поэтому закрывает и cian-путь;
- миграция 285 пересчитывает уже собранные domklik-строки оконной lag() по
  (listing_id, change_time); идемпотентна (UPDATE только IS DISTINCT FROM).

Closes #3225
Author
Collaborator

Прод-замер радиуса миграции 285 ДО мержа (SQL воркера, 05.09 19:50 UTC):

шт
domklik-строк в offer_price_history 22 020
изменится (IS DISTINCT FROM пересчёта) 10 832
из них сейчас x
первых записей листинга с непустым diff → NULL 293

Разница 10 832 − 10 335 ≈ 500 строк — рублёвые diff с |x| ≤ 100 (например −50 ₽), которые фильтр по порогу пропустил бы: пересчёт всех domklik-строк — правильное решение. Триггерные строки в изменяемых почти не участвуют (иначе will_change был бы кратно больше) — косвенное подтверждение совпадения формул; deep-ревью сверяет по тексту триггера 131.

Критерий приёмки после деплоя: SELECT count(*) FILTER (WHERE abs(diff_percent) > 100) FROM offer_price_history WHERE source='domklik'0; повторный прогон миграции → 0 изменённых строк.

Прод-замер радиуса миграции 285 ДО мержа (SQL воркера, 05.09 19:50 UTC): | | шт | |---|---:| | domklik-строк в `offer_price_history` | 22 020 | | **изменится** (`IS DISTINCT FROM` пересчёта) | **10 832** | | из них сейчас |x| > 100 (заведомо рубли) | 10 335 | | первых записей листинга с непустым diff → NULL | 293 | Разница 10 832 − 10 335 ≈ 500 строк — рублёвые diff с |x| ≤ 100 (например −50 ₽), которые фильтр по порогу пропустил бы: пересчёт всех domklik-строк — правильное решение. Триггерные строки в изменяемых почти не участвуют (иначе `will_change` был бы кратно больше) — косвенное подтверждение совпадения формул; deep-ревью сверяет по тексту триггера 131. Критерий приёмки после деплоя: `SELECT count(*) FILTER (WHERE abs(diff_percent) > 100) FROM offer_price_history WHERE source='domklik'` → **0**; повторный прогон миграции → 0 изменённых строк.
Light1YT added 1 commit 2026-09-05 20:08:21 +00:00
fix(tradein): миграция 285 не трогает строки триггера (deep-review блокер)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 4m56s
afa93f9a1e
Утверждение «формула триггера = формула миграции» было ложным по ИСТОЧНИКУ базы:
record_listing_price_change (131:76-86) считает процент от listings.OLD.price_rub,
а не от предыдущей строки offer_price_history. Пересчёт по lag() портил честные
значения: у листинга, чья история начинается с триггерной строки, lag() = NULL →
−0.82 уходил в NULL; у триггерной строки с соседом-строкой загрузчика база чужая.

Теперь пересчитываем и берём как базу ТОЛЬКО строки загрузчика
(change_time <> recorded_at — триггер ставит обе метки одним now()). Это ровно то,
что делает починенный код: процент внутри истории самой карточки. Окно lag()
сужено до листингов с domklik-строками — иначе оконная функция шла по всей
таблице под lock_timeout = 5s и валила деплой.

Признак закреплён тестом на INSERT загрузчика (recorded_at не указан → DEFAULT
NOW()); при добавлении recorded_at в INSERT тест краснеет — проверено.
В шапке миграции отмечена асимметрия: старые cian-строки с |x| > 100 не чиним.
Author
Collaborator

Доводка по deep-ревью (afa93f9a): партиция lag() только по строкам загрузчика (change_time <> recorded_at), триггерные строки не пересчитываются и не служат базой; окно сужено до листингов domklik.

Прод-замер ДО/ПОСЛЕ доводки (05.09 20:10 UTC):

до доводки после
изменится строк 10 832 10 378
честных первых значений → NULL 293 0
сейчас x > 100 (рубли)
триггерных строк нетронуто 3 381

Разница 454 = ровно rewritten из SQL ревьюера — те триггерные строки, чья база (listings.OLD.price_rub) отличается от соседа по oph. Блокер закрыт числом, не словом. Признак триггера точный: обе метки одним now() (стабилен в транзакции); загрузчик пишет дату источника, recorded_at — DEFAULT NOW() (023:19) — закреплено тестом (фальсификация: дописать recorded_at в INSERT загрузчика → красный).

Критерий приёмки после деплоя: count(*) FILTER (WHERE abs(diff_percent) > 100) у source='domklik' AND change_time <> recorded_at0; триггерные 3 381 — значения не изменились (сверка по id-выборке).

Доводка по deep-ревью (afa93f9a): партиция `lag()` только по строкам загрузчика (`change_time <> recorded_at`), триггерные строки не пересчитываются и не служат базой; окно сужено до листингов domklik. **Прод-замер ДО/ПОСЛЕ доводки (05.09 20:10 UTC):** | | до доводки | после | |---|---:|---:| | изменится строк | 10 832 | **10 378** | | честных первых значений → NULL | 293 | **0** | | сейчас |x| > 100 (рубли) | 10 335 | 10 335 | | триггерных строк нетронуто | — | **3 381** | Разница 454 = ровно `rewritten` из SQL ревьюера — те триггерные строки, чья база (`listings.OLD.price_rub`) отличается от соседа по oph. Блокер закрыт числом, не словом. Признак триггера точный: обе метки одним `now()` (стабилен в транзакции); загрузчик пишет дату источника, `recorded_at` — DEFAULT NOW() (`023:19`) — закреплено тестом (фальсификация: дописать `recorded_at` в INSERT загрузчика → красный). Критерий приёмки после деплоя: `count(*) FILTER (WHERE abs(diff_percent) > 100)` у `source='domklik' AND change_time <> recorded_at` → **0**; триггерные 3 381 — значения не изменились (сверка по `id`-выборке).
bot-backend merged commit c49d6386c0 into main 2026-09-05 20:14:18 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3370
No description provided.